feat(admin): separate language package lifecycle
This commit is contained in:
@@ -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-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-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-audit") && <AreaLink title="i18n:govoplan-admin.audit.fa1703dd" text="i18n:govoplan-admin.system_level_administrative_history.49f76723" onClick={() => onSelect("system-audit")} />}
|
||||
</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 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"];
|
||||
|
||||
@@ -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 type { FormGrid, ApiSettings } from "@govoplan/core-webui";
|
||||
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 { ToggleSwitch } from "@govoplan/core-webui";
|
||||
import { fetchSystemSettingsDelta, updateSystemSettings, type LanguagePackage, type PrivacyRetentionLimitPermissions, type PrivacyRetentionPolicy, type SystemSettingsDeltaSections, type SystemSettingsItem } from "../../api/admin";
|
||||
import { AdminPageLayout, AdminSelectionList, DocumentationHelpLink, NavigationPreferenceEditor, adminErrorMessage, configurableNavigationItemsForModules, dispatchPlatformModulesChanged, useDeltaWatermarks, usePlatformModules, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
||||
import { fetchSystemSettingsDelta, updateSystemSettings, type SystemSettingsItem } from "../../api/admin";
|
||||
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 { SYSTEM_SETTINGS_FALLBACK, applySystemSettingsSections } from "./systemSettingsModel";
|
||||
|
||||
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;}) {
|
||||
const modules = usePlatformModules();
|
||||
const navigationItems = configurableNavigationItemsForModules(modules);
|
||||
const [draft, setDraft] = useState<SystemSettingsItem>(fallback);
|
||||
const [savedDraft, setSavedDraft] = useState<SystemSettingsItem>(fallback);
|
||||
const [draft, setDraft] = useState<SystemSettingsItem>(SYSTEM_SETTINGS_FALLBACK);
|
||||
const [savedDraft, setSavedDraft] = useState<SystemSettingsItem>(SYSTEM_SETTINGS_FALLBACK);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
const [packageDraft, setPackageDraft] = useState({ code: "", label: "", nativeLabel: "" });
|
||||
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
|
||||
const defaultLocaleOptions = localeOptions(draft.default_locale, draft.enabled_language_codes);
|
||||
const dirty = systemSettingsDraftKey(draft) !== systemSettingsDraftKey(savedDraft);
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty,
|
||||
onSave: save,
|
||||
onDiscard: () => {
|
||||
setDraft(savedDraft);
|
||||
setPackageDraft({ code: "", label: "", nativeLabel: "" });
|
||||
}
|
||||
onDiscard: () => setDraft(savedDraft)
|
||||
});
|
||||
|
||||
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_api_keys: draft.allow_tenant_api_keys,
|
||||
maintenance_mode: draft.maintenance_mode,
|
||||
available_languages: draft.available_languages,
|
||||
enabled_language_codes: draft.enabled_language_codes,
|
||||
navigation: draft.navigation
|
||||
});
|
||||
setDraft(saved);
|
||||
@@ -118,30 +75,6 @@ export default function SystemSettingsPanel({ settings, canWrite, canAccessMaint
|
||||
{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 (
|
||||
<AdminPageLayout
|
||||
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></>}>
|
||||
|
||||
<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">
|
||||
<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" />
|
||||
@@ -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 {
|
||||
return JSON.stringify({
|
||||
default_locale: item.default_locale,
|
||||
allow_tenant_custom_groups: item.allow_tenant_custom_groups,
|
||||
allow_tenant_custom_roles: item.allow_tenant_custom_roles,
|
||||
allow_tenant_api_keys: item.allow_tenant_api_keys,
|
||||
maintenance_mode: item.maintenance_mode,
|
||||
available_languages: item.available_languages,
|
||||
enabled_language_codes: item.enabled_language_codes,
|
||||
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}`;
|
||||
}
|
||||
@@ -219,6 +219,20 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-admin.language_name_placeholder": "French",
|
||||
"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_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_placeholder": "Francais",
|
||||
"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_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_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_placeholder": "Francais",
|
||||
"i18n:govoplan-admin.defaults_for_newly_created_tenants.9d0f4ccd": "Standards für neu erstellte Mandanten",
|
||||
|
||||
@@ -11,4 +11,5 @@ export {
|
||||
export { default as GovernanceTemplatesPanel } from "./features/admin/GovernanceTemplatesPanel";
|
||||
export { default as TenantModuleManagementPanel } from "./features/admin/TenantModuleManagementPanel";
|
||||
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";
|
||||
|
||||
@@ -11,6 +11,7 @@ const GovernanceTemplatesPanel = lazy(() => import("./features/admin/GovernanceT
|
||||
const ModuleManagementPanel = lazy(() => import("./features/admin/ModuleManagementPanel"));
|
||||
const TenantModuleManagementPanel = lazy(() => import("./features/admin/TenantModuleManagementPanel"));
|
||||
const SystemSettingsPanel = lazy(() => import("./features/admin/SystemSettingsPanel"));
|
||||
const LanguagePackagesPanel = lazy(() => import("./features/admin/LanguagePackagesPanel"));
|
||||
const DataSubjectRequestsPanel = lazy(() => import("./features/admin/DataSubjectRequestsPanel"));
|
||||
|
||||
const translations = {
|
||||
@@ -50,6 +51,20 @@ const adminSections: AdminSectionsUiCapability = {
|
||||
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",
|
||||
moduleId: "admin",
|
||||
@@ -184,6 +199,7 @@ export const adminModule: PlatformWebModule = {
|
||||
viewSurfaces: [
|
||||
{ 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-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-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 },
|
||||
|
||||
Reference in New Issue
Block a user