refactor(tenants): visibility form requires explicit appearance values

Simplifies the tenant Visibility tab to match the tenant-only appearance
model established by the previous commits:

- primary_color: now required. Removed the "No brand color / Use system
  default (no brand accent)" toggle — every tenant carries a concrete
  primary color. Legacy NULL rows render the neutral app default (#2fa4a4)
  in the color picker and are converted to that explicit hex on save.
- default_theme: the select no longer offers a "Use system default" empty
  option. Legacy NULL rows resolve to 'light' at render time; saves always
  persist a valid theme via SettingsAppGateway::normalizeTheme().
- allow_user_theme: replaced the tri-state "Use system default (allowed) /
  Force allow / Force disallow" select with a single boolean switch
  ("Users may choose their own theme"). Legacy NULL rows load as checked.
  Saves persist 0/1 explicitly.

TenantService: sanitize no longer reads primary_color_use_default or
allow_user_theme_mode; it validates primary_color as a required hex and
treats allow_user_theme as a plain boolean. Both create and update paths
write concrete values only — no more NULL writes for these three fields.

DirectorySettingsGateway gains a normalizeTheme() delegate so TenantService
can route through the same gateway it uses for isAllowedTheme().

Removed now-unused app-color-default-toggle JS component + its runtime
registration + its architecture-test entry. i18n cleanup: "No brand
color", "Use system default (no brand accent)", "When enabled the tenant
renders without a brand accent color.", "Use system default (light)",
"Use system default (allowed)", "Force allow user theme", "Force disallow
user theme", "User theme policy is invalid" all removed. New copy: "Users
may choose their own theme" + helper text, plus a tightened tab blockquote.

Tests: TenantServiceTest validInput() updated to send concrete values;
settingsGateway mock gets normalizeTheme() + isAllowedTheme() defaults.
All 1985 tests pass; PHPStan level 5 clean; QG-006 clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 23:10:44 +02:00
parent c4c294edd7
commit 68453fd6ed
9 changed files with 69 additions and 174 deletions

View File

@@ -32,4 +32,9 @@ class DirectorySettingsGateway
{
return $this->settingsAppGateway->isAllowedTheme($theme);
}
public function normalizeTheme(?string $theme): string
{
return $this->settingsAppGateway->normalizeTheme($theme);
}
}

View File

@@ -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;
}