Migrate Organizations interface patterns

This commit is contained in:
2026-08-03 12:09:33 +02:00
parent 3f0f38d226
commit 97acfcba7d
11 changed files with 533 additions and 74 deletions
+2 -1
View File
@@ -7,7 +7,8 @@
"module": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"test:organizations-tree": "node scripts/test-organizations-tree-structure.mjs"
"test:organizations-tree": "node scripts/test-organizations-tree-structure.mjs",
"test:interface-patterns": "node scripts/test-interface-pattern-language.mjs"
},
"exports": {
".": {
@@ -0,0 +1,28 @@
import { readFileSync } from "node:fs";
function source(path) {
return readFileSync(new URL(path, import.meta.url), "utf8");
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
const page = source("../src/features/organizations/OrganizationsPage.tsx");
const settings = source("../src/features/organizations/OrganizationsAdminPanel.tsx");
const patterns = source("../src/features/organizations/interfacePatterns.ts");
const moduleSource = source("../src/module.ts");
const translations = source("../src/i18n/generatedTranslations.ts");
assert(page.includes("DocumentationHelpLink") && settings.includes("DocumentationHelpLink"), "Workspace and tenant settings expose contextual documentation");
assert(page.includes("ActionBlockerHint") && settings.includes("ActionBlockerHint"), "Read-only states identify action, actor, and destination");
assert(page.includes("disabledReason") && settings.includes("disabledReason"), "Unavailable organization actions explain their state");
assert(page.includes("requestDiscard(() => void loadModel())") && settings.includes("requestDiscard(() => void load())"), "Reload preserves dirty organization drafts and settings");
assert(page.includes("hasDirtyDraft ? requestDiscard(discardDrafts)"), "Editor close uses the shared unsaved-change guard");
assert(page.includes("ORGANIZATIONS_FIELD_DOCUMENTATION"), "Model fields link to stable consequence documentation");
assert(patterns.includes('topicId: "organizations.model"') && patterns.includes('topicId: "organizations.reference.fields-and-consequences"'), "Organizations uses manifest-backed help references");
assert(moduleSource.includes('version: "0.1.8"') && moduleSource.includes('label: "i18n:govoplan-organizations.organizations_administration"'), "WebUI metadata matches the module release and localizes its composed surface");
assert(translations.includes('"i18n:govoplan-organizations.read_only_summary"'), "Blocker explanations are present in the translation catalogue");
assert(!page.includes("window.confirm"), "Organizations does not use browser-native consequential confirmation");
console.log("Organizations surfaces satisfy the recorded interface pattern-language contract.");
@@ -1,12 +1,15 @@
import { useEffect, useState } from "react";
import {
ActionBlockerHint,
AdminPageLayout,
Button,
Card,
DocumentationHelpLink,
FormField,
ToggleSwitch,
adminErrorMessage,
hasAnyScope,
useUnsavedChanges,
useUnsavedDraftGuard,
type ApiSettings,
type AuthInfo
@@ -17,6 +20,12 @@ import {
type OrganizationAuditDetailLevel,
type OrganizationSettingsItem
} from "../../api/organizations";
import {
ORGANIZATIONS_DOCUMENTATION,
ORGANIZATIONS_FIELD_DOCUMENTATION,
ORGANIZATIONS_INTERFACE_I18N,
organizationWriteReason
} from "./interfacePatterns";
const FALLBACK_SETTINGS: OrganizationSettingsItem = {
tenant_id: "",
@@ -40,8 +49,16 @@ export default function OrganizationsAdminPanel({ settings, auth }: { settings:
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const { requestDiscard } = useUnsavedChanges();
const canWrite = hasAnyScope(auth, ["organizations:settings:write", "admin:settings:write"]);
const dirty = settingsKey(draft) !== settingsKey(savedDraft);
const reloadDisabledReason = loading
? ORGANIZATIONS_INTERFACE_I18N.loading
: busy
? ORGANIZATIONS_INTERFACE_I18N.busy
: undefined;
const saveDisabledReason = organizationWriteReason(canWrite, busy, "settings")
?? (!dirty ? ORGANIZATIONS_INTERFACE_I18N.noChanges : undefined);
useUnsavedDraftGuard({
dirty,
@@ -99,20 +116,57 @@ export default function OrganizationsAdminPanel({ settings, auth }: { settings:
loadingLabel="i18n:govoplan-organizations.loading_organization_settings.c6008db8"
error={error}
success={success}
actions={<><Button onClick={() => void load()} disabled={loading || busy}>i18n:govoplan-organizations.reload.cce71553</Button><Button variant="primary" onClick={() => void save()} disabled={!canWrite || busy || !dirty}>{busy ? "i18n:govoplan-organizations.saving.56a2285c" : "i18n:govoplan-organizations.save_settings.913aba9f"}</Button></>}
actions={(
<>
<DocumentationHelpLink reference={ORGANIZATIONS_DOCUMENTATION} />
<Button
onClick={() => requestDiscard(() => void load())}
disabled={Boolean(reloadDisabledReason)}
disabledReason={reloadDisabledReason}
>
i18n:govoplan-organizations.reload.cce71553
</Button>
<Button
variant="primary"
onClick={() => void save()}
disabled={Boolean(saveDisabledReason)}
disabledReason={saveDisabledReason}
>
{busy ? "i18n:govoplan-organizations.saving.56a2285c" : "i18n:govoplan-organizations.save_settings.913aba9f"}
</Button>
</>
)}
>
{!canWrite && (
<ActionBlockerHint
reason={{
summary: ORGANIZATIONS_INTERFACE_I18N.readOnlySummary,
requiredAction: ORGANIZATIONS_INTERFACE_I18N.permissionGuidance,
actor: ORGANIZATIONS_INTERFACE_I18N.administrator,
target: ORGANIZATIONS_INTERFACE_I18N.administrationTarget
}}
labels={{
requiredAction: ORGANIZATIONS_INTERFACE_I18N.requiredAction,
actor: ORGANIZATIONS_INTERFACE_I18N.actor,
target: ORGANIZATIONS_INTERFACE_I18N.destination
}}
documentation={ORGANIZATIONS_DOCUMENTATION}
/>
)}
<div className="organizations-settings-grid">
<Card title="i18n:govoplan-organizations.model_governance.6aa18fd0">
<div className="settings-list">
<ToggleSwitch
checked={draft.allow_tenant_model_customization}
disabled={!canWrite || busy}
help={organizationWriteReason(canWrite, busy, "settings")}
onChange={(checked) => setDraft({ ...draft, allow_tenant_model_customization: checked })}
label="i18n:govoplan-organizations.allow_tenant_model_customization.2425d751"
/>
<ToggleSwitch
checked={draft.require_model_change_requests}
disabled={!canWrite || busy}
help={organizationWriteReason(canWrite, busy, "settings")}
onChange={(checked) => setDraft({ ...draft, require_model_change_requests: checked })}
label="i18n:govoplan-organizations.require_model_change_requests.83454cad"
/>
@@ -122,12 +176,20 @@ export default function OrganizationsAdminPanel({ settings, auth }: { settings:
<Card title="i18n:govoplan-organizations.audit_and_retention.3ba1d2fc">
<div className="admin-form-grid two-columns">
<FormField label="i18n:govoplan-organizations.audit_detail_level.7397355d">
<FormField
label="i18n:govoplan-organizations.audit_detail_level.7397355d"
help={organizationWriteReason(canWrite, busy, "settings")}
documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}
>
<select value={draft.audit_detail_level} disabled={!canWrite || busy} onChange={(event) => setDraft({ ...draft, audit_detail_level: event.target.value as OrganizationAuditDetailLevel })}>
{AUDIT_DETAIL_LEVELS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-organizations.change_retention_days.71bbd140">
<FormField
label="i18n:govoplan-organizations.change_retention_days.71bbd140"
help={organizationWriteReason(canWrite, busy, "settings")}
documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}
>
<input
type="number"
min={0}
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from "react";
import { Edit3, Plus, RefreshCw } from "lucide-react";
import {
ActionBlockerHint,
AdminIconButton,
ApiError,
Button,
@@ -8,6 +9,7 @@ import {
DataGrid,
Dialog,
DismissibleAlert,
DocumentationHelpLink,
ExplorerTree,
FormField,
IconButton,
@@ -20,6 +22,7 @@ import {
hasScope,
isViewSurfaceVisible,
useEffectiveView,
useUnsavedChanges,
useUnsavedDraftGuard,
usePlatformUiCapabilities,
useViewSurfaces,
@@ -62,6 +65,12 @@ import {
type StructureCreatePayload,
type UnitCreatePayload
} from "../../api/organizations";
import {
ORGANIZATIONS_DOCUMENTATION,
ORGANIZATIONS_FIELD_DOCUMENTATION,
ORGANIZATIONS_INTERFACE_I18N,
organizationWriteReason
} from "./interfacePatterns";
export type OrganizationSection = "model" | "units" | "relations" | "functions";
type OrganizationsPageMode = "workspace" | "admin";
@@ -302,22 +311,36 @@ function activeStatus(active: boolean): JSX.Element {
function SluggedFields({
draft,
onChange,
disabled
disabled,
disabledReason
}: {
draft: SluggedDraft;
onChange: (next: SluggedDraft) => void;
disabled: boolean;
disabledReason?: string;
}) {
return (
<>
<FormField label="i18n:govoplan-organizations.name.709a2322">
<FormField
label="i18n:govoplan-organizations.name.709a2322"
help={disabledReason}
documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}
>
<input value={draft.name} disabled={disabled} onChange={(event) => onChange({ ...draft, name: event.target.value })} />
</FormField>
<FormField label="i18n:govoplan-organizations.slug.094da9b9">
<FormField
label="i18n:govoplan-organizations.slug.094da9b9"
help={disabledReason}
documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}
>
<input value={draft.slug} disabled={disabled} onChange={(event) => onChange({ ...draft, slug: event.target.value })} />
</FormField>
<div className="wide">
<FormField label="i18n:govoplan-organizations.description.55f8ebc8">
<FormField
label="i18n:govoplan-organizations.description.55f8ebc8"
help={disabledReason}
documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}
>
<textarea rows={3} value={draft.description} disabled={disabled} onChange={(event) => onChange({ ...draft, description: event.target.value })} />
</FormField>
</div>
@@ -325,6 +348,7 @@ function SluggedFields({
<ToggleSwitch
checked={draft.is_active}
disabled={disabled}
help={disabledReason}
onChange={(checked) => onChange({ ...draft, is_active: checked })}
label="i18n:govoplan-organizations.active.a733b809"
/>
@@ -362,6 +386,7 @@ export default function OrganizationsPage({
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const { requestDiscard } = useUnsavedChanges();
const [unitTypeDraft, setUnitTypeDraft] = useState<SluggedDraft>(() => emptySluggedDraft());
const [structureDraft, setStructureDraft] = useState<StructureDraft>(() => emptyStructureDraft());
@@ -871,7 +896,7 @@ export default function OrganizationsPage({
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, sortable: true, filterable: true, value: (row) => row.slug },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "description", header: "i18n:govoplan-organizations.description.55f8ebc8", minWidth: 220, value: (row) => row.description ?? "" },
{ id: "actions", header: "i18n:govoplan-core.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editUnitType(row) }]} /> }
{ id: "actions", header: "i18n:govoplan-core.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, disabledReason: organizationWriteReason(canWriteModel, busy, "model"), onClick: () => editUnitType(row) }]} /> }
];
const structureColumns: DataGridColumn<OrganizationStructureItem>[] = [
@@ -879,7 +904,7 @@ export default function OrganizationsPage({
{ id: "kind", header: "i18n:govoplan-organizations.kind.794c9d9c", width: 140, sortable: true, filterable: true, value: (row) => row.structure_kind },
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "i18n:govoplan-core.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editStructure(row) }]} /> }
{ id: "actions", header: "i18n:govoplan-core.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, disabledReason: organizationWriteReason(canWriteModel, busy, "model"), onClick: () => editStructure(row) }]} /> }
];
const relationTypeColumns: DataGridColumn<OrganizationRelationTypeItem>[] = [
@@ -889,7 +914,7 @@ export default function OrganizationsPage({
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 170, render: (row) => optionalLabel(unitTypeById.get(row.target_unit_type_id || "")?.name) },
{ id: "hierarchical", header: "i18n:govoplan-organizations.hierarchical.8964f313", width: 130, render: (row) => activeStatus(row.is_hierarchical) },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "i18n:govoplan-core.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editRelationType(row) }]} /> }
{ id: "actions", header: "i18n:govoplan-core.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, disabledReason: organizationWriteReason(canWriteModel, busy, "model"), onClick: () => editRelationType(row) }]} /> }
];
const unitColumns: DataGridColumn<OrganizationUnitItem>[] = [
@@ -898,7 +923,7 @@ export default function OrganizationsPage({
{ id: "parent", header: "i18n:govoplan-organizations.parent_unit.1986c35e", minWidth: 180, render: (row) => optionalLabel(unitById.get(row.parent_id || "")?.name) },
{ id: "slug", header: "i18n:govoplan-organizations.slug.094da9b9", width: 160, value: (row) => row.slug },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "i18n:govoplan-core.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteUnits || busy, onClick: () => editUnit(row) }]} /> }
{ id: "actions", header: "i18n:govoplan-core.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteUnits || busy, disabledReason: organizationWriteReason(canWriteUnits, busy, "unit"), onClick: () => editUnit(row) }]} /> }
];
const relationColumns: DataGridColumn<OrganizationRelationItem>[] = [
@@ -907,7 +932,7 @@ export default function OrganizationsPage({
{ id: "source", header: "i18n:govoplan-organizations.source_unit.2fbd8baa", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.source_unit_id)?.name) },
{ id: "target", header: "i18n:govoplan-organizations.target_unit.a1507d86", minWidth: 190, render: (row) => optionalLabel(unitById.get(row.target_unit_id)?.name) },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "i18n:govoplan-core.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteUnits || busy, onClick: () => editRelation(row) }]} /> }
{ id: "actions", header: "i18n:govoplan-core.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteUnits || busy, disabledReason: organizationWriteReason(canWriteUnits, busy, "unit"), onClick: () => editRelation(row) }]} /> }
];
const functionTypeColumns: DataGridColumn<OrganizationFunctionTypeItem>[] = [
@@ -916,7 +941,7 @@ export default function OrganizationsPage({
{ id: "delegable", header: "i18n:govoplan-organizations.delegable.b4f0137d", width: 120, render: (row) => activeStatus(row.delegable) },
{ id: "actInPlace", header: "i18n:govoplan-organizations.act_in_place.49b942bd", width: 140, render: (row) => activeStatus(row.act_in_place_allowed) },
{ id: "status", header: "i18n:govoplan-organizations.status.bae7d5be", width: 120, render: (row) => activeStatus(row.is_active) },
{ id: "actions", header: "i18n:govoplan-core.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, onClick: () => editFunctionType(row) }]} /> }
{ id: "actions", header: "i18n:govoplan-core.actions.c3cd636a", width: 72, sticky: "end", resizable: false, align: "right", render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-organizations.edit.7dce1220", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canWriteModel || busy, disabledReason: organizationWriteReason(canWriteModel, busy, "model"), onClick: () => editFunctionType(row) }]} /> }
];
const functionColumns: DataGridColumn<OrganizationFunctionItem>[] = [
@@ -938,6 +963,7 @@ export default function OrganizationsPage({
label: "i18n:govoplan-organizations.edit.7dce1220",
icon: <Edit3 size={16} aria-hidden="true" />,
disabled: !canWriteFunctions || busy,
disabledReason: organizationWriteReason(canWriteFunctions, busy, "function"),
onClick: () => editFunction(row)
},
...functionActionContributions.map((contribution) => ({
@@ -950,7 +976,26 @@ export default function OrganizationsPage({
}
];
const canWriteActive = active === "model" ? canWriteModel : active === "units" || active === "relations" ? canWriteUnits : canWriteFunctions;
const activeWriteKind = active === "model"
? "model"
: active === "units" || active === "relations"
? "unit"
: "function";
const canWriteActive = activeWriteKind === "model"
? canWriteModel
: activeWriteKind === "unit"
? canWriteUnits
: canWriteFunctions;
const activeWriteReason = organizationWriteReason(
canWriteActive,
busy,
activeWriteKind
);
const reloadDisabledReason = loading
? ORGANIZATIONS_INTERFACE_I18N.loading
: busy
? ORGANIZATIONS_INTERFACE_I18N.busy
: undefined;
const content = (
<div className={`${mode === "workspace" ? "content-pad " : ""}organizations-page ${mode === "admin" ? "organizations-admin-page" : ""}`.trim()}>
@@ -960,7 +1005,14 @@ export default function OrganizationsPage({
<p>{description}</p>
</div>
<div className="organizations-toolbar">
<Button type="button" onClick={() => void loadModel()} disabled={loading || busy} title="i18n:govoplan-organizations.reload.cce71553">
<DocumentationHelpLink reference={ORGANIZATIONS_DOCUMENTATION} />
<Button
type="button"
onClick={() => requestDiscard(() => void loadModel())}
disabled={Boolean(reloadDisabledReason)}
disabledReason={reloadDisabledReason}
title="i18n:govoplan-organizations.reload.cce71553"
>
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-organizations.reload.cce71553
</Button>
</div>
@@ -968,7 +1020,23 @@ export default function OrganizationsPage({
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{success && !error && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
{!canWriteActive && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-organizations.write_permission_required.8b09fd67</DismissibleAlert>}
{!canWriteActive && (
<ActionBlockerHint
reason={{
summary: ORGANIZATIONS_INTERFACE_I18N.readOnlySummary,
details: activeWriteReason,
requiredAction: ORGANIZATIONS_INTERFACE_I18N.permissionGuidance,
actor: ORGANIZATIONS_INTERFACE_I18N.administrator,
target: ORGANIZATIONS_INTERFACE_I18N.administrationTarget
}}
labels={{
requiredAction: ORGANIZATIONS_INTERFACE_I18N.requiredAction,
actor: ORGANIZATIONS_INTERFACE_I18N.actor,
target: ORGANIZATIONS_INTERFACE_I18N.destination
}}
documentation={ORGANIZATIONS_DOCUMENTATION}
/>
)}
<LoadingFrame loading={loading || busy} label="i18n:govoplan-organizations.loading_organization_model.846aa317">
{active === "model" && renderModelSection()}
@@ -984,7 +1052,11 @@ export default function OrganizationsPage({
return (
<div className="workspace organizations-workspace">
<ModuleSubnav active={active} groups={visibleSectionGroups} onSelect={setActive} />
<ModuleSubnav
active={active}
groups={visibleSectionGroups}
onSelect={(section) => requestDiscard(() => setActive(section))}
/>
<main className="workspace-content">
{content}
</main>
@@ -994,16 +1066,16 @@ export default function OrganizationsPage({
function renderModelSection() {
return (
<div className="organizations-table-stack">
<Card title="i18n:govoplan-organizations.unit_types.c7afc174" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit_type.58f2c05a" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} onClick={openUnitTypeCreate} />}>
<Card title="i18n:govoplan-organizations.unit_types.c7afc174" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit_type.58f2c05a" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} onClick={openUnitTypeCreate} />}>
<DataGrid id="organizations-unit-types" rows={model.unit_types} columns={unitTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_unit_types_found.c81fb2a7" initialFit="container" />
</Card>
<Card title="i18n:govoplan-organizations.structures.f9b7f3b4" actions={<AdminIconButton label="i18n:govoplan-organizations.add_structure.b722042a" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} onClick={openStructureCreate} />}>
<Card title="i18n:govoplan-organizations.structures.f9b7f3b4" actions={<AdminIconButton label="i18n:govoplan-organizations.add_structure.b722042a" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} onClick={openStructureCreate} />}>
<DataGrid id="organizations-structures" rows={model.structures} columns={structureColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_structures_found.20b382d0" initialFit="container" />
</Card>
<Card title="i18n:govoplan-organizations.relation_types.e5890528" actions={<AdminIconButton label="i18n:govoplan-organizations.add_relation_type.2ad19d03" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} onClick={openRelationTypeCreate} />}>
<Card title="i18n:govoplan-organizations.relation_types.e5890528" actions={<AdminIconButton label="i18n:govoplan-organizations.add_relation_type.2ad19d03" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} onClick={openRelationTypeCreate} />}>
<DataGrid id="organizations-relation-types" rows={model.relation_types} columns={relationTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_relation_types_found.6f90bb81" initialFit="container" />
</Card>
<Card title="i18n:govoplan-organizations.function_types.172c01fe" actions={<AdminIconButton label="i18n:govoplan-organizations.add_function_type.90d793f1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} onClick={openFunctionTypeCreate} />}>
<Card title="i18n:govoplan-organizations.function_types.172c01fe" actions={<AdminIconButton label="i18n:govoplan-organizations.add_function_type.90d793f1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} onClick={openFunctionTypeCreate} />}>
<DataGrid id="organizations-function-types" rows={model.function_types} columns={functionTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_function_types_found.5eef31b1" initialFit="container" />
</Card>
</div>
@@ -1013,9 +1085,9 @@ export default function OrganizationsPage({
function renderUnitsSection() {
return (
<div className="organizations-table-stack">
<Card title="i18n:govoplan-organizations.organization_tree.e5bfb195" actions={<AdminIconButton label="i18n:govoplan-organizations.add_root_unit.1ef8a9f5" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={() => openUnitCreate()} />}>
<Card title="i18n:govoplan-organizations.organization_tree.e5bfb195" actions={<AdminIconButton label="i18n:govoplan-organizations.add_root_unit.1ef8a9f5" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} disabledReason={organizationWriteReason(canWriteUnits, busy, "unit")} onClick={() => openUnitCreate()} />}>
<div className="explorer-tree-toolbar">
{selectedUnitId && <Button type="button" variant="ghost" disabled={busy} onClick={() => setSelectedUnitId("")}>i18n:govoplan-organizations.none.334c4a4c</Button>}
{selectedUnitId && <Button type="button" variant="ghost" disabled={busy} disabledReason={busy ? ORGANIZATIONS_INTERFACE_I18N.busy : undefined} onClick={() => setSelectedUnitId("")}>i18n:govoplan-organizations.none.334c4a4c</Button>}
</div>
{model.units.length ? (
<ExplorerTree
@@ -1041,13 +1113,14 @@ export default function OrganizationsPage({
icon={<Plus size={16} aria-hidden="true" />}
variant="ghost"
disabled={!canWriteUnits || busy}
disabledReason={organizationWriteReason(canWriteUnits, busy, "unit")}
onClick={() => addSubunit(unit)}
/>
)}
/>
) : <p className="muted small-note">i18n:govoplan-organizations.no_units_found.eea2dd0c</p>}
</Card>
<Card title="i18n:govoplan-organizations.units.e14d0d92" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit.8fa12fb1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={() => openUnitCreate()} />}>
<Card title="i18n:govoplan-organizations.units.e14d0d92" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit.8fa12fb1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} disabledReason={organizationWriteReason(canWriteUnits, busy, "unit")} onClick={() => openUnitCreate()} />}>
<DataGrid id="organizations-units" rows={model.units} columns={unitColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_units_found.eea2dd0c" initialFit="container" />
</Card>
</div>
@@ -1057,7 +1130,7 @@ export default function OrganizationsPage({
function renderRelationsSection() {
return (
<div className="organizations-table-stack">
<Card title="i18n:govoplan-organizations.relations.1c796711" actions={<AdminIconButton label="i18n:govoplan-organizations.add_relation.6b6e67ea" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} onClick={openRelationCreate} />}>
<Card title="i18n:govoplan-organizations.relations.1c796711" actions={<AdminIconButton label="i18n:govoplan-organizations.add_relation.6b6e67ea" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} disabledReason={organizationWriteReason(canWriteUnits, busy, "unit")} onClick={openRelationCreate} />}>
<DataGrid id="organizations-relations" rows={model.relations} columns={relationColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_relations_found.4e75b11d" initialFit="container" />
</Card>
</div>
@@ -1067,7 +1140,7 @@ export default function OrganizationsPage({
function renderFunctionsSection() {
return (
<div className="organizations-table-stack">
<Card title="i18n:govoplan-organizations.functions.805dc49b" actions={<AdminIconButton label="i18n:govoplan-organizations.add_function.6abafee0" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteFunctions || busy} onClick={() => openFunctionCreate()} />}>
<Card title="i18n:govoplan-organizations.functions.805dc49b" actions={<AdminIconButton label="i18n:govoplan-organizations.add_function.6abafee0" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteFunctions || busy} disabledReason={organizationWriteReason(canWriteFunctions, busy, "function")} onClick={() => openFunctionCreate()} />}>
<DataGrid id="organizations-functions" rows={model.functions} columns={functionColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_functions_found.54e759a0" initialFit="container" />
</Card>
</div>
@@ -1077,7 +1150,11 @@ export default function OrganizationsPage({
function renderChangeRequestField() {
return (
<div className="wide organizations-dialog-change-request">
<FormField label="i18n:govoplan-organizations.change_request_id.a6e7fd6b">
<FormField
label="i18n:govoplan-organizations.change_request_id.a6e7fd6b"
help={busy ? ORGANIZATIONS_INTERFACE_I18N.busy : undefined}
documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}
>
<input value={changeRequestId} onChange={(event) => setChangeRequestId(event.target.value)} placeholder="cfgreq-..." disabled={busy} />
</FormField>
<p className="organizations-field-note">i18n:govoplan-organizations.change_request_id_help.0c119a5d</p>
@@ -1099,18 +1176,32 @@ export default function OrganizationsPage({
}
function editorSubmitDisabled(): boolean {
if (busy || !activeEditor) return true;
if ((activeEditor === "unitType" || activeEditor === "structure" || activeEditor === "relationType" || activeEditor === "functionType") && !canWriteModel) return true;
if ((activeEditor === "unit" || activeEditor === "relation") && !canWriteUnits) return true;
if (activeEditor === "function" && !canWriteFunctions) return true;
if (activeEditor === "unitType") return !unitTypeDraft.name.trim();
if (activeEditor === "structure") return !structureDraft.name.trim();
if (activeEditor === "relationType") return !relationTypeDraft.name.trim();
if (activeEditor === "unit") return !unitDraft.name.trim();
if (activeEditor === "relation") return !relationDraft.structure_id || !relationDraft.relation_type_id || !relationDraft.source_unit_id || !relationDraft.target_unit_id;
if (activeEditor === "functionType") return !functionTypeDraft.name.trim();
if (activeEditor === "function") return !functionDraft.name.trim() || !functionDraft.organization_unit_id;
return true;
return Boolean(editorSubmitDisabledReason());
}
function editorSubmitDisabledReason(): string | undefined {
if (busy) return ORGANIZATIONS_INTERFACE_I18N.busy;
if (!activeEditor) return ORGANIZATIONS_INTERFACE_I18N.incomplete;
if (activeEditor === "unitType" || activeEditor === "structure" || activeEditor === "relationType" || activeEditor === "functionType") {
const reason = organizationWriteReason(canWriteModel, busy, "model");
if (reason) return reason;
}
if (activeEditor === "unit" || activeEditor === "relation") {
const reason = organizationWriteReason(canWriteUnits, busy, "unit");
if (reason) return reason;
}
if (activeEditor === "function") {
const reason = organizationWriteReason(canWriteFunctions, busy, "function");
if (reason) return reason;
}
if (activeEditor === "unitType" && !unitTypeDraft.name.trim()) return ORGANIZATIONS_INTERFACE_I18N.incomplete;
if (activeEditor === "structure" && !structureDraft.name.trim()) return ORGANIZATIONS_INTERFACE_I18N.incomplete;
if (activeEditor === "relationType" && !relationTypeDraft.name.trim()) return ORGANIZATIONS_INTERFACE_I18N.incomplete;
if (activeEditor === "unit" && !unitDraft.name.trim()) return ORGANIZATIONS_INTERFACE_I18N.incomplete;
if (activeEditor === "relation" && (!relationDraft.structure_id || !relationDraft.relation_type_id || !relationDraft.source_unit_id || !relationDraft.target_unit_id)) return ORGANIZATIONS_INTERFACE_I18N.incomplete;
if (activeEditor === "functionType" && !functionTypeDraft.name.trim()) return ORGANIZATIONS_INTERFACE_I18N.incomplete;
if (activeEditor === "function" && (!functionDraft.name.trim() || !functionDraft.organization_unit_id)) return ORGANIZATIONS_INTERFACE_I18N.incomplete;
return undefined;
}
function renderEditorDialog() {
@@ -1127,16 +1218,38 @@ export default function OrganizationsPage({
<Dialog
open={Boolean(activeEditor)}
title={editorTitle()}
onClose={() => !busy && discardDrafts()}
onClose={() => {
if (busy) return;
if (hasDirtyDraft) requestDiscard(discardDrafts);
else discardDrafts();
}}
closeDisabled={busy}
className="admin-dialog admin-dialog-wide organizations-editor-dialog"
footer={(
<>
<Button type="button" onClick={discardDrafts} disabled={busy}>i18n:govoplan-organizations.cancel_edit.309c2a6f</Button>
<Button type="submit" form={formId} variant="primary" disabled={editorSubmitDisabled()}>{editorTitle()}</Button>
<Button
type="button"
onClick={() => hasDirtyDraft ? requestDiscard(discardDrafts) : discardDrafts()}
disabled={busy}
disabledReason={busy ? ORGANIZATIONS_INTERFACE_I18N.busy : undefined}
>
i18n:govoplan-organizations.cancel_edit.309c2a6f
</Button>
<Button
type="submit"
form={formId}
variant="primary"
disabled={editorSubmitDisabled()}
disabledReason={editorSubmitDisabledReason()}
>
{editorTitle()}
</Button>
</>
)}
>
<div className="button-row organizations-dialog-help">
<DocumentationHelpLink reference={ORGANIZATIONS_FIELD_DOCUMENTATION} />
</div>
<form id={formId} className="admin-form-grid two-columns" onSubmit={(event) => void submit(event)}>
{renderEditorFields()}
</form>
@@ -1148,7 +1261,7 @@ export default function OrganizationsPage({
if (activeEditor === "unitType") {
return (
<>
<SluggedFields draft={unitTypeDraft} onChange={setUnitTypeDraft} disabled={!canWriteModel || busy} />
<SluggedFields draft={unitTypeDraft} onChange={setUnitTypeDraft} disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} />
{renderChangeRequestField()}
</>
);
@@ -1156,8 +1269,8 @@ export default function OrganizationsPage({
if (activeEditor === "structure") {
return (
<>
<SluggedFields draft={structureDraft} onChange={setStructureDraft} disabled={!canWriteModel || busy} />
<FormField label="i18n:govoplan-organizations.kind.794c9d9c">
<SluggedFields draft={structureDraft} onChange={setStructureDraft} disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} />
<FormField label="i18n:govoplan-organizations.kind.794c9d9c" help={organizationWriteReason(canWriteModel, busy, "model")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={structureDraft.structure_kind} disabled={!canWriteModel || busy} onChange={(event) => setStructureDraft({ ...structureDraft, structure_kind: event.target.value as OrganizationStructureKind })}>
{STRUCTURE_KINDS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
</select>
@@ -1169,28 +1282,28 @@ export default function OrganizationsPage({
if (activeEditor === "relationType") {
return (
<>
<SluggedFields draft={relationTypeDraft} onChange={setRelationTypeDraft} disabled={!canWriteModel || busy} />
<FormField label="i18n:govoplan-organizations.structure.7732fb0b">
<SluggedFields draft={relationTypeDraft} onChange={setRelationTypeDraft} disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} />
<FormField label="i18n:govoplan-organizations.structure.7732fb0b" help={organizationWriteReason(canWriteModel, busy, "model")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={relationTypeDraft.structure_id} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, structure_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
{model.structures.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-organizations.source_unit.2fbd8baa">
<FormField label="i18n:govoplan-organizations.source_unit.2fbd8baa" help={organizationWriteReason(canWriteModel, busy, "model")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={relationTypeDraft.source_unit_type_id} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, source_unit_type_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
{model.unit_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-organizations.target_unit.a1507d86">
<FormField label="i18n:govoplan-organizations.target_unit.a1507d86" help={organizationWriteReason(canWriteModel, busy, "model")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={relationTypeDraft.target_unit_type_id} disabled={!canWriteModel || busy} onChange={(event) => setRelationTypeDraft({ ...relationTypeDraft, target_unit_type_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
{model.unit_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<div className="organizations-check-list">
<ToggleSwitch checked={relationTypeDraft.is_hierarchical} disabled={!canWriteModel || busy} onChange={(checked) => setRelationTypeDraft({ ...relationTypeDraft, is_hierarchical: checked })} label="i18n:govoplan-organizations.hierarchical.8964f313" />
<ToggleSwitch checked={relationTypeDraft.allow_cycles} disabled={!canWriteModel || busy} onChange={(checked) => setRelationTypeDraft({ ...relationTypeDraft, allow_cycles: checked })} label="i18n:govoplan-organizations.allow_cycles.31327578" />
<ToggleSwitch checked={relationTypeDraft.is_hierarchical} disabled={!canWriteModel || busy} help={organizationWriteReason(canWriteModel, busy, "model")} onChange={(checked) => setRelationTypeDraft({ ...relationTypeDraft, is_hierarchical: checked })} label="i18n:govoplan-organizations.hierarchical.8964f313" />
<ToggleSwitch checked={relationTypeDraft.allow_cycles} disabled={!canWriteModel || busy} help={organizationWriteReason(canWriteModel, busy, "model")} onChange={(checked) => setRelationTypeDraft({ ...relationTypeDraft, allow_cycles: checked })} label="i18n:govoplan-organizations.allow_cycles.31327578" />
</div>
{renderChangeRequestField()}
</>
@@ -1199,14 +1312,14 @@ export default function OrganizationsPage({
if (activeEditor === "unit") {
return (
<>
<SluggedFields draft={unitDraft} onChange={setUnitDraft} disabled={!canWriteUnits || busy} />
<FormField label="i18n:govoplan-organizations.unit_type.9e62810d">
<SluggedFields draft={unitDraft} onChange={setUnitDraft} disabled={!canWriteUnits || busy} disabledReason={organizationWriteReason(canWriteUnits, busy, "unit")} />
<FormField label="i18n:govoplan-organizations.unit_type.9e62810d" help={organizationWriteReason(canWriteUnits, busy, "unit")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={unitDraft.unit_type_id} disabled={!canWriteUnits || busy} onChange={(event) => setUnitDraft({ ...unitDraft, unit_type_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
{model.unit_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-organizations.parent_unit.1986c35e">
<FormField label="i18n:govoplan-organizations.parent_unit.1986c35e" help={organizationWriteReason(canWriteUnits, busy, "unit")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={unitDraft.parent_id} disabled={!canWriteUnits || busy} onChange={(event) => setUnitDraft({ ...unitDraft, parent_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
{model.units.filter((item) => item.id !== editingUnitId).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
@@ -1224,25 +1337,25 @@ export default function OrganizationsPage({
if (activeEditor === "relation") {
return (
<>
<FormField label="i18n:govoplan-organizations.structure.7732fb0b">
<FormField label="i18n:govoplan-organizations.structure.7732fb0b" help={organizationWriteReason(canWriteUnits, busy, "unit")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={relationDraft.structure_id} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, structure_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.select_structure.c10f551c</option>
{model.structures.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-organizations.relation_type.d0aee2e7">
<FormField label="i18n:govoplan-organizations.relation_type.d0aee2e7" help={organizationWriteReason(canWriteUnits, busy, "unit")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={relationDraft.relation_type_id} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, relation_type_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.select_relation_type.73f92478</option>
{model.relation_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-organizations.source_unit.2fbd8baa">
<FormField label="i18n:govoplan-organizations.source_unit.2fbd8baa" help={organizationWriteReason(canWriteUnits, busy, "unit")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={relationDraft.source_unit_id} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, source_unit_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.select_source_unit.9b5a29c8</option>
{model.units.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-organizations.target_unit.a1507d86">
<FormField label="i18n:govoplan-organizations.target_unit.a1507d86" help={organizationWriteReason(canWriteUnits, busy, "unit")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={relationDraft.target_unit_id} disabled={!canWriteUnits || busy} onChange={(event) => setRelationDraft({ ...relationDraft, target_unit_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.select_target_unit.8d607541</option>
{model.units.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
@@ -1252,6 +1365,7 @@ export default function OrganizationsPage({
<ToggleSwitch
checked={relationDraft.is_active}
disabled={!canWriteUnits || busy}
help={organizationWriteReason(canWriteUnits, busy, "unit")}
onChange={(checked) => setRelationDraft({ ...relationDraft, is_active: checked })}
label="i18n:govoplan-organizations.active.a733b809"
/>
@@ -1263,16 +1377,16 @@ export default function OrganizationsPage({
if (activeEditor === "functionType") {
return (
<>
<SluggedFields draft={functionTypeDraft} onChange={setFunctionTypeDraft} disabled={!canWriteModel || busy} />
<FormField label="i18n:govoplan-organizations.unit_type.9e62810d">
<SluggedFields draft={functionTypeDraft} onChange={setFunctionTypeDraft} disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} />
<FormField label="i18n:govoplan-organizations.unit_type.9e62810d" help={organizationWriteReason(canWriteModel, busy, "model")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={functionTypeDraft.organization_unit_type_id} disabled={!canWriteModel || busy} onChange={(event) => setFunctionTypeDraft({ ...functionTypeDraft, organization_unit_type_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
{model.unit_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<div className="organizations-check-list">
<ToggleSwitch checked={functionTypeDraft.delegable} disabled={!canWriteModel || busy} onChange={(checked) => setFunctionTypeDraft({ ...functionTypeDraft, delegable: checked })} label="i18n:govoplan-organizations.delegable.b4f0137d" />
<ToggleSwitch checked={functionTypeDraft.act_in_place_allowed} disabled={!canWriteModel || busy} onChange={(checked) => setFunctionTypeDraft({ ...functionTypeDraft, act_in_place_allowed: checked })} label="i18n:govoplan-organizations.act_in_place.49b942bd" />
<ToggleSwitch checked={functionTypeDraft.delegable} disabled={!canWriteModel || busy} help={organizationWriteReason(canWriteModel, busy, "model")} onChange={(checked) => setFunctionTypeDraft({ ...functionTypeDraft, delegable: checked })} label="i18n:govoplan-organizations.delegable.b4f0137d" />
<ToggleSwitch checked={functionTypeDraft.act_in_place_allowed} disabled={!canWriteModel || busy} help={organizationWriteReason(canWriteModel, busy, "model")} onChange={(checked) => setFunctionTypeDraft({ ...functionTypeDraft, act_in_place_allowed: checked })} label="i18n:govoplan-organizations.act_in_place.49b942bd" />
</div>
{renderChangeRequestField()}
</>
@@ -1280,22 +1394,22 @@ export default function OrganizationsPage({
}
return (
<>
<SluggedFields draft={functionDraft} onChange={setFunctionDraft} disabled={!canWriteFunctions || busy} />
<FormField label="i18n:govoplan-organizations.unit.8fe4d595">
<SluggedFields draft={functionDraft} onChange={setFunctionDraft} disabled={!canWriteFunctions || busy} disabledReason={organizationWriteReason(canWriteFunctions, busy, "function")} />
<FormField label="i18n:govoplan-organizations.unit.8fe4d595" help={organizationWriteReason(canWriteFunctions, busy, "function")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={functionDraft.organization_unit_id} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, organization_unit_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.select_unit.013bf13a</option>
{model.units.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-organizations.function_type.501fe7c0">
<FormField label="i18n:govoplan-organizations.function_type.501fe7c0" help={organizationWriteReason(canWriteFunctions, busy, "function")} documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}>
<select value={functionDraft.function_type_id} disabled={!canWriteFunctions || busy} onChange={(event) => setFunctionDraft({ ...functionDraft, function_type_id: event.target.value })}>
<option value="">i18n:govoplan-organizations.none.334c4a4c</option>
{model.function_types.map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<div className="organizations-check-list">
<ToggleSwitch checked={functionDraft.delegable} disabled={!canWriteFunctions || busy} onChange={(checked) => setFunctionDraft({ ...functionDraft, delegable: checked })} label="i18n:govoplan-organizations.delegable.b4f0137d" />
<ToggleSwitch checked={functionDraft.act_in_place_allowed} disabled={!canWriteFunctions || busy} onChange={(checked) => setFunctionDraft({ ...functionDraft, act_in_place_allowed: checked })} label="i18n:govoplan-organizations.act_in_place.49b942bd" />
<ToggleSwitch checked={functionDraft.delegable} disabled={!canWriteFunctions || busy} help={organizationWriteReason(canWriteFunctions, busy, "function")} onChange={(checked) => setFunctionDraft({ ...functionDraft, delegable: checked })} label="i18n:govoplan-organizations.delegable.b4f0137d" />
<ToggleSwitch checked={functionDraft.act_in_place_allowed} disabled={!canWriteFunctions || busy} help={organizationWriteReason(canWriteFunctions, busy, "function")} onChange={(checked) => setFunctionDraft({ ...functionDraft, act_in_place_allowed: checked })} label="i18n:govoplan-organizations.act_in_place.49b942bd" />
</div>
{renderChangeRequestField()}
</>
@@ -0,0 +1,44 @@
import type { DocumentationHelpReference } from "@govoplan/core-webui";
export const ORGANIZATIONS_DOCUMENTATION = {
topicId: "organizations.model",
documentationType: "admin"
} satisfies DocumentationHelpReference;
export const ORGANIZATIONS_FIELD_DOCUMENTATION = {
topicId: "organizations.reference.fields-and-consequences",
documentationType: "admin"
} satisfies DocumentationHelpReference;
export const ORGANIZATIONS_INTERFACE_I18N = {
loading: "i18n:govoplan-organizations.loading_reason",
busy: "i18n:govoplan-organizations.busy_reason",
noChanges: "i18n:govoplan-organizations.no_changes_reason",
modelWrite: "i18n:govoplan-organizations.model_write_reason",
unitWrite: "i18n:govoplan-organizations.unit_write_reason",
functionWrite: "i18n:govoplan-organizations.function_write_reason",
settingsWrite: "i18n:govoplan-organizations.settings_write_reason",
incomplete: "i18n:govoplan-organizations.incomplete_editor_reason",
readOnlySummary: "i18n:govoplan-organizations.read_only_summary",
requiredAction: "i18n:govoplan-organizations.required_action",
actor: "i18n:govoplan-organizations.responsible_actor",
destination: "i18n:govoplan-organizations.destination",
permissionGuidance: "i18n:govoplan-organizations.request_write_permission",
administrator: "i18n:govoplan-organizations.organization_or_access_administrator",
administrationTarget: "i18n:govoplan-organizations.administration_roles_target"
} as const;
export function organizationWriteReason(
permitted: boolean,
busy: boolean,
kind: "model" | "unit" | "function" | "settings"
): string | undefined {
if (busy) return ORGANIZATIONS_INTERFACE_I18N.busy;
if (permitted) return undefined;
return {
model: ORGANIZATIONS_INTERFACE_I18N.modelWrite,
unit: ORGANIZATIONS_INTERFACE_I18N.unitWrite,
function: ORGANIZATIONS_INTERFACE_I18N.functionWrite,
settings: ORGANIZATIONS_INTERFACE_I18N.settingsWrite
}[kind];
}
+34 -2
View File
@@ -122,7 +122,23 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.update_structure.b2e25446": "Update structure",
"i18n:govoplan-organizations.update_unit.67e2500f": "Update unit",
"i18n:govoplan-organizations.update_unit_type.b96fc0ad": "Update unit type",
"i18n:govoplan-organizations.write_permission_required.8b09fd67": "You do not have write permission for this organization section."
"i18n:govoplan-organizations.write_permission_required.8b09fd67": "You do not have write permission for this organization section.",
"i18n:govoplan-organizations.organizations_administration": "Organizations administration",
"i18n:govoplan-organizations.loading_reason": "Organization data is loading.",
"i18n:govoplan-organizations.busy_reason": "An organization change is in progress.",
"i18n:govoplan-organizations.no_changes_reason": "Make a change before saving.",
"i18n:govoplan-organizations.model_write_reason": "Organization model write permission is required.",
"i18n:govoplan-organizations.unit_write_reason": "Organization unit write permission is required.",
"i18n:govoplan-organizations.function_write_reason": "Organization function write permission is required.",
"i18n:govoplan-organizations.settings_write_reason": "Organization settings write permission is required.",
"i18n:govoplan-organizations.incomplete_editor_reason": "Complete the required fields before saving.",
"i18n:govoplan-organizations.read_only_summary": "This organization section is read-only.",
"i18n:govoplan-organizations.required_action": "Required action",
"i18n:govoplan-organizations.responsible_actor": "Responsible actor",
"i18n:govoplan-organizations.destination": "Destination",
"i18n:govoplan-organizations.request_write_permission": "Request the matching organization model, unit, function, or settings write permission.",
"i18n:govoplan-organizations.organization_or_access_administrator": "An organization or Access administrator",
"i18n:govoplan-organizations.administration_roles_target": "Administration > Role templates and function mappings"
},
de: {
"i18n:govoplan-organizations.act_in_place.49b942bd": "In Vertretung handeln",
@@ -245,6 +261,22 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-organizations.update_structure.b2e25446": "Struktur aktualisieren",
"i18n:govoplan-organizations.update_unit.67e2500f": "Einheit aktualisieren",
"i18n:govoplan-organizations.update_unit_type.b96fc0ad": "Einheitstyp aktualisieren",
"i18n:govoplan-organizations.write_permission_required.8b09fd67": "Du hast keine Schreibberechtigung für diesen Organisationsbereich."
"i18n:govoplan-organizations.write_permission_required.8b09fd67": "Du hast keine Schreibberechtigung für diesen Organisationsbereich.",
"i18n:govoplan-organizations.organizations_administration": "Organisationsverwaltung",
"i18n:govoplan-organizations.loading_reason": "Organisationsdaten werden geladen.",
"i18n:govoplan-organizations.busy_reason": "Eine Organisationsänderung wird verarbeitet.",
"i18n:govoplan-organizations.no_changes_reason": "Nehmen Sie vor dem Speichern eine Änderung vor.",
"i18n:govoplan-organizations.model_write_reason": "Eine Schreibberechtigung für das Organisationsmodell ist erforderlich.",
"i18n:govoplan-organizations.unit_write_reason": "Eine Schreibberechtigung für Organisationseinheiten ist erforderlich.",
"i18n:govoplan-organizations.function_write_reason": "Eine Schreibberechtigung für Organisationsfunktionen ist erforderlich.",
"i18n:govoplan-organizations.settings_write_reason": "Eine Schreibberechtigung für Organisationseinstellungen ist erforderlich.",
"i18n:govoplan-organizations.incomplete_editor_reason": "Füllen Sie vor dem Speichern die Pflichtfelder aus.",
"i18n:govoplan-organizations.read_only_summary": "Dieser Organisationsbereich ist schreibgeschützt.",
"i18n:govoplan-organizations.required_action": "Erforderliche Aktion",
"i18n:govoplan-organizations.responsible_actor": "Verantwortliche Stelle",
"i18n:govoplan-organizations.destination": "Ziel",
"i18n:govoplan-organizations.request_write_permission": "Fordern Sie die passende Schreibberechtigung für Organisationsmodell, Einheiten, Funktionen oder Einstellungen an.",
"i18n:govoplan-organizations.organization_or_access_administrator": "Eine Organisations- oder Access-Administration",
"i18n:govoplan-organizations.administration_roles_target": "Administration > Rollenvorlagen und Funktionszuordnungen"
}
};
+2 -2
View File
@@ -44,7 +44,7 @@ const organizationFunctionPicker: OrganizationFunctionPickerUiCapability = {
export const organizationsModule: PlatformWebModule = {
id: "organizations",
label: "i18n:govoplan-organizations.organizations.220edf64",
version: "1.0.0",
version: "0.1.8",
dependencies: ["access"],
optionalDependencies: ["admin"],
translations,
@@ -53,7 +53,7 @@ export const organizationsModule: PlatformWebModule = {
id: "organizations.admin.tenant",
moduleId: "organizations",
kind: "section",
label: "Organizations administration",
label: "i18n:govoplan-organizations.organizations_administration",
order: 85
}
],