Migrate IDM interface patterns

This commit is contained in:
2026-08-03 12:36:49 +02:00
parent 6f6c45f6e2
commit d8643174d7
11 changed files with 766 additions and 84 deletions
+3
View File
@@ -6,6 +6,9 @@
"main": "src/index.ts",
"module": "src/index.ts",
"types": "src/index.ts",
"scripts": {
"test:interface-patterns": "node scripts/test-interface-pattern-language.mjs"
},
"exports": {
".": {
"types": "./src/index.ts",
@@ -0,0 +1,29 @@
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/IdmPage.tsx");
const changes = source("../src/features/FunctionAssignmentChangesPanel.tsx");
const patterns = source("../src/features/interfacePatterns.ts");
const moduleSource = source("../src/module.ts");
const translations = source("../src/i18n/generatedTranslations.ts");
assert(page.includes("ActionBlockerHint") && page.includes("DocumentationHelpLink"), "IDM permissions and prerequisites expose actionable help");
assert(page.includes("assignmentBaseline") && page.includes("draftKey(assignmentDraft) !== draftKey(assignmentBaseline)"), "Existing assignments compare against their loaded draft baseline");
assert(page.includes("requestDiscard(() => void loadData())") && page.includes("closeAssignmentEditor"), "Reload and dialog close preserve actual dirty drafts");
assert(page.includes("disabledReason") && changes.includes("disabledReason"), "Unavailable assignment and decision actions explain their state");
assert(changes.includes("ConfirmDialog") && changes.includes("confirm_function_action"), "Governed function decisions require explicit shared confirmation");
assert(changes.includes("useUnsavedDraftGuard") && changes.includes("usePlatformLanguage"), "Governed editors protect drafts and format dates with the platform locale");
assert(patterns.includes('topicId: "idm.reference.fields-and-consequences"'), "IDM fields use manifest-backed consequence help");
assert(moduleSource.includes('version: "0.1.8"') && moduleSource.includes('label: "i18n:govoplan-idm.view_assignments.2d40d6a5"'), "WebUI metadata matches the module release and localizes its action surface");
assert(translations.includes('"i18n:govoplan-idm.state_awaiting_authority"'), "Governed states and decisions are in the translation catalogue");
assert(!page.includes("window.confirm") && !changes.includes("window.confirm"), "IDM does not use browser-native consequential confirmation");
console.log("IDM surfaces satisfy the recorded interface pattern-language contract.");
@@ -5,14 +5,21 @@ import {
ApiError,
Button,
Card,
ConfirmDialog,
DataGrid,
Dialog,
DismissibleAlert,
DocumentationHelpLink,
FormField,
LoadingFrame,
SegmentedControl,
StatusBadge,
TableActionGroup,
hasScope,
i18nMessage,
usePlatformLanguage,
useUnsavedChanges,
useUnsavedDraftGuard,
type ApiSettings,
type AuthInfo,
type DataGridColumn
@@ -28,6 +35,12 @@ import {
type IdentityOption,
type OrganizationModel
} from "../api/idm";
import {
IDM_FIELD_DOCUMENTATION,
IDM_GOVERNANCE_DOCUMENTATION,
IDM_INTERFACE_I18N,
idmDisabledReason
} from "./interfacePatterns";
type Props = {
settings: ApiSettings;
@@ -85,15 +98,49 @@ function statusTone(state: string): string {
return "warning";
}
const DOMAIN_LABELS: Record<string, string> = {
request: "i18n:govoplan-idm.kind_request",
grant: "i18n:govoplan-idm.kind_grant",
submitted: "i18n:govoplan-idm.state_submitted",
awaiting_holder: "i18n:govoplan-idm.state_awaiting_holder",
awaiting_authority: "i18n:govoplan-idm.state_awaiting_authority",
awaiting_recipient: "i18n:govoplan-idm.state_awaiting_recipient",
changes_requested: "i18n:govoplan-idm.state_changes_requested",
applied: "i18n:govoplan-idm.state_applied",
rejected: "i18n:govoplan-idm.state_rejected",
expired: "i18n:govoplan-idm.state_expired",
withdrawn: "i18n:govoplan-idm.state_withdrawn",
cancelled: "i18n:govoplan-idm.state_cancelled",
blocked: "i18n:govoplan-idm.state_blocked",
failed_manual_review: "i18n:govoplan-idm.state_failed_manual_review",
approve_holder: "i18n:govoplan-idm.step_approve_holder",
approve_authority: "i18n:govoplan-idm.step_approve_authority",
accept_recipient: "i18n:govoplan-idm.step_accept_recipient",
holder_review: "i18n:govoplan-idm.profile_holder_review",
authority_review: "i18n:govoplan-idm.profile_authority_review",
recipient_review: "i18n:govoplan-idm.profile_recipient_review",
approve: "i18n:govoplan-idm.action_approve",
reject: "i18n:govoplan-idm.action_reject",
accept: "i18n:govoplan-idm.action_accept",
request_changes: "i18n:govoplan-idm.action_request_changes",
respond: "i18n:govoplan-idm.action_respond",
withdraw: "i18n:govoplan-idm.action_withdraw",
recover: "i18n:govoplan-idm.action_recheck"
};
function domainLabel(value: string): string {
return DOMAIN_LABELS[value] ?? value.replaceAll("_", " ");
}
function actionLabel(action: FunctionAssignmentChangeAction): string {
return {
approve: "Approve",
reject: "Reject",
accept: "Accept",
request_changes: "Request changes",
respond: "Respond",
withdraw: "Withdraw",
recover: "Recheck"
approve: "i18n:govoplan-idm.action_approve",
reject: "i18n:govoplan-idm.action_reject",
accept: "i18n:govoplan-idm.action_accept",
request_changes: "i18n:govoplan-idm.action_request_changes",
respond: "i18n:govoplan-idm.action_respond",
withdraw: "i18n:govoplan-idm.action_withdraw",
recover: "i18n:govoplan-idm.action_recheck"
}[action];
}
@@ -113,6 +160,7 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
const initialKind: FunctionAssignmentChangeKind = canRequest ? "request" : "grant";
const [changes, setChanges] = useState<FunctionAssignmentChange[]>([]);
const [draft, setDraft] = useState<Draft>(() => emptyDraft(auth, initialKind));
const [draftBaseline, setDraftBaseline] = useState<Draft>(() => emptyDraft(auth, initialKind));
const [selected, setSelected] = useState<FunctionAssignmentChange | null>(null);
const [createOpen, setCreateOpen] = useState(false);
const [detailOpen, setDetailOpen] = useState(false);
@@ -120,6 +168,9 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
const [loading, setLoading] = useState(false);
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [pendingAction, setPendingAction] = useState<FunctionAssignmentChangeAction | null>(null);
const { language } = usePlatformLanguage();
const { requestDiscard } = useUnsavedChanges();
const initialChangeId = useMemo(() => {
if (typeof window === "undefined") return "";
return new URLSearchParams(window.location.search).get("change") ?? "";
@@ -128,6 +179,15 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
const functionById = useMemo(() => new Map(model.functions.map((item) => [item.id, item])), [model.functions]);
const identityById = useMemo(() => new Map(identities.map((item) => [item.id, item])), [identities]);
const selectedIdentity = identityById.get(draft.identityId);
const draftDirty = createOpen && draftKey(draft) !== draftKey(draftBaseline);
useUnsavedDraftGuard({
dirty: draftDirty,
title: "Unsaved function change",
message: "Save or discard the function request or grant before leaving this surface.",
onSave: submit,
onDiscard: discardCreateDraft
});
const load = useCallback(async () => {
if (!visible) return;
@@ -156,7 +216,29 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
if (!visible) return null;
function setKind(kind: FunctionAssignmentChangeKind) {
setDraft(emptyDraft(auth, kind));
const next = emptyDraft(auth, kind);
setDraft(next);
setDraftBaseline(next);
}
function openCreate() {
const next = emptyDraft(auth, initialKind);
setDraft(next);
setDraftBaseline(next);
setCreateOpen(true);
}
function discardCreateDraft() {
const next = emptyDraft(auth, initialKind);
setDraft(next);
setDraftBaseline(next);
setCreateOpen(false);
}
function closeCreate() {
if (busy) return;
if (draftDirty) requestDiscard(discardCreateDraft);
else discardCreateDraft();
}
function setIdentity(identityId: string) {
@@ -183,8 +265,9 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
}
}
async function submit(event: FormEvent) {
event.preventDefault();
async function submit(event?: FormEvent): Promise<boolean> {
event?.preventDefault();
if (!draft.functionId || !draft.identityId || !draft.justification.trim()) return false;
setBusy(true);
setError("");
try {
@@ -201,12 +284,16 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
metadata: {}
});
setCreateOpen(false);
setDraft(emptyDraft(auth, initialKind));
const next = emptyDraft(auth, initialKind);
setDraft(next);
setDraftBaseline(next);
setSelected(created);
setDetailOpen(true);
await load();
return true;
} catch (caught) {
setError(errorMessage(caught));
return false;
} finally {
setBusy(false);
}
@@ -229,7 +316,7 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
}
const columns: DataGridColumn<FunctionAssignmentChange>[] = [
{ id: "kind", header: "Kind", width: 110, sortable: true, filterable: true, value: (row) => row.kind },
{ id: "kind", header: "Kind", width: 110, sortable: true, filterable: true, value: (row) => row.kind, render: (row) => domainLabel(row.kind) },
{
id: "function",
header: "Function",
@@ -248,9 +335,9 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
value: (row) => identityById.get(row.candidate_identity_id)?.display_name ?? row.candidate_identity_id,
render: (row) => identityById.get(row.candidate_identity_id)?.display_name ?? row.candidate_identity_id
},
{ id: "state", header: "State", width: 170, sortable: true, filterable: true, value: (row) => row.state, render: (row) => <StatusBadge status={statusTone(row.state)} label={row.state.replaceAll("_", " ")} /> },
{ id: "state", header: "State", width: 170, sortable: true, filterable: true, value: (row) => row.state, render: (row) => <StatusBadge status={statusTone(row.state)} label={domainLabel(row.state)} /> },
{ id: "progress", header: "Decisions", width: 140, value: (row) => `${row.completed_steps.length}/${row.required_steps.length}`, render: (row) => `${row.completed_steps.length} / ${row.required_steps.length}` },
{ id: "updated", header: "Updated", width: 170, sortable: true, value: (row) => row.updated_at, render: (row) => new Date(row.updated_at).toLocaleString() },
{ id: "updated", header: "Updated", width: 170, sortable: true, value: (row) => row.updated_at, render: (row) => new Date(row.updated_at).toLocaleString(language) },
{ id: "actions", header: "Actions", width: 72, sticky: "end", render: (row) => <TableActionGroup actions={[{ id: "view", label: "Open change", icon: <Eye size={16} aria-hidden="true" />, onClick: () => void openDetail(row) }]} /> }
];
@@ -261,18 +348,25 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
title="Function requests and grants"
collapsible
collapseKey="idm.function-assignment-changes"
actions={(canRequest || canGrant) ? <AdminIconButton label="Start governed change" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={busy} onClick={() => setCreateOpen(true)} /> : undefined}
actions={(
<div className="button-row compact-actions">
<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />
{(canRequest || canGrant) ? <AdminIconButton label="Start governed change" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={busy} disabledReason={idmDisabledReason(false, busy)} onClick={openCreate} /> : null}
</div>
)}
>
<DataGrid id="idm-function-assignment-changes" rows={changes} columns={columns} getRowKey={(row) => row.id} emptyText={loading ? "Loading changes..." : "No governed function changes"} initialFit="container" />
<LoadingFrame loading={loading} label="Loading governed function changes">
<DataGrid id="idm-function-assignment-changes" rows={changes} columns={columns} getRowKey={(row) => row.id} emptyText="No governed function changes" initialFit="container" />
</LoadingFrame>
</Card>
<Dialog
open={createOpen}
title="Start governed function change"
className="admin-dialog admin-dialog-wide idm-change-dialog"
onClose={() => !busy && setCreateOpen(false)}
onClose={closeCreate}
closeDisabled={busy}
footer={<><Button type="button" onClick={() => setCreateOpen(false)} disabled={busy}>Cancel</Button><Button type="submit" form="idm-change-create" variant="primary" disabled={busy || !draft.functionId || !draft.identityId || !draft.justification.trim()}>Submit</Button></>}
footer={<><Button type="button" onClick={closeCreate} disabled={busy} disabledReason={idmDisabledReason(false, busy)}>Cancel</Button><Button type="submit" form="idm-change-create" variant="primary" disabled={busy || !draft.functionId || !draft.identityId || !draft.justification.trim()} disabledReason={idmDisabledReason(false, busy) ?? ((!draft.functionId || !draft.identityId || !draft.justification.trim()) ? IDM_INTERFACE_I18N.incomplete : undefined)}>Submit</Button></>}
>
<form id="idm-change-create" className="admin-form-grid two-columns" onSubmit={(event) => void submit(event)}>
<div className="wide">
@@ -286,37 +380,37 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
]}
/>
</div>
<FormField label="Function">
<FormField label="Function" documentation={IDM_FIELD_DOCUMENTATION}>
<select value={draft.functionId} onChange={(event) => setDraft((current) => ({ ...current, functionId: event.target.value }))} disabled={busy}>
<option value="">Select function</option>
{model.functions.filter((item) => item.is_active).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
</select>
</FormField>
<FormField label="Candidate identity">
<FormField label="Candidate identity" documentation={IDM_FIELD_DOCUMENTATION}>
<select value={draft.identityId} onChange={(event) => setIdentity(event.target.value)} disabled={busy || draft.kind === "request"}>
<option value="">Select identity</option>
{identities.map((item) => <option key={item.id} value={item.id}>{item.display_name || item.external_subject || item.id}</option>)}
</select>
</FormField>
<FormField label="Candidate account">
<FormField label="Candidate account" documentation={IDM_FIELD_DOCUMENTATION}>
<select value={draft.accountId} onChange={(event) => setDraft((current) => ({ ...current, accountId: event.target.value }))} disabled={busy}>
<option value="">No linked account</option>
{(selectedIdentity?.account_ids ?? []).map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
</select>
</FormField>
<FormField label="Valid from">
<FormField label="Valid from" documentation={IDM_FIELD_DOCUMENTATION}>
<input type="datetime-local" value={draft.validFrom} onChange={(event) => setDraft((current) => ({ ...current, validFrom: event.target.value }))} disabled={busy} />
</FormField>
<FormField label="Valid until">
<FormField label="Valid until" documentation={IDM_FIELD_DOCUMENTATION}>
<input type="datetime-local" value={draft.validUntil} onChange={(event) => setDraft((current) => ({ ...current, validUntil: event.target.value }))} disabled={busy} />
</FormField>
<div className="wide">
<FormField label="Justification">
<FormField label="Justification" documentation={IDM_FIELD_DOCUMENTATION}>
<textarea rows={4} value={draft.justification} onChange={(event) => setDraft((current) => ({ ...current, justification: event.target.value }))} disabled={busy} />
</FormField>
</div>
<div className="wide">
<FormField label="Evidence references (one per line)">
<FormField label="Evidence references (one per line)" documentation={IDM_FIELD_DOCUMENTATION}>
<textarea rows={3} value={draft.evidence} onChange={(event) => setDraft((current) => ({ ...current, evidence: event.target.value }))} disabled={busy} />
</FormField>
</div>
@@ -325,32 +419,39 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
<Dialog
open={detailOpen && selected !== null}
title={selected ? `${selected.kind === "request" ? "Function request" : "Function grant"}: ${functionById.get(selected.function_id)?.name ?? selected.function_id}` : "Function change"}
title={selected
? i18nMessage(
selected.kind === "request"
? "i18n:govoplan-idm.function_request_title"
: "i18n:govoplan-idm.function_grant_title",
{ function: functionById.get(selected.function_id)?.name ?? selected.function_id }
)
: "Function change"}
className="admin-dialog admin-dialog-wide idm-change-dialog"
onClose={() => !busy && setDetailOpen(false)}
closeDisabled={busy}
footer={<Button type="button" onClick={() => setDetailOpen(false)} disabled={busy}>Close</Button>}
footer={<Button type="button" onClick={() => setDetailOpen(false)} disabled={busy} disabledReason={idmDisabledReason(false, busy)}>Close</Button>}
>
{selected && (
<div className="idm-change-detail">
<dl className="idm-change-summary">
<div><dt>State</dt><dd><StatusBadge status={statusTone(selected.state)} label={selected.state.replaceAll("_", " ")} /></dd></div>
<div><dt>State</dt><dd><StatusBadge status={statusTone(selected.state)} label={domainLabel(selected.state)} /></dd></div>
<div><dt>Candidate</dt><dd>{identityById.get(selected.candidate_identity_id)?.display_name ?? selected.candidate_identity_id}</dd></div>
<div><dt>Profile</dt><dd>{selected.profile}</dd></div>
<div><dt>Profile</dt><dd>{domainLabel(selected.profile)}</dd></div>
<div><dt>Workflow revision</dt><dd>{selected.workflow_definition_revision ?? "-"}</dd></div>
<div><dt>Required decisions</dt><dd>{selected.required_steps.join(", ") || "None"}</dd></div>
<div><dt>Completed decisions</dt><dd>{selected.completed_steps.join(", ") || "None"}</dd></div>
<div><dt>Required decisions</dt><dd>{selected.required_steps.map(domainLabel).join(", ") || "None"}</dd></div>
<div><dt>Completed decisions</dt><dd>{selected.completed_steps.map(domainLabel).join(", ") || "None"}</dd></div>
<div className="wide"><dt>Justification</dt><dd>{selected.justification}</dd></div>
{selected.outcome_reason && <div className="wide"><dt>Explanation</dt><dd>{selected.outcome_reason}</dd></div>}
</dl>
{selected.available_actions.length > 0 && (
<div className="idm-change-actions">
<FormField label="Decision comment">
<FormField label="Decision comment" documentation={IDM_FIELD_DOCUMENTATION}>
<textarea rows={2} value={comment} onChange={(event) => setComment(event.target.value)} disabled={busy} />
</FormField>
<div className="button-row compact-actions">
{selected.available_actions.map((action) => (
<Button key={action} type="button" variant={action === "reject" ? "danger" : action === "approve" || action === "accept" ? "primary" : "secondary"} disabled={busy} onClick={() => void performAction(action)}>
<Button key={action} type="button" variant={action === "reject" ? "danger" : action === "approve" || action === "accept" ? "primary" : "secondary"} disabled={busy} disabledReason={idmDisabledReason(false, busy)} onClick={() => setPendingAction(action)}>
{actionIcon(action)} {actionLabel(action)}
</Button>
))}
@@ -361,12 +462,36 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
<div>
<h3>History</h3>
<ol className="idm-change-history">
{selected.events.map((event) => <li key={event.id}><strong>{event.action}</strong><span>{new Date(event.created_at).toLocaleString()}</span><span>{event.from_state ? `${event.from_state} -> ` : ""}{event.to_state}</span>{event.comment && <p>{event.comment}</p>}</li>)}
{selected.events.map((event) => <li key={event.id}><strong>{domainLabel(event.action)}</strong><span>{new Date(event.created_at).toLocaleString(language)}</span><span>{event.from_state ? `${domainLabel(event.from_state)} -> ` : ""}{domainLabel(event.to_state)}</span>{event.comment && <p>{event.comment}</p>}</li>)}
</ol>
</div>
</div>
)}
</Dialog>
<ConfirmDialog
open={Boolean(pendingAction && selected)}
title="Confirm function decision"
message={pendingAction && selected
? i18nMessage("i18n:govoplan-idm.confirm_function_action", {
action: actionLabel(pendingAction),
function: functionById.get(selected.function_id)?.name ?? selected.function_id
})
: ""}
confirmLabel={pendingAction ? actionLabel(pendingAction) : "Confirm"}
tone={pendingAction === "reject" || pendingAction === "withdraw" ? "danger" : "default"}
busy={busy}
onConfirm={() => {
if (!pendingAction) return;
const action = pendingAction;
setPendingAction(null);
void performAction(action);
}}
onCancel={() => setPendingAction(null)}
/>
</>
);
}
function draftKey(value: unknown): string {
return JSON.stringify(value);
}
+140 -46
View File
@@ -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,
FormField,
LoadingFrame,
PageScrollViewport,
@@ -16,6 +18,7 @@ import {
TableActionGroup,
ToggleSwitch,
hasScope,
useUnsavedChanges,
useUnsavedDraftGuard,
type ApiSettings,
type AuthInfo,
@@ -38,6 +41,13 @@ import {
type OrganizationUnitItem
} from "../api/idm";
import FunctionAssignmentChangesPanel from "./FunctionAssignmentChangesPanel";
import {
IDM_DOCUMENTATION,
IDM_FIELD_DOCUMENTATION,
IDM_GOVERNANCE_DOCUMENTATION,
IDM_INTERFACE_I18N,
idmDisabledReason
} from "./interfacePatterns";
type IdmPageProps = {
settings: ApiSettings;
@@ -139,21 +149,6 @@ function assignmentDraftFrom(item: OrganizationFunctionAssignmentItem): Assignme
};
}
function isAssignmentDirty(draft: AssignmentDraft): boolean {
return Boolean(
draft.identity_id ||
draft.account_id ||
draft.function_id ||
draft.applies_to_subunits ||
draft.source !== "direct" ||
draft.delegated_from_assignment_id ||
draft.acting_for_account_id ||
draft.governance_override_reason ||
draft.governance_override_evidence ||
!draft.is_active
);
}
function settingsDraftFrom(item: IdmSettings | null): SettingsDraft {
const source = item ?? DEFAULT_IDM_SETTINGS;
return {
@@ -269,6 +264,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
const [actingForOptions, setActingForOptions] = useState<IdentityOption[]>([]);
const [actingForLoading, setActingForLoading] = useState(false);
const [assignmentDraft, setAssignmentDraft] = useState<AssignmentDraft>(() => emptyAssignmentDraft());
const [assignmentBaseline, setAssignmentBaseline] = useState<AssignmentDraft>(() => emptyAssignmentDraft());
const [settingsDraft, setSettingsDraft] = useState<SettingsDraft>(() => settingsDraftFrom(null));
const [editingAssignmentId, setEditingAssignmentId] = useState<string | null>(null);
const [assignmentEditorOpen, setAssignmentEditorOpen] = useState(false);
@@ -279,6 +275,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
const [success, setSuccess] = useState("");
const initialQuery = useMemo(() => idmInitialQuery(), []);
const appliedInitialQueryRef = useRef(false);
const { requestDiscard } = useUnsavedChanges();
const canManage = hasScope(auth, "idm:organization_assignment:write") || hasScope(auth, "organizations:function:assign");
const canSearchIdentities = canManage || hasScope(auth, "idm:organization_identity:read") || hasScope(auth, "admin:users:read");
@@ -330,7 +327,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
return Array.from(values).sort((left, right) => left.localeCompare(right));
}, [actingForOptionById, actingForOptions, assignmentDraft.acting_for_account_id, identityOptionById, sourceAssignment]);
const initialFunctionFilter = initialQuery.functionId;
const hasDirtyAssignmentDraft = assignmentEditorOpen && isAssignmentDirty(assignmentDraft);
const hasDirtyAssignmentDraft = assignmentEditorOpen && draftKey(assignmentDraft) !== draftKey(assignmentBaseline);
const hasDirtySettingsDraft = canReadSettings && isSettingsDirty(settingsDraft, idmSettings);
const hasDirtyDraft = hasDirtyAssignmentDraft || hasDirtySettingsDraft;
@@ -352,7 +349,9 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
if (initialAssignment) {
appliedInitialQueryRef.current = true;
setEditingAssignmentId(initialAssignment.id);
setAssignmentDraft(assignmentDraftFrom(initialAssignment));
const nextDraft = assignmentDraftFrom(initialAssignment);
setAssignmentDraft(nextDraft);
setAssignmentBaseline(nextDraft);
setAssignmentEditorOpen(true);
}
}
@@ -437,7 +436,9 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
}, [loadData]);
const discardAssignmentDraft = useCallback(() => {
setAssignmentDraft(emptyAssignmentDraft());
const nextDraft = emptyAssignmentDraft();
setAssignmentDraft(nextDraft);
setAssignmentBaseline(nextDraft);
setEditingAssignmentId(null);
setAssignmentEditorOpen(false);
setAssignmentChangeRequestId("");
@@ -505,18 +506,28 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
const openCreateAssignment = useCallback(() => {
setEditingAssignmentId(null);
setAssignmentDraft({ ...emptyAssignmentDraft(), function_id: initialFunctionFilter && functionById.has(initialFunctionFilter) ? initialFunctionFilter : "" });
const nextDraft = { ...emptyAssignmentDraft(), function_id: initialFunctionFilter && functionById.has(initialFunctionFilter) ? initialFunctionFilter : "" };
setAssignmentDraft(nextDraft);
setAssignmentBaseline(nextDraft);
setAssignmentChangeRequestId("");
setAssignmentEditorOpen(true);
}, [functionById, initialFunctionFilter]);
const editAssignment = useCallback((item: OrganizationFunctionAssignmentItem) => {
const nextDraft = assignmentDraftFrom(item);
setEditingAssignmentId(item.id);
setAssignmentDraft(assignmentDraftFrom(item));
setAssignmentDraft(nextDraft);
setAssignmentBaseline(nextDraft);
setAssignmentChangeRequestId("");
setAssignmentEditorOpen(true);
}, []);
const closeAssignmentEditor = useCallback(() => {
if (busy) return;
if (hasDirtyAssignmentDraft) requestDiscard(discardAssignmentDraft);
else discardAssignmentDraft();
}, [busy, discardAssignmentDraft, hasDirtyAssignmentDraft, requestDiscard]);
function onIdentityChange(identityId: string) {
const identity = identityOptionById.get(identityId);
setAssignmentDraft({
@@ -594,7 +605,14 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
header: "Actions",
width: 72,
sticky: "end",
render: (row) => <TableActionGroup actions={[{ id: "edit", label: "i18n:govoplan-idm.edit.a5a0f3cc", icon: <Edit3 size={16} aria-hidden="true" />, disabled: !canManage || busy, onClick: () => editAssignment(row) }]} />
render: (row) => <TableActionGroup actions={[{
id: "edit",
label: "i18n:govoplan-idm.edit.a5a0f3cc",
icon: <Edit3 size={16} aria-hidden="true" />,
disabled: !canManage || busy,
disabledReason: idmDisabledReason(false, busy, canManage),
onClick: () => editAssignment(row)
}]} />
}
];
@@ -607,7 +625,14 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
<p>i18n:govoplan-idm.identity_links_intro.45fed9dd</p>
</div>
<div className="idm-toolbar">
<Button type="button" onClick={() => void loadData()} disabled={loading || busy} title="i18n:govoplan-idm.reload.870ca3ec">
<DocumentationHelpLink reference={IDM_DOCUMENTATION} />
<Button
type="button"
onClick={() => requestDiscard(() => void loadData())}
disabled={loading || busy}
disabledReason={idmDisabledReason(loading, busy)}
title="i18n:govoplan-idm.reload.870ca3ec"
>
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-idm.reload.870ca3ec
</Button>
</div>
@@ -615,17 +640,53 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{success && !error && <DismissibleAlert tone="success" resetKey={success} floating>{success}</DismissibleAlert>}
{!canManage && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-idm.write_permission_required.c7dde7c6</DismissibleAlert>}
{!canManage && (
<ActionBlockerHint
reason={{
summary: IDM_INTERFACE_I18N.writeReason,
requiredAction: IDM_INTERFACE_I18N.permissionAction,
actor: IDM_INTERFACE_I18N.permissionActor,
target: IDM_INTERFACE_I18N.permissionDestination
}}
labels={{
requiredAction: IDM_INTERFACE_I18N.requiredAction,
actor: IDM_INTERFACE_I18N.actor,
target: IDM_INTERFACE_I18N.destination
}}
documentation={IDM_DOCUMENTATION}
/>
)}
{!model.functions.length && !loading && (
<ActionBlockerHint
reason={{
summary: IDM_INTERFACE_I18N.noFunctions,
requiredAction: IDM_INTERFACE_I18N.functionAction,
actor: IDM_INTERFACE_I18N.functionActor,
target: IDM_INTERFACE_I18N.functionDestination
}}
labels={{
requiredAction: IDM_INTERFACE_I18N.requiredAction,
actor: IDM_INTERFACE_I18N.actor,
target: IDM_INTERFACE_I18N.destination
}}
documentation={IDM_DOCUMENTATION}
/>
)}
<LoadingFrame loading={loading || busy} label="i18n:govoplan-idm.loading_idm_assignments.0b1501bd">
<div className="idm-table-stack">
{canReadSettings && (
<Card title="i18n:govoplan-idm.idm_governance.6e4f3251" collapsible collapseKey="idm.governance">
<Card
title="i18n:govoplan-idm.idm_governance.6e4f3251"
collapsible
collapseKey="idm.governance"
actions={<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />}
>
<form className="admin-form-grid two-columns" onSubmit={(event) => { event.preventDefault(); void submitSettings(); }}>
<div className="idm-check-list wide">
<ToggleSwitch label="i18n:govoplan-idm.require_assignment_change_requests.697718a1" checked={settingsDraft.require_assignment_change_requests} disabled={!canManageSettings || busy} onChange={(require_assignment_change_requests) => setSettingsDraft({ ...settingsDraft, require_assignment_change_requests })} />
<ToggleSwitch label="i18n:govoplan-idm.require_assignment_change_requests.697718a1" checked={settingsDraft.require_assignment_change_requests} disabled={!canManageSettings || busy} help={idmDisabledReason(false, busy, canManageSettings)} onChange={(require_assignment_change_requests) => setSettingsDraft({ ...settingsDraft, require_assignment_change_requests })} />
</div>
<FormField label="i18n:govoplan-idm.audit_detail_level.eb2e6fd2">
<FormField label="i18n:govoplan-idm.audit_detail_level.eb2e6fd2" documentation={IDM_FIELD_DOCUMENTATION}>
<select
value={settingsDraft.audit_detail_level}
disabled={!canManageSettings || busy}
@@ -636,7 +697,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
<option value="full">i18n:govoplan-idm.full.7f021a14</option>
</select>
</FormField>
<FormField label="i18n:govoplan-idm.change_retention_days.4a91f7d3">
<FormField label="i18n:govoplan-idm.change_retention_days.4a91f7d3" documentation={IDM_FIELD_DOCUMENTATION}>
<input
type="number"
min="0"
@@ -646,7 +707,12 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
/>
</FormField>
<div className="button-row compact-actions wide">
<Button type="submit" variant="primary" disabled={!canManageSettings || busy || !hasDirtySettingsDraft}>
<Button
type="submit"
variant="primary"
disabled={!canManageSettings || busy || !hasDirtySettingsDraft}
disabledReason={idmDisabledReason(false, busy, canManageSettings) ?? (!hasDirtySettingsDraft ? IDM_INTERFACE_I18N.noChanges : undefined)}
>
i18n:govoplan-idm.save_settings.4602c430
</Button>
</div>
@@ -661,7 +727,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
identities={identityOptions}
/>
<Card title="i18n:govoplan-idm.assignments.a0d19ec5" collapsible collapseKey="idm.assignments" actions={<AdminIconButton label="i18n:govoplan-idm.add_assignment.08f2a0d5" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canManage || busy} onClick={openCreateAssignment} />}>
<Card title="i18n:govoplan-idm.assignments.a0d19ec5" collapsible collapseKey="idm.assignments" actions={<AdminIconButton label="i18n:govoplan-idm.add_assignment.08f2a0d5" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canManage || busy || !model.functions.length} disabledReason={idmDisabledReason(false, busy, canManage) ?? (!model.functions.length ? IDM_INTERFACE_I18N.noFunctions : undefined)} onClick={openCreateAssignment} />}>
<DataGrid
id="idm-organization-function-assignments"
rows={assignments}
@@ -685,20 +751,26 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
<Dialog
open={assignmentEditorOpen}
title={editingAssignmentId ? "i18n:govoplan-idm.update_assignment.e20f52aa" : "i18n:govoplan-idm.add_assignment.08f2a0d5"}
onClose={() => !busy && discardAssignmentDraft()}
onClose={closeAssignmentEditor}
closeDisabled={busy}
className="admin-dialog admin-dialog-wide idm-editor-dialog"
footer={(
<>
<Button type="button" onClick={discardAssignmentDraft} disabled={busy}>i18n:govoplan-idm.cancel_edit.ea4781e0</Button>
<Button type="submit" form={formId} variant="primary" disabled={!canManage || busy || !assignmentDraft.identity_id || !assignmentDraft.function_id || (selectedFunctionIsGoverned && !assignmentDraft.governance_override_reason.trim())}>
<Button type="button" onClick={closeAssignmentEditor} disabled={busy} disabledReason={idmDisabledReason(false, busy)}>i18n:govoplan-idm.cancel_edit.ea4781e0</Button>
<Button
type="submit"
form={formId}
variant="primary"
disabled={!canManage || busy || !assignmentDraft.identity_id || !assignmentDraft.function_id || (selectedFunctionIsGoverned && !assignmentDraft.governance_override_reason.trim())}
disabledReason={idmDisabledReason(false, busy, canManage) ?? ((!assignmentDraft.identity_id || !assignmentDraft.function_id || (selectedFunctionIsGoverned && !assignmentDraft.governance_override_reason.trim())) ? IDM_INTERFACE_I18N.incomplete : undefined)}
>
{editingAssignmentId ? "i18n:govoplan-idm.update_assignment.e20f52aa" : "i18n:govoplan-idm.add_assignment.08f2a0d5"}
</Button>
</>
)}
>
<form id={formId} className="admin-form-grid two-columns" onSubmit={(event) => void submitAssignment(event)}>
<FormField label="i18n:govoplan-idm.identity_search.d3460fcf">
<FormField label="i18n:govoplan-idm.identity_search.d3460fcf" documentation={IDM_FIELD_DOCUMENTATION}>
<input
value={identitySearch}
placeholder="i18n:govoplan-idm.search_identities.88a9ef15"
@@ -706,7 +778,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
onChange={(event) => setIdentitySearch(event.target.value)}
/>
</FormField>
<FormField label="i18n:govoplan-idm.select_identity.91d31615">
<FormField label="i18n:govoplan-idm.select_identity.91d31615" documentation={IDM_FIELD_DOCUMENTATION}>
<select value={assignmentDraft.identity_id} disabled={!canManage || busy} onChange={(event) => onIdentityChange(event.target.value)}>
<option value="">i18n:govoplan-idm.select_identity.91d31615</option>
{identitySelectOptions.map((item) => (
@@ -714,25 +786,25 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
))}
</select>
</FormField>
<FormField label="i18n:govoplan-idm.account.2b2936f8">
<FormField label="i18n:govoplan-idm.account.2b2936f8" documentation={IDM_FIELD_DOCUMENTATION}>
<select value={assignmentDraft.account_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, account_id: event.target.value })}>
<option value="">i18n:govoplan-idm.none.2baf5c66</option>
{accountIds.map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-idm.select_function.2bec86e0">
<FormField label="i18n:govoplan-idm.select_function.2bec86e0" documentation={IDM_FIELD_DOCUMENTATION}>
<select value={assignmentDraft.function_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, function_id: event.target.value })}>
<option value="">i18n:govoplan-idm.select_function.2bec86e0</option>
{model.functions.map((item) => <option key={item.id} value={item.id}>{functionLabel(item, unitById)}</option>)}
</select>
</FormField>
<FormField label="i18n:govoplan-idm.source.d15b50c9">
<FormField label="i18n:govoplan-idm.source.d15b50c9" documentation={IDM_FIELD_DOCUMENTATION}>
<select value={assignmentDraft.source} disabled={!canManage || busy} onChange={(event) => onSourceChange(event.target.value)}>
{SOURCE_OPTIONS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
</select>
</FormField>
{(assignmentDraft.source === "delegated" || assignmentDraft.source === "acting_for") && (
<FormField label="i18n:govoplan-idm.delegated_from_assignment_id.20d4a548">
<FormField label="i18n:govoplan-idm.delegated_from_assignment_id.20d4a548" documentation={IDM_FIELD_DOCUMENTATION}>
<select value={assignmentDraft.delegated_from_assignment_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, delegated_from_assignment_id: event.target.value, acting_for_account_id: "" })}>
<option value="">i18n:govoplan-idm.none.2baf5c66</option>
{assignmentOptions.map((item) => (
@@ -743,7 +815,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
)}
{assignmentDraft.source === "acting_for" && (
<>
<FormField label="i18n:govoplan-idm.acting_for_search.b7c526c7">
<FormField label="i18n:govoplan-idm.acting_for_search.b7c526c7" documentation={IDM_FIELD_DOCUMENTATION}>
<input
value={actingForSearch}
placeholder="i18n:govoplan-idm.search_identities.88a9ef15"
@@ -751,7 +823,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
onChange={(event) => setActingForSearch(event.target.value)}
/>
</FormField>
<FormField label="i18n:govoplan-idm.acting_for_account_id.5d7ade5b">
<FormField label="i18n:govoplan-idm.acting_for_account_id.5d7ade5b" documentation={IDM_FIELD_DOCUMENTATION}>
<select value={assignmentDraft.acting_for_account_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, acting_for_account_id: event.target.value })}>
<option value="">i18n:govoplan-idm.select_account.982ee1ad</option>
{actingForAccountIds.map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
@@ -760,15 +832,16 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
</>
)}
<div className="idm-check-list">
<ToggleSwitch label="i18n:govoplan-idm.applies_to_subunits.2e31b50b" checked={assignmentDraft.applies_to_subunits} disabled={!canManage || busy} onChange={(applies_to_subunits) => setAssignmentDraft({ ...assignmentDraft, applies_to_subunits })} />
<ToggleSwitch label="i18n:govoplan-idm.active.7bd0e9f8" checked={assignmentDraft.is_active} disabled={!canManage || busy} onChange={(is_active) => setAssignmentDraft({ ...assignmentDraft, is_active })} />
<ToggleSwitch label="i18n:govoplan-idm.applies_to_subunits.2e31b50b" checked={assignmentDraft.applies_to_subunits} disabled={!canManage || busy} help={idmDisabledReason(false, busy, canManage)} onChange={(applies_to_subunits) => setAssignmentDraft({ ...assignmentDraft, applies_to_subunits })} />
<ToggleSwitch label="i18n:govoplan-idm.active.7bd0e9f8" checked={assignmentDraft.is_active} disabled={!canManage || busy} help={idmDisabledReason(false, busy, canManage)} onChange={(is_active) => setAssignmentDraft({ ...assignmentDraft, is_active })} />
</div>
{selectedFunctionIsGoverned && (
<div className="wide idm-governance-override">
<DismissibleAlert tone="warning" dismissible={false}>
Direct changes to this governed function are emergency overrides. Use a request or grant above for the normal process.
<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />
</DismissibleAlert>
<FormField label="Emergency override reason">
<FormField label="Emergency override reason" documentation={IDM_FIELD_DOCUMENTATION}>
<textarea
rows={3}
value={assignmentDraft.governance_override_reason}
@@ -776,7 +849,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
disabled={!canManage || busy}
/>
</FormField>
<FormField label="Override evidence references (one per line)">
<FormField label="Override evidence references (one per line)" documentation={IDM_FIELD_DOCUMENTATION}>
<textarea
rows={2}
value={assignmentDraft.governance_override_evidence}
@@ -787,12 +860,29 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
</div>
)}
<div className="wide idm-dialog-change-request">
<FormField label="i18n:govoplan-idm.change_request_id.b7d816db">
<FormField label="i18n:govoplan-idm.change_request_id.b7d816db" documentation={IDM_FIELD_DOCUMENTATION}>
<input value={assignmentChangeRequestId} onChange={(event) => setAssignmentChangeRequestId(event.target.value)} placeholder="cfgreq-..." disabled={busy} />
</FormField>
<p className="idm-muted">i18n:govoplan-idm.change_request_id_help.cc7de508</p>
</div>
{!identityLookupAvailable && <p className="idm-muted wide">i18n:govoplan-idm.identity_lookup_unavailable.b76f7714</p>}
{!identityLookupAvailable && (
<div className="wide">
<ActionBlockerHint
reason={{
summary: "i18n:govoplan-idm.identity_lookup_unavailable.b76f7714",
requiredAction: IDM_INTERFACE_I18N.permissionAction,
actor: IDM_INTERFACE_I18N.permissionActor,
target: IDM_INTERFACE_I18N.permissionDestination
}}
labels={{
requiredAction: IDM_INTERFACE_I18N.requiredAction,
actor: IDM_INTERFACE_I18N.actor,
target: IDM_INTERFACE_I18N.destination
}}
documentation={IDM_DOCUMENTATION}
/>
</div>
)}
{identityLoading && <p className="idm-muted wide">i18n:govoplan-idm.loading_identities.f3b84693</p>}
{actingForLoading && <p className="idm-muted wide">i18n:govoplan-idm.loading_acting_for_accounts.c9894b1e</p>}
{!model.functions.length && <p className="idm-muted wide">i18n:govoplan-idm.no_functions_available.51ba08eb</p>}
@@ -801,3 +891,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
);
}
}
function draftKey(value: unknown): string {
return JSON.stringify(value);
}
+48
View File
@@ -0,0 +1,48 @@
import type { DocumentationHelpReference } from "@govoplan/core-webui";
export const IDM_DOCUMENTATION = {
topicId: "idm.workflow.assign-function-to-identity",
documentationType: "user"
} satisfies DocumentationHelpReference;
export const IDM_GOVERNANCE_DOCUMENTATION = {
topicId: "idm.reference.assignment-governance",
documentationType: "admin"
} satisfies DocumentationHelpReference;
export const IDM_FIELD_DOCUMENTATION = {
topicId: "idm.reference.fields-and-consequences",
documentationType: "admin"
} satisfies DocumentationHelpReference;
export const IDM_INTERFACE_I18N = {
loading: "i18n:govoplan-idm.loading_reason",
busy: "i18n:govoplan-idm.busy_reason",
writeReason: "i18n:govoplan-idm.write_permission_required.c7dde7c6",
settingsReason: "i18n:govoplan-idm.settings_write_permission_required.37f37efa",
searchReason: "i18n:govoplan-idm.identity_search_permission_reason",
incomplete: "i18n:govoplan-idm.incomplete_draft_reason",
noChanges: "i18n:govoplan-idm.no_changes_reason",
noFunctions: "i18n:govoplan-idm.no_functions_available.51ba08eb",
requiredAction: "i18n:govoplan-idm.required_action",
actor: "i18n:govoplan-idm.responsible_actor",
destination: "i18n:govoplan-idm.destination",
permissionAction: "i18n:govoplan-idm.permission_required_action",
permissionActor: "i18n:govoplan-idm.permission_responsible_actor",
permissionDestination: "i18n:govoplan-idm.permission_destination",
functionAction: "i18n:govoplan-idm.function_required_action",
functionActor: "i18n:govoplan-idm.function_responsible_actor",
functionDestination: "i18n:govoplan-idm.function_destination"
} as const;
export function idmDisabledReason(
loading: boolean,
busy: boolean,
permitted = true
): string | undefined {
if (loading) return IDM_INTERFACE_I18N.loading;
if (busy) return IDM_INTERFACE_I18N.busy;
if (!permitted) return IDM_INTERFACE_I18N.writeReason;
return undefined;
}
+180 -2
View File
@@ -62,7 +62,96 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-idm.unit.a94c2fbd": "Unit",
"i18n:govoplan-idm.update_assignment.e20f52aa": "Update assignment",
"i18n:govoplan-idm.view_assignments.2d40d6a5": "View assignments",
"i18n:govoplan-idm.write_permission_required.c7dde7c6": "You do not have permission to manage IDM organization assignments."
"i18n:govoplan-idm.write_permission_required.c7dde7c6": "You do not have permission to manage IDM organization assignments.",
"i18n:govoplan-idm.loading_reason": "IDM data is still loading.",
"i18n:govoplan-idm.busy_reason": "Another IDM action is still running.",
"i18n:govoplan-idm.identity_search_permission_reason": "Your account may not search identity candidates.",
"i18n:govoplan-idm.incomplete_draft_reason": "Complete all required fields before submitting.",
"i18n:govoplan-idm.no_changes_reason": "There are no settings changes to save.",
"i18n:govoplan-idm.required_action": "Required action",
"i18n:govoplan-idm.responsible_actor": "Responsible actor",
"i18n:govoplan-idm.destination": "Destination",
"i18n:govoplan-idm.permission_required_action": "Ask for the corresponding IDM management permission.",
"i18n:govoplan-idm.permission_responsible_actor": "An Access or tenant administrator",
"i18n:govoplan-idm.permission_destination": "Access role assignments",
"i18n:govoplan-idm.function_required_action": "Create or activate an organization function first.",
"i18n:govoplan-idm.function_responsible_actor": "An organization administrator",
"i18n:govoplan-idm.function_destination": "Organizations",
"i18n:govoplan-idm.confirm_function_action": "{action} the governed change for {function}? The decision and actor are retained as evidence.",
"i18n:govoplan-idm.function_request_title": "Function request: {function}",
"i18n:govoplan-idm.function_grant_title": "Function grant: {function}",
"i18n:govoplan-idm.action_approve": "Approve",
"i18n:govoplan-idm.action_reject": "Reject",
"i18n:govoplan-idm.action_accept": "Accept",
"i18n:govoplan-idm.action_request_changes": "Request changes",
"i18n:govoplan-idm.action_respond": "Respond",
"i18n:govoplan-idm.action_withdraw": "Withdraw",
"i18n:govoplan-idm.action_recheck": "Recheck",
"i18n:govoplan-idm.kind_request": "Request",
"i18n:govoplan-idm.kind_grant": "Grant",
"i18n:govoplan-idm.state_submitted": "Submitted",
"i18n:govoplan-idm.state_awaiting_holder": "Awaiting holder",
"i18n:govoplan-idm.state_awaiting_authority": "Awaiting authority",
"i18n:govoplan-idm.state_awaiting_recipient": "Awaiting recipient",
"i18n:govoplan-idm.state_changes_requested": "Changes requested",
"i18n:govoplan-idm.state_applied": "Applied",
"i18n:govoplan-idm.state_rejected": "Rejected",
"i18n:govoplan-idm.state_expired": "Expired",
"i18n:govoplan-idm.state_withdrawn": "Withdrawn",
"i18n:govoplan-idm.state_cancelled": "Cancelled",
"i18n:govoplan-idm.state_blocked": "Blocked",
"i18n:govoplan-idm.state_failed_manual_review": "Manual review failed",
"i18n:govoplan-idm.step_approve_holder": "Holder approval",
"i18n:govoplan-idm.step_approve_authority": "Authority approval",
"i18n:govoplan-idm.step_accept_recipient": "Recipient acceptance",
"i18n:govoplan-idm.profile_holder_review": "Holder review",
"i18n:govoplan-idm.profile_authority_review": "Authority review",
"i18n:govoplan-idm.profile_recipient_review": "Recipient review",
"Actions": "Actions",
"Direct changes to this governed function require an emergency override reason.": "Direct changes to this governed function require an emergency override reason.",
"Direct changes to this governed function are emergency overrides. Use a request or grant above for the normal process.": "Direct changes to this governed function are emergency overrides. Use a request or grant above for the normal process.",
"Emergency override reason": "Emergency override reason",
"Override evidence references (one per line)": "Override evidence references (one per line)",
"Unsaved function change": "Unsaved function change",
"Save or discard the function request or grant before leaving this surface.": "Save or discard the function request or grant before leaving this surface.",
"Kind": "Kind",
"Function": "Function",
"Candidate": "Candidate",
"State": "State",
"Decisions": "Decisions",
"Updated": "Updated",
"Open change": "Open change",
"Function requests and grants": "Function requests and grants",
"Start governed change": "Start governed change",
"Loading governed function changes": "Loading governed function changes",
"No governed function changes": "No governed function changes",
"Start governed function change": "Start governed function change",
"Cancel": "Cancel",
"Submit": "Submit",
"Function change kind": "Function change kind",
"Request function": "Request function",
"Grant function": "Grant function",
"Select function": "Select function",
"Candidate identity": "Candidate identity",
"Select identity": "Select identity",
"Candidate account": "Candidate account",
"No linked account": "No linked account",
"Valid from": "Valid from",
"Valid until": "Valid until",
"Justification": "Justification",
"Evidence references (one per line)": "Evidence references (one per line)",
"Function change": "Function change",
"Close": "Close",
"Profile": "Profile",
"Workflow revision": "Workflow revision",
"Required decisions": "Required decisions",
"Completed decisions": "Completed decisions",
"Explanation": "Explanation",
"Decision comment": "Decision comment",
"History": "History",
"Confirm function decision": "Confirm function decision",
"Confirm": "Confirm",
"None": "None"
},
de: {
"i18n:govoplan-idm.account.2b2936f8": "Konto",
@@ -125,6 +214,95 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-idm.unit.a94c2fbd": "Einheit",
"i18n:govoplan-idm.update_assignment.e20f52aa": "Zuordnung aktualisieren",
"i18n:govoplan-idm.view_assignments.2d40d6a5": "Zuordnungen anzeigen",
"i18n:govoplan-idm.write_permission_required.c7dde7c6": "Du hast keine Berechtigung, IDM-Organisationszuordnungen zu verwalten."
"i18n:govoplan-idm.write_permission_required.c7dde7c6": "Du hast keine Berechtigung, IDM-Organisationszuordnungen zu verwalten.",
"i18n:govoplan-idm.loading_reason": "IDM-Daten werden noch geladen.",
"i18n:govoplan-idm.busy_reason": "Eine andere IDM-Aktion läuft noch.",
"i18n:govoplan-idm.identity_search_permission_reason": "Ihr Konto darf keine Identitätskandidaten suchen.",
"i18n:govoplan-idm.incomplete_draft_reason": "Füllen Sie vor dem Absenden alle Pflichtfelder aus.",
"i18n:govoplan-idm.no_changes_reason": "Es gibt keine Einstellungsänderungen zu speichern.",
"i18n:govoplan-idm.required_action": "Erforderliche Aktion",
"i18n:govoplan-idm.responsible_actor": "Verantwortliche Stelle",
"i18n:govoplan-idm.destination": "Ziel",
"i18n:govoplan-idm.permission_required_action": "Fordern Sie die entsprechende IDM-Verwaltungsberechtigung an.",
"i18n:govoplan-idm.permission_responsible_actor": "Access- oder Mandantenadministration",
"i18n:govoplan-idm.permission_destination": "Access-Rollenzuordnungen",
"i18n:govoplan-idm.function_required_action": "Erstellen oder aktivieren Sie zuerst eine Organisationsfunktion.",
"i18n:govoplan-idm.function_responsible_actor": "Organisationsadministration",
"i18n:govoplan-idm.function_destination": "Organisationen",
"i18n:govoplan-idm.confirm_function_action": "{action} der gesteuerten Änderung für {function}? Entscheidung und handelnde Person werden als Nachweis aufbewahrt.",
"i18n:govoplan-idm.function_request_title": "Funktionsantrag: {function}",
"i18n:govoplan-idm.function_grant_title": "Funktionsvergabe: {function}",
"i18n:govoplan-idm.action_approve": "Genehmigen",
"i18n:govoplan-idm.action_reject": "Ablehnen",
"i18n:govoplan-idm.action_accept": "Annehmen",
"i18n:govoplan-idm.action_request_changes": "Änderungen anfordern",
"i18n:govoplan-idm.action_respond": "Antworten",
"i18n:govoplan-idm.action_withdraw": "Zurückziehen",
"i18n:govoplan-idm.action_recheck": "Erneut prüfen",
"i18n:govoplan-idm.kind_request": "Antrag",
"i18n:govoplan-idm.kind_grant": "Vergabe",
"i18n:govoplan-idm.state_submitted": "Eingereicht",
"i18n:govoplan-idm.state_awaiting_holder": "Wartet auf Funktionsinhaber",
"i18n:govoplan-idm.state_awaiting_authority": "Wartet auf zuständige Stelle",
"i18n:govoplan-idm.state_awaiting_recipient": "Wartet auf Empfänger",
"i18n:govoplan-idm.state_changes_requested": "Änderungen angefordert",
"i18n:govoplan-idm.state_applied": "Angewendet",
"i18n:govoplan-idm.state_rejected": "Abgelehnt",
"i18n:govoplan-idm.state_expired": "Abgelaufen",
"i18n:govoplan-idm.state_withdrawn": "Zurückgezogen",
"i18n:govoplan-idm.state_cancelled": "Abgebrochen",
"i18n:govoplan-idm.state_blocked": "Blockiert",
"i18n:govoplan-idm.state_failed_manual_review": "Manuelle Prüfung fehlgeschlagen",
"i18n:govoplan-idm.step_approve_holder": "Genehmigung durch Funktionsinhaber",
"i18n:govoplan-idm.step_approve_authority": "Genehmigung durch zuständige Stelle",
"i18n:govoplan-idm.step_accept_recipient": "Annahme durch Empfänger",
"i18n:govoplan-idm.profile_holder_review": "Prüfung durch Funktionsinhaber",
"i18n:govoplan-idm.profile_authority_review": "Prüfung durch zuständige Stelle",
"i18n:govoplan-idm.profile_recipient_review": "Prüfung durch Empfänger",
"Actions": "Aktionen",
"Direct changes to this governed function require an emergency override reason.": "Direkte Änderungen an dieser gesteuerten Funktion erfordern eine Begründung für die Notfallübersteuerung.",
"Direct changes to this governed function are emergency overrides. Use a request or grant above for the normal process.": "Direkte Änderungen an dieser gesteuerten Funktion sind Notfallübersteuerungen. Verwenden Sie für den regulären Prozess einen Antrag oder eine Vergabe.",
"Emergency override reason": "Begründung der Notfallübersteuerung",
"Override evidence references (one per line)": "Nachweisreferenzen der Übersteuerung (eine pro Zeile)",
"Unsaved function change": "Ungespeicherte Funktionsänderung",
"Save or discard the function request or grant before leaving this surface.": "Speichern oder verwerfen Sie den Funktionsantrag oder die Vergabe, bevor Sie diesen Bereich verlassen.",
"Kind": "Art",
"Function": "Funktion",
"Candidate": "Kandidat",
"State": "Status",
"Decisions": "Entscheidungen",
"Updated": "Aktualisiert",
"Open change": "Änderung öffnen",
"Function requests and grants": "Funktionsanträge und -vergaben",
"Start governed change": "Gesteuerte Änderung starten",
"Loading governed function changes": "Gesteuerte Funktionsänderungen werden geladen",
"No governed function changes": "Keine gesteuerten Funktionsänderungen",
"Start governed function change": "Gesteuerte Funktionsänderung starten",
"Cancel": "Abbrechen",
"Submit": "Absenden",
"Function change kind": "Art der Funktionsänderung",
"Request function": "Funktion beantragen",
"Grant function": "Funktion vergeben",
"Select function": "Funktion auswählen",
"Candidate identity": "Kandidatenidentität",
"Select identity": "Identität auswählen",
"Candidate account": "Kandidatenkonto",
"No linked account": "Kein verknüpftes Konto",
"Valid from": "Gültig ab",
"Valid until": "Gültig bis",
"Justification": "Begründung",
"Evidence references (one per line)": "Nachweisreferenzen (eine pro Zeile)",
"Function change": "Funktionsänderung",
"Close": "Schließen",
"Profile": "Profil",
"Workflow revision": "Workflow-Revision",
"Required decisions": "Erforderliche Entscheidungen",
"Completed decisions": "Abgeschlossene Entscheidungen",
"Explanation": "Erläuterung",
"Decision comment": "Entscheidungskommentar",
"History": "Verlauf",
"Confirm function decision": "Funktionsentscheidung bestätigen",
"Confirm": "Bestätigen",
"None": "Keine"
}
};
+2 -2
View File
@@ -38,7 +38,7 @@ const organizationFunctionActions: OrganizationFunctionActionsUiCapability = {
export const idmModule: PlatformWebModule = {
id: "idm",
label: "i18n:govoplan-idm.idm.61f4a7a2",
version: "0.1.6",
version: "0.1.8",
dependencies: ["identity", "organizations"],
translations,
viewSurfaces: [
@@ -46,7 +46,7 @@ export const idmModule: PlatformWebModule = {
id: "idm.action.view-function-assignments",
moduleId: "idm",
kind: "action",
label: "View function assignments",
label: "i18n:govoplan-idm.view_assignments.2d40d6a5",
order: 40
}
],