diff --git a/src/components/settings/AccessibilitySettings.tsx b/src/components/settings/AccessibilitySettings.tsx index 1973bde8..65fc629c 100644 --- a/src/components/settings/AccessibilitySettings.tsx +++ b/src/components/settings/AccessibilitySettings.tsx @@ -4,6 +4,8 @@ import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-s import Button from '../Button'; import { spacing } from '../../theme/tokens'; import { useLocaleStore } from '../../stores/locale-store'; +import { usePolicyStore, useSettingHidden, useSettingLocked } from '../../stores/policy-store'; +import { policyAllowsValue } from '../../lib/org-policy'; import { APP_FONTS, FONT_SIZES, useSettingsStore, type AppFont, type FontSize } from '../../stores/settings-store'; import { uiFontsBundled } from '../../theme/reading-fonts'; import { toast } from '../../stores/toast-store'; @@ -81,6 +83,16 @@ export function AccessibilitySettings() { const animationsEnabled = useSettingsStore((s) => s.animationsEnabled); const toastDuration = useSettingsStore((s) => s.toastDuration); const sendConfirmation = useSettingsStore((s) => s.sendConfirmation); + // What the organisation pins or hides (webmail accessibility-settings.tsx): + // a minimum notification duration leaves out the shorter choices. + const policyRules = usePolicyStore((s) => s.rules); + const toastDurationLocked = useSettingLocked('toastDuration'); + const sendConfirmationLocked = useSettingLocked('sendConfirmation'); + const sendConfirmationHidden = useSettingHidden('sendConfirmation'); + const toastChoices = React.useMemo( + () => TOAST_DURATION_CHOICES.filter((value) => value === toastDuration || policyAllowsValue('toastDuration', value, policyRules)), + [toastDuration, policyRules], + ); const confirmDelete = useSettingsStore((s) => s.confirmDelete); const confirmArchive = useSettingsStore((s) => s.confirmArchive); @@ -214,23 +226,27 @@ export function AccessibilitySettings() { update('markAsReadDelay', Number(v))} - options={[ - { value: '0', label: t('settings.email_behavior.mark_read.instant', "Instantly") }, - { value: '3000', label: t('settings.email_behavior.mark_read.delay_3s', "After 3 seconds") }, - { value: '5000', label: t('settings.email_behavior.mark_read.delay_5s', "After 5 seconds") }, - { value: '-1', label: t('settings.email_behavior.mark_read.never', "Never") }, - ]} - /> - + {!markAsReadHidden && ( + + update('deleteAction', v as DeleteAction)} - options={[ - { value: 'trash', label: t('settings.email_behavior.delete_action.trash', "Move to Trash") }, - { value: 'trash-and-read', label: t('settings.email_behavior.delete_action.trash_and_read', "Move to Trash and mark as read") }, - { value: 'permanent', label: t('settings.email_behavior.delete_action.permanent', "Delete Permanently") }, - ]} - /> - {deleteAction === 'permanent' && ( - - - {t('settings.email_behavior.delete_action.warning', "Emails will be permanently deleted and cannot be recovered. This action is irreversible.")} + {!deleteActionHidden && ( + + + + update('messageSpacing', v as MessageSpacing)} - options={[ - { value: 'auto', label: t('settings.email_behavior.message_spacing.auto', "Auto (recommended)") }, - { value: 'always', label: t('settings.email_behavior.message_spacing.always', "Always add spacing") }, - { value: 'edge', label: t('settings.email_behavior.message_spacing.edge', "Edge to edge") }, - ]} - /> - + {!messageSpacingHidden && ( + + - - update('emailsPerPage', Number(v))} + options={[ + { value: '10', label: '10' }, + { value: '25', label: '25' }, + { value: '50', label: '50' }, + { value: '100', label: '100' }, + ]} + /> + + )} ); @@ -364,6 +394,7 @@ export function ReadingSettings() { function makeStyles(c: ThemePalette) { return StyleSheet.create({ group: {}, + lockedControl: { opacity: 0.6 }, warning: { flexDirection: 'row', gap: spacing.sm, diff --git a/src/components/settings/__tests__/org-policy-settings.test.ts b/src/components/settings/__tests__/org-policy-settings.test.ts index 97665eb9..a5bd4be8 100644 --- a/src/components/settings/__tests__/org-policy-settings.test.ts +++ b/src/components/settings/__tests__/org-policy-settings.test.ts @@ -63,4 +63,52 @@ describe('organisation policy on the settings pages', () => { expect(reading).toContain("const archiveEnabled = usePolicyFeature('archiveActionEnabled');"); expect(reading).toMatch(/\{archiveEnabled && \(\s*\s* = [ + ['AppearanceSettings', 'density', true], + ['AccessibilitySettings', 'toastDuration', false], + ['AccessibilitySettings', 'sendConfirmation', true], + ['ReadingSettings', 'markAsReadDelay', true], + ['ReadingSettings', 'deleteAction', true], + ['ReadingSettings', 'showPreview', true], + ['ReadingSettings', 'includeGroupInUnified', true], + ['ReadingSettings', 'plainTextMode', false], + ['ReadingSettings', 'autoSelectReplyIdentity', false], + ['ReadingSettings', 'messageSpacing', true], + ['ReadingSettings', 'emailsPerPage', true], + ['ComposingSettings', 'autoSelectReplyIdentity', false], + ['ComposingSettings', 'replyIdentityMatch', false], + ['ComposingSettings', 'plainTextMode', false], + ['NotificationSettings', 'emailNotificationsEnabled', true], + ['NotificationSettings', 'pushNotifyInboxOnly', true], + ['NotificationSettings', 'calendarNotificationsEnabled', true], + ]; + + it.each(LOCKABLE_ROWS)('%s marks %s as set by the organisation when locked', (page, key, hides) => { + const source = settings(page); + const locked = new RegExp(`const (\\w+) = useSettingLocked\\('${key}'\\);`).exec(source); + expect(locked, `useSettingLocked('${key}')`).not.toBeNull(); + // On the row, or on the control of a row whose control sits outside it. + expect(source).toMatch(new RegExp(`locked=\\{${locked![1]}\\}|pointerEvents=\\{${locked![1]} \\? 'none' : 'auto'\\}`)); + if (hides) { + const hidden = new RegExp(`const (\\w+) = useSettingHidden\\('${key}'\\);`).exec(source); + expect(hidden, `useSettingHidden('${key}')`).not.toBeNull(); + expect(source).toMatch(new RegExp(`\\{!${hidden![1]} && \\(|&& !${hidden![1]} && \\(`)); + } + }); + + it('keeps notification durations under the organisation minimum out of the choices', () => { + const page = settings('AccessibilitySettings'); + expect(page).toContain("policyAllowsValue('toastDuration', value, policyRules)"); + expect(page).toContain('options={toastChoices.map('); + }); + + it('keeps the controls of locked rows that sit outside their row still', () => { + expect(settings('AppearanceSettings')).toMatch(/]*>\s*]*>\s*]*>\s*\{segmented\(REPLY_IDENTITY_MATCHES/); + }); });