Migrate Tenancy interface patterns

This commit is contained in:
2026-08-03 11:42:34 +02:00
parent 2ab89eb809
commit e76fe16870
11 changed files with 453 additions and 92 deletions
+6
View File
@@ -19,3 +19,9 @@ The `@govoplan/tenancy-webui` package contributes `system-tenants` and
module owns the `/admin` shell but does not import these panels. Historical
`access.admin.*` surface identifiers remain stable so existing saved Views keep
working after the ownership move.
The tenant registry and active-tenant settings follow the shared interface
pattern contract documented in
[`docs/INTERFACE_PATTERN_MIGRATION.md`](docs/INTERFACE_PATTERN_MIGRATION.md).
Manifest-provided documentation topics back contextual help for tenant fields,
governance limits, permission blockers, and lifecycle consequences.
+43
View File
@@ -0,0 +1,43 @@
# Tenancy Interface Pattern Migration
This document records the bounded migration of Tenancy-owned WebUI surfaces to
the GovOPlaN interface pattern language. Core owns the shared components and the
Admin host; Tenancy owns the behavior and documentation described here.
## Surface Inventory
| Surface | Archetype | Consequence class | Contract |
| --- | --- | --- | --- |
| `tenancy.admin.system-tenants` | Administration directory and list-detail | Create, configure, suspend | Shared admin layout, DataGrid, stable row actions, adaptive create/edit dialog, lifecycle confirmation, contextual help |
| Tenant details dialog | Read-only evidence/detail | None | Stable labels, effective governance provenance, retained object counts |
| `tenancy.admin.tenant-settings` | Effective configuration | Configure | Shared admin layout, language selection, dirty-state guard, permission blocker, contextual help |
## Consequence And Availability Rules
- Creating a tenant establishes a new data and administration boundary and
provisions a protected initial owner.
- Tenant slugs are immutable after creation.
- System policy caps tenant governance overrides. A tenant can narrow an
allowance but cannot loosen a system denial.
- Suspension retains tenant-owned records and audit evidence. The active
tenant cannot be suspended until the operator changes context.
- Unavailable actions remain visible when they belong to the surface and state
the missing permission, inapplicable state, responsible actor, and
destination where applicable.
- Dirty dialogs and settings use the shared unsaved-change guard. Consequential
suspension continues to use the shared destructive confirmation dialog.
## State And Accessibility Evidence
The panels use Core loading, error, success, empty, disabled-action, blocker,
dialog, and status components. Row actions reserve a stable three-action area,
retain translated accessible labels, and do not disappear for row-specific
permission or lifecycle states. Dialog order follows identity, ownership,
locale/status, description, and governed capabilities. Shared dialogs own focus
containment and restoration, and the existing Admin shell provides responsive
composition.
Stable help references are contributed through the module manifest for the
tenant registry, tenant settings, lifecycle actions, and individual fields.
The WebUI structural test and backend documentation-contract test prevent those
references and state explanations from silently regressing.
+48 -3
View File
@@ -6,7 +6,7 @@ from govoplan_core.core.access import (
CAPABILITY_AUTH_TENANT_CONTEXT_SWITCHER,
CAPABILITY_TENANCY_TENANT_RESOLVER,
)
from govoplan_core.core.modules import DocumentationTopic, FrontendModule, ModuleContext, ModuleManifest
from govoplan_core.core.modules import DocumentationLink, DocumentationTopic, FrontendModule, ModuleContext, ModuleManifest
from govoplan_core.core.provider_governance import declared_module_architecture
from govoplan_core.core.views import ViewSurface
@@ -51,7 +51,10 @@ manifest = ModuleManifest(
documentation_types=("user",),
audience=("user", "tenant_admin"),
related_modules=("access",),
metadata={"kind": "reference"},
metadata={
"kind": "reference",
"help_contexts": ["tenancy.current-context", "tenancy.selector"],
},
),
DocumentationTopic(
id="tenancy.lifecycle-and-settings",
@@ -61,7 +64,49 @@ manifest = ModuleManifest(
documentation_types=("admin",),
audience=("system_admin", "tenant_admin", "operator"),
related_modules=("access", "admin", "audit"),
metadata={"kind": "reference"},
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"),
),
metadata={
"kind": "reference",
"help_contexts": [
"tenancy.admin.system-tenants",
"tenancy.admin.tenant-settings",
"tenancy.admin.lifecycle",
"tenancy.admin.blocked",
],
},
),
DocumentationTopic(
id="tenancy.reference.admin-fields",
title="Tenant administration fields and 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. Locale and enabled languages are bounded by system language packages. 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",),
audience=("system_admin", "tenant_admin", "operator"),
related_modules=("access", "admin", "audit"),
links=(
DocumentationLink(label="Tenant administration", href="/admin", kind="runtime"),
DocumentationLink(label="Tenant registry API", href="/api/v1/admin/tenants", kind="api"),
),
metadata={
"kind": "reference",
"help_contexts": [
"tenancy.field.slug",
"tenancy.field.initial-owner",
"tenancy.field.locale",
"tenancy.field.languages",
"tenancy.field.governance",
"tenancy.action.suspend",
],
"consequence_classes": {
"create": "Creates a new tenant boundary and provisions its protected initial owner.",
"update": "Changes tenant-local identity, locale, or governance configuration.",
"suspend": "Blocks normal tenant use while retaining data and audit evidence.",
},
},
),
),
frontend=FrontendModule(
@@ -0,0 +1,34 @@
from __future__ import annotations
import unittest
from govoplan_tenancy.backend.manifest import manifest
class TenancyInterfaceDocumentationContractTests(unittest.TestCase):
def test_tenancy_admin_surfaces_remain_declared(self) -> None:
frontend = manifest.frontend
self.assertIsNotNone(frontend)
surfaces = {surface.id for surface in frontend.view_surfaces} # type: ignore[union-attr]
self.assertEqual(
{
"tenancy.admin.system-tenants",
"tenancy.admin.tenant-settings",
},
surfaces,
)
def test_tenancy_topics_publish_stable_help_contexts(self) -> None:
topics = {topic.id: topic for topic in manifest.documentation}
self.assertIn("tenancy.current-context", topics)
self.assertIn("tenancy.lifecycle-and-settings", topics)
self.assertIn("tenancy.reference.admin-fields", topics)
lifecycle_contexts = set(topics["tenancy.lifecycle-and-settings"].metadata["help_contexts"])
self.assertIn("tenancy.admin.system-tenants", lifecycle_contexts)
self.assertIn("tenancy.admin.tenant-settings", lifecycle_contexts)
self.assertIn("tenancy.action.suspend", topics["tenancy.reference.admin-fields"].metadata["help_contexts"])
if __name__ == "__main__":
unittest.main()
+2 -1
View File
@@ -7,7 +7,8 @@
"module": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"test:tenancy-admin": "node scripts/test-tenancy-admin-structure.mjs"
"test:tenancy-admin": "node scripts/test-tenancy-admin-structure.mjs",
"test:interface-patterns": "node scripts/test-interface-pattern-language.mjs"
},
"exports": {
".": {
@@ -0,0 +1,30 @@
import { readFileSync } from "node:fs";
function source(path) {
return readFileSync(new URL(path, import.meta.url), "utf8");
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
const tenants = source("../src/features/admin/TenantsPanel.tsx");
const settings = source("../src/features/admin/TenantSettingsPanel.tsx");
const patterns = source("../src/features/admin/interfacePatterns.ts");
const moduleSource = source("../src/module.ts");
const translations = source("../src/i18n/generatedTranslations.ts");
assert(tenants.includes("DocumentationHelpLink") && settings.includes("DocumentationHelpLink"), "Both Tenancy admin surfaces expose contextual documentation");
assert(tenants.includes("ActionBlockerHint") && settings.includes("ActionBlockerHint"), "Read-only Tenancy states identify the actor, action, and destination");
assert(tenants.includes("disabledReason") && settings.includes("disabledReason"), "Disabled Tenancy actions explain their state");
assert(tenants.includes("requestDiscard(closeEditor)"), "The tenant editor uses the shared unsaved-change guard when closing");
assert(settings.includes("requestDiscard(() => void load())"), "Tenant settings protect dirty state when reloading");
assert(tenants.includes("ConfirmDialog") && tenants.includes("confirmSuspend"), "Tenant suspension remains explicitly confirmed");
assert(tenants.includes("minimumSlots={3}"), "Tenant row actions reserve stable keyboard and visual positions");
assert(!tenants.includes("applicable:"), "Row-specific unavailable actions stay visible with an explanation");
assert(patterns.includes('topicId: "tenancy.lifecycle-and-settings"') && patterns.includes('topicId: "tenancy.reference.admin-fields"'), "Tenancy uses stable manifest-backed help references");
assert(moduleSource.includes('version: "0.1.8"'), "The WebUI contribution reports the module release version");
assert(moduleSource.includes('label: "i18n:govoplan-tenancy.tenants.1f7ae776"') && moduleSource.includes('label: "i18n:govoplan-tenancy.tenant_general_settings.db1c3ba8"'), "View-surface labels are localized");
assert(translations.includes('"i18n:govoplan-tenancy.tenant_administration_is_read_only.7c9a1011"'), "Availability explanations are present in the translation catalog");
console.log("Tenancy surfaces satisfy the recorded interface pattern-language contract.");
@@ -1,10 +1,25 @@
import { useEffect, useState } from "react";
import type { ApiSettings } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import {
ActionBlockerHint,
AdminPageLayout,
AdminSelectionList,
Button,
Card,
DocumentationHelpLink,
FormField,
adminErrorMessage,
useDeltaWatermarks,
useUnsavedChanges,
useUnsavedDraftGuard,
type ApiSettings
} from "@govoplan/core-webui";
import { fetchTenantSettingsDelta, updateTenantSettings, type TenantSettingsDeltaSections, type TenantSettingsItem } from "../../api/tenancy";
import { AdminPageLayout, AdminSelectionList, adminErrorMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui";
import {
TENANCY_ADMIN_DOCUMENTATION,
TENANCY_FIELD_DOCUMENTATION,
TENANCY_INTERFACE_I18N,
tenantMutationDisabledReason
} from "./interfacePatterns";
const DELTA_KEY = "tenancy:tenant-settings";
@@ -31,6 +46,7 @@ export default function TenantSettingsPanel({
}: {settings: ApiSettings;canWrite: boolean;onAuthRefresh: () => Promise<void>;}) {
const { requestDiscard } = useUnsavedChanges();
const [draft, setDraft] = useState<TenantSettingsItem>(fallback);
const [savedDraft, setSavedDraft] = useState<TenantSettingsItem>(fallback);
const [loading, setLoading] = useState(true);
@@ -40,6 +56,13 @@ export default function TenantSettingsPanel({
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
const defaultLocaleOptions = localeOptions(draft.default_locale, draft.enabled_language_codes);
const dirty = tenantSettingsDraftKey(draft) !== tenantSettingsDraftKey(savedDraft);
const saveDisabledReason = tenantMutationDisabledReason({
busy,
permitted: canWrite,
complete: Boolean(draft.default_locale.trim() && draft.enabled_language_codes.length),
changed: dirty,
permissionReason: TENANCY_INTERFACE_I18N.settingsWriteRequired
});
useUnsavedDraftGuard({
dirty,
@@ -108,11 +131,26 @@ export default function TenantSettingsPanel({
loading={loading}
error={error}
success={success}
actions={<><Button onClick={() => void load()} disabled={loading}>i18n:govoplan-tenancy.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={!canWrite || busy || !draft.default_locale.trim()}>{busy ? "i18n:govoplan-tenancy.saving.ae7e8875" : "i18n:govoplan-tenancy.save_general_settings.5c90f8c4"}</Button></>}>
actions={<><DocumentationHelpLink reference={TENANCY_ADMIN_DOCUMENTATION} /><Button onClick={() => requestDiscard(() => void load())} disabled={loading || busy} disabledReason={loading ? TENANCY_INTERFACE_I18N.loading : busy ? TENANCY_INTERFACE_I18N.busy : undefined}>i18n:govoplan-tenancy.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={Boolean(saveDisabledReason)} disabledReason={saveDisabledReason}>{busy ? "i18n:govoplan-tenancy.saving.ae7e8875" : "i18n:govoplan-tenancy.save_general_settings.5c90f8c4"}</Button></>}>
{!canWrite && <ActionBlockerHint
reason={{
summary: TENANCY_INTERFACE_I18N.readOnlySummary,
requiredAction: TENANCY_INTERFACE_I18N.settingsPermissionGuidance,
actor: TENANCY_INTERFACE_I18N.administratorActor,
target: TENANCY_INTERFACE_I18N.tenantSettingsTarget
}}
labels={{
requiredAction: TENANCY_INTERFACE_I18N.requiredActionLabel,
actor: TENANCY_INTERFACE_I18N.actorLabel,
target: TENANCY_INTERFACE_I18N.targetLabel
}}
documentation={TENANCY_ADMIN_DOCUMENTATION}
/>}
<div className="admin-settings-form">
<Card title="i18n:govoplan-tenancy.locale.8970f0e6">
<FormField label="i18n:govoplan-tenancy.tenant_locale.8fc19914" help="i18n:govoplan-tenancy.used_as_this_tenant_s_locale_default_for_tenant_.cf298b8b">
<FormField label="i18n:govoplan-tenancy.tenant_locale.8fc19914" help={!canWrite ? TENANCY_INTERFACE_I18N.settingsWriteRequired : "i18n:govoplan-tenancy.used_as_this_tenant_s_locale_default_for_tenant_.cf298b8b"} documentation={TENANCY_FIELD_DOCUMENTATION}>
<select value={draft.default_locale} disabled={!canWrite || busy || defaultLocaleOptions.length === 0} onChange={(event) => setDraft({ ...draft, default_locale: event.target.value })}>
{defaultLocaleOptions.map((code) => {
const language = draft.available_languages.find((item) => item.code === code);
@@ -128,7 +166,7 @@ export default function TenantSettingsPanel({
selected={draft.enabled_language_codes}
onChange={setEnabledLanguages}
/>
<p className="muted small-note">i18n:govoplan-tenancy.tenant_languages_help</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">
<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>
+90 -36
View File
@@ -1,16 +1,35 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { Pencil, Plus, Search, Trash2 } from "lucide-react";
import type { ApiSettings, AuthInfo } from "@govoplan/core-webui";
import type { ApiSettings, AuthInfo, TenantAdminItem } from "@govoplan/core-webui";
import {
ActionBlockerHint,
AdminIconButton,
AdminPageLayout,
Button,
ConfirmDialog,
DataGrid,
Dialog,
DocumentationHelpLink,
FormField,
StatusBadge,
TableActionGroup,
adminErrorMessage,
formatAdminDateTime as formatDateTime,
i18nMessage,
useDeltaWatermarks,
usePlatformLanguage,
useUnsavedChanges,
useUnsavedDraftGuard,
type DataGridColumn
} from "@govoplan/core-webui";
import { createTenant, fetchSystemSettings, fetchTenantOwnerCandidates, fetchTenantsDelta, updateTenant, type SystemSettingsItem, type TenantOwnerCandidate } from "../../api/tenancy";
import type { TenantAdminItem } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui";
import { DataGrid, type DataGridColumn } from "@govoplan/core-webui";
import { Dialog } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import { ConfirmDialog } from "@govoplan/core-webui";
import { AdminIconButton, AdminPageLayout, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime, i18nMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui";
import { loadDeltaRows } from "./utils/deltaRows";
import {
TENANCY_ADMIN_DOCUMENTATION,
TENANCY_FIELD_DOCUMENTATION,
TENANCY_INTERFACE_I18N,
tenantMutationDisabledReason
} from "./interfacePatterns";
type OverrideValue = "inherit" | "allow" | "deny";
type TenantDraft = {
@@ -64,6 +83,8 @@ export default function TenantsPanel({
}: {settings: ApiSettings;auth: AuthInfo;canCreate: boolean;canUpdate: boolean;canSuspend: boolean;onAuthRefresh: () => Promise<void>;}) {
const { translateText } = usePlatformLanguage();
const { requestDiscard } = useUnsavedChanges();
const [tenants, setTenants] = useState<TenantAdminItem[]>([]);
const [systemSettings, setSystemSettings] = useState<SystemSettingsItem | null>(null);
const [ownerCandidates, setOwnerCandidates] = useState<TenantOwnerCandidate[]>([]);
@@ -144,6 +165,11 @@ export default function TenantsPanel({
setSavedDraftKey(draftKey(emptyDraft));
}
function requestCloseEditor() {
if (busy) return;
requestDiscard(closeEditor);
}
async function save(): Promise<boolean> {
setBusy(true);
setError("");
@@ -164,7 +190,7 @@ export default function TenantsPanel({
...governance
});
const selectedOwner = ownerCandidates.find((candidate) => candidate.account_id === draft.ownerAccountId);
setSuccess(i18nMessage("i18n:govoplan-tenancy.tenant_value_created_with_value_as_owner.1c18b6fb", { value0: created.name, value1: selectedOwner?.display_name || selectedOwner?.email || "i18n:govoplan-tenancy.the_selected_account.1211bfb9" }));
setSuccess(i18nMessage("i18n:govoplan-tenancy.tenant_value_created_with_value_as_owner.1c18b6fb", { value0: created.name, value1: selectedOwner?.display_name || selectedOwner?.email || translateText("i18n:govoplan-tenancy.the_selected_account.1211bfb9") }));
await onAuthRefresh();
} else if (editing) {
const payload: Parameters<typeof updateTenant>[2] = {};
@@ -214,10 +240,17 @@ export default function TenantsPanel({
const systemAllowsCustomRoles = systemSettings?.allow_tenant_custom_roles !== false;
const systemAllowsApiKeys = systemSettings?.allow_tenant_api_keys !== false;
const systemDeniedGovernance = [
systemAllowsCustomGroups ? "" : "i18n:govoplan-tenancy.custom_groups.453a605c",
systemAllowsCustomRoles ? "" : "i18n:govoplan-tenancy.custom_roles.d48dc976",
systemAllowsApiKeys ? "" : "i18n:govoplan-tenancy.api_keys.94fcf3c2"].
filter(Boolean).join(", ");
systemAllowsCustomGroups ? "" : translateText("i18n:govoplan-tenancy.custom_groups.453a605c"),
systemAllowsCustomRoles ? "" : translateText("i18n:govoplan-tenancy.custom_roles.d48dc976"),
systemAllowsApiKeys ? "" : translateText("i18n:govoplan-tenancy.api_keys.94fcf3c2")
].filter(Boolean).join(", ");
const saveDisabledReason = tenantMutationDisabledReason({
busy,
permitted: editing === "new" ? canCreate : canUpdate,
complete: Boolean(draft.name.trim() && draft.slug.trim() && (editing !== "new" || draft.ownerAccountId)),
changed: editing === "new" || dirty,
permissionReason: editing === "new" ? TENANCY_INTERFACE_I18N.createRequired : TENANCY_INTERFACE_I18N.updateRequired
});
const columns = useMemo<DataGridColumn<TenantAdminItem>[]>(() => [
{ id: "name", header: "i18n:govoplan-tenancy.tenant.3ca93c78", width: "minmax(210px, 1fr)", minWidth: 190, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => `${row.name} ${row.slug}`, render: (row) => <div><strong>{row.name}</strong><div className="muted small-note">{row.slug}</div></div> },
{ id: "users", header: "i18n:govoplan-tenancy.users.57f2b181", width: 100, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.counts.users ?? 0, render: (row) => `${row.counts.active_users ?? 0}/${row.counts.users ?? 0}` },
@@ -228,10 +261,10 @@ export default function TenantsPanel({
{ id: "status", header: "i18n:govoplan-tenancy.status.bae7d5be", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active ? "active" : "inactive", render: (row) => <StatusBadge status={row.is_active ? "active" : "inactive"} /> },
{ id: "actions", header: "i18n:govoplan-tenancy.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[
{ id: "inspect", label: i18nMessage("i18n:govoplan-tenancy.inspect_value.9d5d1071", { value0: row.name }), icon: <Search />, onClick: () => setViewing(row) },
{ id: "edit", label: i18nMessage("i18n:govoplan-tenancy.edit_value.fad75899", { value0: row.name }), icon: <Pencil />, disabled: !canUpdate, onClick: () => openEdit(row) },
{ id: "suspend", label: i18nMessage("i18n:govoplan-tenancy.suspend_value.03a74b32", { value0: row.name }), icon: <Trash2 />, variant: "danger", applicable: row.is_active, disabled: !canSuspend || row.id === activeTenantId, onClick: () => setConfirmSuspend(row) }
]} /> }],
[activeTenantId, canSuspend, canUpdate]);
{ id: "edit", label: i18nMessage("i18n:govoplan-tenancy.edit_value.fad75899", { value0: row.name }), icon: <Pencil />, disabled: !canUpdate || busy, disabledReason: busy ? TENANCY_INTERFACE_I18N.busy : !canUpdate ? TENANCY_INTERFACE_I18N.updateRequired : undefined, onClick: () => openEdit(row) },
{ id: "suspend", label: i18nMessage("i18n:govoplan-tenancy.suspend_value.03a74b32", { value0: row.name }), icon: <Trash2 />, variant: "danger", disabled: !canSuspend || row.id === activeTenantId || !row.is_active || busy, disabledReason: busy ? TENANCY_INTERFACE_I18N.busy : !canSuspend ? TENANCY_INTERFACE_I18N.suspendRequired : row.id === activeTenantId ? TENANCY_INTERFACE_I18N.activeTenant : !row.is_active ? TENANCY_INTERFACE_I18N.alreadySuspended : undefined, onClick: () => setConfirmSuspend(row) }
]} minimumSlots={3} /> }],
[activeTenantId, busy, canSuspend, canUpdate]);
return (
<>
@@ -241,28 +274,43 @@ export default function TenantsPanel({
loading={loading}
error={error}
success={success}
actions={<><Button onClick={() => void load()} disabled={loading}>i18n:govoplan-tenancy.reload.cce71553</Button><AdminIconButton label="i18n:govoplan-tenancy.add_tenant.b8e32af0" icon={<Plus />} variant="primary" onClick={openCreate} disabled={!canCreate} /></>}>
actions={<><DocumentationHelpLink reference={TENANCY_ADMIN_DOCUMENTATION} /><Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? TENANCY_INTERFACE_I18N.loading : busy ? TENANCY_INTERFACE_I18N.busy : undefined}>i18n:govoplan-tenancy.reload.cce71553</Button><AdminIconButton label="i18n:govoplan-tenancy.add_tenant.b8e32af0" icon={<Plus />} variant="primary" onClick={openCreate} disabled={!canCreate || busy} disabledReason={busy ? TENANCY_INTERFACE_I18N.busy : !canCreate ? TENANCY_INTERFACE_I18N.createRequired : undefined} /></>}>
{!canCreate && !canUpdate && !canSuspend && <ActionBlockerHint
reason={{
summary: TENANCY_INTERFACE_I18N.readOnlySummary,
requiredAction: TENANCY_INTERFACE_I18N.registryPermissionGuidance,
actor: TENANCY_INTERFACE_I18N.administratorActor,
target: TENANCY_INTERFACE_I18N.tenantRegistryTarget
}}
labels={{
requiredAction: TENANCY_INTERFACE_I18N.requiredActionLabel,
actor: TENANCY_INTERFACE_I18N.actorLabel,
target: TENANCY_INTERFACE_I18N.targetLabel
}}
documentation={TENANCY_ADMIN_DOCUMENTATION}
/>}
<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>
<Dialog open={editing !== null} title={editing === "new" ? "i18n:govoplan-tenancy.create_tenant.4dbd55d9" : "i18n:govoplan-tenancy.edit_tenant.e2ba43f9"} onClose={() => !busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<><Button onClick={() => setEditing(null)} disabled={busy}>i18n:govoplan-tenancy.cancel.77dfd213</Button><Button variant="primary" onClick={() => void save()} disabled={!(editing === "new" ? canCreate : canUpdate) || busy || !draft.name.trim() || !draft.slug.trim() || editing === "new" && !draft.ownerAccountId}>{busy ? "i18n:govoplan-tenancy.saving.56a2285c" : "i18n:govoplan-tenancy.save_tenant.9eb2ac74"}</Button></>}>
<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></>}>
<div className="admin-form-grid two-columns">
<FormField label="i18n:govoplan-tenancy.name.709a2322"><input value={draft.name} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, name: event.target.value })} /></FormField>
<FormField label="i18n:govoplan-tenancy.slug.094da9b9"><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"><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"><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"><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"><textarea rows={4} value={draft.description} disabled={editing !== "new" && !canUpdate} onChange={(event) => setDraft({ ...draft, description: 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>
{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>
{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>
</div>
<h3>i18n:govoplan-tenancy.system_governance_overrides.97cdf3ce</h3>
<div className="admin-form-grid two-columns">
<GovernanceSelect disabled={editing !== "new" && !canUpdate} allowDisabled={!systemAllowsCustomGroups} label="i18n:govoplan-tenancy.custom_tenant_groups.570ee603" value={draft.customGroups} onChange={(customGroups) => setDraft({ ...draft, customGroups })} />
<GovernanceSelect disabled={editing !== "new" && !canUpdate} allowDisabled={!systemAllowsCustomRoles} label="i18n:govoplan-tenancy.custom_tenant_roles.a738c37c" value={draft.customRoles} onChange={(customRoles) => setDraft({ ...draft, customRoles })} />
<GovernanceSelect disabled={editing !== "new" && !canUpdate} 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={!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={!systemAllowsApiKeys} label="i18n:govoplan-tenancy.tenant_api_keys.4b1d81f8" value={draft.apiKeys} onChange={(apiKeys) => setDraft({ ...draft, apiKeys })} />
</div>
<p className="muted small-note">i18n:govoplan-tenancy.inherit_follows_the_current_system_setting_expli.60d4d868</p>
{systemDeniedGovernance && <p className="muted small-note">i18n:govoplan-tenancy.explicit_allow_is_unavailable_for.8d05fd4a {systemDeniedGovernance} i18n:govoplan-tenancy.because_the_current_system_setting_denies_it.3f59c244</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 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>}>
@@ -270,10 +318,10 @@ export default function TenantsPanel({
<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>
<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>
<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>
<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"} ({fromOverride(viewing.allow_custom_groups)})</dd></div>
<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"} ({fromOverride(viewing.allow_custom_roles)})</dd></div>
<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"} ({fromOverride(viewing.allow_api_keys)})</dd></div>
<div><dt>i18n:govoplan-tenancy.objects.72a83add</dt><dd>{viewing.counts.users ?? 0} i18n:govoplan-tenancy.users.81651889 {viewing.counts.groups ?? 0} i18n:govoplan-tenancy.groups.07551586 {viewing.counts.campaigns ?? 0} i18n:govoplan-tenancy.campaigns.2282ffeb {viewing.counts.files ?? 0} files</dd></div>
<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>
<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>
<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>
<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>
</dl>{viewing.description && <p>{viewing.description}</p>}</>}
</Dialog>
@@ -282,8 +330,14 @@ export default function TenantsPanel({
}
function GovernanceSelect({ label, value, onChange, disabled = false, allowDisabled = false }: {label: string;value: OverrideValue;onChange: (value: OverrideValue) => void;disabled?: boolean;allowDisabled?: boolean;}) {
return <FormField label={label}><select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as OverrideValue)}><option value="inherit">i18n:govoplan-tenancy.inherit_system_setting.7f125156</option><option value="allow" disabled={allowDisabled}>i18n:govoplan-tenancy.allow_when_system_allows.4c5178cb</option><option value="deny">i18n:govoplan-tenancy.explicitly_deny.17ad945a</option></select></FormField>;
function GovernanceSelect({ label, value, onChange, disabled = false, disabledReason, allowDisabled = false }: {label: string;value: OverrideValue;onChange: (value: OverrideValue) => void;disabled?: boolean;disabledReason?: string;allowDisabled?: boolean;}) {
return <FormField label={label} help={disabledReason ?? (allowDisabled ? TENANCY_INTERFACE_I18N.governanceSystemLimit : undefined)} documentation={TENANCY_FIELD_DOCUMENTATION}><select value={value} disabled={disabled} onChange={(event) => onChange(event.target.value as OverrideValue)}><option value="inherit">i18n:govoplan-tenancy.inherit_system_setting.7f125156</option><option value="allow" disabled={allowDisabled}>i18n:govoplan-tenancy.allow_when_system_allows.4c5178cb</option><option value="deny">i18n:govoplan-tenancy.explicitly_deny.17ad945a</option></select></FormField>;
}
function overrideLabel(value: OverrideValue): string {
if (value === "allow") return TENANCY_INTERFACE_I18N.allowLabel;
if (value === "deny") return TENANCY_INTERFACE_I18N.denyLabel;
return TENANCY_INTERFACE_I18N.inheritLabel;
}
function draftKey(draft: TenantDraft): string {
@@ -0,0 +1,58 @@
import type { DocumentationHelpReference } from "@govoplan/core-webui";
export const TENANCY_ADMIN_DOCUMENTATION = {
topicId: "tenancy.lifecycle-and-settings",
documentationType: "admin"
} satisfies DocumentationHelpReference;
export const TENANCY_FIELD_DOCUMENTATION = {
topicId: "tenancy.reference.admin-fields",
documentationType: "admin"
} satisfies DocumentationHelpReference;
export const TENANCY_INTERFACE_I18N = {
loading: "i18n:govoplan-tenancy.tenant_information_is_loading.7c9a1001",
busy: "i18n:govoplan-tenancy.a_tenant_change_is_in_progress.7c9a1002",
createRequired: "i18n:govoplan-tenancy.tenant_creation_permission_is_required.7c9a1003",
updateRequired: "i18n:govoplan-tenancy.tenant_update_permission_is_required.7c9a1004",
suspendRequired: "i18n:govoplan-tenancy.tenant_suspension_permission_is_required.7c9a1005",
settingsWriteRequired: "i18n:govoplan-tenancy.tenant_settings_write_permission_is_required.7c9a1006",
completeRequiredFields: "i18n:govoplan-tenancy.complete_all_required_tenant_fields_before_saving.7c9a1007",
noPendingChanges: "i18n:govoplan-tenancy.make_a_change_before_saving.7c9a1008",
activeTenant: "i18n:govoplan-tenancy.switch_to_another_tenant_before_suspending_the_active_tenant.7c9a1009",
alreadySuspended: "i18n:govoplan-tenancy.this_tenant_is_already_suspended.7c9a1010",
readOnlySummary: "i18n:govoplan-tenancy.tenant_administration_is_read_only.7c9a1011",
requiredActionLabel: "i18n:govoplan-tenancy.required_action.7c9a1012",
actorLabel: "i18n:govoplan-tenancy.responsible_actor.7c9a1013",
targetLabel: "i18n:govoplan-tenancy.destination.7c9a1014",
administratorActor: "i18n:govoplan-tenancy.a_system_or_tenant_owner_with_the_required_permission.7c9a1015",
tenantRegistryTarget: "i18n:govoplan-tenancy.administration_tenants.7c9a1016",
tenantSettingsTarget: "i18n:govoplan-tenancy.administration_tenant_settings.7c9a1017",
registryPermissionGuidance: "i18n:govoplan-tenancy.request_a_tenant_management_permission_or_contact_a_system_owner.7c9a1018",
settingsPermissionGuidance: "i18n:govoplan-tenancy.request_tenant_settings_write_permission_or_contact_a_tenant_owner.7c9a1019",
defaultLanguageRequired: "i18n:govoplan-tenancy.the_default_language_must_remain_enabled.7c9a1020",
governanceSystemLimit: "i18n:govoplan-tenancy.system_policy_prevents_this_tenant_from_enabling_the_capability.7c9a1021",
inheritLabel: "i18n:govoplan-tenancy.inherit.7c9a1022",
allowLabel: "i18n:govoplan-tenancy.allow.7c9a1023",
denyLabel: "i18n:govoplan-tenancy.deny.7c9a1024"
} as const;
export function tenantMutationDisabledReason({
busy,
permitted,
complete = true,
changed = true,
permissionReason
}: {
busy: boolean;
permitted: boolean;
complete?: boolean;
changed?: boolean;
permissionReason: string;
}): string | undefined {
if (busy) return TENANCY_INTERFACE_I18N.busy;
if (!permitted) return permissionReason;
if (!complete) return TENANCY_INTERFACE_I18N.completeRequiredFields;
if (!changed) return TENANCY_INTERFACE_I18N.noPendingChanges;
return undefined;
}
+93 -41
View File
@@ -2,79 +2,131 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
export const generatedTranslations: PlatformTranslations = {
"en": {
"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.tenant_creation_permission_is_required.7c9a1003": "Tenant creation permission is required.",
"i18n:govoplan-tenancy.tenant_update_permission_is_required.7c9a1004": "Tenant update permission is required.",
"i18n:govoplan-tenancy.tenant_suspension_permission_is_required.7c9a1005": "Tenant suspension permission is required.",
"i18n:govoplan-tenancy.tenant_settings_write_permission_is_required.7c9a1006": "Tenant settings write permission is required.",
"i18n:govoplan-tenancy.complete_all_required_tenant_fields_before_saving.7c9a1007": "Complete all required tenant fields before saving.",
"i18n:govoplan-tenancy.make_a_change_before_saving.7c9a1008": "Make a change before saving.",
"i18n:govoplan-tenancy.switch_to_another_tenant_before_suspending_the_active_tenant.7c9a1009": "Switch to another tenant before suspending the active tenant.",
"i18n:govoplan-tenancy.this_tenant_is_already_suspended.7c9a1010": "This tenant is already suspended.",
"i18n:govoplan-tenancy.tenant_administration_is_read_only.7c9a1011": "Tenant administration is read-only.",
"i18n:govoplan-tenancy.required_action.7c9a1012": "Required action",
"i18n:govoplan-tenancy.responsible_actor.7c9a1013": "Responsible actor",
"i18n:govoplan-tenancy.destination.7c9a1014": "Destination",
"i18n:govoplan-tenancy.a_system_or_tenant_owner_with_the_required_permission.7c9a1015": "A system or tenant owner with the required permission",
"i18n:govoplan-tenancy.administration_tenants.7c9a1016": "Administration > Tenants",
"i18n:govoplan-tenancy.administration_tenant_settings.7c9a1017": "Administration > Tenant settings",
"i18n:govoplan-tenancy.request_a_tenant_management_permission_or_contact_a_system_owner.7c9a1018": "Request a tenant-management permission or contact a system owner.",
"i18n:govoplan-tenancy.request_tenant_settings_write_permission_or_contact_a_tenant_owner.7c9a1019": "Request tenant-settings write permission or contact a tenant owner.",
"i18n:govoplan-tenancy.the_default_language_must_remain_enabled.7c9a1020": "The default language must remain enabled.",
"i18n:govoplan-tenancy.system_policy_prevents_this_tenant_from_enabling_the_capability.7c9a1021": "System policy prevents this tenant from enabling the capability.",
"i18n:govoplan-tenancy.inherit.7c9a1022": "inherit",
"i18n:govoplan-tenancy.allow.7c9a1023": "allow",
"i18n:govoplan-tenancy.deny.7c9a1024": "deny",
"i18n:govoplan-tenancy.the_tenant_slug_is_immutable_after_creation.7c9a1025": "The tenant slug is immutable after creation.",
"i18n:govoplan-tenancy.files_lowercase.7c9a1026": "files",
"i18n:govoplan-tenancy.actions.c3cd636a": "Actions",
"i18n:govoplan-tenancy.active.a733b809": "Active",
"i18n:govoplan-tenancy.add_tenant.b8e32af0": "Add tenant",
"i18n:govoplan-tenancy.allow_when_system_allows.4c5178cb": "Allow when system allows",
"i18n:govoplan-tenancy.allowed.77c7b490": "Allowed",
"i18n:govoplan-tenancy.allow_when_system_allows.4c5178cb": "Erlauben, wenn systemweit zulässig",
"i18n:govoplan-tenancy.allowed.77c7b490": "Erlaubt",
"i18n:govoplan-tenancy.api_keys.94fcf3c2": "API keys",
"i18n:govoplan-tenancy.available.7c62a142": "Available",
"i18n:govoplan-tenancy.because_the_current_system_setting_denies_it.3f59c244": "because the current system setting denies it.",
"i18n:govoplan-tenancy.because_the_current_system_setting_denies_it.3f59c244": "da die aktuelle Systemeinstellung dies verweigert.",
"i18n:govoplan-tenancy.campaigns.01a23a28": "Campaigns",
"i18n:govoplan-tenancy.campaigns.2282ffeb": "campaigns,",
"i18n:govoplan-tenancy.campaigns.2282ffeb": "Kampagnen,",
"i18n:govoplan-tenancy.cancel.77dfd213": "Cancel",
"i18n:govoplan-tenancy.close.bbfa773e": "Close",
"i18n:govoplan-tenancy.create_and_govern_tenant_spaces_suspension_retai.1b76d377": "Create and govern tenant spaces. Suspension retains campaigns, files and audit evidence; the tenant backing the current session cannot be suspended.",
"i18n:govoplan-tenancy.create_and_govern_tenant_spaces_suspension_retai.1b76d377": "Mandantenbereiche erstellen und verwalten. Eine Sperrung bewahrt Kampagnen, Dateien und Nachweise; der Mandant der aktuellen Sitzung kann nicht gesperrt werden.",
"i18n:govoplan-tenancy.create_tenant.4dbd55d9": "Create tenant",
"i18n:govoplan-tenancy.created.accf40c8": "Created",
"i18n:govoplan-tenancy.custom_groups.1f7b7c8f": "Custom groups",
"i18n:govoplan-tenancy.custom_groups.453a605c": "custom groups",
"i18n:govoplan-tenancy.custom_roles.d48dc976": "custom roles",
"i18n:govoplan-tenancy.custom_roles.e78ef63d": "Custom roles",
"i18n:govoplan-tenancy.custom_tenant_groups.570ee603": "Custom tenant groups",
"i18n:govoplan-tenancy.custom_tenant_roles.a738c37c": "Custom tenant roles",
"i18n:govoplan-tenancy.custom_groups.1f7b7c8f": "Eigene Gruppen",
"i18n:govoplan-tenancy.custom_groups.453a605c": "eigene Gruppen",
"i18n:govoplan-tenancy.custom_roles.d48dc976": "eigene Rollen",
"i18n:govoplan-tenancy.custom_roles.e78ef63d": "Eigene Rollen",
"i18n:govoplan-tenancy.custom_tenant_groups.570ee603": "Eigene Mandantengruppen",
"i18n:govoplan-tenancy.custom_tenant_roles.a738c37c": "Eigene Mandantenrollen",
"i18n:govoplan-tenancy.default_locale.b99d021f": "Default locale",
"i18n:govoplan-tenancy.denied.63b16bd4": "Denied",
"i18n:govoplan-tenancy.denied.63b16bd4": "Verweigert",
"i18n:govoplan-tenancy.description.55f8ebc8": "Description",
"i18n:govoplan-tenancy.edit_tenant.e2ba43f9": "Edit tenant",
"i18n:govoplan-tenancy.edit_value.fad75899": "Edit {value0}",
"i18n:govoplan-tenancy.explicit_allow_is_unavailable_for.8d05fd4a": "Explicit allow is unavailable for",
"i18n:govoplan-tenancy.explicitly_deny.17ad945a": "Explicitly deny",
"i18n:govoplan-tenancy.edit_value.fad75899": "{value0} bearbeiten",
"i18n:govoplan-tenancy.explicit_allow_is_unavailable_for.8d05fd4a": "Eine ausdrückliche Erlaubnis ist nicht verfügbar für",
"i18n:govoplan-tenancy.explicitly_deny.17ad945a": "Ausdrücklich verweigern",
"i18n:govoplan-tenancy.files.6ce6c512": "Files",
"i18n:govoplan-tenancy.general.9239ee2c": "General",
"i18n:govoplan-tenancy.groups.07551586": "groups,",
"i18n:govoplan-tenancy.groups.07551586": "Gruppen,",
"i18n:govoplan-tenancy.groups.ae9629f4": "Groups",
"i18n:govoplan-tenancy.inherit_follows_the_current_system_setting_expli.60d4d868": "Inherit follows the current system setting. Explicit deny narrows access; explicit allow is valid only while the system setting allows it.",
"i18n:govoplan-tenancy.inherit_system_setting.7f125156": "Inherit system setting",
"i18n:govoplan-tenancy.initial_tenant_owner.682291a9": "Initial tenant owner",
"i18n:govoplan-tenancy.inspect_value.9d5d1071": "Inspect {value0}",
"i18n:govoplan-tenancy.inherit_follows_the_current_system_setting_expli.60d4d868": "Vererben folgt der aktuellen Systemeinstellung. Eine ausdrückliche Verweigerung schränkt ein; eine ausdrückliche Erlaubnis gilt nur, solange sie systemweit zulässig ist.",
"i18n:govoplan-tenancy.inherit_system_setting.7f125156": "Systemeinstellung vererben",
"i18n:govoplan-tenancy.initial_tenant_owner.682291a9": "Anfängliche Mandanteneigentümerschaft",
"i18n:govoplan-tenancy.inspect_value.9d5d1071": "{value0} anzeigen",
"i18n:govoplan-tenancy.locale.8970f0e6": "Locale",
"i18n:govoplan-tenancy.name.709a2322": "Name",
"i18n:govoplan-tenancy.no_tenants_found.72d04cf4": "No tenants found.",
"i18n:govoplan-tenancy.objects.72a83add": "Objects",
"i18n:govoplan-tenancy.no_tenants_found.72d04cf4": "Keine Mandanten gefunden.",
"i18n:govoplan-tenancy.objects.72a83add": "Objekte",
"i18n:govoplan-tenancy.reload.cce71553": "Reload",
"i18n:govoplan-tenancy.save_general_settings.5c90f8c4": "Save general settings",
"i18n:govoplan-tenancy.save_general_settings.5c90f8c4": "Allgemeine Einstellungen speichern",
"i18n:govoplan-tenancy.save_tenant.9eb2ac74": "Save tenant",
"i18n:govoplan-tenancy.saving.56a2285c": "Saving…",
"i18n:govoplan-tenancy.saving.ae7e8875": "Saving...",
"i18n:govoplan-tenancy.settings_for_the_active_tenant_context.ad267b86": "Settings for the active tenant context.",
"i18n:govoplan-tenancy.saving.56a2285c": "Speichern…",
"i18n:govoplan-tenancy.saving.ae7e8875": "Speichern...",
"i18n:govoplan-tenancy.settings_for_the_active_tenant_context.ad267b86": "Einstellungen für den aktiven Mandantenkontext.",
"i18n:govoplan-tenancy.slug.094da9b9": "Slug",
"i18n:govoplan-tenancy.status.bae7d5be": "Status",
"i18n:govoplan-tenancy.suspend_tenant.151d283a": "Suspend tenant",
"i18n:govoplan-tenancy.suspend_value.03a74b32": "Suspend {value0}",
"i18n:govoplan-tenancy.suspend_value_existing_data_remains_retained_but.19bccd78": "Suspend {value0}? Existing data remains retained, but its members cannot use the tenant.",
"i18n:govoplan-tenancy.suspended.794696a7": "Suspended",
"i18n:govoplan-tenancy.system_governance_overrides.97cdf3ce": "System governance overrides",
"i18n:govoplan-tenancy.suspend_tenant.151d283a": "Mandant sperren",
"i18n:govoplan-tenancy.suspend_value.03a74b32": "{value0} sperren",
"i18n:govoplan-tenancy.suspend_value_existing_data_remains_retained_but.19bccd78": "{value0} sperren? Bestehende Daten bleiben erhalten, aber Mitglieder können den Mandanten nicht mehr verwenden.",
"i18n:govoplan-tenancy.suspended.794696a7": "Gesperrt",
"i18n:govoplan-tenancy.system_governance_overrides.97cdf3ce": "Systemvorgaben",
"i18n:govoplan-tenancy.tenancy": "Tenancy",
"i18n:govoplan-tenancy.tenant.3ca93c78": "Tenant",
"i18n:govoplan-tenancy.tenant_api_keys.4b1d81f8": "Tenant API keys",
"i18n:govoplan-tenancy.tenant_details.5976ba72": "Tenant details",
"i18n:govoplan-tenancy.tenant_general_settings.db1c3ba8": "Tenant general settings",
"i18n:govoplan-tenancy.tenant_general_settings_saved.485e7681": "Tenant general settings saved.",
"i18n:govoplan-tenancy.tenant_details.5976ba72": "Mandantendetails",
"i18n:govoplan-tenancy.tenant_general_settings.db1c3ba8": "Allgemeine Mandanteneinstellungen",
"i18n:govoplan-tenancy.tenant_general_settings_saved.485e7681": "Allgemeine Mandanteneinstellungen gespeichert.",
"i18n:govoplan-tenancy.tenant_languages_help": "Tenant languages can only be selected from languages enabled by the system. Users can choose from the tenant-enabled set.",
"i18n:govoplan-tenancy.tenant_locale.8fc19914": "Tenant locale",
"i18n:govoplan-tenancy.tenant_value_created_with_value_as_owner.1c18b6fb": "Tenant {value0} created with {value1} as Owner.",
"i18n:govoplan-tenancy.tenant_value_updated.25b2c855": "Tenant {value0} updated.",
"i18n:govoplan-tenancy.tenant_locale.8fc19914": "Mandantensprache",
"i18n:govoplan-tenancy.tenant_value_created_with_value_as_owner.1c18b6fb": "Mandant {value0} mit {value1} als Eigentümerschaft erstellt.",
"i18n:govoplan-tenancy.tenant_value_updated.25b2c855": "Mandant {value0} aktualisiert.",
"i18n:govoplan-tenancy.tenants.1f7ae776": "Tenants",
"i18n:govoplan-tenancy.the_selected_account.1211bfb9": "the selected account",
"i18n:govoplan-tenancy.the_selected_account.1211bfb9": "das ausgewählte Konto",
"i18n:govoplan-tenancy.updated.f2f8570d": "Updated",
"i18n:govoplan-tenancy.used_as_this_tenant_s_locale_default_for_tenant_.cf298b8b": "Used as this tenant's locale default for tenant-aware views and future formatting defaults.",
"i18n:govoplan-tenancy.used_as_this_tenant_s_locale_default_for_tenant_.cf298b8b": "Wird als Standardsprache dieses Mandanten für mandantenbezogene Ansichten und Formatierungen verwendet.",
"i18n:govoplan-tenancy.users.57f2b181": "Users",
"i18n:govoplan-tenancy.users.81651889": "users,",
"i18n:govoplan-tenancy.value_suspended.31731a28": "{value0} suspended.",
"i18n:govoplan-tenancy.users.81651889": "Benutzer,",
"i18n:govoplan-tenancy.value_suspended.31731a28": "{value0} gesperrt.",
"i18n:govoplan-tenancy.value_value.c189e8bc": "{value0} ({value1})"
},
"de": {
"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.tenant_creation_permission_is_required.7c9a1003": "Die Berechtigung zum Erstellen von Mandanten ist erforderlich.",
"i18n:govoplan-tenancy.tenant_update_permission_is_required.7c9a1004": "Die Berechtigung zum Bearbeiten von Mandanten ist erforderlich.",
"i18n:govoplan-tenancy.tenant_suspension_permission_is_required.7c9a1005": "Die Berechtigung zum Sperren von Mandanten ist erforderlich.",
"i18n:govoplan-tenancy.tenant_settings_write_permission_is_required.7c9a1006": "Die Schreibberechtigung für Mandanteneinstellungen ist erforderlich.",
"i18n:govoplan-tenancy.complete_all_required_tenant_fields_before_saving.7c9a1007": "Füllen Sie vor dem Speichern alle erforderlichen Mandantenfelder aus.",
"i18n:govoplan-tenancy.make_a_change_before_saving.7c9a1008": "Nehmen Sie vor dem Speichern eine Änderung vor.",
"i18n:govoplan-tenancy.switch_to_another_tenant_before_suspending_the_active_tenant.7c9a1009": "Wechseln Sie zu einem anderen Mandanten, bevor Sie den aktiven Mandanten sperren.",
"i18n:govoplan-tenancy.this_tenant_is_already_suspended.7c9a1010": "Dieser Mandant ist bereits gesperrt.",
"i18n:govoplan-tenancy.tenant_administration_is_read_only.7c9a1011": "Die Mandantenverwaltung ist schreibgeschützt.",
"i18n:govoplan-tenancy.required_action.7c9a1012": "Erforderliche Aktion",
"i18n:govoplan-tenancy.responsible_actor.7c9a1013": "Zuständige Stelle",
"i18n:govoplan-tenancy.destination.7c9a1014": "Ziel",
"i18n:govoplan-tenancy.a_system_or_tenant_owner_with_the_required_permission.7c9a1015": "Eine System- oder Mandanteneigentümerschaft mit der erforderlichen Berechtigung",
"i18n:govoplan-tenancy.administration_tenants.7c9a1016": "Administration > Mandanten",
"i18n:govoplan-tenancy.administration_tenant_settings.7c9a1017": "Administration > Mandanteneinstellungen",
"i18n:govoplan-tenancy.request_a_tenant_management_permission_or_contact_a_system_owner.7c9a1018": "Fordern Sie eine Mandantenverwaltungsberechtigung an oder wenden Sie sich an eine Systemeigentümerschaft.",
"i18n:govoplan-tenancy.request_tenant_settings_write_permission_or_contact_a_tenant_owner.7c9a1019": "Fordern Sie die Schreibberechtigung für Mandanteneinstellungen an oder wenden Sie sich an eine Mandanteneigentümerschaft.",
"i18n:govoplan-tenancy.the_default_language_must_remain_enabled.7c9a1020": "Die Standardsprache muss aktiviert bleiben.",
"i18n:govoplan-tenancy.system_policy_prevents_this_tenant_from_enabling_the_capability.7c9a1021": "Die Systemrichtlinie verhindert, dass dieser Mandant die Funktion aktiviert.",
"i18n:govoplan-tenancy.inherit.7c9a1022": "vererbt",
"i18n:govoplan-tenancy.allow.7c9a1023": "erlauben",
"i18n:govoplan-tenancy.deny.7c9a1024": "verweigern",
"i18n:govoplan-tenancy.the_tenant_slug_is_immutable_after_creation.7c9a1025": "Der Mandanten-Slug kann nach der Erstellung nicht geändert werden.",
"i18n:govoplan-tenancy.files_lowercase.7c9a1026": "Dateien",
"i18n:govoplan-tenancy.actions.c3cd636a": "Aktionen",
"i18n:govoplan-tenancy.active.a733b809": "Aktiv",
"i18n:govoplan-tenancy.add_tenant.b8e32af0": "Mandant hinzufügen",
+3 -3
View File
@@ -53,7 +53,7 @@ const adminSections: AdminSectionsUiCapability = {
export const tenancyModule: PlatformWebModule = {
id: "tenancy",
label: "i18n:govoplan-tenancy.tenancy",
version: "1.0.0",
version: "0.1.8",
optionalDependencies: ["access"],
translations: {
en: generatedTranslations.en,
@@ -64,14 +64,14 @@ export const tenancyModule: PlatformWebModule = {
id: "tenancy.admin.system-tenants",
moduleId: "tenancy",
kind: "section",
label: "System tenants",
label: "i18n:govoplan-tenancy.tenants.1f7ae776",
order: 10
},
{
id: "tenancy.admin.tenant-settings",
moduleId: "tenancy",
kind: "section",
label: "Tenant settings",
label: "i18n:govoplan-tenancy.tenant_general_settings.db1c3ba8",
order: 90
}
],