mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02: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
|
||||
* using a path array (e.g., ["query", "resource_type"])
|
||||
* @param {any} data
|
||||
* @param {Array<PropertyKey>} path
|
||||
*/
|
||||
function getPathValue(data, path) {
|
||||
let current = data
|
||||
@@ -24,6 +26,10 @@ function getPathValue(data, path) {
|
||||
return current
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} issue
|
||||
* @param {any} value
|
||||
*/
|
||||
const isRequiredError = (issue, value) =>
|
||||
value === undefined &&
|
||||
(issue.code === 'invalid_type' || issue.code === 'invalid_union')
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import RadioButtonSetting, { RadioOption } from '../radio-button-setting'
|
||||
import { useState } from 'react'
|
||||
|
||||
type NotificationLevel = 'all' | 'replies' | 'off'
|
||||
import {
|
||||
NotificationLevel,
|
||||
useProjectNotificationPreferences,
|
||||
} from '../../../hooks/use-project-notification-preferences'
|
||||
|
||||
export default function ProjectNotificationsSetting() {
|
||||
const { t } = useTranslation()
|
||||
// TODO: Connect to project settings context when backend support is added
|
||||
const [notificationLevel, setNotificationLevel] =
|
||||
useState<NotificationLevel>('all')
|
||||
const { notificationLevel, setNotificationLevel, isLoading } =
|
||||
useProjectNotificationPreferences()
|
||||
|
||||
const options: Array<RadioOption<NotificationLevel>> = [
|
||||
{
|
||||
@@ -33,7 +33,7 @@ export default function ProjectNotificationsSetting() {
|
||||
<RadioButtonSetting
|
||||
id="projectNotifications"
|
||||
options={options}
|
||||
value={notificationLevel}
|
||||
value={isLoading ? undefined : notificationLevel}
|
||||
onChange={setNotificationLevel}
|
||||
/>
|
||||
<div className="project-notifications-beta-note">
|
||||
|
||||
@@ -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 { ObjectId } from '../../../../app/src/infrastructure/mongodb.mjs'
|
||||
|
||||
class MockChatApi extends AbstractMockApi {
|
||||
reset() {
|
||||
@@ -21,7 +22,7 @@ class MockChatApi extends AbstractMockApi {
|
||||
|
||||
sendMessage(projectId, threadId, props) {
|
||||
const message = {
|
||||
id: Math.random().toString(),
|
||||
id: new ObjectId().toString(),
|
||||
content: props.content,
|
||||
timestamp: Date.now(),
|
||||
user_id: props.user_id,
|
||||
|
||||
Reference in New Issue
Block a user