fix: keep accessibility presets off settings the organisation locks or limits

This commit is contained in:
Linus Rath
2026-10-31 16:13:07 +01:00
parent bc47c6a1af
commit c66e6209ed
3 changed files with 65 additions and 5 deletions
@@ -5,7 +5,7 @@ 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 { isLockedSetting, 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';
@@ -104,7 +104,15 @@ export function AccessibilitySettings() {
const applyPreset = (id: AccessibilityPresetId) => {
const name = presetName(id);
const changes = presetChanges(id, useSettingsStore.getState());
// Settings the organisation pins or bounds stay as they are (webmail
// accessibility-settings.tsx isSettingLocked, allowsValue).
const rules = usePolicyStore.getState().rules;
const changes = presetChanges(
id,
useSettingsStore.getState(),
(key) => isLockedSetting(rules, key),
(key, value) => policyAllowsValue(key, value, rules),
);
if (changes.length === 0) {
toast.info(t('settings.accessibility.presets.nothing_changed', '{name}: your settings already match.', { name }));
return;
@@ -0,0 +1,43 @@
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { presetChanges } from '../accessibility-presets';
import { EMPTY_SETTINGS_RULES, isLockedSetting, policyAllowsValue, type SettingsRules } from '../org-policy';
// Presets leave alone what the organisation pins or bounds (webmail
// lib/accessibility-presets.ts presetChanges with isSettingLocked and
// allowsValue). The app's presetChanges changed locked settings too.
const read = (rel: string) => fs.readFileSync(path.resolve(__dirname, '..', '..', rel), 'utf8');
describe('accessibility presets and the organisation policy', () => {
const state = { fontSize: 'medium' as const, showToolbarLabels: false };
it('skips a setting the organisation locked', () => {
expect(presetChanges('low-vision', state, (key) => key === 'fontSize'))
.toEqual([{ key: 'showToolbarLabels', from: false, to: true }]);
});
it('skips a value the organisation does not allow', () => {
const rules: SettingsRules = {
...EMPTY_SETTINGS_RULES,
restrictions: { fontSize: { allowedValues: ['medium', 'large'] } },
};
const changes = presetChanges(
'low-vision',
state,
(key) => isLockedSetting(rules, key),
(key, value) => policyAllowsValue(key, value, rules),
);
expect(changes).toEqual([{ key: 'showToolbarLabels', from: false, to: true }]);
});
it('changes everything without a policy', () => {
expect(presetChanges('low-vision', state).map((c) => c.key)).toEqual(['fontSize', 'showToolbarLabels']);
});
it('passes the locks and bounds from Settings > Accessibility', () => {
const src = read('components/settings/AccessibilitySettings.tsx');
expect(src).toMatch(/presetChanges\(\s+id,\s+useSettingsStore\.getState\(\),\s+\(key\) => isLockedSetting\(rules, key\),\s+\(key, value\) => policyAllowsValue\(key, value, rules\),\s+\)/);
});
});
+12 -3
View File
@@ -45,11 +45,20 @@ export interface PresetChange {
to: unknown;
}
/** The changes a preset would make to `state`: only values that differ. */
export function presetChanges(id: AccessibilityPresetId, state: Partial<PersistedSettings>): PresetChange[] {
/**
* The changes a preset would make to `state`: only values that differ, and
* none the organisation has locked (`isLocked`) or bounded (`allows`, such
* as a minimum toast duration).
*/
export function presetChanges(
id: AccessibilityPresetId,
state: Partial<PersistedSettings>,
isLocked: (key: string) => boolean = () => false,
allows: (key: string, value: unknown) => boolean = () => true,
): PresetChange[] {
const changes: PresetChange[] = [];
for (const [key, to] of Object.entries(ACCESSIBILITY_PRESETS[id]) as Array<[keyof PersistedSettings, unknown]>) {
if (!(key in state) || state[key] === to) continue;
if (!(key in state) || state[key] === to || isLocked(key) || !allows(key, to)) continue;
changes.push({ key, from: state[key], to });
}
return changes;