diff --git a/libraries/validation-tools/validateSchema.js b/libraries/validation-tools/validateSchema.js index f765e52033..f2ec807924 100644 --- a/libraries/validation-tools/validateSchema.js +++ b/libraries/validation-tools/validateSchema.js @@ -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} 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') diff --git a/services/web/frontend/js/features/ide-redesign/components/settings/editor-settings/project-notifications-setting.tsx b/services/web/frontend/js/features/ide-redesign/components/settings/editor-settings/project-notifications-setting.tsx index 4ba6b2176e..ef43691a80 100644 --- a/services/web/frontend/js/features/ide-redesign/components/settings/editor-settings/project-notifications-setting.tsx +++ b/services/web/frontend/js/features/ide-redesign/components/settings/editor-settings/project-notifications-setting.tsx @@ -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('all') + const { notificationLevel, setNotificationLevel, isLoading } = + useProjectNotificationPreferences() const options: Array> = [ { @@ -33,7 +33,7 @@ export default function ProjectNotificationsSetting() {
diff --git a/services/web/frontend/js/features/ide-redesign/hooks/use-project-notification-preferences.ts b/services/web/frontend/js/features/ide-redesign/hooks/use-project-notification-preferences.ts new file mode 100644 index 0000000000..8206cd77ab --- /dev/null +++ b/services/web/frontend/js/features/ide-redesign/hooks/use-project-notification-preferences.ts @@ -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('all') + const [isLoading, setIsLoading] = useState(true) + + // Load preferences on mount + useEffect(() => { + getJSON( + `/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, + } +} diff --git a/services/web/test/acceptance/src/mocks/MockChatApi.mjs b/services/web/test/acceptance/src/mocks/MockChatApi.mjs index 687cb919b4..1d1c536ff9 100644 --- a/services/web/test/acceptance/src/mocks/MockChatApi.mjs +++ b/services/web/test/acceptance/src/mocks/MockChatApi.mjs @@ -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,