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
|
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.
|
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
|
## Migration And Permission Transition
|
||||||
|
|
||||||
The initial IDM migration creates new IDM-owned tables only. No data is migrated
|
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.views import ViewSurface
|
||||||
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
|
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
|
||||||
from govoplan_core.core.modules import (
|
from govoplan_core.core.modules import (
|
||||||
|
DocumentationCondition,
|
||||||
|
DocumentationLink,
|
||||||
DocumentationTopic,
|
DocumentationTopic,
|
||||||
FrontendModule,
|
FrontendModule,
|
||||||
FrontendRoute,
|
FrontendRoute,
|
||||||
@@ -327,6 +329,34 @@ manifest = ModuleManifest(
|
|||||||
documentation_types=("admin",),
|
documentation_types=("admin",),
|
||||||
audience=("tenant_admin", "access_admin", "operator"),
|
audience=("tenant_admin", "access_admin", "operator"),
|
||||||
related_modules=("identity", "organizations", "access", "audit", "policy"),
|
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,
|
order=27,
|
||||||
),
|
),
|
||||||
DocumentationTopic(
|
DocumentationTopic(
|
||||||
@@ -359,8 +389,89 @@ manifest = ModuleManifest(
|
|||||||
documentation_types=("admin", "user"),
|
documentation_types=("admin", "user"),
|
||||||
audience=("tenant_admin", "access_admin", "operator", "user"),
|
audience=("tenant_admin", "access_admin", "operator", "user"),
|
||||||
related_modules=("identity", "organizations", "access"),
|
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,
|
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(
|
architecture=declared_module_architecture(
|
||||||
layer="institutional_foundation",
|
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",
|
"main": "src/index.ts",
|
||||||
"module": "src/index.ts",
|
"module": "src/index.ts",
|
||||||
"types": "src/index.ts",
|
"types": "src/index.ts",
|
||||||
|
"scripts": {
|
||||||
|
"test:interface-patterns": "node scripts/test-interface-pattern-language.mjs"
|
||||||
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./src/index.ts",
|
"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,
|
ApiError,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
ConfirmDialog,
|
||||||
DataGrid,
|
DataGrid,
|
||||||
Dialog,
|
Dialog,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
|
DocumentationHelpLink,
|
||||||
FormField,
|
FormField,
|
||||||
|
LoadingFrame,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
TableActionGroup,
|
TableActionGroup,
|
||||||
hasScope,
|
hasScope,
|
||||||
|
i18nMessage,
|
||||||
|
usePlatformLanguage,
|
||||||
|
useUnsavedChanges,
|
||||||
|
useUnsavedDraftGuard,
|
||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
type AuthInfo,
|
type AuthInfo,
|
||||||
type DataGridColumn
|
type DataGridColumn
|
||||||
@@ -28,6 +35,12 @@ import {
|
|||||||
type IdentityOption,
|
type IdentityOption,
|
||||||
type OrganizationModel
|
type OrganizationModel
|
||||||
} from "../api/idm";
|
} from "../api/idm";
|
||||||
|
import {
|
||||||
|
IDM_FIELD_DOCUMENTATION,
|
||||||
|
IDM_GOVERNANCE_DOCUMENTATION,
|
||||||
|
IDM_INTERFACE_I18N,
|
||||||
|
idmDisabledReason
|
||||||
|
} from "./interfacePatterns";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
settings: ApiSettings;
|
settings: ApiSettings;
|
||||||
@@ -85,15 +98,49 @@ function statusTone(state: string): string {
|
|||||||
return "warning";
|
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 {
|
function actionLabel(action: FunctionAssignmentChangeAction): string {
|
||||||
return {
|
return {
|
||||||
approve: "Approve",
|
approve: "i18n:govoplan-idm.action_approve",
|
||||||
reject: "Reject",
|
reject: "i18n:govoplan-idm.action_reject",
|
||||||
accept: "Accept",
|
accept: "i18n:govoplan-idm.action_accept",
|
||||||
request_changes: "Request changes",
|
request_changes: "i18n:govoplan-idm.action_request_changes",
|
||||||
respond: "Respond",
|
respond: "i18n:govoplan-idm.action_respond",
|
||||||
withdraw: "Withdraw",
|
withdraw: "i18n:govoplan-idm.action_withdraw",
|
||||||
recover: "Recheck"
|
recover: "i18n:govoplan-idm.action_recheck"
|
||||||
}[action];
|
}[action];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +160,7 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
|
|||||||
const initialKind: FunctionAssignmentChangeKind = canRequest ? "request" : "grant";
|
const initialKind: FunctionAssignmentChangeKind = canRequest ? "request" : "grant";
|
||||||
const [changes, setChanges] = useState<FunctionAssignmentChange[]>([]);
|
const [changes, setChanges] = useState<FunctionAssignmentChange[]>([]);
|
||||||
const [draft, setDraft] = useState<Draft>(() => emptyDraft(auth, initialKind));
|
const [draft, setDraft] = useState<Draft>(() => emptyDraft(auth, initialKind));
|
||||||
|
const [draftBaseline, setDraftBaseline] = useState<Draft>(() => emptyDraft(auth, initialKind));
|
||||||
const [selected, setSelected] = useState<FunctionAssignmentChange | null>(null);
|
const [selected, setSelected] = useState<FunctionAssignmentChange | null>(null);
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
const [detailOpen, setDetailOpen] = useState(false);
|
const [detailOpen, setDetailOpen] = useState(false);
|
||||||
@@ -120,6 +168,9 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
|
const [pendingAction, setPendingAction] = useState<FunctionAssignmentChangeAction | null>(null);
|
||||||
|
const { language } = usePlatformLanguage();
|
||||||
|
const { requestDiscard } = useUnsavedChanges();
|
||||||
const initialChangeId = useMemo(() => {
|
const initialChangeId = useMemo(() => {
|
||||||
if (typeof window === "undefined") return "";
|
if (typeof window === "undefined") return "";
|
||||||
return new URLSearchParams(window.location.search).get("change") ?? "";
|
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 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 identityById = useMemo(() => new Map(identities.map((item) => [item.id, item])), [identities]);
|
||||||
const selectedIdentity = identityById.get(draft.identityId);
|
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 () => {
|
const load = useCallback(async () => {
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
@@ -156,7 +216,29 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
|
|||||||
if (!visible) return null;
|
if (!visible) return null;
|
||||||
|
|
||||||
function setKind(kind: FunctionAssignmentChangeKind) {
|
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) {
|
function setIdentity(identityId: string) {
|
||||||
@@ -183,8 +265,9 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submit(event: FormEvent) {
|
async function submit(event?: FormEvent): Promise<boolean> {
|
||||||
event.preventDefault();
|
event?.preventDefault();
|
||||||
|
if (!draft.functionId || !draft.identityId || !draft.justification.trim()) return false;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
@@ -201,12 +284,16 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
|
|||||||
metadata: {}
|
metadata: {}
|
||||||
});
|
});
|
||||||
setCreateOpen(false);
|
setCreateOpen(false);
|
||||||
setDraft(emptyDraft(auth, initialKind));
|
const next = emptyDraft(auth, initialKind);
|
||||||
|
setDraft(next);
|
||||||
|
setDraftBaseline(next);
|
||||||
setSelected(created);
|
setSelected(created);
|
||||||
setDetailOpen(true);
|
setDetailOpen(true);
|
||||||
await load();
|
await load();
|
||||||
|
return true;
|
||||||
} catch (caught) {
|
} catch (caught) {
|
||||||
setError(errorMessage(caught));
|
setError(errorMessage(caught));
|
||||||
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
@@ -229,7 +316,7 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns: DataGridColumn<FunctionAssignmentChange>[] = [
|
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",
|
id: "function",
|
||||||
header: "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,
|
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
|
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: "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) }]} /> }
|
{ 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"
|
title="Function requests and grants"
|
||||||
collapsible
|
collapsible
|
||||||
collapseKey="idm.function-assignment-changes"
|
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>
|
</Card>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={createOpen}
|
open={createOpen}
|
||||||
title="Start governed function change"
|
title="Start governed function change"
|
||||||
className="admin-dialog admin-dialog-wide idm-change-dialog"
|
className="admin-dialog admin-dialog-wide idm-change-dialog"
|
||||||
onClose={() => !busy && setCreateOpen(false)}
|
onClose={closeCreate}
|
||||||
closeDisabled={busy}
|
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)}>
|
<form id="idm-change-create" className="admin-form-grid two-columns" onSubmit={(event) => void submit(event)}>
|
||||||
<div className="wide">
|
<div className="wide">
|
||||||
@@ -286,37 +380,37 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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}>
|
<select value={draft.functionId} onChange={(event) => setDraft((current) => ({ ...current, functionId: event.target.value }))} disabled={busy}>
|
||||||
<option value="">Select function</option>
|
<option value="">Select function</option>
|
||||||
{model.functions.filter((item) => item.is_active).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
{model.functions.filter((item) => item.is_active).map((item) => <option key={item.id} value={item.id}>{item.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</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"}>
|
<select value={draft.identityId} onChange={(event) => setIdentity(event.target.value)} disabled={busy || draft.kind === "request"}>
|
||||||
<option value="">Select identity</option>
|
<option value="">Select identity</option>
|
||||||
{identities.map((item) => <option key={item.id} value={item.id}>{item.display_name || item.external_subject || item.id}</option>)}
|
{identities.map((item) => <option key={item.id} value={item.id}>{item.display_name || item.external_subject || item.id}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</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}>
|
<select value={draft.accountId} onChange={(event) => setDraft((current) => ({ ...current, accountId: event.target.value }))} disabled={busy}>
|
||||||
<option value="">No linked account</option>
|
<option value="">No linked account</option>
|
||||||
{(selectedIdentity?.account_ids ?? []).map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
|
{(selectedIdentity?.account_ids ?? []).map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</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} />
|
<input type="datetime-local" value={draft.validFrom} onChange={(event) => setDraft((current) => ({ ...current, validFrom: event.target.value }))} disabled={busy} />
|
||||||
</FormField>
|
</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} />
|
<input type="datetime-local" value={draft.validUntil} onChange={(event) => setDraft((current) => ({ ...current, validUntil: event.target.value }))} disabled={busy} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className="wide">
|
<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} />
|
<textarea rows={4} value={draft.justification} onChange={(event) => setDraft((current) => ({ ...current, justification: event.target.value }))} disabled={busy} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
<div className="wide">
|
<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} />
|
<textarea rows={3} value={draft.evidence} onChange={(event) => setDraft((current) => ({ ...current, evidence: event.target.value }))} disabled={busy} />
|
||||||
</FormField>
|
</FormField>
|
||||||
</div>
|
</div>
|
||||||
@@ -325,32 +419,39 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
|
|||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={detailOpen && selected !== null}
|
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"
|
className="admin-dialog admin-dialog-wide idm-change-dialog"
|
||||||
onClose={() => !busy && setDetailOpen(false)}
|
onClose={() => !busy && setDetailOpen(false)}
|
||||||
closeDisabled={busy}
|
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 && (
|
{selected && (
|
||||||
<div className="idm-change-detail">
|
<div className="idm-change-detail">
|
||||||
<dl className="idm-change-summary">
|
<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>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>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>Required decisions</dt><dd>{selected.required_steps.map(domainLabel).join(", ") || "None"}</dd></div>
|
||||||
<div><dt>Completed decisions</dt><dd>{selected.completed_steps.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>
|
<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>}
|
{selected.outcome_reason && <div className="wide"><dt>Explanation</dt><dd>{selected.outcome_reason}</dd></div>}
|
||||||
</dl>
|
</dl>
|
||||||
{selected.available_actions.length > 0 && (
|
{selected.available_actions.length > 0 && (
|
||||||
<div className="idm-change-actions">
|
<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} />
|
<textarea rows={2} value={comment} onChange={(event) => setComment(event.target.value)} disabled={busy} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className="button-row compact-actions">
|
<div className="button-row compact-actions">
|
||||||
{selected.available_actions.map((action) => (
|
{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)}
|
{actionIcon(action)} {actionLabel(action)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
@@ -361,12 +462,36 @@ export default function FunctionAssignmentChangesPanel({ settings, auth, model,
|
|||||||
<div>
|
<div>
|
||||||
<h3>History</h3>
|
<h3>History</h3>
|
||||||
<ol className="idm-change-history">
|
<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>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Dialog>
|
</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 { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from "react";
|
||||||
import { Edit3, Plus, RefreshCw } from "lucide-react";
|
import { Edit3, Plus, RefreshCw } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
|
ActionBlockerHint,
|
||||||
AdminIconButton,
|
AdminIconButton,
|
||||||
ApiError,
|
ApiError,
|
||||||
Button,
|
Button,
|
||||||
@@ -8,6 +9,7 @@ import {
|
|||||||
DataGrid,
|
DataGrid,
|
||||||
Dialog,
|
Dialog,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
|
DocumentationHelpLink,
|
||||||
FormField,
|
FormField,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
PageScrollViewport,
|
PageScrollViewport,
|
||||||
@@ -16,6 +18,7 @@ import {
|
|||||||
TableActionGroup,
|
TableActionGroup,
|
||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
hasScope,
|
hasScope,
|
||||||
|
useUnsavedChanges,
|
||||||
useUnsavedDraftGuard,
|
useUnsavedDraftGuard,
|
||||||
type ApiSettings,
|
type ApiSettings,
|
||||||
type AuthInfo,
|
type AuthInfo,
|
||||||
@@ -38,6 +41,13 @@ import {
|
|||||||
type OrganizationUnitItem
|
type OrganizationUnitItem
|
||||||
} from "../api/idm";
|
} from "../api/idm";
|
||||||
import FunctionAssignmentChangesPanel from "./FunctionAssignmentChangesPanel";
|
import FunctionAssignmentChangesPanel from "./FunctionAssignmentChangesPanel";
|
||||||
|
import {
|
||||||
|
IDM_DOCUMENTATION,
|
||||||
|
IDM_FIELD_DOCUMENTATION,
|
||||||
|
IDM_GOVERNANCE_DOCUMENTATION,
|
||||||
|
IDM_INTERFACE_I18N,
|
||||||
|
idmDisabledReason
|
||||||
|
} from "./interfacePatterns";
|
||||||
|
|
||||||
type IdmPageProps = {
|
type IdmPageProps = {
|
||||||
settings: ApiSettings;
|
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 {
|
function settingsDraftFrom(item: IdmSettings | null): SettingsDraft {
|
||||||
const source = item ?? DEFAULT_IDM_SETTINGS;
|
const source = item ?? DEFAULT_IDM_SETTINGS;
|
||||||
return {
|
return {
|
||||||
@@ -269,6 +264,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
const [actingForOptions, setActingForOptions] = useState<IdentityOption[]>([]);
|
const [actingForOptions, setActingForOptions] = useState<IdentityOption[]>([]);
|
||||||
const [actingForLoading, setActingForLoading] = useState(false);
|
const [actingForLoading, setActingForLoading] = useState(false);
|
||||||
const [assignmentDraft, setAssignmentDraft] = useState<AssignmentDraft>(() => emptyAssignmentDraft());
|
const [assignmentDraft, setAssignmentDraft] = useState<AssignmentDraft>(() => emptyAssignmentDraft());
|
||||||
|
const [assignmentBaseline, setAssignmentBaseline] = useState<AssignmentDraft>(() => emptyAssignmentDraft());
|
||||||
const [settingsDraft, setSettingsDraft] = useState<SettingsDraft>(() => settingsDraftFrom(null));
|
const [settingsDraft, setSettingsDraft] = useState<SettingsDraft>(() => settingsDraftFrom(null));
|
||||||
const [editingAssignmentId, setEditingAssignmentId] = useState<string | null>(null);
|
const [editingAssignmentId, setEditingAssignmentId] = useState<string | null>(null);
|
||||||
const [assignmentEditorOpen, setAssignmentEditorOpen] = useState(false);
|
const [assignmentEditorOpen, setAssignmentEditorOpen] = useState(false);
|
||||||
@@ -279,6 +275,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
const [success, setSuccess] = useState("");
|
const [success, setSuccess] = useState("");
|
||||||
const initialQuery = useMemo(() => idmInitialQuery(), []);
|
const initialQuery = useMemo(() => idmInitialQuery(), []);
|
||||||
const appliedInitialQueryRef = useRef(false);
|
const appliedInitialQueryRef = useRef(false);
|
||||||
|
const { requestDiscard } = useUnsavedChanges();
|
||||||
|
|
||||||
const canManage = hasScope(auth, "idm:organization_assignment:write") || hasScope(auth, "organizations:function:assign");
|
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");
|
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));
|
return Array.from(values).sort((left, right) => left.localeCompare(right));
|
||||||
}, [actingForOptionById, actingForOptions, assignmentDraft.acting_for_account_id, identityOptionById, sourceAssignment]);
|
}, [actingForOptionById, actingForOptions, assignmentDraft.acting_for_account_id, identityOptionById, sourceAssignment]);
|
||||||
const initialFunctionFilter = initialQuery.functionId;
|
const initialFunctionFilter = initialQuery.functionId;
|
||||||
const hasDirtyAssignmentDraft = assignmentEditorOpen && isAssignmentDirty(assignmentDraft);
|
const hasDirtyAssignmentDraft = assignmentEditorOpen && draftKey(assignmentDraft) !== draftKey(assignmentBaseline);
|
||||||
const hasDirtySettingsDraft = canReadSettings && isSettingsDirty(settingsDraft, idmSettings);
|
const hasDirtySettingsDraft = canReadSettings && isSettingsDirty(settingsDraft, idmSettings);
|
||||||
const hasDirtyDraft = hasDirtyAssignmentDraft || hasDirtySettingsDraft;
|
const hasDirtyDraft = hasDirtyAssignmentDraft || hasDirtySettingsDraft;
|
||||||
|
|
||||||
@@ -352,7 +349,9 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
if (initialAssignment) {
|
if (initialAssignment) {
|
||||||
appliedInitialQueryRef.current = true;
|
appliedInitialQueryRef.current = true;
|
||||||
setEditingAssignmentId(initialAssignment.id);
|
setEditingAssignmentId(initialAssignment.id);
|
||||||
setAssignmentDraft(assignmentDraftFrom(initialAssignment));
|
const nextDraft = assignmentDraftFrom(initialAssignment);
|
||||||
|
setAssignmentDraft(nextDraft);
|
||||||
|
setAssignmentBaseline(nextDraft);
|
||||||
setAssignmentEditorOpen(true);
|
setAssignmentEditorOpen(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -437,7 +436,9 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
}, [loadData]);
|
}, [loadData]);
|
||||||
|
|
||||||
const discardAssignmentDraft = useCallback(() => {
|
const discardAssignmentDraft = useCallback(() => {
|
||||||
setAssignmentDraft(emptyAssignmentDraft());
|
const nextDraft = emptyAssignmentDraft();
|
||||||
|
setAssignmentDraft(nextDraft);
|
||||||
|
setAssignmentBaseline(nextDraft);
|
||||||
setEditingAssignmentId(null);
|
setEditingAssignmentId(null);
|
||||||
setAssignmentEditorOpen(false);
|
setAssignmentEditorOpen(false);
|
||||||
setAssignmentChangeRequestId("");
|
setAssignmentChangeRequestId("");
|
||||||
@@ -505,18 +506,28 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
|
|
||||||
const openCreateAssignment = useCallback(() => {
|
const openCreateAssignment = useCallback(() => {
|
||||||
setEditingAssignmentId(null);
|
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("");
|
setAssignmentChangeRequestId("");
|
||||||
setAssignmentEditorOpen(true);
|
setAssignmentEditorOpen(true);
|
||||||
}, [functionById, initialFunctionFilter]);
|
}, [functionById, initialFunctionFilter]);
|
||||||
|
|
||||||
const editAssignment = useCallback((item: OrganizationFunctionAssignmentItem) => {
|
const editAssignment = useCallback((item: OrganizationFunctionAssignmentItem) => {
|
||||||
|
const nextDraft = assignmentDraftFrom(item);
|
||||||
setEditingAssignmentId(item.id);
|
setEditingAssignmentId(item.id);
|
||||||
setAssignmentDraft(assignmentDraftFrom(item));
|
setAssignmentDraft(nextDraft);
|
||||||
|
setAssignmentBaseline(nextDraft);
|
||||||
setAssignmentChangeRequestId("");
|
setAssignmentChangeRequestId("");
|
||||||
setAssignmentEditorOpen(true);
|
setAssignmentEditorOpen(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const closeAssignmentEditor = useCallback(() => {
|
||||||
|
if (busy) return;
|
||||||
|
if (hasDirtyAssignmentDraft) requestDiscard(discardAssignmentDraft);
|
||||||
|
else discardAssignmentDraft();
|
||||||
|
}, [busy, discardAssignmentDraft, hasDirtyAssignmentDraft, requestDiscard]);
|
||||||
|
|
||||||
function onIdentityChange(identityId: string) {
|
function onIdentityChange(identityId: string) {
|
||||||
const identity = identityOptionById.get(identityId);
|
const identity = identityOptionById.get(identityId);
|
||||||
setAssignmentDraft({
|
setAssignmentDraft({
|
||||||
@@ -594,7 +605,14 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
header: "Actions",
|
header: "Actions",
|
||||||
width: 72,
|
width: 72,
|
||||||
sticky: "end",
|
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>
|
<p>i18n:govoplan-idm.identity_links_intro.45fed9dd</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="idm-toolbar">
|
<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
|
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-idm.reload.870ca3ec
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -615,17 +640,53 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
|
|
||||||
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
|
||||||
{success && !error && <DismissibleAlert tone="success" resetKey={success} floating>{success}</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">
|
<LoadingFrame loading={loading || busy} label="i18n:govoplan-idm.loading_idm_assignments.0b1501bd">
|
||||||
<div className="idm-table-stack">
|
<div className="idm-table-stack">
|
||||||
{canReadSettings && (
|
{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(); }}>
|
<form className="admin-form-grid two-columns" onSubmit={(event) => { event.preventDefault(); void submitSettings(); }}>
|
||||||
<div className="idm-check-list wide">
|
<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>
|
</div>
|
||||||
<FormField label="i18n:govoplan-idm.audit_detail_level.eb2e6fd2">
|
<FormField label="i18n:govoplan-idm.audit_detail_level.eb2e6fd2" documentation={IDM_FIELD_DOCUMENTATION}>
|
||||||
<select
|
<select
|
||||||
value={settingsDraft.audit_detail_level}
|
value={settingsDraft.audit_detail_level}
|
||||||
disabled={!canManageSettings || busy}
|
disabled={!canManageSettings || busy}
|
||||||
@@ -636,7 +697,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
<option value="full">i18n:govoplan-idm.full.7f021a14</option>
|
<option value="full">i18n:govoplan-idm.full.7f021a14</option>
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="i18n:govoplan-idm.change_retention_days.4a91f7d3">
|
<FormField label="i18n:govoplan-idm.change_retention_days.4a91f7d3" documentation={IDM_FIELD_DOCUMENTATION}>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
@@ -646,7 +707,12 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<div className="button-row compact-actions wide">
|
<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
|
i18n:govoplan-idm.save_settings.4602c430
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -661,7 +727,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
identities={identityOptions}
|
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
|
<DataGrid
|
||||||
id="idm-organization-function-assignments"
|
id="idm-organization-function-assignments"
|
||||||
rows={assignments}
|
rows={assignments}
|
||||||
@@ -685,20 +751,26 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
<Dialog
|
<Dialog
|
||||||
open={assignmentEditorOpen}
|
open={assignmentEditorOpen}
|
||||||
title={editingAssignmentId ? "i18n:govoplan-idm.update_assignment.e20f52aa" : "i18n:govoplan-idm.add_assignment.08f2a0d5"}
|
title={editingAssignmentId ? "i18n:govoplan-idm.update_assignment.e20f52aa" : "i18n:govoplan-idm.add_assignment.08f2a0d5"}
|
||||||
onClose={() => !busy && discardAssignmentDraft()}
|
onClose={closeAssignmentEditor}
|
||||||
closeDisabled={busy}
|
closeDisabled={busy}
|
||||||
className="admin-dialog admin-dialog-wide idm-editor-dialog"
|
className="admin-dialog admin-dialog-wide idm-editor-dialog"
|
||||||
footer={(
|
footer={(
|
||||||
<>
|
<>
|
||||||
<Button type="button" onClick={discardAssignmentDraft} disabled={busy}>i18n:govoplan-idm.cancel_edit.ea4781e0</Button>
|
<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())}>
|
<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"}
|
{editingAssignmentId ? "i18n:govoplan-idm.update_assignment.e20f52aa" : "i18n:govoplan-idm.add_assignment.08f2a0d5"}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<form id={formId} className="admin-form-grid two-columns" onSubmit={(event) => void submitAssignment(event)}>
|
<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
|
<input
|
||||||
value={identitySearch}
|
value={identitySearch}
|
||||||
placeholder="i18n:govoplan-idm.search_identities.88a9ef15"
|
placeholder="i18n:govoplan-idm.search_identities.88a9ef15"
|
||||||
@@ -706,7 +778,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
onChange={(event) => setIdentitySearch(event.target.value)}
|
onChange={(event) => setIdentitySearch(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</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)}>
|
<select value={assignmentDraft.identity_id} disabled={!canManage || busy} onChange={(event) => onIdentityChange(event.target.value)}>
|
||||||
<option value="">i18n:govoplan-idm.select_identity.91d31615</option>
|
<option value="">i18n:govoplan-idm.select_identity.91d31615</option>
|
||||||
{identitySelectOptions.map((item) => (
|
{identitySelectOptions.map((item) => (
|
||||||
@@ -714,25 +786,25 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</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 })}>
|
<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>
|
<option value="">i18n:govoplan-idm.none.2baf5c66</option>
|
||||||
{accountIds.map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
|
{accountIds.map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</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 })}>
|
<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>
|
<option value="">i18n:govoplan-idm.select_function.2bec86e0</option>
|
||||||
{model.functions.map((item) => <option key={item.id} value={item.id}>{functionLabel(item, unitById)}</option>)}
|
{model.functions.map((item) => <option key={item.id} value={item.id}>{functionLabel(item, unitById)}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</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)}>
|
<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>)}
|
{SOURCE_OPTIONS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</FormField>
|
</FormField>
|
||||||
{(assignmentDraft.source === "delegated" || assignmentDraft.source === "acting_for") && (
|
{(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: "" })}>
|
<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>
|
<option value="">i18n:govoplan-idm.none.2baf5c66</option>
|
||||||
{assignmentOptions.map((item) => (
|
{assignmentOptions.map((item) => (
|
||||||
@@ -743,7 +815,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
)}
|
)}
|
||||||
{assignmentDraft.source === "acting_for" && (
|
{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
|
<input
|
||||||
value={actingForSearch}
|
value={actingForSearch}
|
||||||
placeholder="i18n:govoplan-idm.search_identities.88a9ef15"
|
placeholder="i18n:govoplan-idm.search_identities.88a9ef15"
|
||||||
@@ -751,7 +823,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
onChange={(event) => setActingForSearch(event.target.value)}
|
onChange={(event) => setActingForSearch(event.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</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 })}>
|
<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>
|
<option value="">i18n:govoplan-idm.select_account.982ee1ad</option>
|
||||||
{actingForAccountIds.map((accountId) => <option key={accountId} value={accountId}>{accountId}</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">
|
<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.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} onChange={(is_active) => setAssignmentDraft({ ...assignmentDraft, is_active })} />
|
<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>
|
</div>
|
||||||
{selectedFunctionIsGoverned && (
|
{selectedFunctionIsGoverned && (
|
||||||
<div className="wide idm-governance-override">
|
<div className="wide idm-governance-override">
|
||||||
<DismissibleAlert tone="warning" dismissible={false}>
|
<DismissibleAlert tone="warning" dismissible={false}>
|
||||||
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.
|
||||||
|
<DocumentationHelpLink reference={IDM_GOVERNANCE_DOCUMENTATION} />
|
||||||
</DismissibleAlert>
|
</DismissibleAlert>
|
||||||
<FormField label="Emergency override reason">
|
<FormField label="Emergency override reason" documentation={IDM_FIELD_DOCUMENTATION}>
|
||||||
<textarea
|
<textarea
|
||||||
rows={3}
|
rows={3}
|
||||||
value={assignmentDraft.governance_override_reason}
|
value={assignmentDraft.governance_override_reason}
|
||||||
@@ -776,7 +849,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
disabled={!canManage || busy}
|
disabled={!canManage || busy}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Override evidence references (one per line)">
|
<FormField label="Override evidence references (one per line)" documentation={IDM_FIELD_DOCUMENTATION}>
|
||||||
<textarea
|
<textarea
|
||||||
rows={2}
|
rows={2}
|
||||||
value={assignmentDraft.governance_override_evidence}
|
value={assignmentDraft.governance_override_evidence}
|
||||||
@@ -787,12 +860,29 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="wide idm-dialog-change-request">
|
<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} />
|
<input value={assignmentChangeRequestId} onChange={(event) => setAssignmentChangeRequestId(event.target.value)} placeholder="cfgreq-..." disabled={busy} />
|
||||||
</FormField>
|
</FormField>
|
||||||
<p className="idm-muted">i18n:govoplan-idm.change_request_id_help.cc7de508</p>
|
<p className="idm-muted">i18n:govoplan-idm.change_request_id_help.cc7de508</p>
|
||||||
</div>
|
</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>}
|
{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>}
|
{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>}
|
{!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.unit.a94c2fbd": "Unit",
|
||||||
"i18n:govoplan-idm.update_assignment.e20f52aa": "Update assignment",
|
"i18n:govoplan-idm.update_assignment.e20f52aa": "Update assignment",
|
||||||
"i18n:govoplan-idm.view_assignments.2d40d6a5": "View assignments",
|
"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: {
|
de: {
|
||||||
"i18n:govoplan-idm.account.2b2936f8": "Konto",
|
"i18n:govoplan-idm.account.2b2936f8": "Konto",
|
||||||
@@ -125,6 +214,95 @@ export const generatedTranslations: PlatformTranslations = {
|
|||||||
"i18n:govoplan-idm.unit.a94c2fbd": "Einheit",
|
"i18n:govoplan-idm.unit.a94c2fbd": "Einheit",
|
||||||
"i18n:govoplan-idm.update_assignment.e20f52aa": "Zuordnung aktualisieren",
|
"i18n:govoplan-idm.update_assignment.e20f52aa": "Zuordnung aktualisieren",
|
||||||
"i18n:govoplan-idm.view_assignments.2d40d6a5": "Zuordnungen anzeigen",
|
"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 = {
|
export const idmModule: PlatformWebModule = {
|
||||||
id: "idm",
|
id: "idm",
|
||||||
label: "i18n:govoplan-idm.idm.61f4a7a2",
|
label: "i18n:govoplan-idm.idm.61f4a7a2",
|
||||||
version: "0.1.6",
|
version: "0.1.8",
|
||||||
dependencies: ["identity", "organizations"],
|
dependencies: ["identity", "organizations"],
|
||||||
translations,
|
translations,
|
||||||
viewSurfaces: [
|
viewSurfaces: [
|
||||||
@@ -46,7 +46,7 @@ export const idmModule: PlatformWebModule = {
|
|||||||
id: "idm.action.view-function-assignments",
|
id: "idm.action.view-function-assignments",
|
||||||
moduleId: "idm",
|
moduleId: "idm",
|
||||||
kind: "action",
|
kind: "action",
|
||||||
label: "View function assignments",
|
label: "i18n:govoplan-idm.view_assignments.2d40d6a5",
|
||||||
order: 40
|
order: 40
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user