diff --git a/core/Service/Directory/DirectorySettingsGateway.php b/core/Service/Directory/DirectorySettingsGateway.php index f431939..09fc8b2 100644 --- a/core/Service/Directory/DirectorySettingsGateway.php +++ b/core/Service/Directory/DirectorySettingsGateway.php @@ -32,4 +32,9 @@ class DirectorySettingsGateway { return $this->settingsAppGateway->isAllowedTheme($theme); } + + public function normalizeTheme(?string $theme): string + { + return $this->settingsAppGateway->normalizeTheme($theme); + } } diff --git a/core/Service/Tenant/TenantService.php b/core/Service/Tenant/TenantService.php index 0b1f2ff..82eb8e3 100644 --- a/core/Service/Tenant/TenantService.php +++ b/core/Service/Tenant/TenantService.php @@ -68,12 +68,12 @@ class TenantService 'website' => $form['website'], 'privacy_url' => $form['privacy_url'], 'imprint_url' => $form['imprint_url'], - // null means "inherit from global settings" for color, theme, and theme-toggle policy. - 'primary_color' => $form['primary_color_use_default'] - ? null - : ($form['primary_color'] !== '' ? $form['primary_color'] : null), - 'default_theme' => $form['default_theme'] !== '' ? $form['default_theme'] : null, - 'allow_user_theme' => $form['allow_user_theme_mode'] === '' ? null : (int) $form['allow_user_theme_mode'], + // Appearance is tenant-scoped with explicit, required values. Every + // tenant has a primary color, a default theme, and an explicit + // allow-user-theme flag — there is no global fallback to inherit. + 'primary_color' => strtolower($form['primary_color']), + 'default_theme' => $this->settingsGateway->normalizeTheme($form['default_theme']), + 'allow_user_theme' => $form['allow_user_theme'] ? 1 : 0, 'status' => $form['status'], 'status_changed_at' => $statusChangedAt, 'status_changed_by' => $currentUserId > 0 ? $currentUserId : null, @@ -135,11 +135,9 @@ class TenantService 'website' => $form['website'], 'privacy_url' => $form['privacy_url'], 'imprint_url' => $form['imprint_url'], - 'primary_color' => $form['primary_color_use_default'] - ? null - : ($form['primary_color'] !== '' ? $form['primary_color'] : null), - 'default_theme' => $form['default_theme'] !== '' ? $form['default_theme'] : null, - 'allow_user_theme' => $form['allow_user_theme_mode'] === '' ? null : (int) $form['allow_user_theme_mode'], + 'primary_color' => strtolower($form['primary_color']), + 'default_theme' => $this->settingsGateway->normalizeTheme($form['default_theme']), + 'allow_user_theme' => $form['allow_user_theme'] ? 1 : 0, 'status' => $form['status'], 'modified_by' => $currentUserId > 0 ? $currentUserId : null, ]; @@ -223,9 +221,8 @@ class TenantService 'privacy_url' => trim((string) ($input['privacy_url'] ?? '')), 'imprint_url' => trim((string) ($input['imprint_url'] ?? '')), 'primary_color' => trim((string) ($input['primary_color'] ?? '')), - 'primary_color_use_default' => !empty($input['primary_color_use_default']), 'default_theme' => strtolower(trim((string) ($input['default_theme'] ?? ''))), - 'allow_user_theme_mode' => trim((string) ($input['allow_user_theme_mode'] ?? '')), + 'allow_user_theme' => isset($input['allow_user_theme']) ? (bool) $input['allow_user_theme'] : true, 'status' => trim((string) ($input['status'] ?? '')), ]; } @@ -240,18 +237,14 @@ class TenantService $errors[] = t('Status is invalid'); } if ( - !$form['primary_color_use_default'] - && $form['primary_color'] !== '' - && !preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $form['primary_color']) + $form['primary_color'] === '' + || !preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $form['primary_color']) ) { $errors[] = t('Primary color is invalid'); } if ($form['default_theme'] !== '' && !$this->settingsGateway->isAllowedTheme((string) $form['default_theme'])) { $errors[] = t('Default theme is invalid'); } - if (!in_array($form['allow_user_theme_mode'], ['', '0', '1'], true)) { - $errors[] = t('User theme policy is invalid'); - } return $errors; } diff --git a/i18n/default_de.json b/i18n/default_de.json index b9875a9..58ce2e6 100644 --- a/i18n/default_de.json +++ b/i18n/default_de.json @@ -425,13 +425,10 @@ "Appearance": "Darstellung", "API docs": "API-Doku", "Can view API documentation": "Kann API-Dokumentation anzeigen", - "Appearance is controlled per tenant. If a field is left at the system default, the app renders in light theme without a brand accent.": "Die Darstellung wird pro Mandant gesteuert. Wenn ein Feld auf Systemstandard bleibt, rendert die App im hellen Theme ohne Markenakzent.", - "No brand color": "Keine Markenfarbe", - "Use system default (no brand accent)": "Systemstandard verwenden (kein Markenakzent)", - "When enabled the tenant renders without a brand accent color.": "Wenn aktiv, rendert der Mandant ohne Markenakzent-Farbe.", - "Use system default (light)": "Systemstandard verwenden (hell)", + "Appearance is controlled per tenant. Every tenant carries its own primary color and default theme.": "Die Darstellung wird pro Mandant gesteuert. Jeder Mandant hat eine eigene Primärfarbe und ein eigenes Standard-Theme.", "The theme used for users in this tenant who have no personal preference.": "Das Theme für Nutzer dieses Mandanten, die keine persönliche Präferenz gesetzt haben.", - "Use system default (allowed)": "Systemstandard verwenden (erlaubt)", + "Users may choose their own theme": "Benutzer dürfen ihr eigenes Theme wählen", + "Controls whether users in this tenant can override the tenant default with a personal theme preference.": "Steuert, ob Benutzer dieses Mandanten den Mandanten-Standard durch eine persönliche Theme-Präferenz überschreiben dürfen.", "Inactive tenants are excluded from user access and tenant-scoped data.": "Inaktive Mandanten werden vom Benutzerzugriff und mandantenspezifischen Daten ausgeschlossen.", "Status & meta": "Status & Meta", "Audit": "Audit", @@ -759,10 +756,7 @@ "Filterable fields are available in address book filters.": "Filterbare Felder sind im Adressbuch als Filter verfügbar.", "Inactive fields are hidden in forms and filters.": "Inaktive Felder sind in Formularen und Filtern ausgeblendet.", "Default theme is invalid": "Standard-Theme ist ungültig", - "User theme policy is invalid": "Benutzer-Theme-Richtlinie ist ungültig", "Theme change is disabled for this tenant": "Theme-Wechsel ist für diesen Mandanten deaktiviert", - "Force allow user theme": "Benutzer-Theme erlauben (erzwingen)", - "Force disallow user theme": "Benutzer-Theme verbieten (erzwingen)", "Controls whether users in this tenant can choose their own theme.": "Steuert, ob Benutzer in diesem Mandanten ihr eigenes Theme wählen können.", "Tenants can override color and theme behavior in their appearance settings.": "Mandanten können Farbe und Theme-Verhalten in ihren Darstellungs-Einstellungen überschreiben.", "User theme policy": "Benutzer-Theme-Richtlinie", diff --git a/i18n/default_en.json b/i18n/default_en.json index 02c4bcc..85d72b3 100644 --- a/i18n/default_en.json +++ b/i18n/default_en.json @@ -425,13 +425,10 @@ "Appearance": "Appearance", "API docs": "API docs", "Can view API documentation": "Can view API documentation", - "Appearance is controlled per tenant. If a field is left at the system default, the app renders in light theme without a brand accent.": "Appearance is controlled per tenant. If a field is left at the system default, the app renders in light theme without a brand accent.", - "No brand color": "No brand color", - "Use system default (no brand accent)": "Use system default (no brand accent)", - "When enabled the tenant renders without a brand accent color.": "When enabled the tenant renders without a brand accent color.", - "Use system default (light)": "Use system default (light)", + "Appearance is controlled per tenant. Every tenant carries its own primary color and default theme.": "Appearance is controlled per tenant. Every tenant carries its own primary color and default theme.", + "Users may choose their own theme": "Users may choose their own theme", + "Controls whether users in this tenant can override the tenant default with a personal theme preference.": "Controls whether users in this tenant can override the tenant default with a personal theme preference.", "The theme used for users in this tenant who have no personal preference.": "The theme used for users in this tenant who have no personal preference.", - "Use system default (allowed)": "Use system default (allowed)", "Inactive tenants are excluded from user access and tenant-scoped data.": "Inactive tenants are excluded from user access and tenant-scoped data.", "Status & meta": "Status & meta", "Audit": "Audit", @@ -759,10 +756,7 @@ "Filterable fields are available in address book filters.": "Filterable fields are available in address book filters.", "Inactive fields are hidden in forms and filters.": "Inactive fields are hidden in forms and filters.", "Default theme is invalid": "Default theme is invalid", - "User theme policy is invalid": "User theme policy is invalid", "Theme change is disabled for this tenant": "Theme change is disabled for this tenant", - "Force allow user theme": "Force allow user theme", - "Force disallow user theme": "Force disallow user theme", "Controls whether users in this tenant can choose their own theme.": "Controls whether users in this tenant can choose their own theme.", "Tenants can override color and theme behavior in their appearance settings.": "Tenants can override color and theme behavior in their appearance settings.", "User theme policy": "User theme policy", diff --git a/pages/admin/tenants/_form.phtml b/pages/admin/tenants/_form.phtml index 920aef9..7edfd5c 100644 --- a/pages/admin/tenants/_form.phtml +++ b/pages/admin/tenants/_form.phtml @@ -34,22 +34,27 @@ $customFieldDefinitions = is_array($customFieldDefinitions ?? null) ? $customFie $readonlyAttr = $isReadOnly ? 'readonly' : ''; $disabledAttr = $isReadOnly ? 'disabled' : ''; $themes = appThemes(); +// Primary color is required per tenant — appearance is tenant-scoped and +// there is no "no brand color" opt-out. Legacy rows (NULL) are rendered +// with the neutral app default; saving converts NULL to that explicit hex. $primaryColor = (string) ($values['primary_color'] ?? ''); -// No global fallback — appearance is tenant-scoped. The color picker's -// "use default" preview uses a neutral hex so the UI is never empty. -$defaultPrimaryColor = '#2fa4a4'; -$useDefaultPrimaryColor = $primaryColor === ''; -$tenantDefaultTheme = strtolower(trim((string) ($values['default_theme'] ?? ''))); -if ($tenantDefaultTheme !== '' && !isset($themes[$tenantDefaultTheme])) { - $tenantDefaultTheme = ''; +if ($primaryColor === '' || !preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $primaryColor)) { + $primaryColor = '#2fa4a4'; } -$rawAllowUserThemeMode = $values['allow_user_theme_mode'] ?? ($values['allow_user_theme'] ?? null); -if ($rawAllowUserThemeMode === null || $rawAllowUserThemeMode === '') { - $tenantAllowUserThemeMode = ''; -} elseif (in_array(strtolower((string) $rawAllowUserThemeMode), ['1', 'true', 'yes', 'on'], true)) { - $tenantAllowUserThemeMode = '1'; +// Resolve tenant's default theme against the catalog; fall back to 'light' +// for legacy rows or unknown values. The select never renders an empty +// option — appearance is tenant-scoped, no global setting to inherit. +$tenantDefaultTheme = strtolower(trim((string) ($values['default_theme'] ?? ''))); +if ($tenantDefaultTheme === '' || !isset($themes[$tenantDefaultTheme])) { + $tenantDefaultTheme = isset($themes['light']) ? 'light' : (array_key_first($themes) ?: 'light'); +} +// allow_user_theme: boolean — true when set or unset (legacy NULL), false +// only when explicitly disabled. Eliminates the old tri-state select. +$rawAllowUserTheme = $values['allow_user_theme'] ?? null; +if ($rawAllowUserTheme === null || $rawAllowUserTheme === '') { + $tenantAllowUserTheme = true; } else { - $tenantAllowUserThemeMode = '0'; + $tenantAllowUserTheme = in_array(strtolower((string) $rawAllowUserTheme), ['1', 'true', 'yes', 'on'], true); } $microsoftEnabledRaw = $values['microsoft_enabled'] ?? false; $microsoftEnabled = in_array(strtolower(trim((string) $microsoftEnabledRaw)), ['1', 'true', 'yes', 'on'], true); @@ -933,45 +938,25 @@ $openOverrideCard = $detailsOpenAll;
- +-