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[] { const groups: ModuleSubnavGroup[] = [ { 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("mail.profiles"); const fileConnectorsUi = usePlatformUiCapability("files.connectors"); const settingsSectionCapabilities = usePlatformUiCapabilities("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(currentUiPreferences.theme); const [palette, setPalette] = useState(currentUiPreferences.palette); const [navigation, setNavigation] = useState(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 { 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 { 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(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 ( } primaryLabel="i18n:govoplan-core.settings.c7f73bb5" contentLabel="i18n:govoplan-core.settings.c7f73bb5" > } 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 }} /> ) : ( } /> )} mode="workspace" > {active === "profile" && setProfileName(event.target.value)} placeholder={auth.user.email} /> setTenantProfileName(event.target.value)} placeholder="i18n:govoplan-core.use_account_display_name.570926b0" /> {profileResult && {profileResult}}
i18n:govoplan-core.account_id.c5e0db28
{auth.user.account_id}
i18n:govoplan-core.active_tenant.39b16ee9
{auth.active_tenant?.name || auth.tenant.name}
i18n:govoplan-core.tenant_roles.51aca82d
{auth.roles.filter((role) => role.level !== "system").map((role) => role.name).join(", ") || "i18n:govoplan-core.none.6eef6648"}
i18n:govoplan-core.system_roles.a9461aa6
{auth.roles.filter((role) => role.level === "system").map((role) => role.name).join(", ") || "i18n:govoplan-core.none.6eef6648"}

i18n:govoplan-core.email_changes_and_password_management_require_de.f7443b69

} {active === "mail-profiles" && MailProfileScopeManager && } {active === "file-connectors" && FileConnectorScopeManager && } {active === "credentials" && } {active === "interface" && {uiResult && {uiResult}} ({ id: item.value, label: item.label }))} value={theme} onChange={setTheme} role="group" size="equal" width="fill" ariaLabel="i18n:govoplan-core.theme.a797e309" />
i18n:govoplan-core.theme.a797e309
{themeLabel(theme)}
i18n:govoplan-core.accent_color.e49578ed
{paletteLabel(palette ?? auth.user.appearance?.inherited_palette ?? "default")}
i18n:govoplan-core.effective_source
{appearanceSourceLabel(auth.user.appearance?.source)}
i18n:govoplan-core.accessibility
i18n:govoplan-core.palette_contrast_validated
i18n:govoplan-core.advanced_theme_overrides
i18n:govoplan-core.not_configured
i18n:govoplan-core.language.89b86ab0
{languageLabel}
i18n:govoplan-core.enabled.df174a3f
{enabledLanguages.map((item) => item.code.toUpperCase()).join(", ")}
i18n:govoplan-core.available.7c62a142
{availableLanguages.map((item) => item.code.toUpperCase()).join(", ")}
i18n:govoplan-core.density.f9160c22
{compactTables ? "i18n:govoplan-core.compact_preview.3e06901d" : "i18n:govoplan-core.comfortable.2313707a"}

i18n:govoplan-core.advanced_theme_overrides_follow_up

} {active === "workspace" && undefined} /> {uiResult && {uiResult}}
i18n:govoplan-core.manual_save_with_unsaved_change_guard.ef42e803 i18n:govoplan-core.readable_chooser_fields_for_file_path_selection.d3aee1a7 i18n:govoplan-core.field_type_aware_recipient_data_inputs.2892f175 i18n:govoplan-core.template_placeholder_chips_and_preview_overlays.11634d55
} {active === "local-connection" && onSettingsChange({ ...settings, apiBaseUrl: e.target.value })} placeholder="https://example.org or empty" /> onSettingsChange({ ...settings, apiKey })} />
{testResult && {testResult}}
i18n:govoplan-core.browser_session.cc021a31
i18n:govoplan-core.active_via_httponly_cookie.9d05e6ba
i18n:govoplan-core.automation_key.78f1ddbc
{settings.apiKey ? "i18n:govoplan-core.configured.668c5fff" : "i18n:govoplan-core.not_configured.811931bb"}
i18n:govoplan-core.backend_mode.c863723a
{settings.apiBaseUrl ? "i18n:govoplan-core.explicit_api_url.b117b4b8" : "i18n:govoplan-core.same_origin_proxied.c39e6e2b"}

i18n:govoplan-core.tenant_user_mail_server_and_policy_administratio.2f551271

} {activeContributedSection && activeContributedSection.render({ settings, auth, onAuthChange, activeSection: active, availableSections: availableSectionIds, selectSection }) }
); } function settingsSectionAvailable(sections: ReadonlySet, 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 = { 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 | 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 = { 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; }