feat(admin): separate language package lifecycle

This commit is contained in:
2026-08-20 05:48:47 +02:00
parent a1d7b649aa
commit 89ba59fcea
10 changed files with 523 additions and 138 deletions
+6 -3
View File
@@ -129,16 +129,18 @@ manifest = ModuleManifest(
title="Manage reference language and language packages", title="Manage reference language and language packages",
summary="German is the reference and new-installation default while configured tenant and user language choices remain effective.", summary="German is the reference and new-installation default while configured tenant and user language choices remain effective.",
body=( body=(
"German is the first-class product acceptance language and the default for a new system or tenant. English remains installed as the source-code fallback. System administrators control available and enabled language packages and the system default; tenant and user choices can select only languages allowed above them. Existing explicit preferences are preserved when this baseline is introduced. Disabling a language must not discard translated content or silently rewrite a stored preference." "German is the first-class product acceptance language and the default for a new system or tenant. English remains installed as the source-code fallback. The dedicated Language packages administration surface separates package installation, activation, deactivation, uninstall eligibility, and the default for newly created tenants from general system settings. Its compatibility matrix derives each package state from the installed module ID, exact module version, and available runtime translation catalog; a missing catalog is shown as incompatible rather than treated as translated. German, English, and the active default cannot be uninstalled. System administrators control available and enabled language packages and the system default; tenant and user choices can select only languages allowed above them. Existing explicit preferences are preserved when this baseline is introduced. Disabling a language must not discard translated content or silently rewrite a stored preference."
), ),
documentation_types=("admin",), documentation_types=("admin",),
audience=("system_admin", "tenant_admin"), audience=("system_admin", "tenant_admin"),
related_modules=("tenancy", "access", "docs"), related_modules=("tenancy", "access", "docs"),
metadata={ metadata={
"kind": "reference", "kind": "reference",
"route": "/admin?section=system-language-packages",
"screen": "Language packages",
"help_contexts": [ "help_contexts": [
"admin.system-settings.locale", "admin.system-language-packages",
"admin.system-settings.languages", "admin.system-language-packages.compatibility",
], ],
}, },
), ),
@@ -229,6 +231,7 @@ manifest = ModuleManifest(
view_surfaces=( view_surfaces=(
ViewSurface(id="admin.section.overview", module_id="admin", kind="section", label="Administration overview", order=0), ViewSurface(id="admin.section.overview", module_id="admin", kind="section", label="Administration overview", order=0),
ViewSurface(id="admin.section.system-settings", module_id="admin", kind="section", label="System settings", order=10), ViewSurface(id="admin.section.system-settings", module_id="admin", kind="section", label="System settings", order=10),
ViewSurface(id="admin.section.system-language-packages", module_id="admin", kind="section", label="Language packages", order=15),
ViewSurface(id="admin.section.system-configuration-changes", module_id="admin", kind="section", label="Configuration changes", order=20), ViewSurface(id="admin.section.system-configuration-changes", module_id="admin", kind="section", label="Configuration changes", order=20),
ViewSurface(id="admin.section.system-configuration-packages", module_id="admin", kind="section", label="Configuration packages", order=30), ViewSurface(id="admin.section.system-configuration-packages", module_id="admin", kind="section", label="Configuration packages", order=30),
ViewSurface(id="admin.section.system-role-templates", module_id="admin", kind="section", label="Role templates", order=40), ViewSurface(id="admin.section.system-role-templates", module_id="admin", kind="section", label="Role templates", order=40),
@@ -16,6 +16,10 @@ class InterfaceDocumentationContractTests(unittest.TestCase):
"admin.configuration-packages", "admin.configuration-packages",
"admin.governance-templates", "admin.governance-templates",
}, },
"admin.language-reference-and-packages": {
"admin.system-language-packages",
"admin.system-language-packages.compatibility",
},
"admin.module-lifecycle-workflow": { "admin.module-lifecycle-workflow": {
"admin.module-lifecycle", "admin.module-lifecycle",
"admin.module-lifecycle.queue-blocker", "admin.module-lifecycle.queue-blocker",
@@ -44,6 +48,7 @@ class InterfaceDocumentationContractTests(unittest.TestCase):
{ {
"admin.section.overview", "admin.section.overview",
"admin.section.system-settings", "admin.section.system-settings",
"admin.section.system-language-packages",
"admin.section.system-configuration-changes", "admin.section.system-configuration-changes",
"admin.section.system-configuration-packages", "admin.section.system-configuration-packages",
"admin.section.system-role-templates", "admin.section.system-role-templates",
+49
View File
@@ -0,0 +1,49 @@
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_language_lifecycle_has_a_dedicated_admin_section() -> None:
module_source = (ROOT / "webui/src/module.ts").read_text(encoding="utf-8")
panel_source = (
ROOT / "webui/src/features/admin/LanguagePackagesPanel.tsx"
).read_text(encoding="utf-8")
general_source = (
ROOT / "webui/src/features/admin/SystemSettingsPanel.tsx"
).read_text(encoding="utf-8")
assert 'id: "system-language-packages"' in module_source
assert 'surfaceId: "admin.section.system-language-packages"' in module_source
assert "data-language-package-lifecycle" in panel_source
assert "module.version" in panel_source
assert "module.translations?.[language.code]" in panel_source
assert "uninstallPackage" in panel_source
assert "setPackageActive" in panel_source
assert "default_locale" in panel_source
assert "language_packages" not in general_source
assert "available_languages:" not in general_source
def test_language_lifecycle_names_every_required_state() -> None:
source = (
ROOT / "webui/src/features/admin/LanguagePackagesPanel.tsx"
).read_text(encoding="utf-8")
translations = (
ROOT / "webui/src/i18n/generatedTranslations.ts"
).read_text(encoding="utf-8")
for marker in (
"installed.7bb4405c",
"available.7c8f1005",
"active.a733b809",
"inactive.09af574c",
"incompatible.lp001",
"uninstallable.lp001",
):
assert marker in source
for locale_marker in ('"en": {', '"de": {'):
assert locale_marker in translations
assert translations.count("language_package_administration.lp001") == 2
@@ -48,6 +48,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
{availableSections.has("system-tenant-modules") && <AreaLink title="Tenant modules" text="Set per-tenant availability, forced modules, and current selection." onClick={() => onSelect("system-tenant-modules")} />} {availableSections.has("system-tenant-modules") && <AreaLink title="Tenant modules" text="Set per-tenant availability, forced modules, and current selection." onClick={() => onSelect("system-tenant-modules")} />}
{availableSections.has("system-configuration-packages") && <AreaLink title="i18n:govoplan-admin.configuration_packages.eb2f05f1" text="i18n:govoplan-admin.preflight_approve_apply_and_export_configuration.276bd0f8" onClick={() => onSelect("system-configuration-packages")} />} {availableSections.has("system-configuration-packages") && <AreaLink title="i18n:govoplan-admin.configuration_packages.eb2f05f1" text="i18n:govoplan-admin.preflight_approve_apply_and_export_configuration.276bd0f8" onClick={() => onSelect("system-configuration-packages")} />}
{availableSections.has("system-settings") && <AreaLink title="i18n:govoplan-admin.maintenance.94de303b" text="i18n:govoplan-admin.instance_defaults_and_tenant_governance_capabili.99d6b2fa" onClick={() => onSelect("system-settings")} />} {availableSections.has("system-settings") && <AreaLink title="i18n:govoplan-admin.maintenance.94de303b" text="i18n:govoplan-admin.instance_defaults_and_tenant_governance_capabili.99d6b2fa" onClick={() => onSelect("system-settings")} />}
{availableSections.has("system-language-packages") && <AreaLink title="i18n:govoplan-admin.language_packages" text="i18n:govoplan-admin.language_package_administration_overview.lp001" onClick={() => onSelect("system-language-packages")} />}
{availableSections.has("system-configuration-changes") && <AreaLink title="i18n:govoplan-admin.changes.8aa57de6" text="i18n:govoplan-admin.configuration_requests_approvals_and_version_his.19f37335" onClick={() => onSelect("system-configuration-changes")} />} {availableSections.has("system-configuration-changes") && <AreaLink title="i18n:govoplan-admin.changes.8aa57de6" text="i18n:govoplan-admin.configuration_requests_approvals_and_version_his.19f37335" onClick={() => onSelect("system-configuration-changes")} />}
{availableSections.has("system-audit") && <AreaLink title="i18n:govoplan-admin.audit.fa1703dd" text="i18n:govoplan-admin.system_level_administrative_history.49f76723" onClick={() => onSelect("system-audit")} />} {availableSections.has("system-audit") && <AreaLink title="i18n:govoplan-admin.audit.fa1703dd" text="i18n:govoplan-admin.system_level_administrative_history.49f76723" onClick={() => onSelect("system-audit")} />}
</div> </div>
@@ -113,7 +114,7 @@ export default function AdminOverviewPanel({ settings, onSelect, availableSectio
} }
const platformSectionIds = ["system-modules", "system-tenant-modules", "system-configuration-packages", "system-settings", "system-configuration-changes", "system-audit"]; const platformSectionIds = ["system-modules", "system-tenant-modules", "system-configuration-packages", "system-settings", "system-language-packages", "system-configuration-changes", "system-audit"];
const globalSectionIds = ["system-tenants", "system-roles", "system-role-templates", "system-groups", "system-users", "system-file-connectors", "system-mail-servers", "system-retention", "system-view-policy"]; const globalSectionIds = ["system-tenants", "system-roles", "system-role-templates", "system-groups", "system-users", "system-file-connectors", "system-mail-servers", "system-retention", "system-view-policy"];
const tenantSectionIds = ["tenant-roles", "tenant-groups", "tenant-users", "tenant-file-connectors", "tenant-mail-servers", "tenant-api-keys", "tenant-retention", "tenant-view-policy", "tenant-modules", "tenant-settings", "tenant-audit"]; const tenantSectionIds = ["tenant-roles", "tenant-groups", "tenant-users", "tenant-file-connectors", "tenant-mail-servers", "tenant-api-keys", "tenant-retention", "tenant-view-policy", "tenant-modules", "tenant-settings", "tenant-audit"];
const groupSectionIds = ["tenant-group-file-connectors", "tenant-group-mail-servers", "tenant-group-retention", "group-view-policy"]; const groupSectionIds = ["tenant-group-file-connectors", "tenant-group-mail-servers", "tenant-group-retention", "group-view-policy"];
@@ -0,0 +1,330 @@
import { useEffect, useMemo, useState } from "react";
import type { ApiSettings, PlatformWebModule } from "@govoplan/core-webui";
import {
AdminPageLayout,
Button,
Card,
DocumentationHelpLink,
FormField,
FormGrid,
StatusBadge,
adminErrorMessage,
i18nMessage,
useDeltaWatermarks,
usePlatformModules,
useUnsavedDraftGuard
} from "@govoplan/core-webui";
import {
fetchSystemSettingsDelta,
updateSystemSettings,
type LanguagePackage,
type SystemSettingsItem
} from "../../api/admin";
import {
ADMIN_GOVERNANCE_DOCUMENTATION,
ADMIN_INTERFACE_I18N,
mutationDisabledReason
} from "./interfacePatterns";
import {
SYSTEM_SETTINGS_FALLBACK,
applySystemSettingsSections,
languageOptionLabel,
normalizeLanguageCode
} from "./systemSettingsModel";
const DELTA_KEY = "admin:language-packages";
const BUILT_IN_LANGUAGE_CODES = new Set(["de", "en"]);
type LanguageLifecycle = {
code: string;
package: LanguagePackage | null;
catalogCount: number;
installed: boolean;
active: boolean;
uninstallable: boolean;
};
export default function LanguagePackagesPanel({
settings,
canWrite
}: {
settings: ApiSettings;
canWrite: boolean;
}) {
const modules = usePlatformModules();
const [draft, setDraft] = useState<SystemSettingsItem>(SYSTEM_SETTINGS_FALLBACK);
const [savedDraft, setSavedDraft] = useState<SystemSettingsItem>(SYSTEM_SETTINGS_FALLBACK);
const [packageDraft, setPackageDraft] = useState({ code: "", label: "", nativeLabel: "" });
const [selectedCode, setSelectedCode] = useState("de");
const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
const dirty = languageDraftKey(draft) !== languageDraftKey(savedDraft);
const lifecycle = useMemo(
() => languageLifecycle(draft, modules),
[draft, modules]
);
const selectedLifecycle = lifecycle.find((item) => item.code === selectedCode) ?? lifecycle[0];
useUnsavedDraftGuard({
dirty,
onSave: save,
onDiscard: () => {
setDraft(savedDraft);
setPackageDraft({ code: "", label: "", nativeLabel: "" });
}
});
async function load() {
setLoading(true);
setError("");
try {
const wasDirty = languageDraftKey(draft) !== languageDraftKey(savedDraft);
const response = await fetchSystemSettingsDelta(settings, {
since: getDeltaWatermark(DELTA_KEY)
});
setDeltaWatermark(DELTA_KEY, response.watermark);
if (response.full && response.item) {
setSavedDraft(response.item);
if (!wasDirty) setDraft(response.item);
} else if (response.changed_sections.length) {
setSavedDraft((current) => applySystemSettingsSections(current, response.sections));
if (!wasDirty) {
setDraft((current) => applySystemSettingsSections(current, response.sections));
}
}
} catch (err) {
setError(adminErrorMessage(err));
} finally {
setLoading(false);
}
}
useEffect(() => {
resetDeltaWatermark(DELTA_KEY);
void load();
}, [settings.accessToken, settings.apiBaseUrl, resetDeltaWatermark]);
useEffect(() => {
if (lifecycle.length && !lifecycle.some((item) => item.code === selectedCode)) {
setSelectedCode(lifecycle[0].code);
}
}, [lifecycle, selectedCode]);
async function save(): Promise<boolean> {
setBusy(true);
setError("");
setSuccess("");
try {
const saved = await updateSystemSettings(settings, {
default_locale: draft.default_locale,
allow_tenant_custom_groups: draft.allow_tenant_custom_groups,
allow_tenant_custom_roles: draft.allow_tenant_custom_roles,
allow_tenant_api_keys: draft.allow_tenant_api_keys,
available_languages: draft.available_languages,
enabled_language_codes: draft.enabled_language_codes
});
setDraft(saved);
setSavedDraft(saved);
resetDeltaWatermark(DELTA_KEY);
setSuccess("i18n:govoplan-admin.language_package_settings_saved.lp001");
return true;
} catch (err) {
setError(adminErrorMessage(err));
return false;
} finally {
setBusy(false);
}
}
function installPackage(code: string, label?: string, nativeLabel?: string) {
const normalized = normalizeLanguageCode(code);
if (!normalized || draft.available_languages.some((item) => item.code === normalized)) return;
const resolvedLabel = label?.trim() || normalized.toUpperCase();
setDraft({
...draft,
available_languages: [
...draft.available_languages,
{
code: normalized,
label: resolvedLabel,
native_label: nativeLabel?.trim() || resolvedLabel
}
]
});
setSelectedCode(normalized);
}
function installDraftPackage() {
const code = normalizeLanguageCode(packageDraft.code);
if (!code || !packageDraft.label.trim()) return;
installPackage(code, packageDraft.label, packageDraft.nativeLabel);
setPackageDraft({ code: "", label: "", nativeLabel: "" });
}
function setPackageActive(code: string, active: boolean) {
const enabled = new Set(draft.enabled_language_codes);
if (active) enabled.add(code);
else enabled.delete(code);
setDraft({
...draft,
enabled_language_codes: draft.available_languages
.map((item) => item.code)
.filter((item) => enabled.has(item))
});
}
function uninstallPackage(code: string) {
if (BUILT_IN_LANGUAGE_CODES.has(code) || code === draft.default_locale) return;
setDraft({
...draft,
available_languages: draft.available_languages.filter((item) => item.code !== code),
enabled_language_codes: draft.enabled_language_codes.filter((item) => item !== code)
});
}
const enabledDefaultOptions = draft.available_languages.filter((item) =>
draft.enabled_language_codes.includes(item.code)
);
return (
<AdminPageLayout
title="i18n:govoplan-admin.language_package_administration.lp001"
description="i18n:govoplan-admin.language_package_administration_description.lp001"
loading={loading}
error={error}
success={success}
actions={<>
<DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} />
<Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.reload.cce71553</Button>
<Button variant="primary" onClick={() => void save()} disabledReason={mutationDisabledReason({ busy, permitted: canWrite, changed: dirty })}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : "i18n:govoplan-admin.save_settings.913aba9f"}</Button>
</>}
>
<div className="admin-settings-form" data-language-package-lifecycle>
<Card title="i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd">
<FormField label="i18n:govoplan-admin.default_locale.b99d021f" documentation={ADMIN_GOVERNANCE_DOCUMENTATION}>
<select
value={draft.default_locale}
onChange={(event) => setDraft({ ...draft, default_locale: event.target.value })}
disabled={!canWrite || busy || enabledDefaultOptions.length === 0}
>
{enabledDefaultOptions.map((item) => <option key={item.code} value={item.code}>{languageOptionLabel(item)}</option>)}
</select>
</FormField>
<p className="muted small-note">i18n:govoplan-admin.default_language_inheritance_help.lp001</p>
</Card>
<Card title="i18n:govoplan-admin.language_packages">
<div className="module-management-list">
{lifecycle.map((item) => (
<div className="module-management-row" key={item.code}>
<div className="module-management-main">
<div className="module-management-title">
<strong>{item.package ? languageOptionLabel(item.package) : item.code.toUpperCase()}</strong>
<code>{item.code}</code>
</div>
<div className="module-management-meta">
<StatusBadge status={item.installed ? "success" : "info"} label={item.installed ? "i18n:govoplan-admin.installed.7bb4405c" : "i18n:govoplan-admin.available.7c8f1005"} />
{item.installed && <StatusBadge status={item.active ? "active" : "inactive"} label={item.active ? "i18n:govoplan-admin.active.a733b809" : "i18n:govoplan-admin.inactive.09af574c"} />}
<StatusBadge status={item.catalogCount ? "success" : "warning"} label={item.catalogCount ? "i18n:govoplan-admin.compatible.lp001" : "i18n:govoplan-admin.incompatible.lp001"} />
{item.uninstallable && <StatusBadge status="warning" label="i18n:govoplan-admin.uninstallable.lp001" />}
</div>
<div className="module-management-details">
<span>{i18nMessage("i18n:govoplan-admin.module_version_catalogs.lp001", { value0: item.catalogCount })}</span>
</div>
</div>
<div className="module-management-toggle">
{!item.installed && <Button onClick={() => installPackage(item.code)} disabledReason={mutationDisabledReason({ busy, permitted: canWrite })}>i18n:govoplan-admin.install_language_package</Button>}
{item.installed && <Button onClick={() => setPackageActive(item.code, !item.active)} disabledReason={item.active && item.code === draft.default_locale ? "i18n:govoplan-admin.default_language_must_remain_active.lp001" : mutationDisabledReason({ busy, permitted: canWrite })}>{item.active ? "i18n:govoplan-admin.deactivate.lp001" : "i18n:govoplan-admin.activate.lp001"}</Button>}
{item.uninstallable && <Button variant="danger" onClick={() => uninstallPackage(item.code)} disabledReason={mutationDisabledReason({ busy, permitted: canWrite })}>i18n:govoplan-admin.uninstall.a735da1d</Button>}
</div>
</div>
))}
</div>
<FormGrid columns={3} collapseAt="standard" className="">
<FormField label="i18n:govoplan-admin.language_code">
<input value={packageDraft.code} disabled={!canWrite || busy} placeholder="fr" onChange={(event) => setPackageDraft({ ...packageDraft, code: event.target.value })} />
</FormField>
<FormField label="i18n:govoplan-admin.language_name">
<input value={packageDraft.label} disabled={!canWrite || busy} placeholder="i18n:govoplan-admin.language_name_placeholder" onChange={(event) => setPackageDraft({ ...packageDraft, label: event.target.value })} />
</FormField>
<FormField label="i18n:govoplan-admin.native_language_name">
<input value={packageDraft.nativeLabel} disabled={!canWrite || busy} placeholder="i18n:govoplan-admin.native_language_name_placeholder" onChange={(event) => setPackageDraft({ ...packageDraft, nativeLabel: event.target.value })} />
</FormField>
</FormGrid>
<div className="button-row compact-actions">
<Button onClick={installDraftPackage} disabledReason={mutationDisabledReason({ busy, permitted: canWrite, complete: Boolean(normalizeLanguageCode(packageDraft.code) && packageDraft.label.trim()) })}>i18n:govoplan-admin.install_language_package</Button>
</div>
<p className="muted small-note">i18n:govoplan-admin.language_packages_help</p>
</Card>
<Card title="i18n:govoplan-admin.module_version_compatibility.lp001">
{selectedLifecycle && <FormField label="i18n:govoplan-admin.language_packages">
<select value={selectedLifecycle.code} onChange={(event) => setSelectedCode(event.target.value)}>
{lifecycle.map((item) => <option key={item.code} value={item.code}>{item.code.toUpperCase()}</option>)}
</select>
</FormField>}
{selectedLifecycle && modules.length > 0 ? (
<div className="module-management-list">
{modules.map((module) => <ModuleLanguageState key={module.id} module={module} language={selectedLifecycle} />)}
</div>
) : <p className="muted">i18n:govoplan-admin.no_module_language_catalogs.lp001</p>}
</Card>
</div>
</AdminPageLayout>
);
}
function ModuleLanguageState({ module, language }: { module: PlatformWebModule; language: LanguageLifecycle }) {
const compatible = Boolean(module.translations?.[language.code]);
const state = language.installed && compatible
? "i18n:govoplan-admin.installed.7bb4405c"
: compatible
? "i18n:govoplan-admin.available.7c8f1005"
: "i18n:govoplan-admin.incompatible.lp001";
return (
<div className="module-management-row">
<div className="module-management-main">
<div className="module-management-title">
<strong>{module.label}</strong>
<code>{module.id}</code>
<span>v{module.version}</span>
</div>
<div className="module-management-meta">
<StatusBadge status={compatible ? (language.installed ? "success" : "info") : "warning"} label={state} />
{language.installed && compatible && <StatusBadge status={language.active ? "active" : "inactive"} label={language.active ? "i18n:govoplan-admin.active.a733b809" : "i18n:govoplan-admin.inactive.09af574c"} />}
</div>
</div>
</div>
);
}
function languageLifecycle(item: SystemSettingsItem, modules: PlatformWebModule[]): LanguageLifecycle[] {
const installed = new Map(item.available_languages.map((language) => [language.code, language]));
const codes = new Set(installed.keys());
modules.forEach((module) => Object.keys(module.translations ?? {}).forEach((code) => codes.add(code)));
return [...codes].sort().map((code) => {
const languagePackage = installed.get(code) ?? null;
const catalogCount = modules.filter((module) => Boolean(module.translations?.[code])).length;
return {
code,
package: languagePackage,
catalogCount,
installed: Boolean(languagePackage),
active: item.enabled_language_codes.includes(code),
uninstallable: Boolean(
languagePackage && !BUILT_IN_LANGUAGE_CODES.has(code) && code !== item.default_locale
)
};
});
}
function languageDraftKey(item: SystemSettingsItem): string {
return JSON.stringify({
default_locale: item.default_locale,
available_languages: item.available_languages,
enabled_language_codes: item.enabled_language_codes
});
}
@@ -1,73 +1,32 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import type { FormGrid, ApiSettings } from "@govoplan/core-webui"; import type { ApiSettings } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui"; import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui"; import { Card } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui";
import { ToggleSwitch } from "@govoplan/core-webui"; import { ToggleSwitch } from "@govoplan/core-webui";
import { fetchSystemSettingsDelta, updateSystemSettings, type LanguagePackage, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type SystemSettingsDeltaSections, type SystemSettingsItem } from "../../api/admin"; import { fetchSystemSettingsDelta, updateSystemSettings, type SystemSettingsItem } from "../../api/admin";
import { AdminPageLayout, AdminSelectionList, DocumentationHelpLink, NavigationPreferenceEditor, adminErrorMessage, configurableNavigationItemsForModules, dispatchPlatformModulesChanged, useDeltaWatermarks, usePlatformModules, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { AdminPageLayout, DocumentationHelpLink, NavigationPreferenceEditor, adminErrorMessage, configurableNavigationItemsForModules, dispatchPlatformModulesChanged, useDeltaWatermarks, usePlatformModules, useUnsavedDraftGuard } from "@govoplan/core-webui";
import { ADMIN_GOVERNANCE_DOCUMENTATION, ADMIN_INTERFACE_I18N, mutationDisabledReason } from "./interfacePatterns"; import { ADMIN_GOVERNANCE_DOCUMENTATION, ADMIN_INTERFACE_I18N, mutationDisabledReason } from "./interfacePatterns";
import { SYSTEM_SETTINGS_FALLBACK, applySystemSettingsSections } from "./systemSettingsModel";
const DELTA_KEY = "admin:system-settings"; const DELTA_KEY = "admin:system-settings";
const defaultAllowLowerLevelLimits: PrivacyRetentionLimitPermissions = {
store_raw_campaign_json: true,
raw_campaign_json_retention_days: true,
generated_eml_retention_days: true,
stored_report_detail_retention_days: true,
mock_mailbox_retention_days: true,
audit_detail_retention_days: true,
audit_detail_level: true
};
const defaultPrivacyPolicy: PrivacyRetentionPolicy = {
store_raw_campaign_json: true,
raw_campaign_json_retention_days: null,
generated_eml_retention_days: null,
stored_report_detail_retention_days: null,
mock_mailbox_retention_days: null,
audit_detail_retention_days: null,
audit_detail_level: "full",
allow_lower_level_limits: defaultAllowLowerLevelLimits
};
const fallback: SystemSettingsItem = {
default_locale: "de",
allow_tenant_custom_groups: true,
allow_tenant_custom_roles: true,
allow_tenant_api_keys: true,
privacy_retention_policy: defaultPrivacyPolicy,
maintenance_mode: { enabled: false, message: "" },
available_languages: [
{ code: "de", label: "German", native_label: "Deutsch" },
{ code: "en", label: "English", native_label: "English" }
],
enabled_language_codes: ["de", "en"],
settings: {},
navigation: null
};
export default function SystemSettingsPanel({ settings, canWrite, canAccessMaintenance }: {settings: ApiSettings;canWrite: boolean;canAccessMaintenance: boolean;}) { export default function SystemSettingsPanel({ settings, canWrite, canAccessMaintenance }: {settings: ApiSettings;canWrite: boolean;canAccessMaintenance: boolean;}) {
const modules = usePlatformModules(); const modules = usePlatformModules();
const navigationItems = configurableNavigationItemsForModules(modules); const navigationItems = configurableNavigationItemsForModules(modules);
const [draft, setDraft] = useState<SystemSettingsItem>(fallback); const [draft, setDraft] = useState<SystemSettingsItem>(SYSTEM_SETTINGS_FALLBACK);
const [savedDraft, setSavedDraft] = useState<SystemSettingsItem>(fallback); const [savedDraft, setSavedDraft] = useState<SystemSettingsItem>(SYSTEM_SETTINGS_FALLBACK);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [busy, setBusy] = useState(false); const [busy, setBusy] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [success, setSuccess] = useState(""); const [success, setSuccess] = useState("");
const [packageDraft, setPackageDraft] = useState({ code: "", label: "", nativeLabel: "" });
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
const defaultLocaleOptions = localeOptions(draft.default_locale, draft.enabled_language_codes);
const dirty = systemSettingsDraftKey(draft) !== systemSettingsDraftKey(savedDraft); const dirty = systemSettingsDraftKey(draft) !== systemSettingsDraftKey(savedDraft);
useUnsavedDraftGuard({ useUnsavedDraftGuard({
dirty, dirty,
onSave: save, onSave: save,
onDiscard: () => { onDiscard: () => setDraft(savedDraft)
setDraft(savedDraft);
setPackageDraft({ code: "", label: "", nativeLabel: "" });
}
}); });
async function load() { async function load() {
@@ -104,8 +63,6 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint
allow_tenant_custom_roles: draft.allow_tenant_custom_roles, allow_tenant_custom_roles: draft.allow_tenant_custom_roles,
allow_tenant_api_keys: draft.allow_tenant_api_keys, allow_tenant_api_keys: draft.allow_tenant_api_keys,
maintenance_mode: draft.maintenance_mode, maintenance_mode: draft.maintenance_mode,
available_languages: draft.available_languages,
enabled_language_codes: draft.enabled_language_codes,
navigation: draft.navigation navigation: draft.navigation
}); });
setDraft(saved); setDraft(saved);
@@ -118,30 +75,6 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint
{setBusy(false);} {setBusy(false);}
} }
function setEnabledLanguages(selected: string[]) {
const enabled = new Set(selected);
const nextEnabled = draft.available_languages.map((item) => item.code).filter((item) => enabled.has(item));
const defaultLocale = nextEnabled.includes(draft.default_locale) ? draft.default_locale : (nextEnabled[0] ?? draft.default_locale);
setDraft({ ...draft, enabled_language_codes: nextEnabled, default_locale: defaultLocale });
}
function installLanguagePackage() {
const code = normalizeLanguageCode(packageDraft.code);
if (!code || !packageDraft.label.trim()) return;
const nextPackage: LanguagePackage = {
code,
label: packageDraft.label.trim(),
native_label: packageDraft.nativeLabel.trim() || packageDraft.label.trim()
};
const existing = new Set(draft.available_languages.map((item) => item.code));
const available = existing.has(code)
? draft.available_languages.map((item) => item.code === code ? nextPackage : item)
: [...draft.available_languages, nextPackage];
const enabled = draft.enabled_language_codes.includes(code) ? draft.enabled_language_codes : [...draft.enabled_language_codes, code];
setDraft({ ...draft, available_languages: available, enabled_language_codes: enabled, default_locale: draft.default_locale || code });
setPackageDraft({ code: "", label: "", nativeLabel: "" });
}
return ( return (
<AdminPageLayout <AdminPageLayout
title="i18n:govoplan-admin.system_general_settings.7cd662ed" title="i18n:govoplan-admin.system_general_settings.7cd662ed"
@@ -152,38 +85,6 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint
actions={<><DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} /><Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabledReason={mutationDisabledReason({ busy, permitted: canWrite, changed: dirty })}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : "i18n:govoplan-admin.save_settings.913aba9f"}</Button></>}> actions={<><DocumentationHelpLink reference={ADMIN_GOVERNANCE_DOCUMENTATION} /><Button onClick={() => void load()} disabled={loading || busy} disabledReason={loading ? ADMIN_INTERFACE_I18N.loading : busy ? ADMIN_INTERFACE_I18N.busy : undefined}>i18n:govoplan-admin.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabledReason={mutationDisabledReason({ busy, permitted: canWrite, changed: dirty })}>{busy ? "i18n:govoplan-admin.saving.56a2285c" : "i18n:govoplan-admin.save_settings.913aba9f"}</Button></>}>
<div className="admin-settings-form"> <div className="admin-settings-form">
<Card title="i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd">
<FormField label="i18n:govoplan-admin.default_locale.b99d021f" documentation={ADMIN_GOVERNANCE_DOCUMENTATION}>
<select value={draft.default_locale} onChange={(event) => setDraft({ ...draft, default_locale: event.target.value })} disabled={!canWrite || busy || defaultLocaleOptions.length === 0}>
{defaultLocaleOptions.map((code) => {
const language = draft.available_languages.find((item) => item.code === code);
return <option key={code} value={code}>{languageOptionLabel(language ?? { code, label: code.toUpperCase() })}</option>;
})}
</select>
</FormField>
</Card>
<Card title="i18n:govoplan-admin.language_packages">
<AdminSelectionList
options={draft.available_languages.map((item) => ({ id: item.code, label: item.code.toUpperCase(), description: languageOptionLabel(item), disabled: !canWrite || busy || item.code === draft.default_locale }))}
selected={draft.enabled_language_codes}
onChange={setEnabledLanguages}
/>
<FormGrid columns={1} collapseAt="standard" className="">
<FormField label="i18n:govoplan-admin.language_code">
<input value={packageDraft.code} disabled={!canWrite || busy} placeholder="fr" onChange={(event) => setPackageDraft({ ...packageDraft, code: event.target.value })} />
</FormField>
<FormField label="i18n:govoplan-admin.language_name">
<input value={packageDraft.label} disabled={!canWrite || busy} placeholder="i18n:govoplan-admin.language_name_placeholder" onChange={(event) => setPackageDraft({ ...packageDraft, label: event.target.value })} />
</FormField>
<FormField label="i18n:govoplan-admin.native_language_name">
<input value={packageDraft.nativeLabel} disabled={!canWrite || busy} placeholder="i18n:govoplan-admin.native_language_name_placeholder" onChange={(event) => setPackageDraft({ ...packageDraft, nativeLabel: event.target.value })} />
</FormField>
<div className="button-row compact-actions">
<Button onClick={installLanguagePackage} disabledReason={mutationDisabledReason({ busy, permitted: canWrite, complete: Boolean(normalizeLanguageCode(packageDraft.code) && packageDraft.label.trim()) })}>i18n:govoplan-admin.install_language_package</Button>
</div>
</FormGrid>
<p className="muted small-note">i18n:govoplan-admin.language_packages_help</p>
</Card>
<Card title="i18n:govoplan-admin.tenant_administration_capabilities.5d265972"> <Card title="i18n:govoplan-admin.tenant_administration_capabilities.5d265972">
<div className="settings-list"> <div className="settings-list">
<ToggleSwitch checked={draft.allow_tenant_custom_groups} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_groups: checked })} disabled={!canWrite || busy} help={!canWrite ? ADMIN_INTERFACE_I18N.writeRequired : busy ? ADMIN_INTERFACE_I18N.busy : undefined} label="i18n:govoplan-admin.allow_tenant_defined_groups_by_default.32099d5a" /> <ToggleSwitch checked={draft.allow_tenant_custom_groups} onChange={(checked) => setDraft({ ...draft, allow_tenant_custom_groups: checked })} disabled={!canWrite || busy} help={!canWrite ? ADMIN_INTERFACE_I18N.writeRequired : busy ? ADMIN_INTERFACE_I18N.busy : undefined} label="i18n:govoplan-admin.allow_tenant_defined_groups_by_default.32099d5a" />
@@ -226,40 +127,12 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint
} }
function normalizeLanguageCode(value: string): string {
return value.trim().toLowerCase().replace(/_/g, "-").split(/[^a-z0-9]+/).filter(Boolean).join("-");
}
function localeOptions(current: string, enabled: string[]): string[] {
return [...new Set([current, ...enabled].filter((item) => item && item.trim()))];
}
function languageOptionLabel(language: LanguagePackage): string {
return `${language.code.toUpperCase()} - ${language.native_label || language.label}`;
}
function systemSettingsDraftKey(item: SystemSettingsItem): string { function systemSettingsDraftKey(item: SystemSettingsItem): string {
return JSON.stringify({ return JSON.stringify({
default_locale: item.default_locale,
allow_tenant_custom_groups: item.allow_tenant_custom_groups, allow_tenant_custom_groups: item.allow_tenant_custom_groups,
allow_tenant_custom_roles: item.allow_tenant_custom_roles, allow_tenant_custom_roles: item.allow_tenant_custom_roles,
allow_tenant_api_keys: item.allow_tenant_api_keys, allow_tenant_api_keys: item.allow_tenant_api_keys,
maintenance_mode: item.maintenance_mode, maintenance_mode: item.maintenance_mode,
available_languages: item.available_languages,
enabled_language_codes: item.enabled_language_codes,
navigation: item.navigation navigation: item.navigation
}); });
} }
function applySystemSettingsSections(item: SystemSettingsItem, sections: SystemSettingsDeltaSections): SystemSettingsItem {
return {
...item,
...(sections.defaults ?? {}),
...(sections.tenant_capabilities ?? {}),
...(sections.languages ?? {}),
...(sections.privacy_retention_policy ? { privacy_retention_policy: sections.privacy_retention_policy } : {}),
...(sections.maintenance_mode ? { maintenance_mode: sections.maintenance_mode } : {}),
...(sections.navigation !== undefined ? { navigation: sections.navigation } : {}),
...(sections.settings ? { settings: sections.settings } : {})
};
}
@@ -0,0 +1,79 @@
import type {
PrivacyRetentionLimitPermissions,
PrivacyRetentionPolicy,
SystemSettingsDeltaSections,
SystemSettingsItem
} from "../../api/admin";
const defaultAllowLowerLevelLimits: PrivacyRetentionLimitPermissions = {
store_raw_campaign_json: true,
raw_campaign_json_retention_days: true,
generated_eml_retention_days: true,
stored_report_detail_retention_days: true,
mock_mailbox_retention_days: true,
audit_detail_retention_days: true,
audit_detail_level: true
};
const defaultPrivacyPolicy: PrivacyRetentionPolicy = {
store_raw_campaign_json: true,
raw_campaign_json_retention_days: null,
generated_eml_retention_days: null,
stored_report_detail_retention_days: null,
mock_mailbox_retention_days: null,
audit_detail_retention_days: null,
audit_detail_level: "full",
allow_lower_level_limits: defaultAllowLowerLevelLimits
};
export const SYSTEM_SETTINGS_FALLBACK: SystemSettingsItem = {
default_locale: "de",
allow_tenant_custom_groups: true,
allow_tenant_custom_roles: true,
allow_tenant_api_keys: true,
privacy_retention_policy: defaultPrivacyPolicy,
maintenance_mode: { enabled: false, message: "" },
available_languages: [
{ code: "de", label: "German", native_label: "Deutsch" },
{ code: "en", label: "English", native_label: "English" }
],
enabled_language_codes: ["de", "en"],
settings: {},
navigation: null
};
export function applySystemSettingsSections(
item: SystemSettingsItem,
sections: SystemSettingsDeltaSections
): SystemSettingsItem {
return {
...item,
...(sections.defaults ?? {}),
...(sections.tenant_capabilities ?? {}),
...(sections.languages ?? {}),
...(sections.privacy_retention_policy
? { privacy_retention_policy: sections.privacy_retention_policy }
: {}),
...(sections.maintenance_mode ? { maintenance_mode: sections.maintenance_mode } : {}),
...(sections.navigation !== undefined ? { navigation: sections.navigation } : {}),
...(sections.settings ? { settings: sections.settings } : {})
};
}
export function normalizeLanguageCode(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/_/g, "-")
.split(/[^a-z0-9]+/)
.filter(Boolean)
.join("-");
}
export function languageOptionLabel(language: {
code: string;
label: string;
native_label?: string | null;
}): string {
return `${language.code.toUpperCase()} - ${language.native_label || language.label}`;
}
+28
View File
@@ -219,6 +219,20 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-admin.language_name_placeholder": "French", "i18n:govoplan-admin.language_name_placeholder": "French",
"i18n:govoplan-admin.language_packages": "Language packages", "i18n:govoplan-admin.language_packages": "Language packages",
"i18n:govoplan-admin.language_packages_help": "Installed packages define which language codes can be enabled. A package only becomes fully translated when matching module translation catalogs are present.", "i18n:govoplan-admin.language_packages_help": "Installed packages define which language codes can be enabled. A package only becomes fully translated when matching module translation catalogs are present.",
"i18n:govoplan-admin.language_package_administration.lp001": "Language packages and tenant defaults",
"i18n:govoplan-admin.language_package_administration_description.lp001": "Install and activate language packages, choose the default for new tenants, and inspect exact module-version compatibility.",
"i18n:govoplan-admin.language_package_administration_overview.lp001": "Package lifecycle, default tenant language, and module-version compatibility.",
"i18n:govoplan-admin.language_package_settings_saved.lp001": "Language package settings saved.",
"i18n:govoplan-admin.default_language_inheritance_help.lp001": "New tenants inherit this active language; tenant and user preferences can select only languages enabled above them.",
"i18n:govoplan-admin.compatible.lp001": "Compatible",
"i18n:govoplan-admin.incompatible.lp001": "Incompatible",
"i18n:govoplan-admin.uninstallable.lp001": "Uninstallable",
"i18n:govoplan-admin.module_version_catalogs.lp001": "{value0} module-version catalogs provide this language.",
"i18n:govoplan-admin.activate.lp001": "Activate",
"i18n:govoplan-admin.deactivate.lp001": "Deactivate",
"i18n:govoplan-admin.default_language_must_remain_active.lp001": "The default language must remain active.",
"i18n:govoplan-admin.module_version_compatibility.lp001": "Module-version compatibility",
"i18n:govoplan-admin.no_module_language_catalogs.lp001": "No installed module language catalogs are available. Package and default-language controls remain usable.",
"i18n:govoplan-admin.native_language_name": "Native language name", "i18n:govoplan-admin.native_language_name": "Native language name",
"i18n:govoplan-admin.native_language_name_placeholder": "Francais", "i18n:govoplan-admin.native_language_name_placeholder": "Francais",
"i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Defaults for newly created tenants", "i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Defaults for newly created tenants",
@@ -732,6 +746,20 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-admin.language_name_placeholder": "Franzoesisch", "i18n:govoplan-admin.language_name_placeholder": "Franzoesisch",
"i18n:govoplan-admin.language_packages": "Sprachpakete", "i18n:govoplan-admin.language_packages": "Sprachpakete",
"i18n:govoplan-admin.language_packages_help": "Installierte Pakete legen fest, welche Sprachcodes aktiviert werden koennen. Ein Paket ist erst vollstaendig uebersetzt, wenn passende Uebersetzungskataloge der Module vorhanden sind.", "i18n:govoplan-admin.language_packages_help": "Installierte Pakete legen fest, welche Sprachcodes aktiviert werden koennen. Ein Paket ist erst vollstaendig uebersetzt, wenn passende Uebersetzungskataloge der Module vorhanden sind.",
"i18n:govoplan-admin.language_package_administration.lp001": "Sprachpakete und Mandantenstandards",
"i18n:govoplan-admin.language_package_administration_description.lp001": "Sprachpakete installieren und aktivieren, den Standard fuer neue Mandanten waehlen und die exakte Modulversions-Kompatibilitaet pruefen.",
"i18n:govoplan-admin.language_package_administration_overview.lp001": "Paketlebenszyklus, Standard-Mandantensprache und Modulversions-Kompatibilitaet.",
"i18n:govoplan-admin.language_package_settings_saved.lp001": "Sprachpaket-Einstellungen gespeichert.",
"i18n:govoplan-admin.default_language_inheritance_help.lp001": "Neue Mandanten erben diese aktive Sprache; Mandanten- und Benutzereinstellungen koennen nur oberhalb aktivierte Sprachen waehlen.",
"i18n:govoplan-admin.compatible.lp001": "Kompatibel",
"i18n:govoplan-admin.incompatible.lp001": "Inkompatibel",
"i18n:govoplan-admin.uninstallable.lp001": "Deinstallierbar",
"i18n:govoplan-admin.module_version_catalogs.lp001": "{value0} Modulversions-Kataloge stellen diese Sprache bereit.",
"i18n:govoplan-admin.activate.lp001": "Aktivieren",
"i18n:govoplan-admin.deactivate.lp001": "Deaktivieren",
"i18n:govoplan-admin.default_language_must_remain_active.lp001": "Die Standardsprache muss aktiv bleiben.",
"i18n:govoplan-admin.module_version_compatibility.lp001": "Modulversions-Kompatibilitaet",
"i18n:govoplan-admin.no_module_language_catalogs.lp001": "Es sind keine Sprachkataloge installierter Module verfuegbar. Paket- und Standardspracheinstellungen bleiben nutzbar.",
"i18n:govoplan-admin.native_language_name": "Nativer Sprachname", "i18n:govoplan-admin.native_language_name": "Nativer Sprachname",
"i18n:govoplan-admin.native_language_name_placeholder": "Francais", "i18n:govoplan-admin.native_language_name_placeholder": "Francais",
"i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Standards für neu erstellte Mandanten", "i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Standards für neu erstellte Mandanten",
+1
View File
@@ -11,4 +11,5 @@ export {
export { default as GovernanceTemplatesPanel } from "./features/admin/GovernanceTemplatesPanel"; export { default as GovernanceTemplatesPanel } from "./features/admin/GovernanceTemplatesPanel";
export { default as TenantModuleManagementPanel } from "./features/admin/TenantModuleManagementPanel"; export { default as TenantModuleManagementPanel } from "./features/admin/TenantModuleManagementPanel";
export { default as SystemSettingsPanel } from "./features/admin/SystemSettingsPanel"; export { default as SystemSettingsPanel } from "./features/admin/SystemSettingsPanel";
export { default as LanguagePackagesPanel } from "./features/admin/LanguagePackagesPanel";
export type { PlatformWebModule, PlatformNavItem, PlatformRouteContribution, PlatformRouteContext } from "@govoplan/core-webui"; export type { PlatformWebModule, PlatformNavItem, PlatformRouteContribution, PlatformRouteContext } from "@govoplan/core-webui";
+16
View File
@@ -11,6 +11,7 @@ const GovernanceTemplatesPanel = lazy(() => import("./features/admin/GovernanceT
const ModuleManagementPanel = lazy(() => import("./features/admin/ModuleManagementPanel")); const ModuleManagementPanel = lazy(() => import("./features/admin/ModuleManagementPanel"));
const TenantModuleManagementPanel = lazy(() => import("./features/admin/TenantModuleManagementPanel")); const TenantModuleManagementPanel = lazy(() => import("./features/admin/TenantModuleManagementPanel"));
const SystemSettingsPanel = lazy(() => import("./features/admin/SystemSettingsPanel")); const SystemSettingsPanel = lazy(() => import("./features/admin/SystemSettingsPanel"));
const LanguagePackagesPanel = lazy(() => import("./features/admin/LanguagePackagesPanel"));
const DataSubjectRequestsPanel = lazy(() => import("./features/admin/DataSubjectRequestsPanel")); const DataSubjectRequestsPanel = lazy(() => import("./features/admin/DataSubjectRequestsPanel"));
const translations = { const translations = {
@@ -50,6 +51,20 @@ const adminSections: AdminSectionsUiCapability = {
canAccessMaintenance: hasScope(auth, "system:maintenance:access") canAccessMaintenance: hasScope(auth, "system:maintenance:access")
}) })
}, },
{
id: "system-language-packages",
moduleId: "admin",
kind: "settings",
surfaceId: "admin.section.system-language-packages",
label: "i18n:govoplan-admin.language_packages",
group: "SYSTEM",
order: 15,
allOf: ["system:settings:read"],
render: ({ settings, auth }) => createElement(LanguagePackagesPanel, {
settings,
canWrite: hasScope(auth, "system:settings:write")
})
},
{ {
id: "system-configuration-changes", id: "system-configuration-changes",
moduleId: "admin", moduleId: "admin",
@@ -184,6 +199,7 @@ export const adminModule: PlatformWebModule = {
viewSurfaces: [ viewSurfaces: [
{ id: "admin.section.overview", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.overview.0efc2e6b", order: 0 }, { id: "admin.section.overview", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.overview.0efc2e6b", order: 0 },
{ id: "admin.section.system-settings", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.system_general_settings.7cd662ed", order: 10 }, { id: "admin.section.system-settings", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.system_general_settings.7cd662ed", order: 10 },
{ id: "admin.section.system-language-packages", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.language_packages", order: 15 },
{ id: "admin.section.system-configuration-changes", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.configuration_changes.82933bbb", order: 20 }, { id: "admin.section.system-configuration-changes", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.configuration_changes.82933bbb", order: 20 },
{ id: "admin.section.system-configuration-packages", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.configuration_packages.eb2f05f1", order: 30 }, { id: "admin.section.system-configuration-packages", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.configuration_packages.eb2f05f1", order: 30 },
{ id: "admin.section.system-role-templates", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.tenant_roles.51aca82d", order: 40 }, { id: "admin.section.system-role-templates", moduleId: "admin", kind: "section", label: "i18n:govoplan-admin.tenant_roles.51aca82d", order: 40 },