mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 13:13:45 +01:00
Add routes to save/get project notification preferences (#30114)
* Add routes to save/get project notification preferences * update route url * improve zod schema * remove unused json response * update schema and fix tests * add jsdoc types to pass type-check * remove using zod strict() GitOrigin-RevId: f3ab5c88b58bd5af71e0504d0efbe03bdf9b243c
This commit is contained in:
committed by
Copybot
parent
3b32b0a61e
commit
7b00e5d9f5
@@ -12,6 +12,8 @@ const { isZodErrorLike } = require('zod-validation-error')
|
|||||||
/**
|
/**
|
||||||
* A helper function to safely get a nested value from an object
|
* A helper function to safely get a nested value from an object
|
||||||
* using a path array (e.g., ["query", "resource_type"])
|
* using a path array (e.g., ["query", "resource_type"])
|
||||||
|
* @param {any} data
|
||||||
|
* @param {Array<PropertyKey>} path
|
||||||
*/
|
*/
|
||||||
function getPathValue(data, path) {
|
function getPathValue(data, path) {
|
||||||
let current = data
|
let current = data
|
||||||
@@ -24,6 +26,10 @@ function getPathValue(data, path) {
|
|||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} issue
|
||||||
|
* @param {any} value
|
||||||
|
*/
|
||||||
const isRequiredError = (issue, value) =>
|
const isRequiredError = (issue, value) =>
|
||||||
value === undefined &&
|
value === undefined &&
|
||||||
(issue.code === 'invalid_type' || issue.code === 'invalid_union')
|
(issue.code === 'invalid_type' || issue.code === 'invalid_union')
|
||||||
|
|||||||
+7
-7
@@ -1,14 +1,14 @@
|
|||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import RadioButtonSetting, { RadioOption } from '../radio-button-setting'
|
import RadioButtonSetting, { RadioOption } from '../radio-button-setting'
|
||||||
import { useState } from 'react'
|
import {
|
||||||
|
NotificationLevel,
|
||||||
type NotificationLevel = 'all' | 'replies' | 'off'
|
useProjectNotificationPreferences,
|
||||||
|
} from '../../../hooks/use-project-notification-preferences'
|
||||||
|
|
||||||
export default function ProjectNotificationsSetting() {
|
export default function ProjectNotificationsSetting() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
// TODO: Connect to project settings context when backend support is added
|
const { notificationLevel, setNotificationLevel, isLoading } =
|
||||||
const [notificationLevel, setNotificationLevel] =
|
useProjectNotificationPreferences()
|
||||||
useState<NotificationLevel>('all')
|
|
||||||
|
|
||||||
const options: Array<RadioOption<NotificationLevel>> = [
|
const options: Array<RadioOption<NotificationLevel>> = [
|
||||||
{
|
{
|
||||||
@@ -33,7 +33,7 @@ export default function ProjectNotificationsSetting() {
|
|||||||
<RadioButtonSetting
|
<RadioButtonSetting
|
||||||
id="projectNotifications"
|
id="projectNotifications"
|
||||||
options={options}
|
options={options}
|
||||||
value={notificationLevel}
|
value={isLoading ? undefined : notificationLevel}
|
||||||
onChange={setNotificationLevel}
|
onChange={setNotificationLevel}
|
||||||
/>
|
/>
|
||||||
<div className="project-notifications-beta-note">
|
<div className="project-notifications-beta-note">
|
||||||
|
|||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useProjectContext } from '@/shared/context/project-context'
|
||||||
|
import { getJSON, postJSON } from '@/infrastructure/fetch-json'
|
||||||
|
import { debugConsole } from '@/utils/debugging'
|
||||||
|
import type { NotificationPreferencesSchema } from '../../../../../modules/notifications/app/src/types.ts'
|
||||||
|
|
||||||
|
export type NotificationLevel = 'all' | 'replies' | 'off'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map UI notification level to backend preferences
|
||||||
|
*/
|
||||||
|
function levelToPreferences(
|
||||||
|
level: NotificationLevel
|
||||||
|
): NotificationPreferencesSchema {
|
||||||
|
switch (level) {
|
||||||
|
case 'all':
|
||||||
|
return {
|
||||||
|
commentOnOwnProject: true,
|
||||||
|
commentOnInvitedProject: true,
|
||||||
|
repliesOnOwnProject: true,
|
||||||
|
repliesOnInvitedProject: true,
|
||||||
|
repliesOnAuthoredThread: true,
|
||||||
|
repliesOnParticipatingThread: true,
|
||||||
|
}
|
||||||
|
case 'replies':
|
||||||
|
return {
|
||||||
|
commentOnOwnProject: false,
|
||||||
|
commentOnInvitedProject: false,
|
||||||
|
repliesOnOwnProject: false,
|
||||||
|
repliesOnInvitedProject: false,
|
||||||
|
repliesOnAuthoredThread: true,
|
||||||
|
repliesOnParticipatingThread: true,
|
||||||
|
}
|
||||||
|
case 'off':
|
||||||
|
return {
|
||||||
|
commentOnOwnProject: false,
|
||||||
|
commentOnInvitedProject: false,
|
||||||
|
repliesOnOwnProject: false,
|
||||||
|
repliesOnInvitedProject: false,
|
||||||
|
repliesOnAuthoredThread: false,
|
||||||
|
repliesOnParticipatingThread: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map backend preferences to UI notification level
|
||||||
|
*/
|
||||||
|
function preferencesToLevel(
|
||||||
|
preferences: NotificationPreferencesSchema
|
||||||
|
): NotificationLevel {
|
||||||
|
// If all notifications are off
|
||||||
|
if (
|
||||||
|
!preferences.commentOnOwnProject &&
|
||||||
|
!preferences.commentOnInvitedProject &&
|
||||||
|
!preferences.repliesOnOwnProject &&
|
||||||
|
!preferences.repliesOnInvitedProject &&
|
||||||
|
!preferences.repliesOnAuthoredThread &&
|
||||||
|
!preferences.repliesOnParticipatingThread
|
||||||
|
) {
|
||||||
|
return 'off'
|
||||||
|
}
|
||||||
|
|
||||||
|
// If only reply-related notifications are on
|
||||||
|
if (
|
||||||
|
!preferences.commentOnOwnProject &&
|
||||||
|
!preferences.commentOnInvitedProject &&
|
||||||
|
(preferences.repliesOnAuthoredThread ||
|
||||||
|
preferences.repliesOnParticipatingThread)
|
||||||
|
) {
|
||||||
|
return 'replies'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to 'all' for any other combination
|
||||||
|
return 'all'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProjectNotificationPreferences() {
|
||||||
|
const { projectId } = useProjectContext()
|
||||||
|
const [notificationLevel, setNotificationLevel] =
|
||||||
|
useState<NotificationLevel>('all')
|
||||||
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
|
// Load preferences on mount
|
||||||
|
useEffect(() => {
|
||||||
|
getJSON<NotificationPreferencesSchema>(
|
||||||
|
`/notifications/preferences/project/${projectId}`
|
||||||
|
)
|
||||||
|
.then(prefs => {
|
||||||
|
setNotificationLevel(preferencesToLevel(prefs))
|
||||||
|
})
|
||||||
|
.catch(debugConsole.error)
|
||||||
|
.finally(() => setIsLoading(false))
|
||||||
|
}, [projectId])
|
||||||
|
|
||||||
|
const setLevel = useCallback(
|
||||||
|
(level: NotificationLevel) => {
|
||||||
|
setNotificationLevel(level)
|
||||||
|
const preferences = levelToPreferences(level)
|
||||||
|
postJSON(`/notifications/preferences/project/${projectId}`, {
|
||||||
|
body: preferences,
|
||||||
|
}).catch(debugConsole.error)
|
||||||
|
},
|
||||||
|
[projectId]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
notificationLevel,
|
||||||
|
setNotificationLevel: setLevel,
|
||||||
|
isLoading,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import AbstractMockApi from './AbstractMockApi.mjs'
|
import AbstractMockApi from './AbstractMockApi.mjs'
|
||||||
|
import { ObjectId } from '../../../../app/src/infrastructure/mongodb.mjs'
|
||||||
|
|
||||||
class MockChatApi extends AbstractMockApi {
|
class MockChatApi extends AbstractMockApi {
|
||||||
reset() {
|
reset() {
|
||||||
@@ -21,7 +22,7 @@ class MockChatApi extends AbstractMockApi {
|
|||||||
|
|
||||||
sendMessage(projectId, threadId, props) {
|
sendMessage(projectId, threadId, props) {
|
||||||
const message = {
|
const message = {
|
||||||
id: Math.random().toString(),
|
id: new ObjectId().toString(),
|
||||||
content: props.content,
|
content: props.content,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
user_id: props.user_id,
|
user_id: props.user_id,
|
||||||
|
|||||||
Reference in New Issue
Block a user