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:
@@ -32,4 +32,9 @@ class DirectorySettingsGateway
|
|||||||
{
|
{
|
||||||
return $this->settingsAppGateway->isAllowedTheme($theme);
|
return $this->settingsAppGateway->isAllowedTheme($theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function normalizeTheme(?string $theme): string
|
||||||
|
{
|
||||||
|
return $this->settingsAppGateway->normalizeTheme($theme);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,12 +68,12 @@ class TenantService
|
|||||||
'website' => $form['website'],
|
'website' => $form['website'],
|
||||||
'privacy_url' => $form['privacy_url'],
|
'privacy_url' => $form['privacy_url'],
|
||||||
'imprint_url' => $form['imprint_url'],
|
'imprint_url' => $form['imprint_url'],
|
||||||
// null means "inherit from global settings" for color, theme, and theme-toggle policy.
|
// Appearance is tenant-scoped with explicit, required values. Every
|
||||||
'primary_color' => $form['primary_color_use_default']
|
// tenant has a primary color, a default theme, and an explicit
|
||||||
? null
|
// allow-user-theme flag — there is no global fallback to inherit.
|
||||||
: ($form['primary_color'] !== '' ? $form['primary_color'] : null),
|
'primary_color' => strtolower($form['primary_color']),
|
||||||
'default_theme' => $form['default_theme'] !== '' ? $form['default_theme'] : null,
|
'default_theme' => $this->settingsGateway->normalizeTheme($form['default_theme']),
|
||||||
'allow_user_theme' => $form['allow_user_theme_mode'] === '' ? null : (int) $form['allow_user_theme_mode'],
|
'allow_user_theme' => $form['allow_user_theme'] ? 1 : 0,
|
||||||
'status' => $form['status'],
|
'status' => $form['status'],
|
||||||
'status_changed_at' => $statusChangedAt,
|
'status_changed_at' => $statusChangedAt,
|
||||||
'status_changed_by' => $currentUserId > 0 ? $currentUserId : null,
|
'status_changed_by' => $currentUserId > 0 ? $currentUserId : null,
|
||||||
@@ -135,11 +135,9 @@ class TenantService
|
|||||||
'website' => $form['website'],
|
'website' => $form['website'],
|
||||||
'privacy_url' => $form['privacy_url'],
|
'privacy_url' => $form['privacy_url'],
|
||||||
'imprint_url' => $form['imprint_url'],
|
'imprint_url' => $form['imprint_url'],
|
||||||
'primary_color' => $form['primary_color_use_default']
|
'primary_color' => strtolower($form['primary_color']),
|
||||||
? null
|
'default_theme' => $this->settingsGateway->normalizeTheme($form['default_theme']),
|
||||||
: ($form['primary_color'] !== '' ? $form['primary_color'] : null),
|
'allow_user_theme' => $form['allow_user_theme'] ? 1 : 0,
|
||||||
'default_theme' => $form['default_theme'] !== '' ? $form['default_theme'] : null,
|
|
||||||
'allow_user_theme' => $form['allow_user_theme_mode'] === '' ? null : (int) $form['allow_user_theme_mode'],
|
|
||||||
'status' => $form['status'],
|
'status' => $form['status'],
|
||||||
'modified_by' => $currentUserId > 0 ? $currentUserId : null,
|
'modified_by' => $currentUserId > 0 ? $currentUserId : null,
|
||||||
];
|
];
|
||||||
@@ -223,9 +221,8 @@ class TenantService
|
|||||||
'privacy_url' => trim((string) ($input['privacy_url'] ?? '')),
|
'privacy_url' => trim((string) ($input['privacy_url'] ?? '')),
|
||||||
'imprint_url' => trim((string) ($input['imprint_url'] ?? '')),
|
'imprint_url' => trim((string) ($input['imprint_url'] ?? '')),
|
||||||
'primary_color' => trim((string) ($input['primary_color'] ?? '')),
|
'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'] ?? ''))),
|
'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'] ?? '')),
|
'status' => trim((string) ($input['status'] ?? '')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -240,18 +237,14 @@ class TenantService
|
|||||||
$errors[] = t('Status is invalid');
|
$errors[] = t('Status is invalid');
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
!$form['primary_color_use_default']
|
$form['primary_color'] === ''
|
||||||
&& $form['primary_color'] !== ''
|
|| !preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $form['primary_color'])
|
||||||
&& !preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $form['primary_color'])
|
|
||||||
) {
|
) {
|
||||||
$errors[] = t('Primary color is invalid');
|
$errors[] = t('Primary color is invalid');
|
||||||
}
|
}
|
||||||
if ($form['default_theme'] !== '' && !$this->settingsGateway->isAllowedTheme((string) $form['default_theme'])) {
|
if ($form['default_theme'] !== '' && !$this->settingsGateway->isAllowedTheme((string) $form['default_theme'])) {
|
||||||
$errors[] = t('Default theme is invalid');
|
$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;
|
return $errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -425,13 +425,10 @@
|
|||||||
"Appearance": "Darstellung",
|
"Appearance": "Darstellung",
|
||||||
"API docs": "API-Doku",
|
"API docs": "API-Doku",
|
||||||
"Can view API documentation": "Kann API-Dokumentation anzeigen",
|
"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.",
|
"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.",
|
||||||
"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)",
|
|
||||||
"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.",
|
"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.",
|
"Inactive tenants are excluded from user access and tenant-scoped data.": "Inaktive Mandanten werden vom Benutzerzugriff und mandantenspezifischen Daten ausgeschlossen.",
|
||||||
"Status & meta": "Status & Meta",
|
"Status & meta": "Status & Meta",
|
||||||
"Audit": "Audit",
|
"Audit": "Audit",
|
||||||
@@ -759,10 +756,7 @@
|
|||||||
"Filterable fields are available in address book filters.": "Filterbare Felder sind im Adressbuch als Filter verfügbar.",
|
"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.",
|
"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",
|
"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",
|
"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.",
|
"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.",
|
"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",
|
"User theme policy": "Benutzer-Theme-Richtlinie",
|
||||||
|
|||||||
@@ -425,13 +425,10 @@
|
|||||||
"Appearance": "Appearance",
|
"Appearance": "Appearance",
|
||||||
"API docs": "API docs",
|
"API docs": "API docs",
|
||||||
"Can view API documentation": "Can view API documentation",
|
"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.",
|
"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.",
|
||||||
"No brand color": "No brand color",
|
"Users may choose their own theme": "Users may choose their own theme",
|
||||||
"Use system default (no brand accent)": "Use system default (no brand accent)",
|
"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.",
|
||||||
"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)",
|
|
||||||
"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.",
|
"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.",
|
"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",
|
"Status & meta": "Status & meta",
|
||||||
"Audit": "Audit",
|
"Audit": "Audit",
|
||||||
@@ -759,10 +756,7 @@
|
|||||||
"Filterable fields are available in address book filters.": "Filterable fields are available in address book filters.",
|
"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.",
|
"Inactive fields are hidden in forms and filters.": "Inactive fields are hidden in forms and filters.",
|
||||||
"Default theme is invalid": "Default theme is invalid",
|
"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",
|
"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.",
|
"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.",
|
"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",
|
"User theme policy": "User theme policy",
|
||||||
|
|||||||
@@ -34,22 +34,27 @@ $customFieldDefinitions = is_array($customFieldDefinitions ?? null) ? $customFie
|
|||||||
$readonlyAttr = $isReadOnly ? 'readonly' : '';
|
$readonlyAttr = $isReadOnly ? 'readonly' : '';
|
||||||
$disabledAttr = $isReadOnly ? 'disabled' : '';
|
$disabledAttr = $isReadOnly ? 'disabled' : '';
|
||||||
$themes = appThemes();
|
$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'] ?? '');
|
$primaryColor = (string) ($values['primary_color'] ?? '');
|
||||||
// No global fallback — appearance is tenant-scoped. The color picker's
|
if ($primaryColor === '' || !preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $primaryColor)) {
|
||||||
// "use default" preview uses a neutral hex so the UI is never empty.
|
$primaryColor = '#2fa4a4';
|
||||||
$defaultPrimaryColor = '#2fa4a4';
|
|
||||||
$useDefaultPrimaryColor = $primaryColor === '';
|
|
||||||
$tenantDefaultTheme = strtolower(trim((string) ($values['default_theme'] ?? '')));
|
|
||||||
if ($tenantDefaultTheme !== '' && !isset($themes[$tenantDefaultTheme])) {
|
|
||||||
$tenantDefaultTheme = '';
|
|
||||||
}
|
}
|
||||||
$rawAllowUserThemeMode = $values['allow_user_theme_mode'] ?? ($values['allow_user_theme'] ?? null);
|
// Resolve tenant's default theme against the catalog; fall back to 'light'
|
||||||
if ($rawAllowUserThemeMode === null || $rawAllowUserThemeMode === '') {
|
// for legacy rows or unknown values. The select never renders an empty
|
||||||
$tenantAllowUserThemeMode = '';
|
// option — appearance is tenant-scoped, no global setting to inherit.
|
||||||
} elseif (in_array(strtolower((string) $rawAllowUserThemeMode), ['1', 'true', 'yes', 'on'], true)) {
|
$tenantDefaultTheme = strtolower(trim((string) ($values['default_theme'] ?? '')));
|
||||||
$tenantAllowUserThemeMode = '1';
|
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 {
|
} else {
|
||||||
$tenantAllowUserThemeMode = '0';
|
$tenantAllowUserTheme = in_array(strtolower((string) $rawAllowUserTheme), ['1', 'true', 'yes', 'on'], true);
|
||||||
}
|
}
|
||||||
$microsoftEnabledRaw = $values['microsoft_enabled'] ?? false;
|
$microsoftEnabledRaw = $values['microsoft_enabled'] ?? false;
|
||||||
$microsoftEnabled = in_array(strtolower(trim((string) $microsoftEnabledRaw)), ['1', 'true', 'yes', 'on'], true);
|
$microsoftEnabled = in_array(strtolower(trim((string) $microsoftEnabledRaw)), ['1', 'true', 'yes', 'on'], true);
|
||||||
@@ -933,45 +938,25 @@ $openOverrideCard = $detailsOpenAll;
|
|||||||
|
|
||||||
<div data-tab-panel="visibility">
|
<div data-tab-panel="visibility">
|
||||||
<blockquote data-variant="info">
|
<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>
|
</blockquote>
|
||||||
<div class="grid">
|
<fieldset>
|
||||||
<fieldset>
|
<legend><small><?php e(t('Primary color')); ?></small></legend>
|
||||||
<legend><small><?php e(t('Primary color')); ?></small></legend>
|
<label for="primary_color">
|
||||||
<label for="primary_color">
|
<input type="color" name="primary_color" id="primary_color"
|
||||||
<input type="color" name="primary_color" id="primary_color"
|
value="<?php e($primaryColor); ?>"
|
||||||
value="<?php e($primaryColor !== '' ? $primaryColor : $defaultPrimaryColor); ?>"
|
required
|
||||||
<?php e($readonlyAttr); ?> />
|
<?php e($readonlyAttr); ?> />
|
||||||
<div>
|
<div>
|
||||||
<?php e(t('Choose color')); ?>
|
<?php e(t('Choose color')); ?>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<small class="muted"><?php e(t('The primary color affects buttons and accent elements across the app.')); ?></small>
|
<small class="muted"><?php e(t('The primary color affects buttons and accent elements across the app.')); ?></small>
|
||||||
</fieldset>
|
</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>
|
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><small><?php e(t('Default theme')); ?></small></legend>
|
<legend><small><?php e(t('Default theme')); ?></small></legend>
|
||||||
<select name="default_theme" <?php e($disabledAttr); ?>>
|
<select name="default_theme" <?php e($disabledAttr); ?>>
|
||||||
<option value=""><?php e(t('Use system default (light)')); ?></option>
|
|
||||||
<?php foreach ($themes as $themeKey => $themeLabel): ?>
|
<?php foreach ($themes as $themeKey => $themeLabel): ?>
|
||||||
<option value="<?php e($themeKey); ?>" <?php e($tenantDefaultTheme === $themeKey ? 'selected' : ''); ?>>
|
<option value="<?php e($themeKey); ?>" <?php e($tenantDefaultTheme === $themeKey ? 'selected' : ''); ?>>
|
||||||
<?php e(t($themeLabel)); ?>
|
<?php e(t($themeLabel)); ?>
|
||||||
@@ -982,18 +967,14 @@ $openOverrideCard = $detailsOpenAll;
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend><small><?php e(t('User theme policy')); ?></small></legend>
|
<legend><small><?php e(t('User theme policy')); ?></small></legend>
|
||||||
<select name="allow_user_theme_mode" <?php e($disabledAttr); ?>>
|
<label class="app-field">
|
||||||
<option value="" <?php e($tenantAllowUserThemeMode === '' ? 'selected' : ''); ?>>
|
<input type="hidden" name="allow_user_theme" value="0">
|
||||||
<?php e(t('Use system default (allowed)')); ?>
|
<input type="checkbox" role="switch" name="allow_user_theme" value="1"
|
||||||
</option>
|
<?php e($tenantAllowUserTheme ? 'checked' : ''); ?>
|
||||||
<option value="1" <?php e($tenantAllowUserThemeMode === '1' ? 'selected' : ''); ?>>
|
<?php e($readonlyAttr); ?>>
|
||||||
<?php e(t('Force allow user theme')); ?>
|
<span><?php e(t('Users may choose their own theme')); ?></span>
|
||||||
</option>
|
</label>
|
||||||
<option value="0" <?php e($tenantAllowUserThemeMode === '0' ? 'selected' : ''); ?>>
|
<small class="muted"><?php e(t('Controls whether users in this tenant can override the tenant default with a personal theme preference.')); ?></small>
|
||||||
<?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>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ trait FrontendRuntimeContractSupport
|
|||||||
'web/js/components/app-confirm-actions.js',
|
'web/js/components/app-confirm-actions.js',
|
||||||
'web/js/components/app-details-state.js',
|
'web/js/components/app-details-state.js',
|
||||||
'web/js/components/app-tenant-switcher.js',
|
'web/js/components/app-tenant-switcher.js',
|
||||||
'web/js/components/app-color-default-toggle.js',
|
|
||||||
'web/js/components/app-custom-field-options-toggle.js',
|
'web/js/components/app-custom-field-options-toggle.js',
|
||||||
'web/js/components/app-tenant-sso-toggle.js',
|
'web/js/components/app-tenant-sso-toggle.js',
|
||||||
'web/js/components/app-settings-telemetry.js',
|
'web/js/components/app-settings-telemetry.js',
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ class TenantServiceTest extends TestCase
|
|||||||
$this->tenantRepository = $this->createMock(TenantRepositoryInterface::class);
|
$this->tenantRepository = $this->createMock(TenantRepositoryInterface::class);
|
||||||
$this->departmentRepository = $this->createMock(DepartmentRepositoryInterface::class);
|
$this->departmentRepository = $this->createMock(DepartmentRepositoryInterface::class);
|
||||||
$this->settingsGateway = $this->createMock(DirectorySettingsGateway::class);
|
$this->settingsGateway = $this->createMock(DirectorySettingsGateway::class);
|
||||||
|
$this->settingsGateway->method('isAllowedTheme')->willReturn(true);
|
||||||
|
$this->settingsGateway->method('normalizeTheme')
|
||||||
|
->willReturnCallback(static fn (?string $t): string => $t ?? 'light');
|
||||||
$this->systemAuditService = $this->createMock(AuditRecorderInterface::class);
|
$this->systemAuditService = $this->createMock(AuditRecorderInterface::class);
|
||||||
|
|
||||||
$this->service = new TenantService(
|
$this->service = new TenantService(
|
||||||
@@ -196,10 +199,9 @@ class TenantServiceTest extends TestCase
|
|||||||
'website' => '',
|
'website' => '',
|
||||||
'privacy_url' => '',
|
'privacy_url' => '',
|
||||||
'imprint_url' => '',
|
'imprint_url' => '',
|
||||||
'primary_color' => '',
|
'primary_color' => '#2fa4a4',
|
||||||
'primary_color_use_default' => true,
|
'default_theme' => 'light',
|
||||||
'default_theme' => '',
|
'allow_user_theme' => '1',
|
||||||
'allow_user_theme_mode' => '',
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import { initContrastToggle } from './components/app-contrast-toggle.js';
|
|||||||
import { initConfirmActions } from './components/app-confirm-actions.js';
|
import { initConfirmActions } from './components/app-confirm-actions.js';
|
||||||
import { initDetailsState } from './components/app-details-state.js';
|
import { initDetailsState } from './components/app-details-state.js';
|
||||||
import { initTenantSwitcher } from './components/app-tenant-switcher.js';
|
import { initTenantSwitcher } from './components/app-tenant-switcher.js';
|
||||||
import { initColorDefaultToggle } from './components/app-color-default-toggle.js';
|
|
||||||
import { initCustomFieldOptionsToggle } from './components/app-custom-field-options-toggle.js';
|
import { initCustomFieldOptionsToggle } from './components/app-custom-field-options-toggle.js';
|
||||||
import { initTenantSsoToggle } from './components/app-tenant-sso-toggle.js';
|
import { initTenantSsoToggle } from './components/app-tenant-sso-toggle.js';
|
||||||
import { initTenantLdapToggle } from './components/app-tenant-ldap-toggle.js';
|
import { initTenantLdapToggle } from './components/app-tenant-ldap-toggle.js';
|
||||||
@@ -233,10 +232,6 @@ const bootRuntime = async () => {
|
|||||||
selector: '[data-app-component="tenant-switcher"]',
|
selector: '[data-app-component="tenant-switcher"]',
|
||||||
configPath: 'components.tenantSwitcher',
|
configPath: 'components.tenantSwitcher',
|
||||||
});
|
});
|
||||||
runtime.register('color-default-toggle', initColorDefaultToggle, {
|
|
||||||
scope: 'global',
|
|
||||||
configPath: 'components.colorDefaultToggle',
|
|
||||||
});
|
|
||||||
runtime.register('custom-field-options-toggle', initCustomFieldOptionsToggle, {
|
runtime.register('custom-field-options-toggle', initCustomFieldOptionsToggle, {
|
||||||
scope: 'global',
|
scope: 'global',
|
||||||
configPath: 'components.customFieldOptionsToggle',
|
configPath: 'components.customFieldOptionsToggle',
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
/**
|
|
||||||
* Toggles color input between custom value and default/inherited color.
|
|
||||||
*/
|
|
||||||
import { warnOnce } from '../core/app-dom.js';
|
|
||||||
import { createConditionalToggleInit } from '../core/app-conditional-controls.js';
|
|
||||||
|
|
||||||
const syncToggle = (toggle, scope) => {
|
|
||||||
const targetSelector = toggle.dataset.colorTarget || '';
|
|
||||||
if (!targetSelector) {
|
|
||||||
warnOnce('UI_EL_MISSING', 'Missing data-color-target', { module: 'color-default-toggle' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const target = scope.querySelector(targetSelector);
|
|
||||||
if (!(target instanceof HTMLInputElement)) {
|
|
||||||
warnOnce('UI_EL_MISSING', `Missing color target: ${targetSelector}`, { module: 'color-default-toggle' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const defaultValue = toggle.dataset.colorDefault || '#2fa4a4';
|
|
||||||
if (toggle.checked) {
|
|
||||||
if (!target.dataset.customValue) {
|
|
||||||
target.dataset.customValue = target.value;
|
|
||||||
}
|
|
||||||
target.value = defaultValue;
|
|
||||||
target.setAttribute('disabled', 'disabled');
|
|
||||||
} else {
|
|
||||||
target.removeAttribute('disabled');
|
|
||||||
if (target.dataset.customValue) {
|
|
||||||
target.value = target.dataset.customValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const initRoot = (toggle) => {
|
|
||||||
if (!(toggle instanceof HTMLInputElement)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scope = document;
|
|
||||||
const targetSelector = toggle.dataset.colorTarget || '';
|
|
||||||
const target = targetSelector ? scope.querySelector(targetSelector) : null;
|
|
||||||
if (targetSelector && !target) {
|
|
||||||
warnOnce('UI_EL_MISSING', `Missing color target: ${targetSelector}`, { module: 'color-default-toggle' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const onToggleChange = () => syncToggle(toggle, scope);
|
|
||||||
toggle.addEventListener('change', onToggleChange);
|
|
||||||
const cleanups = [() => toggle.removeEventListener('change', onToggleChange)];
|
|
||||||
|
|
||||||
if (target instanceof HTMLInputElement) {
|
|
||||||
const onTargetInput = () => {
|
|
||||||
if (toggle.checked) {
|
|
||||||
toggle.checked = false;
|
|
||||||
target.removeAttribute('disabled');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
target.addEventListener('input', onTargetInput);
|
|
||||||
cleanups.push(() => target.removeEventListener('input', onTargetInput));
|
|
||||||
}
|
|
||||||
|
|
||||||
syncToggle(toggle, scope);
|
|
||||||
return () => cleanups.forEach((fn) => fn());
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initColorDefaultToggle = createConditionalToggleInit({
|
|
||||||
rootSelector: '[data-color-default-toggle]',
|
|
||||||
boundKey: 'colorDefaultToggleBound',
|
|
||||||
initRoot,
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user