From efb92593b24b7fc9c501af35377254f21af87d49 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 15:34:06 +0100 Subject: [PATCH] feat: take archive out of the swipe and quick action settings when the organisation turns it off --- src/components/settings/LayoutSettings.tsx | 20 +++--- src/components/settings/ReadingSettings.tsx | 72 ++++++++++--------- .../__tests__/org-policy-settings.test.ts | 14 ++++ src/lib/__tests__/swipe-actions.test.ts | 22 ++++++ src/lib/swipe-actions.ts | 24 +++++++ 5 files changed, 111 insertions(+), 41 deletions(-) create mode 100644 src/lib/__tests__/swipe-actions.test.ts create mode 100644 src/lib/swipe-actions.ts diff --git a/src/components/settings/LayoutSettings.tsx b/src/components/settings/LayoutSettings.tsx index 88dea75a..4fad183f 100644 --- a/src/components/settings/LayoutSettings.tsx +++ b/src/components/settings/LayoutSettings.tsx @@ -7,7 +7,6 @@ import { useColors } from '../../theme/colors'; import { useLocaleStore } from '../../stores/locale-store'; import { useSettingsStore, - normalizeBottomQuickActions, REPLY_QUICK_ACTIONS, ALL_QUICK_ACTIONS, type SwipeAction, @@ -16,7 +15,8 @@ import { } from '../../stores/settings-store'; import { useEmailStore } from '../../stores/email-store'; import { useKeywordsStore } from '../../stores/keywords-store'; -import { useSettingLocked } from '../../stores/policy-store'; +import { usePolicyFeature, useSettingLocked } from '../../stores/policy-store'; +import { bottomQuickActionsFor, swipeActionShown, withoutArchive } from '../../lib/swipe-actions'; import { ORDER_PRESETS, detectPreset, presetLevels, sanitizeSortLevels, type OrderPreset, type MessageListOrderScope, @@ -74,12 +74,16 @@ export function LayoutSettings() { const hydrated = useSettingsStore((s) => s.hydrated); const hydrate = useSettingsStore((s) => s.hydrate); const t = useLocaleStore((s) => s.t); - const SWIPE_OPTIONS = React.useMemo(() => swipeOptions(t), [t]); + // With the organisation's Archive action off, Archive is no swipe or quick + // action, and a stored one shows as what it does (webmail + // message-list-settings.tsx). + const archiveEnabled = usePolicyFeature('archiveActionEnabled'); + const SWIPE_OPTIONS = React.useMemo(() => withoutArchive(swipeOptions(t), archiveEnabled), [t, archiveEnabled]); const SWIPE_MODE_OPTIONS = React.useMemo(() => swipeModeOptions(t), [t]); const QUICK_ACTION_LABELS = React.useMemo(() => quickActionLabels(t), [t]); const QUICK_ACTION_OPTIONS = React.useMemo( - () => ALL_QUICK_ACTIONS.map((value) => ({ value, label: QUICK_ACTION_LABELS[value] })), - [QUICK_ACTION_LABELS], + () => withoutArchive(ALL_QUICK_ACTIONS.map((value) => ({ value, label: QUICK_ACTION_LABELS[value] })), archiveEnabled), + [QUICK_ACTION_LABELS, archiveEnabled], ); useEffect(() => { if (!hydrated) void hydrate(); }, [hydrated, hydrate]); @@ -125,7 +129,7 @@ export function LayoutSettings() { { value: 'all', label: t('settings.message_list_order.scope_all', 'All folders') }, ]; - const bottomActions = normalizeBottomQuickActions(bottomQuickActionsRaw); + const bottomActions = bottomQuickActionsFor(bottomQuickActionsRaw, archiveEnabled); // Pick an action for a bar slot. If the chosen action already occupies // another slot, swap the two so the three slots stay unique. const setQuickActionSlot = (index: number, value: QuickAction) => { @@ -242,7 +246,7 @@ export function LayoutSettings() { {t('settings.email_behavior.swipe_right_action.description_mobile', "Action when you drag a row from its left edge towards the right.")} setSwipeRightAction(v as SwipeAction)} options={SWIPE_OPTIONS} /> @@ -257,7 +261,7 @@ export function LayoutSettings() { {t('settings.email_behavior.swipe_left_action.description_mobile', "Action when you drag a row from its right edge towards the left.")} setSwipeLeftAction(v as SwipeAction)} options={SWIPE_OPTIONS} /> diff --git a/src/components/settings/ReadingSettings.tsx b/src/components/settings/ReadingSettings.tsx index ed57262c..d9fa47a2 100644 --- a/src/components/settings/ReadingSettings.tsx +++ b/src/components/settings/ReadingSettings.tsx @@ -18,6 +18,7 @@ import { useEmailStore } from '../../stores/email-store'; import { archiveEmails, queryEmails, getEmails } from '../../api/email'; import { ownMailboxes } from '../../lib/mailbox-tree'; import { useLocaleStore } from '../../stores/locale-store'; +import { usePolicyFeature } from '../../stores/policy-store'; export function ReadingSettings() { const c = useColors(); @@ -28,6 +29,9 @@ export function ReadingSettings() { const update = useSettingsStore((s) => s.updateSetting); const archiveMode = useSettingsStore((s) => s.archiveMode); + // Where archived mail goes means nothing with the organisation's Archive + // action off (webmail message-list-settings.tsx). + const archiveEnabled = usePolicyFeature('archiveActionEnabled'); const markAsReadDelay = useSettingsStore((s) => s.markAsReadDelay); const deleteAction = useSettingsStore((s) => s.deleteAction); const permanentlyDeleteJunk = useSettingsStore((s) => s.permanentlyDeleteJunk); @@ -147,40 +151,42 @@ export function ReadingSettings() { - - - update('archiveMode', v as ArchiveMode)} + options={[ + { value: 'single', label: t('settings.email_behavior.archive_mode.single', "A single folder") }, + { value: 'year', label: t('settings.email_behavior.archive_mode.year', "A folder per year") }, + { value: 'month', label: t('settings.email_behavior.archive_mode.month', "A folder per month") }, + ]} + /> + {archiveMode !== 'single' && ( + <> + + {reorganizing ? ( + + ) : ( + + )} + + {reorganizing ? t('settings.email_behavior.archive_mode.reorganizing', "Reorganizing…") : t('settings.email_behavior.archive_mode.reorganize', "Reorganize existing archive")} + + + {reorganizeResult && ( + {reorganizeResult} )} - - {reorganizing ? t('settings.email_behavior.archive_mode.reorganizing', "Reorganizing…") : t('settings.email_behavior.archive_mode.reorganize', "Reorganize existing archive")} - - - {reorganizeResult && ( - {reorganizeResult} - )} - - )} - - + + )} + + + )} update('permanentlyDeleteJunk', v)} /> diff --git a/src/components/settings/__tests__/org-policy-settings.test.ts b/src/components/settings/__tests__/org-policy-settings.test.ts index 73fce48f..97665eb9 100644 --- a/src/components/settings/__tests__/org-policy-settings.test.ts +++ b/src/components/settings/__tests__/org-policy-settings.test.ts @@ -49,4 +49,18 @@ describe('organisation policy on the settings pages', () => { expect(page).toContain("const exportAllowed = usePolicyFeature('settingsExportEnabled');"); expect(page).toMatch(/\{exportAllowed && \(\s*\s*\)\}/); }); + + it('takes Archive out of the swipe and quick actions when the organisation turns it off', () => { + const layout = settings('LayoutSettings'); + expect(layout).toContain("const archiveEnabled = usePolicyFeature('archiveActionEnabled');"); + expect(layout).toContain('withoutArchive(swipeOptions(t), archiveEnabled)'); + expect(layout).toContain('value={swipeActionShown(swipeRightAction, archiveEnabled)}'); + expect(layout).toContain('value={swipeActionShown(swipeLeftAction, archiveEnabled)}'); + expect(layout).toMatch(/withoutArchive\(ALL_QUICK_ACTIONS\.map\([\s\S]*?\), archiveEnabled\)/); + expect(layout).toContain('const bottomActions = bottomQuickActionsFor(bottomQuickActionsRaw, archiveEnabled);'); + + const reading = settings('ReadingSettings'); + expect(reading).toContain("const archiveEnabled = usePolicyFeature('archiveActionEnabled');"); + expect(reading).toMatch(/\{archiveEnabled && \(\s*\s* { + it('shows a stored Archive swipe as none while archiving is off', () => { + expect(swipeActionShown('archive', false)).toBe('none'); + expect(swipeActionShown('archive', true)).toBe('archive'); + expect(swipeActionShown('delete', false)).toBe('delete'); + }); + + it('drops Archive from the pickers while archiving is off', () => { + const options = [{ value: 'none' }, { value: 'archive' }, { value: 'delete' }]; + expect(withoutArchive(options, false).map((o) => o.value)).toEqual(['none', 'delete']); + expect(withoutArchive(options, true).map((o) => o.value)).toEqual(['none', 'archive', 'delete']); + }); + + it('fills a stored Archive quick action with a reply action, keeping three', () => { + expect(bottomQuickActionsFor(['archive', 'delete', 'reply'], false)).toEqual(['delete', 'reply', 'replyAll']); + expect(bottomQuickActionsFor(['reply', 'archive', 'replyAll'], false)).toEqual(['reply', 'replyAll', 'forward']); + expect(bottomQuickActionsFor(['archive', 'delete', 'reply'], true)).toEqual(['archive', 'delete', 'reply']); + }); +}); diff --git a/src/lib/swipe-actions.ts b/src/lib/swipe-actions.ts new file mode 100644 index 00000000..bf688533 --- /dev/null +++ b/src/lib/swipe-actions.ts @@ -0,0 +1,24 @@ +import { normalizeBottomQuickActions, type QuickAction, type SwipeAction } from '../stores/settings-store'; + +// The organisation's archiveActionEnabled gate (webmail +// message-list-settings.tsx): with it off, Archive is no swipe action, no +// row action and no quick action, and a stored one does nothing. + +/** What a swipe does: a stored Archive swipe does nothing while archiving is off. */ +export function swipeActionShown(action: SwipeAction, archiveEnabled: boolean): SwipeAction { + return !archiveEnabled && action === 'archive' ? 'none' : action; +} + +/** The actions a picker offers: without Archive while archiving is off. */ +export function withoutArchive(options: readonly T[], archiveEnabled: boolean): T[] { + return archiveEnabled ? [...options] : options.filter((option) => option.value !== 'archive'); +} + +/** + * The reader's three quick actions: a stored Archive slot gives way to a + * reply action while archiving is off, so the bar keeps three buttons. + */ +export function bottomQuickActionsFor(value: unknown, archiveEnabled: boolean): QuickAction[] { + const actions = normalizeBottomQuickActions(value); + return archiveEnabled ? actions : normalizeBottomQuickActions(actions.filter((action) => action !== 'archive')); +}