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

@@ -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;
<div data-tab-panel="visibility">
<blockquote data-variant="info">
<?php e(t('Appearance is controlled per tenant. If a field is left at the system default, the app renders in light theme without a brand accent.')); ?>
<?php e(t('Appearance is controlled per tenant. Every tenant carries its own primary color and default theme.')); ?>
</blockquote>
<div class="grid">
<fieldset>
<legend><small><?php e(t('Primary color')); ?></small></legend>
<label for="primary_color">
<input type="color" name="primary_color" id="primary_color"
value="<?php e($primaryColor !== '' ? $primaryColor : $defaultPrimaryColor); ?>"
<?php e($readonlyAttr); ?> />
<div>
<?php e(t('Choose color')); ?>
</div>
</label>
<small class="muted"><?php e(t('The primary color affects buttons and accent elements across the app.')); ?></small>
</fieldset>
<fieldset>
<legend>
<small>
<?php e(t('No brand color')); ?>
</small>
</legend>
<label class="app-field">
<input type="hidden" name="primary_color_use_default" value="0">
<input type="checkbox" role="switch" name="primary_color_use_default" value="1"
data-color-default-toggle
data-color-target="#primary_color"
data-color-default="<?php e($defaultPrimaryColor); ?>"
<?php e($useDefaultPrimaryColor ? 'checked' : ''); ?>
<?php e($readonlyAttr); ?>>
<span><?php e(t('Use system default (no brand accent)')); ?></span>
</label>
<small class="muted"><?php e(t('When enabled the tenant renders without a brand accent color.')); ?></small>
</fieldset>
</div>
<fieldset>
<legend><small><?php e(t('Primary color')); ?></small></legend>
<label for="primary_color">
<input type="color" name="primary_color" id="primary_color"
value="<?php e($primaryColor); ?>"
required
<?php e($readonlyAttr); ?> />
<div>
<?php e(t('Choose color')); ?>
</div>
</label>
<small class="muted"><?php e(t('The primary color affects buttons and accent elements across the app.')); ?></small>
</fieldset>
<div class="grid">
<fieldset>
<legend><small><?php e(t('Default theme')); ?></small></legend>
<select name="default_theme" <?php e($disabledAttr); ?>>
<option value=""><?php e(t('Use system default (light)')); ?></option>
<?php foreach ($themes as $themeKey => $themeLabel): ?>
<option value="<?php e($themeKey); ?>" <?php e($tenantDefaultTheme === $themeKey ? 'selected' : ''); ?>>
<?php e(t($themeLabel)); ?>
@@ -982,18 +967,14 @@ $openOverrideCard = $detailsOpenAll;
</fieldset>
<fieldset>
<legend><small><?php e(t('User theme policy')); ?></small></legend>
<select name="allow_user_theme_mode" <?php e($disabledAttr); ?>>
<option value="" <?php e($tenantAllowUserThemeMode === '' ? 'selected' : ''); ?>>
<?php e(t('Use system default (allowed)')); ?>
</option>
<option value="1" <?php e($tenantAllowUserThemeMode === '1' ? 'selected' : ''); ?>>
<?php e(t('Force allow user theme')); ?>
</option>
<option value="0" <?php e($tenantAllowUserThemeMode === '0' ? 'selected' : ''); ?>>
<?php e(t('Force disallow user theme')); ?>
</option>
</select>
<small class="muted"><?php e(t('Controls whether users in this tenant can choose their own theme.')); ?></small>
<label class="app-field">
<input type="hidden" name="allow_user_theme" value="0">
<input type="checkbox" role="switch" name="allow_user_theme" value="1"
<?php e($tenantAllowUserTheme ? 'checked' : ''); ?>
<?php e($readonlyAttr); ?>>
<span><?php e(t('Users may choose their own theme')); ?></span>
</label>
<small class="muted"><?php e(t('Controls whether users in this tenant can override the tenant default with a personal theme preference.')); ?></small>
</fieldset>
</div>
<?php