Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cac95fa3e | ||
|
|
c558621550 | ||
|
|
c5119ab868 | ||
|
|
56f0661a10 | ||
|
|
c51ea3f66c | ||
|
|
5d49483369 |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/tenancy-webui",
|
"name": "@govoplan/tenancy-webui",
|
||||||
"version": "0.1.18",
|
"version": "0.1.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "webui/src/index.ts",
|
"main": "webui/src/index.ts",
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-tenancy"
|
name = "govoplan-tenancy"
|
||||||
version = "0.1.18"
|
version = "0.1.19"
|
||||||
description = "GovOPlaN tenancy platform module."
|
description = "GovOPlaN tenancy platform module."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -18,7 +18,19 @@ 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_custom_overrides_policy,
|
||||||
|
appearance_settings,
|
||||||
|
resolve_effective_appearance,
|
||||||
|
update_appearance_custom_overrides_policy,
|
||||||
|
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 (
|
||||||
|
navigation_preferences_from_settings,
|
||||||
|
update_navigation_preferences,
|
||||||
|
)
|
||||||
from govoplan_core.core.principal_cache import invalidate_auth_principals
|
from govoplan_core.core.principal_cache import invalidate_auth_principals
|
||||||
from govoplan_core.core.runtime import get_registry
|
from govoplan_core.core.runtime import get_registry
|
||||||
from govoplan_core.db.session import get_session
|
from govoplan_core.db.session import get_session
|
||||||
@@ -76,7 +88,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", "settings")
|
TENANT_SETTINGS_SECTIONS = ("identity", "locale", "languages", "navigation", "appearance", "settings")
|
||||||
TENANT_NON_STATUS_UPDATE_FIELDS = {
|
TENANT_NON_STATUS_UPDATE_FIELDS = {
|
||||||
"name",
|
"name",
|
||||||
"description",
|
"description",
|
||||||
@@ -251,6 +263,16 @@ def _tenant_settings_item(session: Session, tenant: Tenant) -> TenantSettingsIte
|
|||||||
system_payload = system_i18n_payload(system_settings)
|
system_payload = system_i18n_payload(system_settings)
|
||||||
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)
|
||||||
|
system_palette, system_locked = appearance_settings(system_settings.settings)
|
||||||
|
tenant_palette, tenant_locked = appearance_settings(tenant.settings)
|
||||||
|
system_custom_overrides_allowed = appearance_custom_overrides_policy(system_settings.settings) is True
|
||||||
|
tenant_custom_overrides_allowed = appearance_custom_overrides_policy(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,
|
||||||
@@ -259,6 +281,16 @@ def _tenant_settings_item(session: Session, tenant: Tenant) -> TenantSettingsIte
|
|||||||
available_languages=system_payload["available_languages"],
|
available_languages=system_payload["available_languages"],
|
||||||
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,
|
||||||
|
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,
|
||||||
|
appearance_custom_overrides_allowed=tenant_custom_overrides_allowed,
|
||||||
|
system_appearance_custom_overrides_allowed=system_custom_overrides_allowed,
|
||||||
|
effective_appearance_custom_overrides_allowed=effective_appearance.custom_overrides_allowed,
|
||||||
settings=tenant.settings or {},
|
settings=tenant.settings or {},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -273,6 +305,18 @@ def _tenant_settings_sections(item: TenantSettingsItem) -> dict[str, Any]:
|
|||||||
"system_enabled_language_codes": payload["system_enabled_language_codes"],
|
"system_enabled_language_codes": payload["system_enabled_language_codes"],
|
||||||
"enabled_language_codes": payload["enabled_language_codes"],
|
"enabled_language_codes": payload["enabled_language_codes"],
|
||||||
},
|
},
|
||||||
|
"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"],
|
||||||
|
"appearance_custom_overrides_allowed": payload["appearance_custom_overrides_allowed"],
|
||||||
|
"system_appearance_custom_overrides_allowed": payload["system_appearance_custom_overrides_allowed"],
|
||||||
|
"effective_appearance_custom_overrides_allowed": payload["effective_appearance_custom_overrides_allowed"],
|
||||||
|
},
|
||||||
"settings": payload["settings"],
|
"settings": payload["settings"],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,7 +665,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,
|
||||||
@@ -671,11 +715,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
|
||||||
|
|
||||||
|
|
||||||
@@ -938,8 +981,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]
|
||||||
@@ -967,6 +1010,39 @@ 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)
|
||||||
|
system_custom_overrides_allowed = appearance_custom_overrides_policy(system_settings.settings) is True
|
||||||
|
tenant_custom_overrides_allowed = appearance_custom_overrides_policy(tenant.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
|
||||||
|
)
|
||||||
|
custom_overrides_policy_changed = (
|
||||||
|
"appearance_custom_overrides_allowed" in payload.model_fields_set
|
||||||
|
and payload.appearance_custom_overrides_allowed != tenant_custom_overrides_allowed
|
||||||
|
)
|
||||||
|
if payload.appearance_custom_overrides_allowed is True and not system_custom_overrides_allowed:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
detail="The system appearance policy does not allow personal custom overrides.",
|
||||||
|
)
|
||||||
|
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) or custom_overrides_policy_changed
|
||||||
|
) and not has_scope(principal, "admin:policies:write"):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_422_UNPROCESSABLE_CONTENT,
|
||||||
|
detail="Changing the tenant appearance policy 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")
|
||||||
@@ -978,6 +1054,23 @@ 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 "appearance_custom_overrides_allowed" in payload.model_fields_set:
|
||||||
|
tenant.settings = update_appearance_custom_overrides_policy(
|
||||||
|
tenant.settings,
|
||||||
|
allowed=payload.appearance_custom_overrides_allowed,
|
||||||
|
)
|
||||||
|
if "navigation" in payload.model_fields_set:
|
||||||
|
tenant.settings = update_navigation_preferences(
|
||||||
|
tenant.settings,
|
||||||
|
payload.navigation.model_dump(mode="json") if payload.navigation else None,
|
||||||
|
)
|
||||||
session.add(tenant)
|
session.add(tenant)
|
||||||
audit_event(
|
audit_event(
|
||||||
session,
|
session,
|
||||||
@@ -986,7 +1079,19 @@ def update_tenant_settings(
|
|||||||
action="tenant.settings.updated",
|
action="tenant.settings.updated",
|
||||||
object_type="tenant",
|
object_type="tenant",
|
||||||
object_id=tenant.id,
|
object_id=tenant.id,
|
||||||
details={"default_locale": tenant.default_locale, "enabled_language_codes": enabled},
|
details={
|
||||||
|
"default_locale": tenant.default_locale,
|
||||||
|
"enabled_language_codes": enabled,
|
||||||
|
"navigation_updated": "navigation" in payload.model_fields_set,
|
||||||
|
"appearance_updated": bool(
|
||||||
|
{"appearance_palette", "appearance_palette_locked", "appearance_custom_overrides_allowed"}.intersection(
|
||||||
|
payload.model_fields_set
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"appearance_palette": appearance_settings(tenant.settings)[0],
|
||||||
|
"appearance_palette_locked": appearance_settings(tenant.settings)[1],
|
||||||
|
"appearance_custom_overrides_allowed": appearance_custom_overrides_policy(tenant.settings),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
after_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant))
|
after_sections = _tenant_settings_sections(_tenant_settings_item(session, tenant))
|
||||||
_record_tenant_settings_section_changes(session, tenant_id=tenant.id, before=before_sections, after=after_sections, principal=principal)
|
_record_tenant_settings_section_changes(session, tenant_id=tenant.id, before=before_sections, after=after_sections, principal=principal)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from typing import Any, Literal
|
|||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict, Field
|
||||||
|
|
||||||
from govoplan_core.api.v1.schemas import DeltaDeletedItem
|
from govoplan_core.api.v1.schemas import DeltaDeletedItem, NavigationPreferencesPayload
|
||||||
from govoplan_core.i18n import REFERENCE_LANGUAGE_CODE
|
from govoplan_core.i18n import REFERENCE_LANGUAGE_CODE
|
||||||
|
|
||||||
|
|
||||||
@@ -137,6 +137,16 @@ class TenantSettingsItem(BaseModel):
|
|||||||
available_languages: list[dict[str, Any]] = Field(default_factory=list)
|
available_languages: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
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
|
||||||
|
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"
|
||||||
|
appearance_custom_overrides_allowed: bool | None = None
|
||||||
|
system_appearance_custom_overrides_allowed: bool = False
|
||||||
|
effective_appearance_custom_overrides_allowed: bool = False
|
||||||
settings: dict[str, Any] = Field(default_factory=dict)
|
settings: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@@ -155,3 +165,7 @@ 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
|
||||||
|
appearance_palette: Literal["default", "civic_blue", "forest", "plum"] | None = None
|
||||||
|
appearance_palette_locked: bool | None = None
|
||||||
|
appearance_custom_overrides_allowed: bool | None = None
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ from govoplan_core.core.access import (
|
|||||||
CAPABILITY_AUTH_TENANT_CONTEXT_SWITCHER,
|
CAPABILITY_AUTH_TENANT_CONTEXT_SWITCHER,
|
||||||
CAPABILITY_TENANCY_TENANT_RESOLVER,
|
CAPABILITY_TENANCY_TENANT_RESOLVER,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.modules import DocumentationLink, DocumentationTopic, FrontendModule, ModuleContext, ModuleManifest
|
from govoplan_core.core.modules import (
|
||||||
|
DocumentationCondition,
|
||||||
|
DocumentationLink,
|
||||||
|
DocumentationTopic,
|
||||||
|
FrontendModule,
|
||||||
|
ModuleContext,
|
||||||
|
ModuleManifest,
|
||||||
|
)
|
||||||
from govoplan_core.core.provider_governance import declared_module_architecture
|
from govoplan_core.core.provider_governance import declared_module_architecture
|
||||||
from govoplan_core.core.views import ViewSurface
|
from govoplan_core.core.views import ViewSurface
|
||||||
|
|
||||||
@@ -32,7 +39,7 @@ def _route_factory(context: ModuleContext):
|
|||||||
manifest = ModuleManifest(
|
manifest = ModuleManifest(
|
||||||
id="tenancy",
|
id="tenancy",
|
||||||
name="Tenancy",
|
name="Tenancy",
|
||||||
version="0.1.18",
|
version="0.1.19",
|
||||||
required_capabilities=(
|
required_capabilities=(
|
||||||
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
|
||||||
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
|
||||||
@@ -51,6 +58,20 @@ manifest = ModuleManifest(
|
|||||||
documentation_types=("user",),
|
documentation_types=("user",),
|
||||||
audience=("user", "tenant_admin"),
|
audience=("user", "tenant_admin"),
|
||||||
related_modules=("access",),
|
related_modules=("access",),
|
||||||
|
translations={
|
||||||
|
"de": {
|
||||||
|
"title": "Im richtigen Mandantenkontext arbeiten",
|
||||||
|
"summary": (
|
||||||
|
"Der aktive Mandant bestimmt, welche mandantenbezogenen Daten, Rollen, Einstellungen und Modulkonfigurationen für eine "
|
||||||
|
"Anfrage sichtbar sind."
|
||||||
|
),
|
||||||
|
"body": (
|
||||||
|
"Konten mit Zugriff auf mehrere Mandanten können den Kontext über die Mandantenauswahl der Plattform wechseln. Der "
|
||||||
|
"Wechsel ändert den aktiven Geltungsbereich; er kopiert keine Daten und gewährt keine neue Befugnis. Prüfen Sie den "
|
||||||
|
"ausgewählten Mandanten immer, bevor Sie mandanteneigene Datensätze anlegen oder ändern."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
metadata={
|
metadata={
|
||||||
"kind": "reference",
|
"kind": "reference",
|
||||||
"help_contexts": ["tenancy.current-context", "tenancy.selector"],
|
"help_contexts": ["tenancy.current-context", "tenancy.selector"],
|
||||||
@@ -60,17 +81,65 @@ 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. 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. When the system permits advanced personal color overrides, a policy-authorized tenant administrator may inherit, allow, or block them; the tenant cannot enable a system-denied policy, and palette locks still suppress the editor. Advanced documents cover both light and dark modes and are validated atomically by Core. 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", "user"),
|
||||||
audience=("system_admin", "tenant_admin", "operator"),
|
audience=("system_admin", "tenant_admin", "operator"),
|
||||||
related_modules=("access", "admin", "audit"),
|
related_modules=("access", "admin", "audit"),
|
||||||
links=(
|
conditions=(
|
||||||
DocumentationLink(label="Tenant administration", href="/admin", kind="runtime"),
|
DocumentationCondition(
|
||||||
DocumentationLink(label="Tenant registry API", href="/api/v1/admin/tenants", kind="api"),
|
required_modules=("tenancy", "access"),
|
||||||
DocumentationLink(label="Tenant settings API", href="/api/v1/admin/tenant/settings", kind="api"),
|
any_scopes=(
|
||||||
|
"access:tenant:read",
|
||||||
|
"access:tenant:update",
|
||||||
|
"access:setting:read",
|
||||||
|
"access:setting:write",
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
links=(
|
||||||
|
DocumentationLink(
|
||||||
|
label="Tenant administration", href="/admin", kind="runtime"
|
||||||
|
),
|
||||||
|
DocumentationLink(
|
||||||
|
label="Tenant registry API",
|
||||||
|
href="/api/v1/admin/tenants",
|
||||||
|
kind="api",
|
||||||
|
),
|
||||||
|
DocumentationLink(
|
||||||
|
label="Tenant settings API",
|
||||||
|
href="/api/v1/admin/tenant/settings",
|
||||||
|
kind="api",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
translations={
|
||||||
|
"de": {
|
||||||
|
"title": "Mandantenlebenszyklus und -einstellungen administrieren",
|
||||||
|
"summary": (
|
||||||
|
"Tenancy ergänzt ausdrückliche Mandantenanlage, Aktivierung, Kontextauflösung und mandanteneigene Einstellungen über "
|
||||||
|
"Cores gemeinsamen Bereichsspeicher."
|
||||||
|
),
|
||||||
|
"body": (
|
||||||
|
"Ein Mandant ist eine konkrete Administrations- und Datengrenze. Änderungen am Mandantenlebenszyklus müssen Eigentums- "
|
||||||
|
"und Wiederherstellungsgarantien für modulbezogene Datensätze bewahren. Neue Mandanten verwenden standardmäßig die "
|
||||||
|
"deutsche Referenzsprache, sofern keine andere aktivierte Systemsprache gewählt wird; bestehende Mandanten- und "
|
||||||
|
"Benutzerpräferenzen bleiben unverändert. Mandantenadministrierende können systemweite Reihenfolge und Sichtbarkeit der "
|
||||||
|
"Seitenleiste erben oder überschreiben und Einträge für Benutzende sichtbar sperren; Systemsperren bleiben wirksam. "
|
||||||
|
"Persönliche Navigationspräferenzen behalten Vorrang, können gesperrte Einträge aber nicht ausblenden. Das Erscheinungsbild "
|
||||||
|
"erbt ebenfalls die Systempalette, bis es ausdrücklich gewählt wird. Ein ungesperrter Mandantenstandard erlaubt eine "
|
||||||
|
"persönliche Palette; eine richtlinienautorisierte Mandantensperre unterdrückt sie, und eine Systemsperre hat immer Vorrang. "
|
||||||
|
"Zurücksetzen stellt Vererbung wieder her, statt den aktuellen Systemwert zu kopieren. Erlaubt das System erweiterte "
|
||||||
|
"persönliche Farbanpassungen, darf eine richtlinienautorisierte Mandantenadministration sie erben, erlauben oder blockieren; "
|
||||||
|
"eine systemweite Ablehnung kann nicht gelockert werden und Palettensperren unterdrücken den Editor weiterhin. Erweiterte "
|
||||||
|
"Dokumente umfassen hellen und dunklen Modus und werden von Core atomar validiert. Navigation und Erscheinungsbild gewähren "
|
||||||
|
"niemals Modulberechtigung, View-Sichtbarkeit oder Zugriffsrechte. Tenancy trägt systemweite Mandantenverwaltung und "
|
||||||
|
"Mandanteneinstellungen zum gemeinsamen Admin-Arbeitsbereich bei; ohne das Modul können Core und Access in einem "
|
||||||
|
"Einzelbereichskompatibilitätsmodus arbeiten. Für Core reservierte Modulberechtigungseinstellungen werden ausschließlich "
|
||||||
|
"über Admins Mandanten-Modulrichtlinienendpunkte verwaltet und beim Ersetzen allgemeiner Mandanteneinstellungen bewahrt."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
metadata={
|
metadata={
|
||||||
"kind": "reference",
|
"kind": "workflow",
|
||||||
"help_contexts": [
|
"help_contexts": [
|
||||||
"tenancy.admin.system-tenants",
|
"tenancy.admin.system-tenants",
|
||||||
"tenancy.admin.tenant-settings",
|
"tenancy.admin.tenant-settings",
|
||||||
@@ -83,14 +152,39 @@ 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. 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"),
|
||||||
links=(
|
links=(
|
||||||
DocumentationLink(label="Tenant administration", href="/admin", kind="runtime"),
|
DocumentationLink(
|
||||||
DocumentationLink(label="Tenant registry API", href="/api/v1/admin/tenants", kind="api"),
|
label="Tenant administration", href="/admin", kind="runtime"
|
||||||
|
),
|
||||||
|
DocumentationLink(
|
||||||
|
label="Tenant registry API",
|
||||||
|
href="/api/v1/admin/tenants",
|
||||||
|
kind="api",
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
translations={
|
||||||
|
"de": {
|
||||||
|
"title": "Felder und Folgen der Mandantenadministration",
|
||||||
|
"summary": (
|
||||||
|
"Mandantenidentität, Eigentum, Sprache, Governance-Überschreibungen und Lebenszykluszustand haben unterschiedliche "
|
||||||
|
"Änderungs- und Wiederherstellungsfolgen."
|
||||||
|
),
|
||||||
|
"body": (
|
||||||
|
"Der Slug eines Mandanten ist nach der Anlage unveränderlich und bezeichnet die Administrationsgrenze. Der erste Owner "
|
||||||
|
"erhält die geschützte Tenant-Owner-Rolle. Deutsch ist Referenz und Standard für neue Mandanten; Spracheinstellung und "
|
||||||
|
"aktivierte Sprachen werden durch Systemsprachpakete begrenzt und können ausdrücklich geändert werden. Navigation und "
|
||||||
|
"Erscheinungsbild erben ihre Systemebenen, bis sie gespeichert werden. Paletten verwenden nur validierte Core-Vorgaben. "
|
||||||
|
"Eine Mandantensperre des Erscheinungsbilds verlangt Richtlinienschreibberechtigung, unterdrückt persönliche Paletten und "
|
||||||
|
"kann keine Systemsperre lockern. Governance-Überschreibungen dürfen eine Systemerlaubnis einschränken, aber eine "
|
||||||
|
"Systemablehnung nicht lockern. Eine Suspendierung bewahrt mandanteneigene Daten und Auditnachweise, verhindert jedoch die "
|
||||||
|
"normale Nutzung; die Betriebsperson muss vor der Suspendierung aus dem aktiven Mandanten wechseln."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
metadata={
|
metadata={
|
||||||
"kind": "reference",
|
"kind": "reference",
|
||||||
"help_contexts": [
|
"help_contexts": [
|
||||||
@@ -98,6 +192,7 @@ manifest = ModuleManifest(
|
|||||||
"tenancy.field.initial-owner",
|
"tenancy.field.initial-owner",
|
||||||
"tenancy.field.locale",
|
"tenancy.field.locale",
|
||||||
"tenancy.field.languages",
|
"tenancy.field.languages",
|
||||||
|
"tenancy.admin.tenant-settings",
|
||||||
"tenancy.field.governance",
|
"tenancy.field.governance",
|
||||||
"tenancy.action.suspend",
|
"tenancy.action.suspend",
|
||||||
],
|
],
|
||||||
@@ -135,9 +230,15 @@ manifest = ModuleManifest(
|
|||||||
maturity="vertical_slice",
|
maturity="vertical_slice",
|
||||||
documentation_ref="docs/TENANCY_MODULE_BOUNDARY.md",
|
documentation_ref="docs/TENANCY_MODULE_BOUNDARY.md",
|
||||||
test_ref="tests/test_tenant_lifecycle.py",
|
test_ref="tests/test_tenant_lifecycle.py",
|
||||||
known_limits=("Cross-region tenant relocation and complete major-version recovery evidence are not implemented.",),
|
known_limits=(
|
||||||
|
"Cross-region tenant relocation and complete major-version recovery evidence are not implemented.",
|
||||||
|
),
|
||||||
owned_concepts=("tenant lifecycle", "tenant context", "tenant settings"),
|
owned_concepts=("tenant lifecycle", "tenant context", "tenant settings"),
|
||||||
non_owned_concepts=("account authorization", "organization hierarchy", "module-owned tenant data"),
|
non_owned_concepts=(
|
||||||
|
"account authorization",
|
||||||
|
"organization hierarchy",
|
||||||
|
"module-owned tenant data",
|
||||||
|
),
|
||||||
recovery_docs=("docs/TENANCY_MODULE_BOUNDARY.md",),
|
recovery_docs=("docs/TENANCY_MODULE_BOUNDARY.md",),
|
||||||
security_docs=("docs/TENANCY_MODULE_BOUNDARY.md",),
|
security_docs=("docs/TENANCY_MODULE_BOUNDARY.md",),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,6 +6,14 @@ from govoplan_tenancy.backend.manifest import manifest
|
|||||||
|
|
||||||
|
|
||||||
class TenancyInterfaceDocumentationContractTests(unittest.TestCase):
|
class TenancyInterfaceDocumentationContractTests(unittest.TestCase):
|
||||||
|
def test_all_static_topics_have_complete_german_content(self) -> None:
|
||||||
|
for topic in manifest.documentation:
|
||||||
|
german = (topic.translations or {}).get("de", {})
|
||||||
|
self.assertEqual({"title", "summary", "body"}, set(german), topic.id)
|
||||||
|
self.assertTrue(
|
||||||
|
all(str(value).strip() for value in german.values()), topic.id
|
||||||
|
)
|
||||||
|
|
||||||
def test_tenancy_admin_surfaces_remain_declared(self) -> None:
|
def test_tenancy_admin_surfaces_remain_declared(self) -> None:
|
||||||
frontend = manifest.frontend
|
frontend = manifest.frontend
|
||||||
self.assertIsNotNone(frontend)
|
self.assertIsNotNone(frontend)
|
||||||
@@ -24,10 +32,18 @@ class TenancyInterfaceDocumentationContractTests(unittest.TestCase):
|
|||||||
self.assertIn("tenancy.lifecycle-and-settings", topics)
|
self.assertIn("tenancy.lifecycle-and-settings", topics)
|
||||||
self.assertIn("tenancy.reference.admin-fields", topics)
|
self.assertIn("tenancy.reference.admin-fields", topics)
|
||||||
|
|
||||||
lifecycle_contexts = set(topics["tenancy.lifecycle-and-settings"].metadata["help_contexts"])
|
lifecycle_contexts = set(
|
||||||
|
topics["tenancy.lifecycle-and-settings"].metadata["help_contexts"]
|
||||||
|
)
|
||||||
self.assertIn("tenancy.admin.system-tenants", lifecycle_contexts)
|
self.assertIn("tenancy.admin.system-tenants", lifecycle_contexts)
|
||||||
self.assertIn("tenancy.admin.tenant-settings", lifecycle_contexts)
|
self.assertIn("tenancy.admin.tenant-settings", lifecycle_contexts)
|
||||||
self.assertIn("tenancy.action.suspend", topics["tenancy.reference.admin-fields"].metadata["help_contexts"])
|
self.assertEqual(
|
||||||
|
"workflow", topics["tenancy.lifecycle-and-settings"].metadata["kind"]
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"tenancy.action.suspend",
|
||||||
|
topics["tenancy.reference.admin-fields"].metadata["help_contexts"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/tenancy-webui",
|
"name": "@govoplan/tenancy-webui",
|
||||||
"version": "0.1.18",
|
"version": "0.1.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type {
|
import type {
|
||||||
ApiSettings,
|
ApiSettings,
|
||||||
DeltaDeletedItem,
|
DeltaDeletedItem,
|
||||||
|
NavigationPreferences,
|
||||||
PrivacyRetentionPolicy,
|
PrivacyRetentionPolicy,
|
||||||
TenantAdminItem
|
TenantAdminItem,
|
||||||
|
UserUiPalette
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
apiFetch,
|
apiFetch,
|
||||||
@@ -41,6 +43,16 @@ export type TenantSettingsItem = {
|
|||||||
available_languages: LanguagePackage[];
|
available_languages: LanguagePackage[];
|
||||||
system_enabled_language_codes: string[];
|
system_enabled_language_codes: string[];
|
||||||
enabled_language_codes: string[];
|
enabled_language_codes: string[];
|
||||||
|
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";
|
||||||
|
appearance_custom_overrides_allowed: boolean | null;
|
||||||
|
system_appearance_custom_overrides_allowed: boolean;
|
||||||
|
effective_appearance_custom_overrides_allowed: boolean;
|
||||||
settings: Record<string, unknown>;
|
settings: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,6 +63,8 @@ export type TenantSettingsDeltaSections = Partial<{
|
|||||||
TenantSettingsItem,
|
TenantSettingsItem,
|
||||||
"available_languages" | "system_enabled_language_codes" | "enabled_language_codes"
|
"available_languages" | "system_enabled_language_codes" | "enabled_language_codes"
|
||||||
>;
|
>;
|
||||||
|
navigation: TenantSettingsItem["navigation"];
|
||||||
|
appearance: Pick<TenantSettingsItem, "appearance_palette" | "appearance_palette_locked" | "system_appearance_palette" | "system_appearance_palette_locked" | "effective_appearance_palette" | "effective_appearance_source" | "appearance_custom_overrides_allowed" | "system_appearance_custom_overrides_allowed" | "effective_appearance_custom_overrides_allowed">;
|
||||||
settings: TenantSettingsItem["settings"];
|
settings: TenantSettingsItem["settings"];
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
@@ -147,6 +161,10 @@ export function updateTenantSettings(
|
|||||||
payload: {
|
payload: {
|
||||||
default_locale: string;
|
default_locale: string;
|
||||||
enabled_language_codes?: string[] | null;
|
enabled_language_codes?: string[] | null;
|
||||||
|
navigation?: NavigationPreferences | null;
|
||||||
|
appearance_palette?: UserUiPalette | null;
|
||||||
|
appearance_palette_locked?: boolean;
|
||||||
|
appearance_custom_overrides_allowed?: boolean | null;
|
||||||
}
|
}
|
||||||
): Promise<TenantSettingsItem> {
|
): Promise<TenantSettingsItem> {
|
||||||
return apiFetch(settings, "/api/v1/admin/tenant/settings", {
|
return apiFetch(settings, "/api/v1/admin/tenant/settings", {
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
import {
|
||||||
|
DescriptionList,
|
||||||
|
AppearancePalettePreview,
|
||||||
|
AppearancePaletteSelect,
|
||||||
|
NavigationPreferenceEditor,
|
||||||
|
configurableNavigationItemsForModules,
|
||||||
|
dispatchPlatformModulesChanged,
|
||||||
|
usePlatformModules
|
||||||
|
} from "@govoplan/core-webui";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActionBlockerHint,
|
ActionBlockerHint,
|
||||||
@@ -7,6 +16,7 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
DocumentationHelpLink,
|
DocumentationHelpLink,
|
||||||
FormField,
|
FormField,
|
||||||
|
ToggleSwitch,
|
||||||
adminErrorMessage,
|
adminErrorMessage,
|
||||||
useDeltaWatermarks,
|
useDeltaWatermarks,
|
||||||
useUnsavedChanges,
|
useUnsavedChanges,
|
||||||
@@ -34,19 +44,32 @@ 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"],
|
||||||
settings: {}
|
navigation: null,
|
||||||
|
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",
|
||||||
|
appearance_custom_overrides_allowed: null,
|
||||||
|
system_appearance_custom_overrides_allowed: false,
|
||||||
|
effective_appearance_custom_overrides_allowed: false
|
||||||
};
|
};
|
||||||
|
|
||||||
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 navigationItems = configurableNavigationItemsForModules(modules);
|
||||||
const [draft, setDraft] = useState<TenantSettingsItem>(fallback);
|
const [draft, setDraft] = useState<TenantSettingsItem>(fallback);
|
||||||
const [savedDraft, setSavedDraft] = useState<TenantSettingsItem>(fallback);
|
const [savedDraft, setSavedDraft] = useState<TenantSettingsItem>(fallback);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -56,6 +79,11 @@ export default function TenantSettingsPanel({
|
|||||||
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
|
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
|
||||||
const defaultLocaleOptions = localeOptions(draft.default_locale, draft.enabled_language_codes);
|
const defaultLocaleOptions = localeOptions(draft.default_locale, draft.enabled_language_codes);
|
||||||
const dirty = tenantSettingsDraftKey(draft) !== tenantSettingsDraftKey(savedDraft);
|
const dirty = tenantSettingsDraftKey(draft) !== tenantSettingsDraftKey(savedDraft);
|
||||||
|
const customOverridesEffectivelyAllowed =
|
||||||
|
draft.system_appearance_custom_overrides_allowed
|
||||||
|
&& draft.appearance_custom_overrides_allowed !== false
|
||||||
|
&& !draft.system_appearance_palette_locked
|
||||||
|
&& !draft.appearance_palette_locked;
|
||||||
const saveDisabledReason = tenantMutationDisabledReason({
|
const saveDisabledReason = tenantMutationDisabledReason({
|
||||||
busy,
|
busy,
|
||||||
permitted: canWrite,
|
permitted: canWrite,
|
||||||
@@ -102,10 +130,18 @@ export default function TenantSettingsPanel({
|
|||||||
setError("");
|
setError("");
|
||||||
setSuccess("");
|
setSuccess("");
|
||||||
try {
|
try {
|
||||||
const saved = await updateTenantSettings(settings, { default_locale: draft.default_locale, enabled_language_codes: draft.enabled_language_codes });
|
const saved = await updateTenantSettings(settings, {
|
||||||
|
default_locale: draft.default_locale,
|
||||||
|
enabled_language_codes: draft.enabled_language_codes,
|
||||||
|
navigation: draft.navigation,
|
||||||
|
appearance_palette: draft.appearance_palette,
|
||||||
|
appearance_palette_locked: draft.appearance_palette_locked,
|
||||||
|
appearance_custom_overrides_allowed: draft.appearance_custom_overrides_allowed
|
||||||
|
});
|
||||||
setDraft(saved);
|
setDraft(saved);
|
||||||
setSavedDraft(saved);
|
setSavedDraft(saved);
|
||||||
resetDeltaWatermark(DELTA_KEY);
|
resetDeltaWatermark(DELTA_KEY);
|
||||||
|
dispatchPlatformModulesChanged();
|
||||||
setSuccess("i18n:govoplan-tenancy.tenant_general_settings_saved.485e7681");
|
setSuccess("i18n:govoplan-tenancy.tenant_general_settings_saved.485e7681");
|
||||||
await onAuthRefresh();
|
await onAuthRefresh();
|
||||||
return true;
|
return true;
|
||||||
@@ -167,11 +203,73 @@ export default function TenantSettingsPanel({
|
|||||||
onChange={setEnabledLanguages}
|
onChange={setEnabledLanguages}
|
||||||
/>
|
/>
|
||||||
<p className="muted small-note"><span>i18n:govoplan-tenancy.tenant_languages_help</span>{" "}<span>{TENANCY_INTERFACE_I18N.defaultLanguageRequired}</span></p>
|
<p className="muted small-note"><span>i18n:govoplan-tenancy.tenant_languages_help</span>{" "}<span>{TENANCY_INTERFACE_I18N.defaultLanguageRequired}</span></p>
|
||||||
<dl className="detail-list">
|
<DescriptionList variant="inline">
|
||||||
<div><dt>i18n:govoplan-tenancy.tenant.3ca93c78</dt><dd>{draft.name || "-"}</dd></div>
|
<div><dt>i18n:govoplan-tenancy.tenant.3ca93c78</dt><dd>{draft.name || "-"}</dd></div>
|
||||||
<div><dt>i18n:govoplan-tenancy.slug.094da9b9</dt><dd>{draft.slug || "-"}</dd></div>
|
<div><dt>i18n:govoplan-tenancy.slug.094da9b9</dt><dd>{draft.slug || "-"}</dd></div>
|
||||||
<div><dt>i18n:govoplan-tenancy.available.7c62a142</dt><dd>{draft.available_languages.map((item) => item.code.toUpperCase()).join(", ") || "-"}</dd></div>
|
<div><dt>i18n:govoplan-tenancy.available.7c62a142</dt><dd>{draft.available_languages.map((item) => item.code.toUpperCase()).join(", ") || "-"}</dd></div>
|
||||||
</dl>
|
</DescriptionList>
|
||||||
|
</Card>
|
||||||
|
<Card title="Tenant navigation order">
|
||||||
|
<NavigationPreferenceEditor
|
||||||
|
items={navigationItems}
|
||||||
|
value={draft.navigation}
|
||||||
|
scope="tenant"
|
||||||
|
disabled={!canWrite || busy}
|
||||||
|
onChange={(navigation) => setDraft({ ...draft, navigation })}
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
label="i18n:govoplan-tenancy.custom_overrides_policy"
|
||||||
|
help={!canWritePolicy ? "i18n:govoplan-tenancy.appearance_policy_permission" : "i18n:govoplan-tenancy.custom_overrides_policy_help"}
|
||||||
|
>
|
||||||
|
<select
|
||||||
|
value={draft.appearance_custom_overrides_allowed === null ? "inherit" : draft.appearance_custom_overrides_allowed ? "allow" : "block"}
|
||||||
|
disabled={!canWrite || !canWritePolicy || busy}
|
||||||
|
onChange={(event) => {
|
||||||
|
const appearance_custom_overrides_allowed = event.target.value === "inherit" ? null : event.target.value === "allow";
|
||||||
|
setDraft({
|
||||||
|
...draft,
|
||||||
|
appearance_custom_overrides_allowed,
|
||||||
|
effective_appearance_custom_overrides_allowed:
|
||||||
|
draft.system_appearance_custom_overrides_allowed
|
||||||
|
&& appearance_custom_overrides_allowed !== false
|
||||||
|
&& !draft.system_appearance_palette_locked
|
||||||
|
&& !draft.appearance_palette_locked
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="inherit">i18n:govoplan-tenancy.inherit_system_policy</option>
|
||||||
|
<option value="allow" disabled={!draft.system_appearance_custom_overrides_allowed}>i18n:govoplan-tenancy.allow_for_tenant_users</option>
|
||||||
|
<option value="block">i18n:govoplan-tenancy.block_for_tenant_users</option>
|
||||||
|
</select>
|
||||||
|
</FormField>
|
||||||
|
<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>
|
||||||
|
<div><dt>i18n:govoplan-tenancy.advanced_color_overrides</dt><dd>{customOverridesEffectivelyAllowed ? "i18n:govoplan-tenancy.allowed" : "i18n:govoplan-tenancy.blocked_by_policy"}</dd></div>
|
||||||
|
</DescriptionList>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
</AdminPageLayout>);
|
</AdminPageLayout>);
|
||||||
@@ -189,7 +287,11 @@ function localeOptions(current: string, enabled: string[]): string[] {
|
|||||||
function tenantSettingsDraftKey(item: TenantSettingsItem): string {
|
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,
|
||||||
|
appearance_palette: item.appearance_palette,
|
||||||
|
appearance_palette_locked: item.appearance_palette_locked,
|
||||||
|
appearance_custom_overrides_allowed: item.appearance_custom_overrides_allowed
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +301,8 @@ function applyTenantSettingsSections(item: TenantSettingsItem, sections: TenantS
|
|||||||
...(sections.identity ?? {}),
|
...(sections.identity ?? {}),
|
||||||
...(sections.locale ?? {}),
|
...(sections.locale ?? {}),
|
||||||
...(sections.languages ?? {}),
|
...(sections.languages ?? {}),
|
||||||
|
...(sections.navigation !== undefined ? { navigation: sections.navigation } : {}),
|
||||||
|
...(sections.appearance ?? {}),
|
||||||
...(sections.settings ? { settings: sections.settings } : {})
|
...(sections.settings ? { settings: sections.settings } : {})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { DescriptionItem, DescriptionList } from "@govoplan/core-webui";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Pencil, Plus, Search, Trash2 } from "lucide-react";
|
import { Pencil, Plus, Search, Trash2 } from "lucide-react";
|
||||||
import type { ApiSettings, AuthInfo, TenantAdminItem } from "@govoplan/core-webui";
|
import type { FormGrid, ApiSettings, AuthInfo, TenantAdminItem } from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
ActionBlockerHint,
|
ActionBlockerHint,
|
||||||
AdminIconButton,
|
AdminIconButton,
|
||||||
@@ -294,35 +295,35 @@ export default function TenantsPanel({
|
|||||||
<div className="admin-table-surface"><DataGrid id="admin-tenants-v3" rows={tenants} columns={columns} initialFit="container" getRowKey={(row) => row.id} emptyText="i18n:govoplan-tenancy.no_tenants_found.72d04cf4" /></div>
|
<div className="admin-table-surface"><DataGrid id="admin-tenants-v3" rows={tenants} columns={columns} initialFit="container" getRowKey={(row) => row.id} emptyText="i18n:govoplan-tenancy.no_tenants_found.72d04cf4" /></div>
|
||||||
</AdminPageLayout>
|
</AdminPageLayout>
|
||||||
|
|
||||||
<Dialog open={editing !== null} title={editing === "new" ? "i18n:govoplan-tenancy.create_tenant.4dbd55d9" : "i18n:govoplan-tenancy.edit_tenant.e2ba43f9"} onClose={requestCloseEditor} className="admin-dialog admin-dialog-wide" footer={<><Button onClick={requestCloseEditor} disabled={busy} disabledReason={busy ? TENANCY_INTERFACE_I18N.busy : undefined}>i18n:govoplan-tenancy.cancel.77dfd213</Button><Button variant="primary" onClick={() => void save()} disabled={Boolean(saveDisabledReason)} disabledReason={saveDisabledReason}>{busy ? "i18n:govoplan-tenancy.saving.56a2285c" : "i18n:govoplan-tenancy.save_tenant.9eb2ac74"}</Button></>}>
|
<Dialog variant="administration" size="wide" open={editing !== null} title={editing === "new" ? "i18n:govoplan-tenancy.create_tenant.4dbd55d9" : "i18n:govoplan-tenancy.edit_tenant.e2ba43f9"} onClose={requestCloseEditor} className="" footer={<><Button onClick={requestCloseEditor} disabled={busy} disabledReason={busy ? TENANCY_INTERFACE_I18N.busy : undefined}>i18n:govoplan-tenancy.cancel.77dfd213</Button><Button variant="primary" onClick={() => void save()} disabled={Boolean(saveDisabledReason)} disabledReason={saveDisabledReason}>{busy ? "i18n:govoplan-tenancy.saving.56a2285c" : "i18n:govoplan-tenancy.save_tenant.9eb2ac74"}</Button></>}>
|
||||||
<div className="admin-form-grid two-columns">
|
<FormGrid columns={2} gap="small" collapseAt="workspace" className="">
|
||||||
<FormField label="i18n:govoplan-tenancy.name.709a2322" help={editing !== "new" && !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined} documentation={TENANCY_FIELD_DOCUMENTATION}><input value={draft.name} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
|
<FormField label="i18n:govoplan-tenancy.name.709a2322" help={editing !== "new" && !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined} documentation={TENANCY_FIELD_DOCUMENTATION}><input value={draft.name} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
|
||||||
<FormField label="i18n:govoplan-tenancy.slug.094da9b9" help={editing !== "new" ? "i18n:govoplan-tenancy.the_tenant_slug_is_immutable_after_creation.7c9a1025" : !canCreate ? TENANCY_INTERFACE_I18N.createRequired : undefined} documentation={TENANCY_FIELD_DOCUMENTATION}><input value={draft.slug} disabled={editing !== "new" || !canCreate} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} /></FormField>
|
<FormField label="i18n:govoplan-tenancy.slug.094da9b9" help={editing !== "new" ? "i18n:govoplan-tenancy.the_tenant_slug_is_immutable_after_creation.7c9a1025" : !canCreate ? TENANCY_INTERFACE_I18N.createRequired : undefined} documentation={TENANCY_FIELD_DOCUMENTATION}><input value={draft.slug} disabled={editing !== "new" || !canCreate} onChange={(event) => setDraft({ ...draft, slug: event.target.value })} /></FormField>
|
||||||
{editing === "new" && <FormField label="i18n:govoplan-tenancy.initial_tenant_owner.682291a9" documentation={TENANCY_FIELD_DOCUMENTATION}><select value={draft.ownerAccountId} onChange={(event) => setDraft({ ...draft, ownerAccountId: event.target.value })}>{ownerCandidates.map((candidate) => <option key={candidate.account_id} value={candidate.account_id}>{candidate.display_name ? i18nMessage("i18n:govoplan-tenancy.value_value.c189e8bc", { value0: candidate.display_name, value1: candidate.email }) : candidate.email}</option>)}</select></FormField>}
|
{editing === "new" && <FormField label="i18n:govoplan-tenancy.initial_tenant_owner.682291a9" documentation={TENANCY_FIELD_DOCUMENTATION}><select value={draft.ownerAccountId} onChange={(event) => setDraft({ ...draft, ownerAccountId: event.target.value })}>{ownerCandidates.map((candidate) => <option key={candidate.account_id} value={candidate.account_id}>{candidate.display_name ? i18nMessage("i18n:govoplan-tenancy.value_value.c189e8bc", { value0: candidate.display_name, value1: candidate.email }) : candidate.email}</option>)}</select></FormField>}
|
||||||
<FormField label="i18n:govoplan-tenancy.default_locale.b99d021f" help={editing !== "new" && !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined} documentation={TENANCY_FIELD_DOCUMENTATION}><input value={draft.defaultLocale} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, defaultLocale: event.target.value })} /></FormField>
|
<FormField label="i18n:govoplan-tenancy.default_locale.b99d021f" help={editing !== "new" && !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined} documentation={TENANCY_FIELD_DOCUMENTATION}><input value={draft.defaultLocale} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, defaultLocale: event.target.value })} /></FormField>
|
||||||
{editing !== "new" && <FormField label="i18n:govoplan-tenancy.status.bae7d5be" help={!canSuspend ? TENANCY_INTERFACE_I18N.suspendRequired : undefined} documentation={TENANCY_ADMIN_DOCUMENTATION}><select value={draft.isActive ? "active" : "inactive"} disabled={!canSuspend} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-tenancy.active.a733b809</option><option value="inactive">i18n:govoplan-tenancy.suspended.794696a7</option></select></FormField>}
|
{editing !== "new" && <FormField label="i18n:govoplan-tenancy.status.bae7d5be" help={!canSuspend ? TENANCY_INTERFACE_I18N.suspendRequired : undefined} documentation={TENANCY_ADMIN_DOCUMENTATION}><select value={draft.isActive ? "active" : "inactive"} disabled={!canSuspend} onChange={(event) => setDraft({ ...draft, isActive: event.target.value === "active" })}><option value="active">i18n:govoplan-tenancy.active.a733b809</option><option value="inactive">i18n:govoplan-tenancy.suspended.794696a7</option></select></FormField>}
|
||||||
<FormField label="i18n:govoplan-tenancy.description.55f8ebc8" help={editing !== "new" && !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined} documentation={TENANCY_FIELD_DOCUMENTATION}><textarea rows={4} value={draft.description} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
|
<FormField label="i18n:govoplan-tenancy.description.55f8ebc8" help={editing !== "new" && !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined} documentation={TENANCY_FIELD_DOCUMENTATION}><textarea rows={4} value={draft.description} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, description: event.target.value })} /></FormField>
|
||||||
</div>
|
</FormGrid>
|
||||||
<h3>i18n:govoplan-tenancy.system_governance_overrides.97cdf3ce</h3>
|
<h3>i18n:govoplan-tenancy.system_governance_overrides.97cdf3ce</h3>
|
||||||
<div className="admin-form-grid two-columns">
|
<FormGrid columns={2} gap="small" collapseAt="workspace" className="">
|
||||||
<GovernanceSelect disabled={editing !== "new" && !canUpdate} disabledReason={editing !== "new" && !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined} allowDisabled={!systemAllowsCustomGroups} label="i18n:govoplan-tenancy.custom_tenant_groups.570ee603" value={draft.customGroups} onChange={(customGroups) => setDraft({ ...draft, customGroups })} />
|
<GovernanceSelect disabled={editing !== "new" && !canUpdate} disabledReason={editing !== "new" && !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined} allowDisabled={!systemAllowsCustomGroups} label="i18n:govoplan-tenancy.custom_tenant_groups.570ee603" value={draft.customGroups} onChange={(customGroups) => setDraft({ ...draft, customGroups })} />
|
||||||
<GovernanceSelect disabled={editing !== "new" && !canUpdate} disabledReason={editing !== "new" && !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined} allowDisabled={!systemAllowsCustomRoles} label="i18n:govoplan-tenancy.custom_tenant_roles.a738c37c" value={draft.customRoles} onChange={(customRoles) => setDraft({ ...draft, customRoles })} />
|
<GovernanceSelect disabled={editing !== "new" && !canUpdate} disabledReason={editing !== "new" && !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined} allowDisabled={!systemAllowsCustomRoles} label="i18n:govoplan-tenancy.custom_tenant_roles.a738c37c" value={draft.customRoles} onChange={(customRoles) => setDraft({ ...draft, customRoles })} />
|
||||||
<GovernanceSelect disabled={editing !== "new" && !canUpdate} disabledReason={editing !== "new" && !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined} allowDisabled={!systemAllowsApiKeys} label="i18n:govoplan-tenancy.tenant_api_keys.4b1d81f8" value={draft.apiKeys} onChange={(apiKeys) => setDraft({ ...draft, apiKeys })} />
|
<GovernanceSelect disabled={editing !== "new" && !canUpdate} disabledReason={editing !== "new" && !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined} allowDisabled={!systemAllowsApiKeys} label="i18n:govoplan-tenancy.tenant_api_keys.4b1d81f8" value={draft.apiKeys} onChange={(apiKeys) => setDraft({ ...draft, apiKeys })} />
|
||||||
</div>
|
</FormGrid>
|
||||||
<p className="muted small-note">i18n:govoplan-tenancy.inherit_follows_the_current_system_setting_expli.60d4d868</p>
|
<p className="muted small-note">i18n:govoplan-tenancy.inherit_follows_the_current_system_setting_expli.60d4d868</p>
|
||||||
{systemDeniedGovernance && <p className="muted small-note"><span>i18n:govoplan-tenancy.explicit_allow_is_unavailable_for.8d05fd4a</span>{" "}{systemDeniedGovernance}{" "}<span>i18n:govoplan-tenancy.because_the_current_system_setting_denies_it.3f59c244</span></p>}
|
{systemDeniedGovernance && <p className="muted small-note"><span>i18n:govoplan-tenancy.explicit_allow_is_unavailable_for.8d05fd4a</span>{" "}{systemDeniedGovernance}{" "}<span>i18n:govoplan-tenancy.because_the_current_system_setting_denies_it.3f59c244</span></p>}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={Boolean(viewing)} title="i18n:govoplan-tenancy.tenant_details.5976ba72" onClose={() => setViewing(null)} className="admin-dialog admin-dialog-wide" footer={<Button onClick={() => setViewing(null)}>i18n:govoplan-tenancy.close.bbfa773e</Button>}>
|
<Dialog variant="administration" size="wide" open={Boolean(viewing)} title="i18n:govoplan-tenancy.tenant_details.5976ba72" onClose={() => setViewing(null)} className="" footer={<Button onClick={() => setViewing(null)}>i18n:govoplan-tenancy.close.bbfa773e</Button>}>
|
||||||
{viewing && <><dl className="admin-details-grid">
|
{viewing && <><DescriptionList>
|
||||||
<div><dt>i18n:govoplan-tenancy.tenant.3ca93c78</dt><dd>{viewing.name}</dd></div><div><dt>i18n:govoplan-tenancy.slug.094da9b9</dt><dd>{viewing.slug}</dd></div>
|
<DescriptionItem term={<>i18n:govoplan-tenancy.tenant.3ca93c78</>}>{viewing.name}</DescriptionItem><DescriptionItem term={<>i18n:govoplan-tenancy.slug.094da9b9</>}>{viewing.slug}</DescriptionItem>
|
||||||
<div><dt>i18n:govoplan-tenancy.status.bae7d5be</dt><dd>{viewing.is_active ? "i18n:govoplan-tenancy.active.a733b809" : "i18n:govoplan-tenancy.suspended.794696a7"}</dd></div><div><dt>i18n:govoplan-tenancy.default_locale.b99d021f</dt><dd>{viewing.default_locale}</dd></div>
|
<DescriptionItem term={<>i18n:govoplan-tenancy.status.bae7d5be</>}>{viewing.is_active ? "i18n:govoplan-tenancy.active.a733b809" : "i18n:govoplan-tenancy.suspended.794696a7"}</DescriptionItem><DescriptionItem term={<>i18n:govoplan-tenancy.default_locale.b99d021f</>}>{viewing.default_locale}</DescriptionItem>
|
||||||
<div><dt>i18n:govoplan-tenancy.created.accf40c8</dt><dd>{formatDateTime(viewing.created_at)}</dd></div><div><dt>i18n:govoplan-tenancy.updated.f2f8570d</dt><dd>{formatDateTime(viewing.updated_at)}</dd></div>
|
<DescriptionItem term={<>i18n:govoplan-tenancy.created.accf40c8</>}>{formatDateTime(viewing.created_at)}</DescriptionItem><DescriptionItem term={<>i18n:govoplan-tenancy.updated.f2f8570d</>}>{formatDateTime(viewing.updated_at)}</DescriptionItem>
|
||||||
<div><dt>i18n:govoplan-tenancy.custom_groups.1f7b7c8f</dt><dd>{viewing.effective_governance.allow_custom_groups ? "i18n:govoplan-tenancy.allowed.77c7b490" : "i18n:govoplan-tenancy.denied.63b16bd4"} ({overrideLabel(fromOverride(viewing.allow_custom_groups))})</dd></div>
|
<DescriptionItem term={<>i18n:govoplan-tenancy.custom_groups.1f7b7c8f</>}>{viewing.effective_governance.allow_custom_groups ? "i18n:govoplan-tenancy.allowed.77c7b490" : "i18n:govoplan-tenancy.denied.63b16bd4"} ({overrideLabel(fromOverride(viewing.allow_custom_groups))})</DescriptionItem>
|
||||||
<div><dt>i18n:govoplan-tenancy.custom_roles.e78ef63d</dt><dd>{viewing.effective_governance.allow_custom_roles ? "i18n:govoplan-tenancy.allowed.77c7b490" : "i18n:govoplan-tenancy.denied.63b16bd4"} ({overrideLabel(fromOverride(viewing.allow_custom_roles))})</dd></div>
|
<DescriptionItem term={<>i18n:govoplan-tenancy.custom_roles.e78ef63d</>}>{viewing.effective_governance.allow_custom_roles ? "i18n:govoplan-tenancy.allowed.77c7b490" : "i18n:govoplan-tenancy.denied.63b16bd4"} ({overrideLabel(fromOverride(viewing.allow_custom_roles))})</DescriptionItem>
|
||||||
<div><dt>i18n:govoplan-tenancy.api_keys.94fcf3c2</dt><dd>{viewing.effective_governance.allow_api_keys ? "i18n:govoplan-tenancy.allowed.77c7b490" : "i18n:govoplan-tenancy.denied.63b16bd4"} ({overrideLabel(fromOverride(viewing.allow_api_keys))})</dd></div>
|
<DescriptionItem term={<>i18n:govoplan-tenancy.api_keys.94fcf3c2</>}>{viewing.effective_governance.allow_api_keys ? "i18n:govoplan-tenancy.allowed.77c7b490" : "i18n:govoplan-tenancy.denied.63b16bd4"} ({overrideLabel(fromOverride(viewing.allow_api_keys))})</DescriptionItem>
|
||||||
<div><dt>i18n:govoplan-tenancy.objects.72a83add</dt><dd>{viewing.counts.users ?? 0}{" "}<span>i18n:govoplan-tenancy.users.81651889</span>{" "}{viewing.counts.groups ?? 0}{" "}<span>i18n:govoplan-tenancy.groups.07551586</span>{" "}{viewing.counts.campaigns ?? 0}{" "}<span>i18n:govoplan-tenancy.campaigns.2282ffeb</span>{" "}{viewing.counts.files ?? 0}{" "}<span>i18n:govoplan-tenancy.files_lowercase.7c9a1026</span></dd></div>
|
<DescriptionItem term={<>i18n:govoplan-tenancy.objects.72a83add</>}>{viewing.counts.users ?? 0}{" "}<span>i18n:govoplan-tenancy.users.81651889</span>{" "}{viewing.counts.groups ?? 0}{" "}<span>i18n:govoplan-tenancy.groups.07551586</span>{" "}{viewing.counts.campaigns ?? 0}{" "}<span>i18n:govoplan-tenancy.campaigns.2282ffeb</span>{" "}{viewing.counts.files ?? 0}{" "}<span>i18n:govoplan-tenancy.files_lowercase.7c9a1026</span></DescriptionItem>
|
||||||
</dl>{viewing.description && <p>{viewing.description}</p>}</>}
|
</DescriptionList>{viewing.description && <p>{viewing.description}</p>}</>}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<ConfirmDialog open={Boolean(confirmSuspend)} title="i18n:govoplan-tenancy.suspend_tenant.151d283a" message={i18nMessage("i18n:govoplan-tenancy.suspend_value_existing_data_remains_retained_but.19bccd78", { value0: confirmSuspend?.name })} confirmLabel="i18n:govoplan-tenancy.suspend_tenant.151d283a" tone="danger" busy={busy} onCancel={() => setConfirmSuspend(null)} onConfirm={() => void suspend()} />
|
<ConfirmDialog open={Boolean(confirmSuspend)} title="i18n:govoplan-tenancy.suspend_tenant.151d283a" message={i18nMessage("i18n:govoplan-tenancy.suspend_value_existing_data_remains_retained_but.19bccd78", { value0: confirmSuspend?.name })} confirmLabel="i18n:govoplan-tenancy.suspend_tenant.151d283a" tone="danger" busy={busy} onCancel={() => setConfirmSuspend(null)} onConfirm={() => void suspend()} />
|
||||||
|
|||||||
@@ -2,6 +2,26 @@ 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.custom_overrides_policy": "Personal color override policy",
|
||||||
|
"i18n:govoplan-tenancy.custom_overrides_policy_help": "Inherit the system decision, or explicitly allow or block the validated advanced editor for this tenant.",
|
||||||
|
"i18n:govoplan-tenancy.appearance_policy_permission": "Policy-write permission is required to change this appearance policy.",
|
||||||
|
"i18n:govoplan-tenancy.inherit_system_policy": "Inherit system policy",
|
||||||
|
"i18n:govoplan-tenancy.allow_for_tenant_users": "Allow for tenant users",
|
||||||
|
"i18n:govoplan-tenancy.block_for_tenant_users": "Block for tenant users",
|
||||||
|
"i18n:govoplan-tenancy.advanced_color_overrides": "Advanced color overrides",
|
||||||
|
"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 +121,26 @@ 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.custom_overrides_policy": "Richtlinie für persönliche Farbanpassungen",
|
||||||
|
"i18n:govoplan-tenancy.custom_overrides_policy_help": "Die Systementscheidung übernehmen oder den geprüften erweiterten Editor für diesen Mandanten ausdrücklich zulassen oder sperren.",
|
||||||
|
"i18n:govoplan-tenancy.appearance_policy_permission": "Zum Ändern dieser Darstellungsrichtlinie ist das Recht zum Bearbeiten von Richtlinien erforderlich.",
|
||||||
|
"i18n:govoplan-tenancy.inherit_system_policy": "Systemrichtlinie übernehmen",
|
||||||
|
"i18n:govoplan-tenancy.allow_for_tenant_users": "Für Mandantenbenutzer zulassen",
|
||||||
|
"i18n:govoplan-tenancy.block_for_tenant_users": "Für Mandantenbenutzer sperren",
|
||||||
|
"i18n:govoplan-tenancy.advanced_color_overrides": "Erweiterte Farbanpassungen",
|
||||||
|
"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.",
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user