feat: leave out the settings pages the organisation turns off

This commit is contained in:
Linus Rath
2026-10-31 15:30:15 +01:00
parent 83981e8b8d
commit 1d654bbeaf
3 changed files with 104 additions and 1 deletions
@@ -0,0 +1,56 @@
import { describe, expect, it } from 'vitest';
import fs from 'node:fs';
import path from 'node:path';
import { policyHiddenPages } from '../policy-pages';
import { sanitizePolicy } from '../../../lib/org-policy';
const screen = fs.readFileSync(path.resolve(__dirname, '..', '..', '..', 'screens', 'SettingsScreen.tsx'), 'utf8');
describe('policyHiddenPages', () => {
it('leaves every page without a policy or with the default gates', () => {
expect(policyHiddenPages(null)).toEqual([]);
expect(policyHiddenPages(sanitizePolicy({}))).toEqual([]);
});
it('takes away the pages of the gates the policy turns off', () => {
const policy = sanitizePolicy({
features: {
templatesEnabled: false,
customKeywordsEnabled: false,
sidebarAppsEnabled: false,
themesEnabled: false,
calendarEnabled: false,
contactsEnabled: false,
filesEnabled: false,
},
});
expect(policyHiddenPages(policy).sort()).toEqual(
['calendar', 'contacts', 'files', 'keywords', 'sidebar_apps', 'templates', 'themes'],
);
});
it('takes away Delegates and Recover deleted items when the policy hides them', () => {
const policy = sanitizePolicy({
restrictions: { delegation: { hidden: true }, deletedItemsRecovery: { hidden: true } },
});
expect(policyHiddenPages(policy).sort()).toEqual(['recovery', 'sharing']);
const locked = sanitizePolicy({ restrictions: { delegation: { locked: true } } });
expect(policyHiddenPages(locked)).toEqual([]);
});
it('names pages the Settings list has', () => {
const policy = sanitizePolicy({
features: Object.fromEntries(['templatesEnabled', 'customKeywordsEnabled', 'sidebarAppsEnabled', 'themesEnabled', 'calendarEnabled', 'contactsEnabled', 'filesEnabled'].map((g) => [g, false])),
restrictions: { delegation: { hidden: true }, deletedItemsRecovery: { hidden: true } },
});
for (const page of policyHiddenPages(policy)) expect(screen).toContain(`{ id: '${page}',`);
});
});
describe('Settings list', () => {
it('leaves out the pages the active policy takes away', () => {
expect(screen).toContain('const policy = usePolicyStore((s) => s.effective);');
expect(screen).toContain('for (const page of policyHiddenPages(policy)) set.add(page as Tab);');
expect(screen).toContain('}, [recoveryAvailable, delegationAvailable, policy]);');
});
});
+42
View File
@@ -0,0 +1,42 @@
import { isFeatureEnabled, type FeatureGate, type OrgPolicy } from '../../lib/org-policy';
/**
* Settings pages a feature gate takes away, as the webmail's settings-app.tsx
* leaves their tabs out: Templates (templatesEnabled, in Composing there),
* Keywords (customKeywordsEnabled: Organize turns into Folders), Sidebar apps
* (sidebarAppsEnabled: the app has no plugins, which keep Add-ons there),
* Themes (themesEnabled, appearance-settings.tsx), and Calendar, Contacts and
* Files with their apps.
*/
const GATED_PAGES: ReadonlyArray<[FeatureGate, string]> = [
['templatesEnabled', 'templates'],
['customKeywordsEnabled', 'keywords'],
['sidebarAppsEnabled', 'sidebar_apps'],
['themesEnabled', 'themes'],
['calendarEnabled', 'calendar'],
['contactsEnabled', 'contacts'],
['filesEnabled', 'files'],
];
/**
* Pages a hidden restriction takes away: Delegates (`delegation`, webmail
* settings-app supportsSharing) and Recover deleted items
* (`deletedItemsRecovery`, webmail hooks/use-deleted-items-recovery.ts).
*/
const HIDDEN_PAGES: ReadonlyArray<[string, string]> = [
['delegation', 'sharing'],
['deletedItemsRecovery', 'recovery'],
];
/** The settings pages the active account's policy leaves out, by page id. */
export function policyHiddenPages(policy: OrgPolicy | null | undefined): string[] {
if (!policy) return [];
const out: string[] = [];
for (const [gate, page] of GATED_PAGES) {
if (!isFeatureEnabled(policy, gate)) out.push(page);
}
for (const [key, page] of HIDDEN_PAGES) {
if (policy.restrictions[key]?.hidden === true) out.push(page);
}
return out;
}
+6 -1
View File
@@ -54,6 +54,8 @@ import { useAuthStore } from '../stores/auth-store';
import { useManagedAccountStore } from '../stores/managed-account-store';
import { useAccountStore } from '../stores/account-store';
import { useSettingsStore } from '../stores/settings-store';
import { usePolicyStore } from '../stores/policy-store';
import { policyHiddenPages } from '../components/settings/policy-pages';
import { useUpdatesStore } from '../stores/updates-store';
import { SUPPORTED_LOCALES } from '../i18n';
import { settingsSummary, type SummaryState } from '../lib/settings-summary';
@@ -231,12 +233,15 @@ export default function SettingsScreen({ onLogout, onBack, onTabSelect }: Settin
// list, not shown as "Not on this server", where the server lacks them.
const recoveryAvailable = useDeletedItemsRecovery().available;
const delegationAvailable = useDelegationAvailable();
// Pages the organisation's policy takes away (feature gates, hidden pages).
const policy = usePolicyStore((s) => s.effective);
const hiddenTabs = React.useMemo(() => {
const set = new Set<Tab>();
if (!recoveryAvailable) set.add('recovery');
if (!delegationAvailable) set.add('sharing');
for (const page of policyHiddenPages(policy)) set.add(page as Tab);
return set;
}, [recoveryAvailable, delegationAvailable]);
}, [recoveryAvailable, delegationAvailable, policy]);
// A shared/group account picked under "Shared with me" scopes Settings to
// the panes that account can be managed in (webmail: scoped settings).