675 lines
33 KiB
TypeScript
675 lines
33 KiB
TypeScript
import DescriptionList from "../../components/DescriptionList";
|
|
import ContentGrid, { FormGrid } from "../../components/ContentGrid";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import { useSearchParams } from "react-router";
|
|
import type { ApiSettings, AuthInfo, AuthUpdate, FilesConnectorsUiCapability, MailProfilesUiCapability, NavigationPreferences, SettingsSectionContribution, SettingsSectionsUiCapability, UserUiPalette, UserUiPreferences, UserUiTheme } from "../../types";
|
|
import Card from "../../components/Card";
|
|
import FormField from "../../components/FormField";
|
|
import PasswordField from "../../components/PasswordField";
|
|
import Button from "../../components/Button";
|
|
import PageLayout from "../../components/PageLayout";
|
|
import PageActionBar from "../../components/PageActionBar";
|
|
import ToggleSwitch from "../../components/ToggleSwitch";
|
|
import { apiFetch } from "../../api/client";
|
|
import { fetchAuthProfile, fetchAuthRoles, updateProfile } from "../../api/auth";
|
|
import { dispatchPlatformModulesChanged } from "../../platform/moduleEvents";
|
|
import ModuleSubnav, { type ModuleSubnavGroup } from "../../layout/ModuleSubnav";
|
|
import DismissibleAlert from "../../components/DismissibleAlert";
|
|
import SegmentedControl from "../../components/SegmentedControl";
|
|
import { useUnsavedChanges, useUnsavedDraftGuard } from "../../components/UnsavedChangesGuard";
|
|
import { usePlatformModules, usePlatformUiCapabilities, usePlatformUiCapability } from "../../platform/ModuleContext";
|
|
import { configurableNavigationItemsForModules } from "../../platform/modules";
|
|
import NavigationPreferenceEditor from "../../components/NavigationPreferenceEditor";
|
|
import { useEffectiveView, useViewSurfaces } from "../../platform/ViewContext";
|
|
import { isViewSurfaceVisible } from "../../platform/views";
|
|
import { hasAnyScope, hasScope } from "../../utils/permissions";
|
|
import { usePlatformLanguage } from "../../i18n/LanguageContext";
|
|
import CredentialEnvelopeManager from "../../components/CredentialEnvelopeManager";
|
|
import DocumentationHelpLink from "../../components/help/DocumentationHelpLink";
|
|
import WorkspaceLayout from "../../components/WorkspaceLayout";
|
|
import { AppearancePalettePreview, AppearancePaletteSelect, APPEARANCE_PALETTE_OPTIONS, appearancePaletteLabel } from "../../components/AppearancePaletteControl";
|
|
|
|
type SettingsSection = "profile" | "mail-profiles" | "file-connectors" | "interface" | "workspace" | "local-connection" | string;
|
|
|
|
const DEFAULT_UI_PREFERENCES: UserUiPreferences = {
|
|
compact_tables: false,
|
|
show_inline_help_hints: true,
|
|
reduce_motion: false,
|
|
sticky_section_sidebars: true,
|
|
theme: "system",
|
|
palette: null
|
|
};
|
|
|
|
const UI_THEME_OPTIONS: Array<{ value: UserUiTheme; label: string }> = [
|
|
{ value: "system", label: "i18n:govoplan-core.system_default.1f06f3ed" },
|
|
{ value: "light", label: "i18n:govoplan-core.light_theme.7878f1fa" },
|
|
{ value: "dark", label: "i18n:govoplan-core.dark_theme.164a90d9" }
|
|
];
|
|
|
|
const SETTINGS_DOCUMENTATION = {
|
|
contextId: "core.settings",
|
|
documentationType: "user" as const
|
|
};
|
|
|
|
function settingsGroups(canUseMailProfiles: boolean, canUseFileConnectors: boolean, canUseCredentials: boolean, contributedSections: SettingsSectionContribution[]): ModuleSubnavGroup<SettingsSection>[] {
|
|
const groups: ModuleSubnavGroup<SettingsSection>[] = [
|
|
{
|
|
title: "i18n:govoplan-core.account.f967543b",
|
|
items: [
|
|
{ id: "profile", label: "i18n:govoplan-core.my_profile.2f3df0a9" },
|
|
...(canUseMailProfiles ? [{ id: "mail-profiles" as const, label: "i18n:govoplan-core.mail_profiles.8a8018b7" }] : []),
|
|
...(canUseFileConnectors ? [{ id: "file-connectors" as const, label: "i18n:govoplan-core.file_connections.1e362326" }] : []),
|
|
...(canUseCredentials ? [{ id: "credentials" as const, label: "i18n:govoplan-core.credentials.dd097a22" }] : [])]
|
|
|
|
},
|
|
{
|
|
title: "i18n:govoplan-core.ui_settings.9e9cc5ea",
|
|
items: [
|
|
{ id: "interface", label: "i18n:govoplan-core.interface.7b4db7ef" },
|
|
{ id: "workspace", label: "i18n:govoplan-core.workspace.4ca0a75c" },
|
|
{ id: "local-connection", label: "i18n:govoplan-core.local_connection.42fba65a" }]
|
|
|
|
}];
|
|
|
|
for (const section of contributedSections) {
|
|
const groupId = section.group || "ui";
|
|
const title = section.groupTitle || (groupId === "account" ? "i18n:govoplan-core.account.f967543b" : groupId === "ui" ? "i18n:govoplan-core.ui_settings.9e9cc5ea" : groupId);
|
|
let group = groups.find((item) => item.title === title);
|
|
if (!group) {
|
|
group = { title, items: [] };
|
|
groups.push(group);
|
|
}
|
|
group.items.push({ id: section.id, label: section.label });
|
|
}
|
|
|
|
for (const group of groups) {
|
|
group.items.sort((left, right) => {
|
|
const leftId = "id" in left ? left.id : "";
|
|
const rightId = "id" in right ? right.id : "";
|
|
return (sectionOrder(leftId, contributedSections) - sectionOrder(rightId, contributedSections)) || left.label.localeCompare(right.label);
|
|
});
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
export default function SettingsPage({
|
|
settings,
|
|
auth,
|
|
onSettingsChange,
|
|
onAuthChange
|
|
|
|
|
|
|
|
|
|
|
|
}: {settings: ApiSettings;auth: AuthInfo;onSettingsChange: (settings: ApiSettings) => void;onAuthChange: (auth: AuthUpdate | null, accessToken?: string) => void;}) {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const { requestNavigation } = useUnsavedChanges();
|
|
const mailProfilesUi = usePlatformUiCapability<MailProfilesUiCapability>("mail.profiles");
|
|
const fileConnectorsUi = usePlatformUiCapability<FilesConnectorsUiCapability>("files.connectors");
|
|
const settingsSectionCapabilities = usePlatformUiCapabilities<SettingsSectionsUiCapability>("settings.sections");
|
|
const platformModules = usePlatformModules();
|
|
const navigationItems = useMemo(() => configurableNavigationItemsForModules(platformModules), [platformModules]);
|
|
const effectiveView = useEffectiveView();
|
|
const viewSurfaces = useViewSurfaces();
|
|
const { language, languageLabel, selectableLanguages, availableLanguages, enabledLanguages, setLanguage } = usePlatformLanguage();
|
|
const MailProfileScopeManager = mailProfilesUi?.MailProfileScopeManager ?? null;
|
|
const FileConnectorScopeManager = fileConnectorsUi?.FileConnectorScopeManager ?? null;
|
|
const canUseMailProfiles = isViewSurfaceVisible(effectiveView, "mail.settings.profiles", viewSurfaces) && Boolean(MailProfileScopeManager) && hasAnyScope(auth, [
|
|
"mail_servers:read",
|
|
"mail_servers:write",
|
|
"mail_servers:manage_credentials",
|
|
"mail:profile:write_own",
|
|
"mail:secret:manage_own",
|
|
"admin:policies:read",
|
|
"admin:policies:write"
|
|
]);
|
|
const canUseFileConnectors = isViewSurfaceVisible(effectiveView, "files.settings.connectors", viewSurfaces) && Boolean(FileConnectorScopeManager) && hasAnyScope(auth, ["files:file:read", "files:file:admin", "admin:settings:read", "admin:settings:write"]);
|
|
const canUseCredentials = isViewSurfaceVisible(effectiveView, "access.settings.credentials", viewSurfaces) && hasAnyScope(auth, [
|
|
"access:credential:read",
|
|
"access:credential:write",
|
|
"access:credential:manage_own",
|
|
"mail:secret:manage_own",
|
|
"admin:settings:read",
|
|
"admin:settings:write"
|
|
]);
|
|
const contributedSections = useMemo(
|
|
() =>
|
|
settingsSectionCapabilities
|
|
.flatMap((capability) => capability.sections ?? [])
|
|
.filter((section) => canUseSettingsContribution(auth, section))
|
|
.filter((section) =>
|
|
isViewSurfaceVisible(effectiveView, section.surfaceId, viewSurfaces)
|
|
),
|
|
[auth, effectiveView, settingsSectionCapabilities, viewSurfaces]
|
|
);
|
|
const settingsSubnav = useMemo(() => settingsGroups(canUseMailProfiles, canUseFileConnectors, canUseCredentials, contributedSections), [canUseCredentials, canUseFileConnectors, canUseMailProfiles, contributedSections]);
|
|
const availableSectionIds = useMemo(() => new Set(settingsSubnav.flatMap((group) => group.items.flatMap((item) => "id" in item ? [item.id] : []))), [settingsSubnav]);
|
|
const requestedSection = searchParams.get("section");
|
|
const active: SettingsSection = settingsSectionAvailable(availableSectionIds, requestedSection) ? requestedSection : "interface";
|
|
const activeContributedSection = contributedSections.find((section) => section.id === active) ?? null;
|
|
const currentUiPreferences = normalizeUiPreferences(auth.user.ui_preferences);
|
|
const [testing, setTesting] = useState(false);
|
|
const [testResult, setTestResult] = useState("");
|
|
const [testResultTone, setTestResultTone] = useState<"success" | "warning">("success");
|
|
const [compactTables, setCompactTables] = useState(currentUiPreferences.compact_tables);
|
|
const [showHelpHints, setShowHelpHints] = useState(currentUiPreferences.show_inline_help_hints);
|
|
const [reduceMotion, setReduceMotion] = useState(currentUiPreferences.reduce_motion);
|
|
const [stickySections, setStickySections] = useState(currentUiPreferences.sticky_section_sidebars);
|
|
const [theme, setTheme] = useState<UserUiTheme>(currentUiPreferences.theme);
|
|
const [palette, setPalette] = useState<UserUiPalette | null>(currentUiPreferences.palette);
|
|
const [navigation, setNavigation] = useState<NavigationPreferences | null>(currentUiPreferences.navigation ?? null);
|
|
const [uiBusy, setUiBusy] = useState(false);
|
|
const [uiResult, setUiResult] = useState("");
|
|
const [uiResultTone, setUiResultTone] = useState<"success" | "warning">("success");
|
|
const [profileName, setProfileName] = useState(auth.user.display_name || "");
|
|
const [tenantProfileName, setTenantProfileName] = useState(auth.user.tenant_display_name || "");
|
|
const [profileBusy, setProfileBusy] = useState(false);
|
|
const [profileResult, setProfileResult] = useState("");
|
|
const [profileResultTone, setProfileResultTone] = useState<"success" | "warning">("success");
|
|
const profileDirty = profileName !== (auth.user.display_name || "") || tenantProfileName !== (auth.user.tenant_display_name || "");
|
|
const uiDirty =
|
|
compactTables !== currentUiPreferences.compact_tables ||
|
|
showHelpHints !== currentUiPreferences.show_inline_help_hints ||
|
|
reduceMotion !== currentUiPreferences.reduce_motion ||
|
|
stickySections !== currentUiPreferences.sticky_section_sidebars ||
|
|
theme !== currentUiPreferences.theme ||
|
|
palette !== currentUiPreferences.palette ||
|
|
JSON.stringify(navigation) !== JSON.stringify(currentUiPreferences.navigation ?? null);
|
|
|
|
useUnsavedDraftGuard({
|
|
dirty: active === "profile" && profileDirty,
|
|
onSave: saveProfile,
|
|
onDiscard: () => {
|
|
setProfileName(auth.user.display_name || "");
|
|
setTenantProfileName(auth.user.tenant_display_name || "");
|
|
}
|
|
});
|
|
|
|
useUnsavedDraftGuard({
|
|
dirty: (active === "interface" || active === "workspace") && uiDirty,
|
|
onSave: saveUiPreferences,
|
|
onDiscard: resetUiPreferences
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (requestedSection && !settingsSectionAvailable(availableSectionIds, requestedSection)) {
|
|
const next = new URLSearchParams(searchParams);
|
|
next.set("section", "interface");
|
|
setSearchParams(next, { replace: true });
|
|
}
|
|
}, [availableSectionIds, requestedSection, searchParams, setSearchParams]);
|
|
|
|
useEffect(() => {
|
|
if (auth.profile_loaded) return;
|
|
let cancelled = false;
|
|
fetchAuthProfile(settings)
|
|
.then((next) => {if (!cancelled) onAuthChange(next);})
|
|
.catch(() => undefined);
|
|
return () => {cancelled = true;};
|
|
}, [auth.profile_loaded, auth.user.id, auth.active_tenant?.id, auth.tenant.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
|
|
|
|
useEffect(() => {
|
|
if (auth.roles_loaded) return;
|
|
let cancelled = false;
|
|
fetchAuthRoles(settings)
|
|
.then((next) => {if (!cancelled) onAuthChange(next);})
|
|
.catch(() => undefined);
|
|
return () => {cancelled = true;};
|
|
}, [auth.roles_loaded, auth.user.id, auth.active_tenant?.id, auth.tenant.id, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
|
|
|
|
useEffect(() => {
|
|
setProfileName(auth.user.display_name || "");
|
|
setTenantProfileName(auth.user.tenant_display_name || "");
|
|
}, [auth.user.display_name, auth.user.tenant_display_name]);
|
|
|
|
useEffect(() => {
|
|
setCompactTables(currentUiPreferences.compact_tables);
|
|
setShowHelpHints(currentUiPreferences.show_inline_help_hints);
|
|
setReduceMotion(currentUiPreferences.reduce_motion);
|
|
setStickySections(currentUiPreferences.sticky_section_sidebars);
|
|
setTheme(currentUiPreferences.theme);
|
|
setPalette(currentUiPreferences.palette);
|
|
setNavigation(currentUiPreferences.navigation ?? null);
|
|
}, [
|
|
currentUiPreferences.compact_tables,
|
|
currentUiPreferences.show_inline_help_hints,
|
|
currentUiPreferences.reduce_motion,
|
|
currentUiPreferences.sticky_section_sidebars,
|
|
currentUiPreferences.theme,
|
|
currentUiPreferences.palette,
|
|
currentUiPreferences.navigation
|
|
]);
|
|
|
|
function selectSection(section: SettingsSection) {
|
|
if (section === active) return;
|
|
requestNavigation(() => {
|
|
const next = new URLSearchParams(searchParams);
|
|
next.set("section", section);
|
|
setSearchParams(next);
|
|
});
|
|
}
|
|
|
|
async function saveProfile(): Promise<boolean> {
|
|
setProfileBusy(true);
|
|
setProfileResult("");
|
|
try {
|
|
const next = await updateProfile(settings, {
|
|
display_name: profileName.trim() || null,
|
|
tenant_display_name: tenantProfileName.trim() || null
|
|
});
|
|
onAuthChange(next);
|
|
setProfileResultTone("success");
|
|
setProfileResult("i18n:govoplan-core.profile_saved_the_account_menu_has_been_updated.aee56076");
|
|
return true;
|
|
} catch (err) {
|
|
setProfileResultTone("warning");
|
|
setProfileResult(err instanceof Error ? err.message : String(err));
|
|
return false;
|
|
} finally {
|
|
setProfileBusy(false);
|
|
}
|
|
}
|
|
|
|
function resetUiPreferences() {
|
|
setCompactTables(currentUiPreferences.compact_tables);
|
|
setShowHelpHints(currentUiPreferences.show_inline_help_hints);
|
|
setReduceMotion(currentUiPreferences.reduce_motion);
|
|
setStickySections(currentUiPreferences.sticky_section_sidebars);
|
|
setTheme(currentUiPreferences.theme);
|
|
setPalette(currentUiPreferences.palette);
|
|
setNavigation(currentUiPreferences.navigation ?? null);
|
|
}
|
|
|
|
function uiPreferencePayload(): UserUiPreferences {
|
|
return {
|
|
compact_tables: compactTables,
|
|
show_inline_help_hints: showHelpHints,
|
|
reduce_motion: reduceMotion,
|
|
sticky_section_sidebars: stickySections,
|
|
theme,
|
|
palette,
|
|
navigation
|
|
};
|
|
}
|
|
|
|
async function saveUiPreferences(): Promise<boolean> {
|
|
setUiBusy(true);
|
|
setUiResult("");
|
|
try {
|
|
const next = await updateProfile(settings, { ui_preferences: uiPreferencePayload() });
|
|
onAuthChange(next);
|
|
dispatchPlatformModulesChanged();
|
|
setUiResultTone("success");
|
|
setUiResult("i18n:govoplan-core.preferences_saved.c8cd3501");
|
|
return true;
|
|
} catch (err) {
|
|
setUiResultTone("warning");
|
|
setUiResult(err instanceof Error ? err.message : String(err));
|
|
return false;
|
|
} finally {
|
|
setUiBusy(false);
|
|
}
|
|
}
|
|
|
|
async function testConnection() {
|
|
setTesting(true);
|
|
setTestResult("");
|
|
try {
|
|
await apiFetch<unknown>(settings, "/health");
|
|
setTestResultTone("success");
|
|
setTestResult("i18n:govoplan-core.connection_successful_the_backend_health_endpoin.14eb9295");
|
|
} catch (err) {
|
|
setTestResultTone("warning");
|
|
setTestResult(err instanceof Error ? err.message : String(err));
|
|
} finally {
|
|
setTesting(false);
|
|
}
|
|
}
|
|
|
|
const editorSection = active === "profile" || active === "interface" || active === "workspace";
|
|
const editorDirty = active === "profile" ? profileDirty : editorSection ? uiDirty : false;
|
|
const editorSaving = active === "profile" ? profileBusy : editorSection ? uiBusy : false;
|
|
const discardEditor = () => {
|
|
if (active === "profile") {
|
|
setProfileName(auth.user.display_name || "");
|
|
setTenantProfileName(auth.user.tenant_display_name || "");
|
|
} else {
|
|
resetUiPreferences();
|
|
}
|
|
};
|
|
const saveEditor = () => {
|
|
if (active === "profile") void saveProfile();
|
|
else void saveUiPreferences();
|
|
};
|
|
|
|
return (
|
|
<WorkspaceLayout
|
|
className="module-workspace"
|
|
primary={<ModuleSubnav active={active} groups={settingsSubnav} onSelect={selectSection} />}
|
|
primaryLabel="i18n:govoplan-core.settings.c7f73bb5"
|
|
contentLabel="i18n:govoplan-core.settings.c7f73bb5"
|
|
>
|
|
<PageLayout
|
|
archetype={editorSection ? "editor" : "workspace"}
|
|
title="i18n:govoplan-core.settings.c7f73bb5"
|
|
description="i18n:govoplan-core.your_profile_personal_webui_preferences_and_loca.beda6d56"
|
|
actions={editorSection ? (
|
|
<PageActionBar
|
|
variant="editor"
|
|
state={editorSaving ? "saving" : editorDirty ? "dirty" : "clean"}
|
|
helpAction={<DocumentationHelpLink reference={SETTINGS_DOCUMENTATION} />}
|
|
discardAction={{ label: "i18n:govoplan-core.discard.36fff63c", onClick: discardEditor }}
|
|
saveAction={{
|
|
label: active === "profile" ? "i18n:govoplan-core.save_profile.f597c0e8" : "i18n:govoplan-core.save_preferences.0f1a7e44",
|
|
onClick: saveEditor
|
|
}}
|
|
/>
|
|
) : (
|
|
<PageActionBar variant="workspace" helpAction={<DocumentationHelpLink reference={SETTINGS_DOCUMENTATION} />} />
|
|
)}
|
|
mode="workspace"
|
|
>
|
|
|
|
{active === "profile" &&
|
|
<ContentGrid columns={2} collapseAt="workspace" className="">
|
|
<Card title="i18n:govoplan-core.my_profile.2f3df0a9">
|
|
<FormGrid columns={1} collapseAt="standard" className="">
|
|
<FormField label="i18n:govoplan-core.account_display_name.03ed8fc5" help="i18n:govoplan-core.this_global_name_is_shown_in_the_title_bar_and_f.1d46240e">
|
|
<input value={profileName} onChange={(event) => setProfileName(event.target.value)} placeholder={auth.user.email} />
|
|
</FormField>
|
|
<FormField label="i18n:govoplan-core.tenant_display_name.9ae4495e" help="i18n:govoplan-core.optional_local_alias_for_the_active_tenant_it_do.edbb7b14">
|
|
<input value={tenantProfileName} onChange={(event) => setTenantProfileName(event.target.value)} placeholder="i18n:govoplan-core.use_account_display_name.570926b0" />
|
|
</FormField>
|
|
<FormField label="i18n:govoplan-core.email.84add5b2">
|
|
<input value={auth.user.email} disabled />
|
|
</FormField>
|
|
{profileResult && <DismissibleAlert tone={profileResultTone} resetKey={profileResult} floating>{profileResult}</DismissibleAlert>}
|
|
</FormGrid>
|
|
</Card>
|
|
<Card title="i18n:govoplan-core.account_context.5bb0a918">
|
|
<DescriptionList variant="inline" density="compact">
|
|
<div><dt>i18n:govoplan-core.account_id.c5e0db28</dt><dd>{auth.user.account_id}</dd></div>
|
|
<div><dt>i18n:govoplan-core.active_tenant.39b16ee9</dt><dd>{auth.active_tenant?.name || auth.tenant.name}</dd></div>
|
|
<div><dt>i18n:govoplan-core.tenant_roles.51aca82d</dt><dd>{auth.roles.filter((role) => role.level !== "system").map((role) => role.name).join(", ") || "i18n:govoplan-core.none.6eef6648"}</dd></div>
|
|
<div><dt>i18n:govoplan-core.system_roles.a9461aa6</dt><dd>{auth.roles.filter((role) => role.level === "system").map((role) => role.name).join(", ") || "i18n:govoplan-core.none.6eef6648"}</dd></div>
|
|
</DescriptionList>
|
|
<p className="muted small-note">i18n:govoplan-core.email_changes_and_password_management_require_de.f7443b69</p>
|
|
</Card>
|
|
</ContentGrid>
|
|
}
|
|
|
|
{active === "mail-profiles" && MailProfileScopeManager &&
|
|
<MailProfileScopeManager
|
|
settings={settings}
|
|
scopeType="user"
|
|
scopeId={auth.user.id}
|
|
profileTitle="i18n:govoplan-core.my_mail_server_profiles.c5830798"
|
|
policyTitle="i18n:govoplan-core.my_mail_profile_policy.6e480f23"
|
|
canWriteProfiles={hasAnyScope(auth, ["mail_servers:write", "mail:profile:write_own"])}
|
|
canManageCredentials={hasAnyScope(auth, ["mail_servers:manage_credentials", "mail:secret:manage_own"])}
|
|
canWritePolicy={hasAnyScope(auth, ["admin:policies:write", "mail_servers:write"])} />
|
|
|
|
}
|
|
|
|
{active === "file-connectors" && FileConnectorScopeManager &&
|
|
<FileConnectorScopeManager
|
|
settings={settings}
|
|
scopeType="user"
|
|
scopeId={auth.user.id}
|
|
title="i18n:govoplan-core.my_file_connections.00252f2a"
|
|
canWrite={hasAnyScope(auth, ["files:file:admin", "admin:settings:write"])} />
|
|
|
|
}
|
|
|
|
{active === "credentials" &&
|
|
<CredentialEnvelopeManager
|
|
settings={settings}
|
|
scopeType="user"
|
|
scopeId={auth.user.id}
|
|
title="My reusable credentials"
|
|
canWrite={hasAnyScope(auth, [
|
|
"access:credential:write",
|
|
"access:credential:manage_own",
|
|
"mail:secret:manage_own",
|
|
"admin:settings:write"
|
|
])} />
|
|
|
|
}
|
|
|
|
{active === "interface" &&
|
|
<ContentGrid columns={2} collapseAt="workspace" className="">
|
|
<Card title="i18n:govoplan-core.interface_preferences.b82b39a7">
|
|
<FormGrid columns={1} collapseAt="standard" className="">
|
|
<ToggleSwitch
|
|
label="i18n:govoplan-core.compact_tables.c755d9ee"
|
|
help="i18n:govoplan-core.prepared_ui_preference_for_denser_tables_the_cur.45698d83"
|
|
checked={compactTables}
|
|
onChange={setCompactTables} />
|
|
|
|
<ToggleSwitch
|
|
label="i18n:govoplan-core.show_inline_help_hints.47cf5aaa"
|
|
help="i18n:govoplan-core.controls_contextual_ui_help_markers_once_persist.0eaef9c4"
|
|
checked={showHelpHints}
|
|
onChange={setShowHelpHints} />
|
|
|
|
<ToggleSwitch
|
|
label="i18n:govoplan-core.reduce_motion.25a5aef5"
|
|
help="i18n:govoplan-core.prepared_preference_for_users_who_prefer_fewer_a.b288e8ab"
|
|
checked={reduceMotion}
|
|
onChange={setReduceMotion} />
|
|
|
|
{uiResult && <DismissibleAlert tone={uiResultTone} resetKey={uiResult} floating>{uiResult}</DismissibleAlert>}
|
|
</FormGrid>
|
|
</Card>
|
|
<Card title="i18n:govoplan-core.theme_and_language.60a8cf59">
|
|
<FormGrid columns={1} collapseAt="standard" className="">
|
|
<FormField label="i18n:govoplan-core.language.89b86ab0">
|
|
<select value={language} onChange={(event) => setLanguage(event.target.value)} disabled={selectableLanguages.length <= 1}>
|
|
{selectableLanguages.map((item) =>
|
|
<option key={item.code} value={item.code}>{item.code.toUpperCase()} - {item.nativeLabel || item.label}</option>
|
|
)}
|
|
</select>
|
|
</FormField>
|
|
<FormField label="i18n:govoplan-core.theme.a797e309">
|
|
<SegmentedControl
|
|
options={UI_THEME_OPTIONS.map((item) => ({ id: item.value, label: item.label }))}
|
|
value={theme}
|
|
onChange={setTheme}
|
|
role="group"
|
|
size="equal"
|
|
width="fill"
|
|
ariaLabel="i18n:govoplan-core.theme.a797e309" />
|
|
</FormField>
|
|
<FormField
|
|
label="i18n:govoplan-core.color_palette"
|
|
help="i18n:govoplan-core.color_palette_help"
|
|
>
|
|
<AppearancePaletteSelect value={palette} onChange={setPalette} allowInherit disabled={auth.user.appearance?.locked === true} />
|
|
</FormField>
|
|
<div className="button-row compact-actions">
|
|
<Button onClick={() => setPalette(null)} disabled={palette === null || auth.user.appearance?.locked === true}>
|
|
i18n:govoplan-core.reset_palette
|
|
</Button>
|
|
</div>
|
|
<AppearancePalettePreview theme={theme} palette={palette ?? auth.user.appearance?.inherited_palette ?? "default"} />
|
|
<DescriptionList variant="inline" density="compact">
|
|
<div><dt>i18n:govoplan-core.theme.a797e309</dt><dd>{themeLabel(theme)}</dd></div>
|
|
<div><dt>i18n:govoplan-core.accent_color.e49578ed</dt><dd>{paletteLabel(palette ?? auth.user.appearance?.inherited_palette ?? "default")}</dd></div>
|
|
<div><dt>i18n:govoplan-core.effective_source</dt><dd>{appearanceSourceLabel(auth.user.appearance?.source)}</dd></div>
|
|
<div><dt>i18n:govoplan-core.accessibility</dt><dd>i18n:govoplan-core.palette_contrast_validated</dd></div>
|
|
<div><dt>i18n:govoplan-core.advanced_theme_overrides</dt><dd>i18n:govoplan-core.not_configured</dd></div>
|
|
<div><dt>i18n:govoplan-core.language.89b86ab0</dt><dd>{languageLabel}</dd></div>
|
|
<div><dt>i18n:govoplan-core.enabled.df174a3f</dt><dd>{enabledLanguages.map((item) => item.code.toUpperCase()).join(", ")}</dd></div>
|
|
<div><dt>i18n:govoplan-core.available.7c62a142</dt><dd>{availableLanguages.map((item) => item.code.toUpperCase()).join(", ")}</dd></div>
|
|
<div><dt>i18n:govoplan-core.density.f9160c22</dt><dd>{compactTables ? "i18n:govoplan-core.compact_preview.3e06901d" : "i18n:govoplan-core.comfortable.2313707a"}</dd></div>
|
|
</DescriptionList>
|
|
<p className="muted small-note">i18n:govoplan-core.advanced_theme_overrides_follow_up</p>
|
|
</FormGrid>
|
|
</Card>
|
|
</ContentGrid>
|
|
}
|
|
|
|
{active === "workspace" &&
|
|
<ContentGrid columns={2} collapseAt="workspace" className="">
|
|
<Card title="i18n:govoplan-core.campaign_workspace.c345580f">
|
|
<FormGrid columns={1} collapseAt="standard" className="">
|
|
<ToggleSwitch
|
|
label="i18n:govoplan-core.sticky_section_sidebars.399c92d4"
|
|
help="i18n:govoplan-core.keeps_campaign_and_admin_section_navigation_in_v.f3602938"
|
|
checked={stickySections}
|
|
onChange={setStickySections} />
|
|
|
|
<ToggleSwitch
|
|
label="i18n:govoplan-core.keep_page_shell_visible_while_loading.17fc142a"
|
|
help="i18n:govoplan-core.the_current_ui_already_keeps_the_section_shell_v.c9bbc227"
|
|
checked
|
|
disabled
|
|
onChange={() => undefined} />
|
|
|
|
{uiResult && <DismissibleAlert tone={uiResultTone} resetKey={uiResult} floating>{uiResult}</DismissibleAlert>}
|
|
</FormGrid>
|
|
</Card>
|
|
<Card title="i18n:govoplan-core.editor_behavior.99581bc1">
|
|
<div className="placeholder-stack">
|
|
<span>i18n:govoplan-core.manual_save_with_unsaved_change_guard.ef42e803</span>
|
|
<span>i18n:govoplan-core.readable_chooser_fields_for_file_path_selection.d3aee1a7</span>
|
|
<span>i18n:govoplan-core.field_type_aware_recipient_data_inputs.2892f175</span>
|
|
<span>i18n:govoplan-core.template_placeholder_chips_and_preview_overlays.11634d55</span>
|
|
</div>
|
|
</Card>
|
|
<Card title="Navigation order">
|
|
<NavigationPreferenceEditor
|
|
items={navigationItems}
|
|
value={navigation}
|
|
onChange={setNavigation}
|
|
scope="user"
|
|
disabled={uiBusy}
|
|
/>
|
|
</Card>
|
|
</ContentGrid>
|
|
}
|
|
|
|
{active === "local-connection" &&
|
|
<ContentGrid columns={2} collapseAt="workspace" className="">
|
|
<Card title="i18n:govoplan-core.local_api_connection.bfb02921">
|
|
<FormGrid columns={1} collapseAt="standard" className="">
|
|
<FormField label="i18n:govoplan-core.api_base_url.1358fba4" help="i18n:govoplan-core.leave_empty_to_use_the_same_origin_in_vite_dev_a.9a1c25d7">
|
|
<input value={settings.apiBaseUrl} onChange={(e) => onSettingsChange({ ...settings, apiBaseUrl: e.target.value })} placeholder="https://example.org or empty" />
|
|
</FormField>
|
|
<FormField label="i18n:govoplan-core.automation_api_key.5d4e2e6e" help="i18n:govoplan-core.used_only_when_there_is_no_browser_session_token.9d399e70">
|
|
<PasswordField
|
|
value={settings.apiKey}
|
|
autoComplete="off"
|
|
onValueChange={(apiKey) => onSettingsChange({ ...settings, apiKey })} />
|
|
|
|
</FormField>
|
|
<div className="button-row compact-actions">
|
|
<Button
|
|
variant="primary"
|
|
onClick={testConnection}
|
|
disabled={testing}
|
|
disabledReason={testing ? "The backend connection test is already running." : undefined}
|
|
>
|
|
{testing ? "i18n:govoplan-core.testing.95c4564a" : "i18n:govoplan-core.test_connection.ccf66f07"}
|
|
</Button>
|
|
</div>
|
|
{testResult && <DismissibleAlert tone={testResultTone} resetKey={testResult} floating>{testResult}</DismissibleAlert>}
|
|
</FormGrid>
|
|
</Card>
|
|
<Card title="i18n:govoplan-core.session_state.b7a3d0f4">
|
|
<DescriptionList variant="inline" density="compact">
|
|
<div><dt>i18n:govoplan-core.browser_session.cc021a31</dt><dd>i18n:govoplan-core.active_via_httponly_cookie.9d05e6ba</dd></div>
|
|
<div><dt>i18n:govoplan-core.automation_key.78f1ddbc</dt><dd>{settings.apiKey ? "i18n:govoplan-core.configured.668c5fff" : "i18n:govoplan-core.not_configured.811931bb"}</dd></div>
|
|
<div><dt>i18n:govoplan-core.backend_mode.c863723a</dt><dd>{settings.apiBaseUrl ? "i18n:govoplan-core.explicit_api_url.b117b4b8" : "i18n:govoplan-core.same_origin_proxied.c39e6e2b"}</dd></div>
|
|
</DescriptionList>
|
|
<p className="muted small-note">i18n:govoplan-core.tenant_user_mail_server_and_policy_administratio.2f551271</p>
|
|
</Card>
|
|
</ContentGrid>
|
|
}
|
|
|
|
{activeContributedSection &&
|
|
activeContributedSection.render({
|
|
settings,
|
|
auth,
|
|
onAuthChange,
|
|
activeSection: active,
|
|
availableSections: availableSectionIds,
|
|
selectSection
|
|
})
|
|
}
|
|
</PageLayout>
|
|
</WorkspaceLayout>);
|
|
|
|
}
|
|
|
|
|
|
function settingsSectionAvailable(sections: ReadonlySet<string>, section: string | null | undefined): section is SettingsSection {
|
|
return Boolean(section && sections.has(section));
|
|
}
|
|
|
|
function canUseSettingsContribution(auth: AuthInfo, section: SettingsSectionContribution): boolean {
|
|
if (section.allOf?.some((scope) => !hasScope(auth, scope))) {
|
|
return false;
|
|
}
|
|
if (section.anyOf && !hasAnyScope(auth, section.anyOf)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function sectionOrder(sectionId: string, contributedSections: SettingsSectionContribution[]): number {
|
|
const builtInOrder: Record<string, number> = {
|
|
profile: 10,
|
|
"mail-profiles": 20,
|
|
"file-connectors": 30,
|
|
"credentials": 40,
|
|
interface: 10,
|
|
workspace: 20,
|
|
"local-connection": 30
|
|
};
|
|
return builtInOrder[sectionId] ?? contributedSections.find((section) => section.id === sectionId)?.order ?? 100;
|
|
}
|
|
|
|
function normalizeUiPreferences(value: Partial<UserUiPreferences> | null | undefined): UserUiPreferences {
|
|
const theme = value?.theme === "light" || value?.theme === "dark" || value?.theme === "system" ? value.theme : "system";
|
|
return {
|
|
compact_tables: Boolean(value?.compact_tables ?? DEFAULT_UI_PREFERENCES.compact_tables),
|
|
show_inline_help_hints: Boolean(value?.show_inline_help_hints ?? DEFAULT_UI_PREFERENCES.show_inline_help_hints),
|
|
reduce_motion: Boolean(value?.reduce_motion ?? DEFAULT_UI_PREFERENCES.reduce_motion),
|
|
sticky_section_sidebars: Boolean(value?.sticky_section_sidebars ?? DEFAULT_UI_PREFERENCES.sticky_section_sidebars),
|
|
theme,
|
|
palette: normalizeOptionalUiPalette(value?.palette),
|
|
navigation: value?.navigation ?? null
|
|
};
|
|
}
|
|
|
|
function normalizeUiPalette(value: UserUiPalette | string | null | undefined): UserUiPalette {
|
|
return APPEARANCE_PALETTE_OPTIONS.some((item) => item.value === value)
|
|
? value as UserUiPalette
|
|
: "default";
|
|
}
|
|
|
|
function normalizeOptionalUiPalette(value: UserUiPalette | string | null | undefined): UserUiPalette | null {
|
|
return value == null ? null : normalizeUiPalette(value);
|
|
}
|
|
|
|
function themeLabel(value: UserUiTheme): string {
|
|
return UI_THEME_OPTIONS.find((item) => item.value === value)?.label ?? UI_THEME_OPTIONS[0].label;
|
|
}
|
|
|
|
function paletteLabel(value: UserUiPalette): string {
|
|
return appearancePaletteLabel(value);
|
|
}
|
|
|
|
function appearanceSourceLabel(value: string | null | undefined): string {
|
|
const labels: Record<string, string> = {
|
|
user: "i18n:govoplan-core.appearance_source_user",
|
|
tenant: "i18n:govoplan-core.appearance_source_tenant",
|
|
system: "i18n:govoplan-core.appearance_source_system",
|
|
tenant_lock: "i18n:govoplan-core.appearance_source_tenant_lock",
|
|
system_lock: "i18n:govoplan-core.appearance_source_system_lock"
|
|
};
|
|
return labels[value ?? "system"] ?? labels.system;
|
|
}
|