Migrate IDM interface patterns
This commit is contained in:
@@ -91,6 +91,10 @@ planning.
|
||||
The WebUI exposed by this repository is a normal module UI at `/idm`. It is the
|
||||
editing surface for identity-to-organization-function assignment links.
|
||||
|
||||
Its interface archetypes, consequence classes, contextual-help contract, and
|
||||
accessibility evidence are recorded in
|
||||
[`docs/INTERFACE_PATTERN_MIGRATION.md`](docs/INTERFACE_PATTERN_MIGRATION.md).
|
||||
|
||||
## Migration And Permission Transition
|
||||
|
||||
The initial IDM migration creates new IDM-owned tables only. No data is migrated
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# IDM Interface Pattern Migration
|
||||
|
||||
This document records the bounded migration of IDM-owned WebUI surfaces to the
|
||||
GovOPlaN interface pattern language. Identity owns people and account links,
|
||||
Organizations owns functions and units, Access owns application authority, and
|
||||
IDM owns effective identity-to-function facts and their governed lifecycle.
|
||||
|
||||
## Surface Inventory
|
||||
|
||||
| Surface | Archetype | Consequence class | Contract |
|
||||
| --- | --- | --- | --- |
|
||||
| `/idm` assignments | Repeated administration and governed relationship directory | Create, change, deactivate, delegate, or act for an assignment | Shared DataGrid/dialog/actions, accurate draft baseline, explicit permission and organization prerequisites, contextual field help |
|
||||
| IDM governance settings | Effective tenant configuration | Require change evidence and alter audit retention | Shared card/form/toggles, dirty-state guard, write blocker, consequence help |
|
||||
| Function request/grant list | Governed work queue | Start or inspect a function change | Shared grid/loading/status/action slot, localized state and workflow vocabulary |
|
||||
| Request/grant editor | Guided consequential editor | Submit a governed assignment change | Shared segmented control/dialog/forms, guarded draft, effective dates, justification and evidence help |
|
||||
| Function-change detail | Decision and provenance record | Approve, reject, accept, request changes, withdraw, respond, or recover | Shared confirmation, available-action contract, retained actor/policy/workflow/history evidence |
|
||||
| `idm.action.view-function-assignments` | Contextual cross-module action | Navigate with function context | Declared capability surface, permission guard, no Organizations-private import |
|
||||
|
||||
## Consequence And Availability Rules
|
||||
|
||||
- Opening an existing assignment does not mark it dirty. The guard compares the
|
||||
editor to the loaded baseline and protects only actual changes.
|
||||
- Direct assignment changes require IDM write authority. Governed functions use
|
||||
request/grant workflows for normal changes; direct edits are emergency
|
||||
overrides with a retained reason and optional evidence references.
|
||||
- Delegated and acting-for assignments remain tied to a valid source assignment
|
||||
and the Organizations flags that permit those semantics.
|
||||
- Assignment and governance settings do not create application permissions.
|
||||
Access must separately map accepted institutional facts to assignable roles.
|
||||
- Decisions are confirmed before execution. The resulting actor, comment,
|
||||
policy decision, workflow revision, state transition, and evidence remain in
|
||||
the governed record.
|
||||
- Deactivation and expiry remove a fact from effective resolution while
|
||||
retaining provenance and lifecycle evidence.
|
||||
- Missing permission, identity search, and organization functions identify the
|
||||
required action, responsible administrator, and destination.
|
||||
|
||||
## State And Accessibility Evidence
|
||||
|
||||
The module uses Core page/card/grid/dialog/loading/alert/status/action-blocker,
|
||||
field-help, disabled-reason, confirmation, and unsaved-change controls. Shared
|
||||
dialogs retain focus containment and return behavior; stable grid actions remain
|
||||
keyboard reachable. Existing responsive CSS collapses summaries and histories
|
||||
to one column at narrow widths.
|
||||
|
||||
English and German catalogues cover route metadata, assignment fields, governed
|
||||
states, workflow steps, decisions, confirmations, and accessible labels. Dates
|
||||
follow the selected platform locale. Manifest topics provide stable route,
|
||||
field, blocker, workflow, and consequence references without importing optional
|
||||
Policy, Audit, Notifications, Access, or Workflow Engine implementations.
|
||||
|
||||
@@ -29,6 +29,8 @@ from govoplan_core.core.organizations import (
|
||||
from govoplan_core.core.views import ViewSurface
|
||||
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
|
||||
from govoplan_core.core.modules import (
|
||||
DocumentationCondition,
|
||||
DocumentationLink,
|
||||
DocumentationTopic,
|
||||
FrontendModule,
|
||||
FrontendRoute,
|
||||
@@ -327,6 +329,34 @@ manifest = ModuleManifest(
|
||||
documentation_types=("admin",),
|
||||
audience=("tenant_admin", "access_admin", "operator"),
|
||||
related_modules=("identity", "organizations", "access", "audit", "policy"),
|
||||
links=(
|
||||
DocumentationLink(
|
||||
label="IDM workspace",
|
||||
href="/idm",
|
||||
kind="runtime",
|
||||
),
|
||||
DocumentationLink(
|
||||
label="IDM settings API",
|
||||
href="/api/v1/idm/settings",
|
||||
kind="api",
|
||||
),
|
||||
),
|
||||
metadata={
|
||||
"kind": "reference",
|
||||
"help_contexts": [
|
||||
"idm.governance.settings",
|
||||
"idm.assignment.change-request",
|
||||
"idm.assignment.emergency-override",
|
||||
"idm.function-change.request",
|
||||
"idm.function-change.grant",
|
||||
"idm.function-change.decision",
|
||||
],
|
||||
"consequence_classes": {
|
||||
"governance_settings": "Changes whether direct assignment mutations require approved change evidence.",
|
||||
"emergency_override": "Bypasses the normal governed request or grant path and requires retained reason and evidence.",
|
||||
"function_decision": "Advances or terminates a governed change and retains actor, comment, policy, and workflow evidence.",
|
||||
},
|
||||
},
|
||||
order=27,
|
||||
),
|
||||
DocumentationTopic(
|
||||
@@ -359,8 +389,89 @@ manifest = ModuleManifest(
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("tenant_admin", "access_admin", "operator", "user"),
|
||||
related_modules=("identity", "organizations", "access"),
|
||||
conditions=(
|
||||
DocumentationCondition(any_scopes=IDM_READ_SCOPES),
|
||||
),
|
||||
links=(
|
||||
DocumentationLink(
|
||||
label="IDM assignments",
|
||||
href="/idm",
|
||||
kind="runtime",
|
||||
),
|
||||
DocumentationLink(
|
||||
label="Organization function assignments API",
|
||||
href="/api/v1/idm/organization-function-assignments",
|
||||
kind="api",
|
||||
),
|
||||
),
|
||||
metadata={
|
||||
"kind": "workflow",
|
||||
"help_contexts": [
|
||||
"idm.route.assignments",
|
||||
"idm.action.view-function-assignments",
|
||||
"idm.blocker.permission",
|
||||
"idm.blocker.no-functions",
|
||||
"idm.blocker.identity-search",
|
||||
],
|
||||
},
|
||||
order=28,
|
||||
),
|
||||
DocumentationTopic(
|
||||
id="idm.reference.fields-and-consequences",
|
||||
title="IDM assignment fields and consequences",
|
||||
summary=(
|
||||
"Reference for direct assignments, delegation, acting-for, "
|
||||
"effective dates, governed changes, evidence, and retention."
|
||||
),
|
||||
body=(
|
||||
"Identity and account select who receives the institutional fact; "
|
||||
"function and unit are owned by Organizations. Source distinguishes "
|
||||
"direct, delegated, acting-for, directory, governance, and system facts. "
|
||||
"Delegation and acting-for require a valid source assignment and the "
|
||||
"corresponding function permission. Subunit scope broadens the fact's "
|
||||
"organizational reach. Deactivation and expiry preserve provenance while "
|
||||
"removing the assignment from effective resolution. Governed request and "
|
||||
"grant decisions retain actor, policy, workflow revision, comments, and "
|
||||
"evidence. An emergency override is not the normal process and must carry "
|
||||
"an explicit reason. An IDM assignment alone never grants application "
|
||||
"permissions; Access requires an explicit mapping."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("tenant_admin", "access_admin", "operator", "user"),
|
||||
related_modules=("identity", "organizations", "access", "policy", "audit", "workflow_engine"),
|
||||
links=(
|
||||
DocumentationLink(
|
||||
label="Function assignment workflows",
|
||||
href="docs/FUNCTION_ASSIGNMENT_WORKFLOWS.md",
|
||||
kind="repository",
|
||||
),
|
||||
),
|
||||
metadata={
|
||||
"kind": "reference",
|
||||
"help_contexts": [
|
||||
"idm.field.identity",
|
||||
"idm.field.account",
|
||||
"idm.field.function",
|
||||
"idm.field.source",
|
||||
"idm.field.delegation",
|
||||
"idm.field.acting-for",
|
||||
"idm.field.subunits",
|
||||
"idm.field.effective-dates",
|
||||
"idm.field.justification",
|
||||
"idm.field.evidence",
|
||||
"idm.field.retention",
|
||||
],
|
||||
"consequence_classes": {
|
||||
"assignment": "Changes the effective institutional function fact consumed by optional downstream capabilities.",
|
||||
"deactivate_or_expire": "Removes the fact from effective resolution while retaining provenance and lifecycle evidence.",
|
||||
"delegation": "Creates a bounded derived assignment that remains tied to its source assignment.",
|
||||
"acting_for": "Allows a bounded account to act in place of a source assignment when Organizations permits it.",
|
||||
"retention": "Changes how long detailed assignment-change evidence remains available.",
|
||||
},
|
||||
},
|
||||
order=29,
|
||||
),
|
||||
),
|
||||
architecture=declared_module_architecture(
|
||||
layer="institutional_foundation",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from govoplan_idm.backend.manifest import manifest
|
||||
|
||||
|
||||
class IdmInterfaceDocumentationContractTests(unittest.TestCase):
|
||||
def test_route_and_contributed_action_remain_declared(self) -> None:
|
||||
frontend = manifest.frontend
|
||||
self.assertIsNotNone(frontend)
|
||||
self.assertEqual(
|
||||
{"/idm"},
|
||||
{route.path for route in frontend.routes}, # type: ignore[union-attr]
|
||||
)
|
||||
self.assertEqual(
|
||||
{"idm.action.view-function-assignments"},
|
||||
{surface.id for surface in frontend.view_surfaces}, # type: ignore[union-attr]
|
||||
)
|
||||
self.assertTrue(all(item.icon == "users" for item in manifest.nav_items))
|
||||
|
||||
def test_topics_publish_workflow_blocker_and_consequence_metadata(self) -> None:
|
||||
topics = {topic.id: topic for topic in manifest.documentation}
|
||||
self.assertIn("idm.workflow.assign-function-to-identity", topics)
|
||||
self.assertIn("idm.reference.assignment-governance", topics)
|
||||
self.assertIn("idm.reference.fields-and-consequences", topics)
|
||||
|
||||
workflow = topics["idm.workflow.assign-function-to-identity"]
|
||||
self.assertIn("idm.blocker.permission", workflow.metadata["help_contexts"])
|
||||
governance = topics["idm.reference.assignment-governance"]
|
||||
self.assertIn("function_decision", governance.metadata["consequence_classes"])
|
||||
reference = topics["idm.reference.fields-and-consequences"]
|
||||
self.assertIn("idm.field.acting-for", reference.metadata["help_contexts"])
|
||||
self.assertIn("deactivate_or_expire", reference.metadata["consequence_classes"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user