Browse Source

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
Domagoj Kriskovic 8 months ago
parent
commit
7b00e5d9f5

+ 6 - 0
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<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')

+ 7 - 7
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<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">

+ 112 - 0
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<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,
+  }
+}

+ 2 - 1
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,