From a64d1bbb6aedcf72dfb529a7302ae86236582175 Mon Sep 17 00:00:00 2001 From: Kristina <7614497+khjrtbrg@users.noreply.github.com> Date: Thu, 23 Apr 2026 09:40:29 +0200 Subject: [PATCH] [web] display global off treatment in settings modal (#32942) * display global disabled state * show loading indicator while project notification preferences load GitOrigin-RevId: d7e905aaa3fc7b15b54bf99caeabf60c1e5d8050 --- .../web/frontend/extracted-translations.json | 2 + .../project-notifications-setting.tsx | 77 ++++++++++++------- .../use-project-notification-preferences.ts | 20 +++-- .../stylesheets/pages/editor/settings.scss | 4 - services/web/locales/en.json | 2 + .../project-notifications-setting.test.tsx | 47 +++++++++++ 6 files changed, 114 insertions(+), 38 deletions(-) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 1594a98cec..dc3683ac72 100644 --- a/services/web/frontend/extracted-translations.json +++ b/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": "", diff --git a/services/web/frontend/js/features/settings/components/editor-settings/project-notifications-setting.tsx b/services/web/frontend/js/features/settings/components/editor-settings/project-notifications-setting.tsx index 439a1f6f98..06f6156589 100644 --- a/services/web/frontend/js/features/settings/components/editor-settings/project-notifications-setting.tsx +++ b/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> = [ + if (isLoading) { + return + } + + const options: Array> = [ { value: 'all', label: t('all_project_activity'), @@ -31,38 +36,54 @@ export default function ProjectNotificationsSetting() { return ( <> - -
- - {t('manage_overleaf_email_preferences')} - -
-
- -
- - {t('email_notifications_are_currently_in_beta')}{' '} - {t('these_settings_might_change_in_the_future')}{' '} - - {/* TODO: update forms link */} + {notificationLevel === 'global-off' ? ( +
+ {t('project_notifications_muted_description')}{' '} - {t('give_feedback')} + {t('change_settings')}
-
+ ) : ( + <> + + + +
+ +
+ + {t('email_notifications_are_currently_in_beta')}{' '} + {t('these_settings_might_change_in_the_future')}{' '} + + {/* TODO: update forms link */} + + {t('give_feedback')} + +
+
+ + )} ) } diff --git a/services/web/frontend/js/features/settings/hooks/use-project-notification-preferences.ts b/services/web/frontend/js/features/settings/hooks/use-project-notification-preferences.ts index 97a92d65ca..e49f23aed7 100644 --- a/services/web/frontend/js/features/settings/hooks/use-project-notification-preferences.ts +++ b/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( + getJSON( `/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', { diff --git a/services/web/frontend/stylesheets/pages/editor/settings.scss b/services/web/frontend/stylesheets/pages/editor/settings.scss index 178f6786ce..317d71c131 100644 --- a/services/web/frontend/stylesheets/pages/editor/settings.scss +++ b/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); -} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 9787b3404d..6ede2fcf80 100644 --- a/services/web/locales/en.json +++ b/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) and confirm it. Then click the <0>Make primary button. <1>Learn more about managing your __appName__ emails.", "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.", "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__ the owner of <1>__project__?", "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", diff --git a/services/web/test/frontend/features/settings-modal/settings/project-notifications-setting.test.tsx b/services/web/test/frontend/features/settings-modal/settings/project-notifications-setting.test.tsx index efbb179af0..fbf6ea7131 100644 --- a/services/web/test/frontend/features/settings-modal/settings/project-notifications-setting.test.tsx +++ b/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('', 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('', 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 })