feat: manage tenant appearance defaults

This commit is contained in:
2026-08-20 07:03:28 +02:00
parent 56f0661a10
commit c5119ab868
8 changed files with 162 additions and 18 deletions
+62 -9
View File
@@ -18,6 +18,7 @@ from govoplan_core.core.access import (
TenantContextSwitcher, TenantContextSwitcher,
) )
from govoplan_core.core.change_sequence import ChangeSequenceEntry, decode_sequence_watermark, encode_sequence_watermark, record_change, sequence_watermark_is_expired from govoplan_core.core.change_sequence import ChangeSequenceEntry, decode_sequence_watermark, encode_sequence_watermark, record_change, sequence_watermark_is_expired
from govoplan_core.core.appearance import APPEARANCE_SETTINGS_KEY, appearance_settings, resolve_effective_appearance, update_appearance_settings
from govoplan_core.core.module_entitlements import MODULE_ENTITLEMENTS_KEY from govoplan_core.core.module_entitlements import MODULE_ENTITLEMENTS_KEY
from govoplan_core.core.navigation import ( from govoplan_core.core.navigation import (
navigation_preferences_from_settings, navigation_preferences_from_settings,
@@ -80,7 +81,7 @@ TENANT_SETTINGS_COLLECTION = "tenancy.tenant_settings"
TENANT_SETTINGS_RESOURCE = "tenant_settings_section" TENANT_SETTINGS_RESOURCE = "tenant_settings_section"
ADMIN_MODULE_ID = "admin" ADMIN_MODULE_ID = "admin"
ADMIN_SYSTEM_SETTINGS_COLLECTION = "admin.system_settings" ADMIN_SYSTEM_SETTINGS_COLLECTION = "admin.system_settings"
TENANT_SETTINGS_SECTIONS = ("identity", "locale", "languages", "navigation", "settings") TENANT_SETTINGS_SECTIONS = ("identity", "locale", "languages", "navigation", "appearance", "settings")
TENANT_NON_STATUS_UPDATE_FIELDS = { TENANT_NON_STATUS_UPDATE_FIELDS = {
"name", "name",
"description", "description",
@@ -256,6 +257,13 @@ def _tenant_settings_item(session: Session, tenant: Tenant) -> TenantSettingsIte
system_enabled = system_enabled_language_codes(system_settings.settings, default_locale=system_settings.default_locale) system_enabled = system_enabled_language_codes(system_settings.settings, default_locale=system_settings.default_locale)
enabled = tenant_enabled_language_codes(tenant.settings, system_enabled, default_locale=tenant.default_locale) enabled = tenant_enabled_language_codes(tenant.settings, system_enabled, default_locale=tenant.default_locale)
navigation = navigation_preferences_from_settings(tenant.settings) navigation = navigation_preferences_from_settings(tenant.settings)
system_palette, system_locked = appearance_settings(system_settings.settings)
tenant_palette, tenant_locked = appearance_settings(tenant.settings)
effective_appearance = resolve_effective_appearance(
system_settings=system_settings.settings,
tenant_settings=tenant.settings,
user_settings={},
)
return TenantSettingsItem( return TenantSettingsItem(
id=tenant.id, id=tenant.id,
slug=tenant.slug, slug=tenant.slug,
@@ -265,6 +273,12 @@ def _tenant_settings_item(session: Session, tenant: Tenant) -> TenantSettingsIte
system_enabled_language_codes=system_enabled, system_enabled_language_codes=system_enabled,
enabled_language_codes=enabled, enabled_language_codes=enabled,
navigation=navigation.as_dict() if navigation is not None else None, navigation=navigation.as_dict() if navigation is not None else None,
appearance_palette=tenant_palette,
appearance_palette_locked=tenant_locked,
system_appearance_palette=system_palette or "default",
system_appearance_palette_locked=system_locked,
effective_appearance_palette=effective_appearance.palette,
effective_appearance_source=effective_appearance.source,
settings=tenant.settings or {}, settings=tenant.settings or {},
) )
@@ -280,6 +294,14 @@ def _tenant_settings_sections(item: TenantSettingsItem) -> dict[str, Any]:
"enabled_language_codes": payload["enabled_language_codes"], "enabled_language_codes": payload["enabled_language_codes"],
}, },
"navigation": payload["navigation"], "navigation": payload["navigation"],
"appearance": {
"appearance_palette": payload["appearance_palette"],
"appearance_palette_locked": payload["appearance_palette_locked"],
"system_appearance_palette": payload["system_appearance_palette"],
"system_appearance_palette_locked": payload["system_appearance_palette_locked"],
"effective_appearance_palette": payload["effective_appearance_palette"],
"effective_appearance_source": payload["effective_appearance_source"],
},
"settings": payload["settings"], "settings": payload["settings"],
} }
@@ -628,7 +650,7 @@ def create_tenant(
name=payload.name.strip(), name=payload.name.strip(),
description=payload.description.strip() if payload.description else None, description=payload.description.strip() if payload.description else None,
default_locale=payload.default_locale.strip() or system_defaults.default_locale, default_locale=payload.default_locale.strip() or system_defaults.default_locale,
settings=payload.settings, settings={key: value for key, value in payload.settings.items() if key != APPEARANCE_SETTINGS_KEY},
allow_custom_groups=payload.allow_custom_groups, allow_custom_groups=payload.allow_custom_groups,
allow_custom_roles=payload.allow_custom_roles, allow_custom_roles=payload.allow_custom_roles,
allow_api_keys=payload.allow_api_keys, allow_api_keys=payload.allow_api_keys,
@@ -678,11 +700,10 @@ def _apply_tenant_content_updates(tenant: Tenant, payload: TenantUpdateRequest)
if payload.settings is not None: if payload.settings is not None:
current_settings = dict(tenant.settings or {}) current_settings = dict(tenant.settings or {})
next_settings = dict(payload.settings) next_settings = dict(payload.settings)
next_settings.pop(MODULE_ENTITLEMENTS_KEY, None) for reserved_key in (MODULE_ENTITLEMENTS_KEY, APPEARANCE_SETTINGS_KEY):
if MODULE_ENTITLEMENTS_KEY in current_settings: next_settings.pop(reserved_key, None)
next_settings[MODULE_ENTITLEMENTS_KEY] = current_settings[ if reserved_key in current_settings:
MODULE_ENTITLEMENTS_KEY next_settings[reserved_key] = current_settings[reserved_key]
]
tenant.settings = next_settings tenant.settings = next_settings
@@ -945,8 +966,8 @@ def get_tenant_settings_delta(
return _full_tenant_settings_delta_response(session, tenant) return _full_tenant_settings_delta_response(session, tenant)
changed = set() changed = set()
for entry in entries: for entry in entries:
if entry.module_id == ADMIN_MODULE_ID and entry.collection == ADMIN_SYSTEM_SETTINGS_COLLECTION and entry.resource_id == "languages": if entry.module_id == ADMIN_MODULE_ID and entry.collection == ADMIN_SYSTEM_SETTINGS_COLLECTION and entry.resource_id in {"languages", "appearance"}:
changed.add("languages") changed.add(entry.resource_id)
elif entry.resource_type == TENANT_SETTINGS_RESOURCE: elif entry.resource_type == TENANT_SETTINGS_RESOURCE:
changed.add(entry.resource_id) changed.add(entry.resource_id)
changed_sections = [section for section in TENANT_SETTINGS_SECTIONS if section in changed] changed_sections = [section for section in TENANT_SETTINGS_SECTIONS if section in changed]
@@ -974,6 +995,28 @@ def update_tenant_settings(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found")
before_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant)) before_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant))
system_settings = get_system_settings(session) system_settings = get_system_settings(session)
tenant_palette, tenant_locked = appearance_settings(tenant.settings)
system_palette, system_locked = appearance_settings(system_settings.settings)
appearance_palette_changed = (
"appearance_palette" in payload.model_fields_set
and payload.appearance_palette != tenant_palette
)
appearance_lock_changed = (
"appearance_palette_locked" in payload.model_fields_set
and payload.appearance_palette_locked != tenant_locked
)
if system_locked and (appearance_palette_changed or appearance_lock_changed):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail=f"The system appearance policy locks palette {system_palette or 'default'}.",
)
if (
appearance_lock_changed or (tenant_locked and appearance_palette_changed)
) and not has_scope(principal, "admin:policies:write"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
detail="Changing the tenant appearance lock or its locked value requires admin:policies:write.",
)
system_enabled = system_enabled_language_codes(system_settings.settings, default_locale=system_settings.default_locale) system_enabled = system_enabled_language_codes(system_settings.settings, default_locale=system_settings.default_locale)
current_i18n = i18n_settings(tenant.settings) current_i18n = i18n_settings(tenant.settings)
raw_enabled = payload.enabled_language_codes if "enabled_language_codes" in payload.model_fields_set else current_i18n.get("enabled_language_codes") raw_enabled = payload.enabled_language_codes if "enabled_language_codes" in payload.model_fields_set else current_i18n.get("enabled_language_codes")
@@ -985,6 +1028,13 @@ def update_tenant_settings(
) )
tenant.default_locale = payload.default_locale.strip() or enabled[0] tenant.default_locale = payload.default_locale.strip() or enabled[0]
tenant.settings = update_i18n_settings(tenant.settings, enabled_language_codes=enabled) tenant.settings = update_i18n_settings(tenant.settings, enabled_language_codes=enabled)
if {"appearance_palette", "appearance_palette_locked"}.intersection(payload.model_fields_set):
current_palette, current_locked = appearance_settings(tenant.settings)
tenant.settings = update_appearance_settings(
tenant.settings,
default_palette=payload.appearance_palette if "appearance_palette" in payload.model_fields_set else current_palette,
palette_locked=payload.appearance_palette_locked if payload.appearance_palette_locked is not None else current_locked,
)
if "navigation" in payload.model_fields_set: if "navigation" in payload.model_fields_set:
tenant.settings = update_navigation_preferences( tenant.settings = update_navigation_preferences(
tenant.settings, tenant.settings,
@@ -1002,6 +1052,9 @@ def update_tenant_settings(
"default_locale": tenant.default_locale, "default_locale": tenant.default_locale,
"enabled_language_codes": enabled, "enabled_language_codes": enabled,
"navigation_updated": "navigation" in payload.model_fields_set, "navigation_updated": "navigation" in payload.model_fields_set,
"appearance_updated": bool({"appearance_palette", "appearance_palette_locked"}.intersection(payload.model_fields_set)),
"appearance_palette": appearance_settings(tenant.settings)[0],
"appearance_palette_locked": appearance_settings(tenant.settings)[1],
}, },
) )
after_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant)) after_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant))
@@ -138,6 +138,12 @@ class TenantSettingsItem(BaseModel):
system_enabled_language_codes: list[str] = Field(default_factory=list) system_enabled_language_codes: list[str] = Field(default_factory=list)
enabled_language_codes: list[str] = Field(default_factory=list) enabled_language_codes: list[str] = Field(default_factory=list)
navigation: NavigationPreferencesPayload | None = None navigation: NavigationPreferencesPayload | None = None
appearance_palette: Literal["default", "civic_blue", "forest", "plum"] | None = None
appearance_palette_locked: bool = False
system_appearance_palette: Literal["default", "civic_blue", "forest", "plum"] = "default"
system_appearance_palette_locked: bool = False
effective_appearance_palette: Literal["default", "civic_blue", "forest", "plum"] = "default"
effective_appearance_source: Literal["tenant", "system", "tenant_lock", "system_lock"] = "system"
settings: dict[str, Any] = Field(default_factory=dict) settings: dict[str, Any] = Field(default_factory=dict)
@@ -157,3 +163,5 @@ class TenantSettingsUpdateRequest(BaseModel):
default_locale: str = Field(min_length=1, max_length=20) default_locale: str = Field(min_length=1, max_length=20)
enabled_language_codes: list[str] | None = None enabled_language_codes: list[str] | None = None
navigation: NavigationPreferencesPayload | None = None navigation: NavigationPreferencesPayload | None = None
appearance_palette: Literal["default", "civic_blue", "forest", "plum"] | None = None
appearance_palette_locked: bool | None = None
+2 -2
View File
@@ -60,7 +60,7 @@ manifest = ModuleManifest(
id="tenancy.lifecycle-and-settings", id="tenancy.lifecycle-and-settings",
title="Administer tenant lifecycle and settings", title="Administer tenant lifecycle and settings",
summary="Tenancy adds explicit tenant creation, activation, context resolution, and tenant-owned settings over Core's shared scope storage.", summary="Tenancy adds explicit tenant creation, activation, context resolution, and tenant-owned settings over Core's shared scope storage.",
body="A tenant is a concrete administrative and data boundary. Tenant lifecycle changes must preserve ownership and recovery guarantees for module-owned records. New tenants default to the German reference language unless the administrator selects another enabled system language; existing tenant and user preferences remain unchanged. Tenant administrators can inherit or override the system side-rail order and visibility and can lock entries visible for users; system locks remain effective. Personal navigation preferences still take precedence except that they cannot hide locked entries. Navigation changes never grant module entitlement, View visibility, or permissions. Tenancy contributes system tenant management and tenant settings to the shared administration workspace; without this module, the Core and Access baseline can operate in single-scope compatibility mode. Core-reserved module entitlement settings are managed only through the Admin module's tenant-module policy endpoints and are preserved when generic tenant settings are replaced.", body="A tenant is a concrete administrative and data boundary. Tenant lifecycle changes must preserve ownership and recovery guarantees for module-owned records. New tenants default to the German reference language unless the administrator selects another enabled system language; existing tenant and user preferences remain unchanged. Tenant administrators can inherit or override the system side-rail order and visibility and can lock entries visible for users; system locks remain effective. Personal navigation preferences still take precedence except that they cannot hide locked entries. Tenant appearance likewise inherits the system palette until explicitly selected; an unlocked tenant default permits a personal palette, while a policy-authorized tenant lock suppresses it and a system lock always wins. Resetting the tenant palette restores inheritance rather than copying the current system value. Navigation and appearance changes never grant module entitlement, View visibility, or permissions. Tenancy contributes system tenant management and tenant settings to the shared administration workspace; without this module, the Core and Access baseline can operate in single-scope compatibility mode. Core-reserved module entitlement settings are managed only through the Admin module's tenant-module policy endpoints and are preserved when generic tenant settings are replaced.",
documentation_types=("admin",), documentation_types=("admin",),
audience=("system_admin", "tenant_admin", "operator"), audience=("system_admin", "tenant_admin", "operator"),
related_modules=("access", "admin", "audit"), related_modules=("access", "admin", "audit"),
@@ -83,7 +83,7 @@ manifest = ModuleManifest(
id="tenancy.reference.admin-fields", id="tenancy.reference.admin-fields",
title="Tenant administration fields and consequences", title="Tenant administration fields and consequences",
summary="Tenant identity, ownership, locale, governance overrides, and lifecycle state have different mutation and recovery consequences.", summary="Tenant identity, ownership, locale, governance overrides, and lifecycle state have different mutation and recovery consequences.",
body="A tenant slug is immutable after creation and identifies the administrative boundary. The initial owner receives the protected tenant-owner role. German is the reference and new-tenant default; locale and enabled languages are bounded by system language packages and may be changed explicitly. Tenant navigation inherits the system layer until explicitly saved; tenant locks keep entries visible for users but cannot relax a system lock. Governance overrides may narrow a system allowance but cannot loosen a system denial. Suspension keeps tenant-owned data and audit evidence while preventing normal use; an operator must switch away from the active tenant before suspending it.", body="A tenant slug is immutable after creation and identifies the administrative boundary. The initial owner receives the protected tenant-owner role. German is the reference and new-tenant default; locale and enabled languages are bounded by system language packages and may be changed explicitly. Tenant navigation and appearance inherit their system layers until explicitly saved. Palette choices use validated Core presets only. A tenant appearance lock requires policy-write authority, suppresses personal palette choices, and cannot relax a system lock. Governance overrides may narrow a system allowance but cannot loosen a system denial. Suspension keeps tenant-owned data and audit evidence while preventing normal use; an operator must switch away from the active tenant before suspending it.",
documentation_types=("admin",), documentation_types=("admin",),
audience=("system_admin", "tenant_admin", "operator"), audience=("system_admin", "tenant_admin", "operator"),
related_modules=("access", "admin", "audit"), related_modules=("access", "admin", "audit"),
+6 -2
View File
@@ -17,6 +17,7 @@ from govoplan_tenancy.backend.api.v1.schemas import (
TenantUpdateRequest, TenantUpdateRequest,
) )
from govoplan_core.core.module_entitlements import MODULE_ENTITLEMENTS_KEY from govoplan_core.core.module_entitlements import MODULE_ENTITLEMENTS_KEY
from govoplan_core.core.appearance import APPEARANCE_SETTINGS_KEY
from govoplan_tenancy.backend.lifecycle import ( from govoplan_tenancy.backend.lifecycle import (
TENANT_EVENT_CREATED, TENANT_EVENT_CREATED,
TENANT_EVENT_DELETION_REQUESTED, TENANT_EVENT_DELETION_REQUESTED,
@@ -135,18 +136,20 @@ class TenantUpdateHelperTests(unittest.TestCase):
self.assertEqual("de", tenant.default_locale) self.assertEqual("de", tenant.default_locale)
self.assertEqual({"theme": "contrast"}, tenant.settings) self.assertEqual({"theme": "contrast"}, tenant.settings)
def test_tenant_content_update_preserves_reserved_module_entitlements(self) -> None: def test_tenant_content_update_preserves_reserved_governed_settings(self) -> None:
entitlement = {"schema_version": 1, "revision": 4} entitlement = {"schema_version": 1, "revision": 4}
appearance = {"default_palette": "forest", "palette_locked": True}
tenant = SimpleNamespace( tenant = SimpleNamespace(
name="Old", name="Old",
description=None, description=None,
default_locale="en", default_locale="en",
settings={MODULE_ENTITLEMENTS_KEY: entitlement, "theme": "old"}, settings={MODULE_ENTITLEMENTS_KEY: entitlement, APPEARANCE_SETTINGS_KEY: appearance, "theme": "old"},
) )
payload = TenantUpdateRequest( payload = TenantUpdateRequest(
settings={ settings={
"theme": "contrast", "theme": "contrast",
MODULE_ENTITLEMENTS_KEY: {"revision": 999}, MODULE_ENTITLEMENTS_KEY: {"revision": 999},
APPEARANCE_SETTINGS_KEY: {"default_palette": "plum", "palette_locked": False},
} }
) )
@@ -154,6 +157,7 @@ class TenantUpdateHelperTests(unittest.TestCase):
self.assertEqual("contrast", tenant.settings["theme"]) self.assertEqual("contrast", tenant.settings["theme"])
self.assertEqual(entitlement, tenant.settings[MODULE_ENTITLEMENTS_KEY]) self.assertEqual(entitlement, tenant.settings[MODULE_ENTITLEMENTS_KEY])
self.assertEqual(appearance, tenant.settings[APPEARANCE_SETTINGS_KEY])
def test_tenant_status_update_prevents_suspending_current_tenant(self) -> None: def test_tenant_status_update_prevents_suspending_current_tenant(self) -> None:
tenant = SimpleNamespace(id="tenant-1", is_active=True) tenant = SimpleNamespace(id="tenant-1", is_active=True)
+11 -1
View File
@@ -3,7 +3,8 @@ import type {
DeltaDeletedItem, DeltaDeletedItem,
NavigationPreferences, NavigationPreferences,
PrivacyRetentionPolicy, PrivacyRetentionPolicy,
TenantAdminItem TenantAdminItem,
UserUiPalette
} from "@govoplan/core-webui"; } from "@govoplan/core-webui";
import { import {
apiFetch, apiFetch,
@@ -43,6 +44,12 @@ export type TenantSettingsItem = {
system_enabled_language_codes: string[]; system_enabled_language_codes: string[];
enabled_language_codes: string[]; enabled_language_codes: string[];
navigation?: NavigationPreferences | null; navigation?: NavigationPreferences | null;
appearance_palette: UserUiPalette | null;
appearance_palette_locked: boolean;
system_appearance_palette: UserUiPalette;
system_appearance_palette_locked: boolean;
effective_appearance_palette: UserUiPalette;
effective_appearance_source: "tenant" | "system" | "tenant_lock" | "system_lock";
settings: Record<string, unknown>; settings: Record<string, unknown>;
}; };
@@ -54,6 +61,7 @@ export type TenantSettingsDeltaSections = Partial<{
"available_languages" | "system_enabled_language_codes" | "enabled_language_codes" "available_languages" | "system_enabled_language_codes" | "enabled_language_codes"
>; >;
navigation: TenantSettingsItem["navigation"]; navigation: TenantSettingsItem["navigation"];
appearance: Pick<TenantSettingsItem, "appearance_palette" | "appearance_palette_locked" | "system_appearance_palette" | "system_appearance_palette_locked" | "effective_appearance_palette" | "effective_appearance_source">;
settings: TenantSettingsItem["settings"]; settings: TenantSettingsItem["settings"];
}>; }>;
@@ -151,6 +159,8 @@ export function updateTenantSettings(
default_locale: string; default_locale: string;
enabled_language_codes?: string[] | null; enabled_language_codes?: string[] | null;
navigation?: NavigationPreferences | null; navigation?: NavigationPreferences | null;
appearance_palette?: UserUiPalette | null;
appearance_palette_locked?: boolean;
} }
): Promise<TenantSettingsItem> { ): Promise<TenantSettingsItem> {
return apiFetch(settings, "/api/v1/admin/tenant/settings", { return apiFetch(settings, "/api/v1/admin/tenant/settings", {
@@ -1,5 +1,7 @@
import { import {
DescriptionList, DescriptionList,
AppearancePalettePreview,
AppearancePaletteSelect,
NavigationPreferenceEditor, NavigationPreferenceEditor,
configurableNavigationItemsForModules, configurableNavigationItemsForModules,
dispatchPlatformModulesChanged, dispatchPlatformModulesChanged,
@@ -14,6 +16,7 @@ import {
Card, Card,
DocumentationHelpLink, DocumentationHelpLink,
FormField, FormField,
ToggleSwitch,
adminErrorMessage, adminErrorMessage,
useDeltaWatermarks, useDeltaWatermarks,
useUnsavedChanges, useUnsavedChanges,
@@ -42,18 +45,25 @@ const fallback: TenantSettingsItem = {
system_enabled_language_codes: ["de", "en"], system_enabled_language_codes: ["de", "en"],
enabled_language_codes: ["de", "en"], enabled_language_codes: ["de", "en"],
navigation: null, navigation: null,
settings: {} settings: {},
appearance_palette: null,
appearance_palette_locked: false,
system_appearance_palette: "default",
system_appearance_palette_locked: false,
effective_appearance_palette: "default",
effective_appearance_source: "system"
}; };
export default function TenantSettingsPanel({ export default function TenantSettingsPanel({
settings, settings,
canWrite, canWrite,
canWritePolicy,
onAuthRefresh onAuthRefresh
}: {settings: ApiSettings;canWrite: boolean;onAuthRefresh: () => Promise<void>;}) { }: {settings: ApiSettings;canWrite: boolean;canWritePolicy: boolean;onAuthRefresh: () => Promise<void>;}) {
const { requestDiscard } = useUnsavedChanges(); const { requestDiscard } = useUnsavedChanges();
const { modules } = usePlatformModules(); const { modules } = usePlatformModules();
const navigationItems = configurableNavigationItemsForModules(modules); const navigationItems = configurableNavigationItemsForModules(modules);
@@ -115,7 +125,9 @@ export default function TenantSettingsPanel({
const saved = await updateTenantSettings(settings, { const saved = await updateTenantSettings(settings, {
default_locale: draft.default_locale, default_locale: draft.default_locale,
enabled_language_codes: draft.enabled_language_codes, enabled_language_codes: draft.enabled_language_codes,
navigation: draft.navigation navigation: draft.navigation,
appearance_palette: draft.appearance_palette,
appearance_palette_locked: draft.appearance_palette_locked
}); });
setDraft(saved); setDraft(saved);
setSavedDraft(saved); setSavedDraft(saved);
@@ -197,6 +209,33 @@ export default function TenantSettingsPanel({
onChange={(navigation) => setDraft({ ...draft, navigation })} onChange={(navigation) => setDraft({ ...draft, navigation })}
/> />
</Card> </Card>
<Card title="i18n:govoplan-tenancy.appearance_defaults">
<FormField label="i18n:govoplan-tenancy.tenant_palette_default" help="i18n:govoplan-tenancy.tenant_palette_default_help">
<AppearancePaletteSelect
value={draft.appearance_palette}
onChange={(appearance_palette) => setDraft({
...draft,
appearance_palette,
effective_appearance_palette: appearance_palette ?? draft.system_appearance_palette,
effective_appearance_source: appearance_palette ? "tenant" : "system"
})}
allowInherit
disabled={!canWrite || busy || draft.system_appearance_palette_locked || (draft.appearance_palette_locked && !canWritePolicy)}
/>
</FormField>
<ToggleSwitch
checked={draft.appearance_palette_locked}
onChange={(appearance_palette_locked) => setDraft({ ...draft, appearance_palette_locked })}
disabled={!canWrite || !canWritePolicy || busy || draft.system_appearance_palette_locked}
help={!canWritePolicy ? "i18n:govoplan-tenancy.appearance_lock_policy_permission" : draft.system_appearance_palette_locked ? "i18n:govoplan-tenancy.system_palette_is_locked" : undefined}
label="i18n:govoplan-tenancy.lock_tenant_palette"
/>
<AppearancePalettePreview palette={draft.system_appearance_palette_locked ? draft.system_appearance_palette : draft.appearance_palette ?? draft.system_appearance_palette} />
<DescriptionList variant="inline">
<div><dt>i18n:govoplan-tenancy.effective_source</dt><dd>{draft.system_appearance_palette_locked ? "i18n:govoplan-tenancy.system_lock" : draft.appearance_palette ? "i18n:govoplan-tenancy.tenant_default" : "i18n:govoplan-tenancy.system_default"}</dd></div>
<div><dt>i18n:govoplan-tenancy.user_override</dt><dd>{draft.system_appearance_palette_locked || draft.appearance_palette_locked ? "i18n:govoplan-tenancy.blocked_by_policy" : "i18n:govoplan-tenancy.allowed"}</dd></div>
</DescriptionList>
</Card>
</div> </div>
</AdminPageLayout>); </AdminPageLayout>);
@@ -214,7 +253,9 @@ function tenantSettingsDraftKey(item: TenantSettingsItem): string {
return JSON.stringify({ return JSON.stringify({
default_locale: item.default_locale, default_locale: item.default_locale,
enabled_language_codes: item.enabled_language_codes, enabled_language_codes: item.enabled_language_codes,
navigation: item.navigation navigation: item.navigation,
appearance_palette: item.appearance_palette,
appearance_palette_locked: item.appearance_palette_locked
}); });
} }
@@ -225,6 +266,7 @@ function applyTenantSettingsSections(item: TenantSettingsItem, sections: TenantS
...(sections.locale ?? {}), ...(sections.locale ?? {}),
...(sections.languages ?? {}), ...(sections.languages ?? {}),
...(sections.navigation !== undefined ? { navigation: sections.navigation } : {}), ...(sections.navigation !== undefined ? { navigation: sections.navigation } : {}),
...(sections.appearance ?? {}),
...(sections.settings ? { settings: sections.settings } : {}) ...(sections.settings ? { settings: sections.settings } : {})
}; };
} }
+26
View File
@@ -2,6 +2,19 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
export const generatedTranslations: PlatformTranslations = { export const generatedTranslations: PlatformTranslations = {
"en": { "en": {
"i18n:govoplan-tenancy.appearance_defaults": "Appearance defaults",
"i18n:govoplan-tenancy.tenant_palette_default": "Tenant palette default",
"i18n:govoplan-tenancy.tenant_palette_default_help": "Inherit the system palette or select the default for this tenant.",
"i18n:govoplan-tenancy.appearance_lock_policy_permission": "Policy-write permission is required to change this lock.",
"i18n:govoplan-tenancy.system_palette_is_locked": "The system palette is locked and takes precedence.",
"i18n:govoplan-tenancy.lock_tenant_palette": "Lock the tenant palette",
"i18n:govoplan-tenancy.effective_source": "Effective source",
"i18n:govoplan-tenancy.system_lock": "System policy lock",
"i18n:govoplan-tenancy.tenant_default": "Tenant default",
"i18n:govoplan-tenancy.system_default": "System default",
"i18n:govoplan-tenancy.user_override": "Personal choice",
"i18n:govoplan-tenancy.blocked_by_policy": "Blocked by policy",
"i18n:govoplan-tenancy.allowed": "Allowed",
"i18n:govoplan-tenancy.tenant_information_is_loading.7c9a1001": "Tenant information is loading.", "i18n:govoplan-tenancy.tenant_information_is_loading.7c9a1001": "Tenant information is loading.",
"i18n:govoplan-tenancy.a_tenant_change_is_in_progress.7c9a1002": "A tenant change is in progress.", "i18n:govoplan-tenancy.a_tenant_change_is_in_progress.7c9a1002": "A tenant change is in progress.",
"i18n:govoplan-tenancy.tenant_creation_permission_is_required.7c9a1003": "Tenant creation permission is required.", "i18n:govoplan-tenancy.tenant_creation_permission_is_required.7c9a1003": "Tenant creation permission is required.",
@@ -101,6 +114,19 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-tenancy.value_value.c189e8bc": "{value0} ({value1})" "i18n:govoplan-tenancy.value_value.c189e8bc": "{value0} ({value1})"
}, },
"de": { "de": {
"i18n:govoplan-tenancy.appearance_defaults": "Darstellungsstandards",
"i18n:govoplan-tenancy.tenant_palette_default": "Mandantenstandard für die Farbpalette",
"i18n:govoplan-tenancy.tenant_palette_default_help": "Systempalette übernehmen oder einen Standard für diesen Mandanten auswählen.",
"i18n:govoplan-tenancy.appearance_lock_policy_permission": "Zum Ändern dieser Sperre ist das Recht zum Bearbeiten von Richtlinien erforderlich.",
"i18n:govoplan-tenancy.system_palette_is_locked": "Die Systempalette ist verbindlich und hat Vorrang.",
"i18n:govoplan-tenancy.lock_tenant_palette": "Mandantenpalette verbindlich festlegen",
"i18n:govoplan-tenancy.effective_source": "Wirksame Quelle",
"i18n:govoplan-tenancy.system_lock": "Systemrichtlinie",
"i18n:govoplan-tenancy.tenant_default": "Mandantenstandard",
"i18n:govoplan-tenancy.system_default": "Systemstandard",
"i18n:govoplan-tenancy.user_override": "Persönliche Auswahl",
"i18n:govoplan-tenancy.blocked_by_policy": "Durch Richtlinie gesperrt",
"i18n:govoplan-tenancy.allowed": "Zulässig",
"i18n:govoplan-tenancy.tenant_information_is_loading.7c9a1001": "Mandanteninformationen werden geladen.", "i18n:govoplan-tenancy.tenant_information_is_loading.7c9a1001": "Mandanteninformationen werden geladen.",
"i18n:govoplan-tenancy.a_tenant_change_is_in_progress.7c9a1002": "Eine Mandantenänderung wird gerade ausgeführt.", "i18n:govoplan-tenancy.a_tenant_change_is_in_progress.7c9a1002": "Eine Mandantenänderung wird gerade ausgeführt.",
"i18n:govoplan-tenancy.tenant_creation_permission_is_required.7c9a1003": "Die Berechtigung zum Erstellen von Mandanten ist erforderlich.", "i18n:govoplan-tenancy.tenant_creation_permission_is_required.7c9a1003": "Die Berechtigung zum Erstellen von Mandanten ist erforderlich.",
+1
View File
@@ -44,6 +44,7 @@ const adminSections: AdminSectionsUiCapability = {
createElement(TenantSettingsPanel, { createElement(TenantSettingsPanel, {
settings, settings,
canWrite: auth.scopes.includes("admin:settings:write"), canWrite: auth.scopes.includes("admin:settings:write"),
canWritePolicy: auth.scopes.includes("admin:policies:write"),
onAuthRefresh: refreshAuth onAuthRefresh: refreshAuth
}) })
} }