Przeglądaj źródła

[web] display global off treatment in settings modal (#32942)

* display global disabled state
* show loading indicator while project notification preferences load

GitOrigin-RevId: d7e905aaa3fc7b15b54bf99caeabf60c1e5d8050
Kristina 5 miesięcy temu
rodzic
commit
a64d1bbb6a

+ 2 - 0
services/web/frontend/extracted-translations.json

@@ -266,6 +266,7 @@
   "change_primary_email_address_instructions": "",
   "change_project_owner": "",
   "change_role_and_department": "",
+  "change_settings": "",
   "change_the_ownership_of_your_personal_projects": "",
   "change_to_group_plan": "",
   "change_to_this_plan": "",
@@ -1435,6 +1436,7 @@
   "project_name": "",
   "project_not_linked_to_github": "",
   "project_notifications": "",
+  "project_notifications_muted_description": "",
   "project_ownership_transfer_confirmation_1": "",
   "project_ownership_transfer_confirmation_2": "",
   "project_renamed_or_deleted": "",

+ 49 - 28
services/web/frontend/js/features/settings/components/editor-settings/project-notifications-setting.tsx

@@ -1,17 +1,22 @@
 import { useTranslation } from 'react-i18next'
 import RadioButtonSetting, { RadioOption } from '../radio-button-setting'
 import {
-  NotificationLevel,
+  SettableNotificationLevel,
   useProjectNotificationPreferences,
 } from '../../hooks/use-project-notification-preferences'
 import BetaBadgeIcon from '@/shared/components/beta-badge-icon'
+import LoadingSpinner from '@/shared/components/loading-spinner'
 
 export default function ProjectNotificationsSetting() {
   const { t } = useTranslation()
   const { notificationLevel, setNotificationLevel, isLoading } =
     useProjectNotificationPreferences()
 
-  const options: Array<RadioOption<NotificationLevel>> = [
+  if (isLoading) {
+    return <LoadingSpinner loadingText={t('loading')} />
+  }
+
+  const options: Array<RadioOption<SettableNotificationLevel>> = [
     {
       value: 'all',
       label: t('all_project_activity'),
@@ -31,38 +36,54 @@ export default function ProjectNotificationsSetting() {
 
   return (
     <>
-      <RadioButtonSetting
-        id="projectNotifications"
-        options={options}
-        value={isLoading ? undefined : notificationLevel}
-        onChange={setNotificationLevel}
-      />
-      <div className="global-notifications-link">
-        <a
-          href="/user/notification-preferences"
-          target="_blank"
-          rel="noopener noreferrer"
-        >
-          {t('manage_overleaf_email_preferences')}
-        </a>
-      </div>
-      <div className="project-notifications-beta-note">
-        <BetaBadgeIcon />
-        <div>
-          <span className="beta-note-text">
-            {t('email_notifications_are_currently_in_beta')}{' '}
-            {t('these_settings_might_change_in_the_future')}{' '}
-          </span>
-          {/* TODO: update forms link */}
+      {notificationLevel === 'global-off' ? (
+        <div className="ide-setting-description">
+          {t('project_notifications_muted_description')}{' '}
           <a
+            href="/user/notification-preferences"
             target="_blank"
             rel="noopener noreferrer"
-            href="https://forms.gle/"
           >
-            {t('give_feedback')}
+            {t('change_settings')}
           </a>
         </div>
-      </div>
+      ) : (
+        <>
+          <RadioButtonSetting
+            id="projectNotifications"
+            options={options}
+            value={notificationLevel}
+            onChange={setNotificationLevel}
+          />
+
+          <div className="global-notifications-link">
+            <a
+              href="/user/notification-preferences"
+              target="_blank"
+              rel="noopener noreferrer"
+            >
+              {t('manage_overleaf_email_preferences')}
+            </a>
+          </div>
+          <div className="project-notifications-beta-note">
+            <BetaBadgeIcon />
+            <div>
+              <span className="ide-setting-description">
+                {t('email_notifications_are_currently_in_beta')}{' '}
+                {t('these_settings_might_change_in_the_future')}{' '}
+              </span>
+              {/* TODO: update forms link */}
+              <a
+                target="_blank"
+                rel="noopener noreferrer"
+                href="https://forms.gle/"
+              >
+                {t('give_feedback')}
+              </a>
+            </div>
+          </div>
+        </>
+      )}
     </>
   )
 }

+ 14 - 6
services/web/frontend/js/features/settings/hooks/use-project-notification-preferences.ts

@@ -2,17 +2,21 @@ 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.js'
+import type {
+  GlobalNotificationPreferencesSchema,
+  NotificationPreferencesSchema,
+} from '../../../../../modules/notifications/app/src/types.js'
 import { sendMB } from '@/infrastructure/event-tracking'
 import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
 
-export type NotificationLevel = 'all' | 'replies' | 'off'
+export type SettableNotificationLevel = 'all' | 'replies' | 'off'
+export type NotificationLevel = SettableNotificationLevel | 'global-off'
 
 /**
  * Map UI notification level to backend preferences
  */
 function levelToPreferences(
-  level: NotificationLevel
+  level: SettableNotificationLevel
 ): NotificationPreferencesSchema {
   switch (level) {
     case 'all':
@@ -55,8 +59,12 @@ function levelToPreferences(
  * Map backend preferences to UI notification level
  */
 function preferencesToLevel(
-  preferences: NotificationPreferencesSchema
+  preferences: GlobalNotificationPreferencesSchema
 ): NotificationLevel {
+  if (preferences.muteAllNotifications) {
+    return 'global-off'
+  }
+
   // If all notifications are off
   if (
     !preferences.commentOnOwnProject &&
@@ -92,7 +100,7 @@ export function useProjectNotificationPreferences() {
 
   // Load preferences on mount
   useEffect(() => {
-    getJSON<NotificationPreferencesSchema>(
+    getJSON<GlobalNotificationPreferencesSchema>(
       `/notifications/preferences/project/${projectId}`
     )
       .then(prefs => {
@@ -103,7 +111,7 @@ export function useProjectNotificationPreferences() {
   }, [projectId])
 
   const setLevel = useCallback(
-    (level: NotificationLevel) => {
+    (level: SettableNotificationLevel) => {
       setNotificationLevel(level)
       const preferences = levelToPreferences(level)
       sendMB('setting-changed', {

+ 0 - 4
services/web/frontend/stylesheets/pages/editor/settings.scss

@@ -116,7 +116,3 @@
   align-items: center;
   gap: var(--spacing-05);
 }
-
-.beta-note-text {
-  color: var(--content-secondary);
-}

+ 2 - 0
services/web/locales/en.json

@@ -351,6 +351,7 @@
   "change_primary_email_address_instructions": "To change your primary email, please add your new primary email address first (by clicking <0>Add another email</0>) and confirm it. Then click the <0>Make primary</0> button. <1>Learn more about managing your __appName__ emails</1>.",
   "change_project_owner": "Change project owner",
   "change_role_and_department": "Change role and department",
+  "change_settings": "Change settings",
   "change_the_ownership_of_your_personal_projects": "Change the ownership of your personal projects to the new account. <0>Find out how to change project owner.</0>",
   "change_to_group_plan": "Change to a group plan",
   "change_to_this_plan": "Change to this plan",
@@ -1924,6 +1925,7 @@
   "project_name": "Project name",
   "project_not_linked_to_github": "This project is not linked to a GitHub repository. You can create a repository for it in GitHub:",
   "project_notifications": "Project notifications",
+  "project_notifications_muted_description": "You are not receiving any notifications, as you have disabled all project notifications.",
   "project_ownership_transfer_confirmation_1": "Are you sure you want to make <0>__user__</0> the owner of <1>__project__</1>?",
   "project_ownership_transfer_confirmation_2": "This action cannot be undone. The new owner will be notified and will be able to change project access settings (including removing your own access).",
   "project_renamed_or_deleted": "Project Renamed or Deleted",

+ 47 - 0
services/web/test/frontend/features/settings-modal/settings/project-notifications-setting.test.tsx

@@ -30,6 +30,18 @@ const repliesOnlyPreferences = {
   repliesOnParticipatingThread: true,
 }
 
+const globallyMutedPreferences = {
+  trackedChangesOnOwnProject: false,
+  trackedChangesOnInvitedProject: false,
+  commentOnOwnProject: false,
+  commentOnInvitedProject: false,
+  repliesOnOwnProject: false,
+  repliesOnInvitedProject: false,
+  repliesOnAuthoredThread: false,
+  repliesOnParticipatingThread: false,
+  muteAllNotifications: true,
+}
+
 const allNotificationsOff = {
   trackedChangesOnOwnProject: false,
   trackedChangesOnInvitedProject: false,
@@ -56,6 +68,16 @@ describe('<ProjectNotificationsSetting />', function () {
     fetchMock.removeRoutes().clearHistory()
   })
 
+  it('shows loading indicator while preferences are loading', async function () {
+    fetchMock.get(preferencesUrl, new Promise(() => {}))
+
+    renderComponent()
+
+    expect(await screen.findByRole('status')).to.exist
+    expect(screen.queryByLabelText('All project activity', { exact: false })).to
+      .not.exist
+  })
+
   it('selects "All project activity" when all notifications are on', async function () {
     fetchMock.get(preferencesUrl, allNotificationsOn)
 
@@ -140,6 +162,31 @@ describe('<ProjectNotificationsSetting />', function () {
     ).to.be.false
   })
 
+  it('shows muted message and hides radio buttons when muteAllNotifications is true', async function () {
+    fetchMock.get(preferencesUrl, globallyMutedPreferences)
+
+    renderComponent()
+
+    await waitFor(
+      () =>
+        expect(
+          screen.getByText(
+            'You are not receiving any notifications, as you have disabled all project notifications.',
+            { exact: false }
+          )
+        ).to.exist
+    )
+    expect(
+      screen.getByRole('link', { name: 'Change settings' }).getAttribute('href')
+    ).to.equal('/user/notification-preferences')
+    expect(screen.queryByLabelText('All project activity', { exact: false })).to
+      .not.exist
+    expect(
+      screen.queryByLabelText('Replies to your activity only', { exact: false })
+    ).to.not.exist
+    expect(screen.queryByLabelText('Off', { exact: false })).to.not.exist
+  })
+
   it('POSTs "replies" preferences when "Replies to your activity only" is selected', async function () {
     fetchMock.get(preferencesUrl, allNotificationsOn)
     const saveMock = fetchMock.post(preferencesUrl, { status: 200 })