mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
feat: hide the themes the organisation turns off and keep a locked theme in place
This commit is contained in:
@@ -7,6 +7,8 @@ import { resolvePalette, useColors, useResolvedTheme } from '../../theme/colors'
|
||||
import { BUILTIN_THEMES } from '../../theme/builtin-themes';
|
||||
import { useSettingsStore } from '../../stores/settings-store';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { usePolicyStore, useSettingLocked } from '../../stores/policy-store';
|
||||
import { visibleThemeCards } from './theme-cards';
|
||||
|
||||
interface ThemeCard {
|
||||
id: string | null;
|
||||
@@ -28,19 +30,23 @@ export function ThemesSettings() {
|
||||
const hydrate = useSettingsStore((s) => s.hydrate);
|
||||
const active = useSettingsStore((s) => s.activeThemeId);
|
||||
const update = useSettingsStore((s) => s.updateSetting);
|
||||
// The organisation can turn built-in themes off, and pin one (or the
|
||||
// default, with themes turned off) for everyone (webmail themes-settings.tsx).
|
||||
const policy = usePolicyStore((s) => s.effective);
|
||||
const themeLocked = useSettingLocked('activeThemeId');
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated) void hydrate();
|
||||
}, [hydrated, hydrate]);
|
||||
|
||||
const cards: ThemeCard[] = [
|
||||
const cards = React.useMemo<ThemeCard[]>(() => visibleThemeCards([
|
||||
{
|
||||
id: null,
|
||||
name: t('settings.themes.default_name', 'Bulwark'),
|
||||
description: t('settings.themes.default_description', 'The default light and dark palettes.'),
|
||||
},
|
||||
...BUILTIN_THEMES.map((theme) => ({ id: theme.id, name: theme.name, description: theme.description })),
|
||||
];
|
||||
], policy), [t, policy]);
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
@@ -50,6 +56,11 @@ export function ThemesSettings() {
|
||||
'Pick a colour theme. Themes follow the light/dark setting from Appearance; custom theme packages are managed in the webmail.',
|
||||
)}
|
||||
>
|
||||
{themeLocked && (
|
||||
<View style={styles.lockedNote}>
|
||||
<Text style={styles.lockedNoteText}>{t('settings.themes.locked', 'Theme selection is locked by an administrator.')}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.grid}>
|
||||
{cards.map((card) => {
|
||||
const isActive = (active ?? null) === card.id;
|
||||
@@ -58,10 +69,11 @@ export function ThemesSettings() {
|
||||
<Pressable
|
||||
key={card.id ?? 'default'}
|
||||
onPress={() => update('activeThemeId', card.id)}
|
||||
disabled={themeLocked}
|
||||
accessibilityRole="radio"
|
||||
accessibilityState={{ selected: isActive, checked: isActive }}
|
||||
accessibilityState={{ selected: isActive, checked: isActive, disabled: themeLocked }}
|
||||
accessibilityLabel={card.name}
|
||||
style={[styles.card, isActive && styles.cardActive]}
|
||||
style={[styles.card, isActive && styles.cardActive, themeLocked && !isActive && styles.cardDisabled]}
|
||||
>
|
||||
<View style={[styles.preview, { backgroundColor: preview.background, borderColor: preview.border }]}>
|
||||
<View style={[styles.previewRail, { backgroundColor: preview.surface, borderRightColor: preview.border }]} />
|
||||
@@ -112,6 +124,17 @@ function makeStyles(c: ThemePalette) {
|
||||
borderColor: c.primary,
|
||||
backgroundColor: c.primaryBg,
|
||||
},
|
||||
cardDisabled: { opacity: 0.6 },
|
||||
lockedNote: {
|
||||
marginBottom: spacing.md,
|
||||
paddingHorizontal: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: c.warning,
|
||||
backgroundColor: c.warningBg,
|
||||
},
|
||||
lockedNoteText: { ...typography.caption, color: c.text },
|
||||
preview: {
|
||||
width: '100%',
|
||||
aspectRatio: 16 / 10,
|
||||
|
||||
@@ -16,4 +16,14 @@ describe('organisation policy on the settings pages', () => {
|
||||
expect(page).toContain("const themeLocked = useSettingLocked('theme');");
|
||||
expect(page).toMatch(/label=\{t\('settings\.appearance\.theme\.label', "Theme"\)\}[^>]*locked=\{themeLocked\}>/);
|
||||
});
|
||||
|
||||
it('hides the themes the policy turns off and keeps the grid still while the theme is locked', () => {
|
||||
const page = settings('ThemesSettings');
|
||||
expect(page).toContain('const policy = usePolicyStore((s) => s.effective);');
|
||||
expect(page).toMatch(/visibleThemeCards\(\[[\s\S]*\], policy\), \[t, policy\]\)/);
|
||||
expect(page).toContain('{cards.map((card) => {');
|
||||
expect(page).toContain("const themeLocked = useSettingLocked('activeThemeId');");
|
||||
expect(page).toContain('disabled={themeLocked}');
|
||||
expect(page).toContain("{themeLocked && (");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { visibleThemeCards } from '../theme-cards';
|
||||
import { sanitizePolicy } from '../../../lib/org-policy';
|
||||
|
||||
const CARDS = [
|
||||
{ id: null, name: 'Bulwark' },
|
||||
{ id: 'nord', name: 'Nord' },
|
||||
{ id: 'solarized', name: 'Solarized' },
|
||||
];
|
||||
|
||||
describe('visibleThemeCards', () => {
|
||||
it('offers every card without a policy', () => {
|
||||
expect(visibleThemeCards(CARDS, null).map((c) => c.id)).toEqual([null, 'nord', 'solarized']);
|
||||
});
|
||||
|
||||
it('leaves out the built-in themes the policy turns off', () => {
|
||||
const policy = sanitizePolicy({ themePolicy: { disabledBuiltinThemes: ['nord'] } });
|
||||
expect(visibleThemeCards(CARDS, policy).map((c) => c.id)).toEqual([null, 'solarized']);
|
||||
});
|
||||
|
||||
it('keeps the default card whatever the policy lists', () => {
|
||||
const policy = sanitizePolicy({ themePolicy: { disabledBuiltinThemes: ['nord', 'solarized', 'null', ''] } });
|
||||
expect(visibleThemeCards(CARDS, policy).map((c) => c.id)).toEqual([null]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { OrgPolicy } from '../../lib/org-policy';
|
||||
|
||||
/**
|
||||
* The theme cards the Themes page offers: every card but the built-in themes
|
||||
* the organisation's policy turns off (webmail themes-settings.tsx filters
|
||||
* them with isThemeDisabled). The default card (id null) always stays, so
|
||||
* there is something to pick.
|
||||
*/
|
||||
export function visibleThemeCards<T extends { id: string | null }>(
|
||||
cards: readonly T[],
|
||||
policy: OrgPolicy | null | undefined,
|
||||
): T[] {
|
||||
const disabled = policy?.themePolicy.disabledBuiltinThemes ?? [];
|
||||
if (disabled.length === 0) return [...cards];
|
||||
return cards.filter((card) => card.id === null || !disabled.includes(card.id));
|
||||
}
|
||||
Reference in New Issue
Block a user