Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74652686ca | |||
| b47f71916d | |||
| b9b371a704 | |||
| 3b0e8bc600 | |||
| 05a10846a8 |
@@ -1,5 +1,9 @@
|
||||
# GovOPlaN IDM
|
||||
|
||||
<!-- govoplan-repository-type:start -->
|
||||
**Repository type:** module (platform).
|
||||
<!-- govoplan-repository-type:end -->
|
||||
|
||||
`govoplan-idm` is the planned integration module for external identity
|
||||
management systems. It does not own GovOPlaN's internal identity, organization,
|
||||
account, role, or tenant tables; those remain with `govoplan-identity`,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/idm-webui",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "webui/src/index.ts",
|
||||
@@ -19,7 +19,7 @@
|
||||
"LICENSE"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.6",
|
||||
"@govoplan/core-webui": "^0.1.8",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": "^19.0.0",
|
||||
|
||||
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-idm"
|
||||
version = "0.1.6"
|
||||
version = "0.1.8"
|
||||
description = "GovOPlaN identity management bridge module."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
authors = [{ name = "GovOPlaN" }]
|
||||
dependencies = [
|
||||
"govoplan-core>=0.1.6",
|
||||
"govoplan-identity>=0.1.6",
|
||||
"govoplan-organizations>=0.1.6",
|
||||
"govoplan-core>=0.1.8",
|
||||
"govoplan-identity>=0.1.8",
|
||||
"govoplan-organizations>=0.1.8",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
@@ -158,6 +159,18 @@ def _ensure_assignment_workflow(
|
||||
*,
|
||||
tenant_id: str,
|
||||
) -> None:
|
||||
_ensure_assignment_shape(item)
|
||||
function = _get_tenant_row(session, OrganizationFunction, item.function_id, tenant_id, "Organization function")
|
||||
base = _assignment_source_assignment(session, item, tenant_id=tenant_id)
|
||||
_ensure_assignment_source_rules(
|
||||
item,
|
||||
function=function,
|
||||
base=base,
|
||||
account_linked_to_identity=lambda identity_id, account_id: _account_linked_to_identity(session, identity_id, account_id),
|
||||
)
|
||||
|
||||
|
||||
def _ensure_assignment_shape(item: IdmOrganizationFunctionAssignment) -> None:
|
||||
if item.source not in ASSIGNMENT_SOURCES:
|
||||
raise _invalid("Assignment source is not supported.")
|
||||
if item.valid_from is not None and item.valid_until is not None and item.valid_until <= item.valid_from:
|
||||
@@ -165,43 +178,81 @@ def _ensure_assignment_workflow(
|
||||
if item.delegated_from_assignment_id is not None and item.delegated_from_assignment_id == item.id:
|
||||
raise _invalid("A function assignment cannot delegate from itself.")
|
||||
|
||||
function = _get_tenant_row(session, OrganizationFunction, item.function_id, tenant_id, "Organization function")
|
||||
base: IdmOrganizationFunctionAssignment | None = None
|
||||
if item.delegated_from_assignment_id is not None:
|
||||
base = _get_tenant_row(session, IdmOrganizationFunctionAssignment, item.delegated_from_assignment_id, tenant_id, "Delegated function assignment")
|
||||
if not base.is_active:
|
||||
raise _invalid("Delegation source assignment must be active.")
|
||||
if base.function_id != item.function_id:
|
||||
raise _invalid("Delegated and acting-for assignments must use the same organization function as the source assignment.")
|
||||
|
||||
def _assignment_source_assignment(
|
||||
session: Session,
|
||||
item: IdmOrganizationFunctionAssignment,
|
||||
*,
|
||||
tenant_id: str,
|
||||
) -> IdmOrganizationFunctionAssignment | None:
|
||||
if item.delegated_from_assignment_id is None:
|
||||
return None
|
||||
base = _get_tenant_row(session, IdmOrganizationFunctionAssignment, item.delegated_from_assignment_id, tenant_id, "Delegated function assignment")
|
||||
if not base.is_active:
|
||||
raise _invalid("Delegation source assignment must be active.")
|
||||
if base.function_id != item.function_id:
|
||||
raise _invalid("Delegated and acting-for assignments must use the same organization function as the source assignment.")
|
||||
return base
|
||||
|
||||
|
||||
def _ensure_assignment_source_rules(
|
||||
item: IdmOrganizationFunctionAssignment,
|
||||
*,
|
||||
function: OrganizationFunction,
|
||||
base: IdmOrganizationFunctionAssignment | None,
|
||||
account_linked_to_identity: Callable[[str, str], bool],
|
||||
) -> None:
|
||||
if item.source == "delegated":
|
||||
if base is None:
|
||||
raise _invalid("Delegated assignments require a source assignment.")
|
||||
if not function.delegable:
|
||||
raise _invalid("This organization function does not allow delegation.")
|
||||
if item.acting_for_account_id is not None:
|
||||
raise _invalid("Delegated assignments cannot set an acting-for account.")
|
||||
if item.identity_id == base.identity_id and (item.account_id or "") == (base.account_id or ""):
|
||||
raise _invalid("A delegated assignment must target another identity or account.")
|
||||
elif item.source == "acting_for":
|
||||
if base is None:
|
||||
raise _invalid("Acting-for assignments require a source assignment.")
|
||||
if not function.act_in_place_allowed:
|
||||
raise _invalid("This organization function does not allow acting in place.")
|
||||
if item.acting_for_account_id is None:
|
||||
raise _invalid("Acting-for assignments require an acting-for account.")
|
||||
if base.account_id is not None:
|
||||
if item.acting_for_account_id != base.account_id:
|
||||
raise _invalid("Acting-for account must match the source assignment account.")
|
||||
elif not _account_linked_to_identity(session, base.identity_id, item.acting_for_account_id):
|
||||
raise _invalid("Acting-for account must belong to the source assignment identity.")
|
||||
if item.account_id == item.acting_for_account_id:
|
||||
raise _invalid("The acting account and acting-for account must be different.")
|
||||
else:
|
||||
if item.delegated_from_assignment_id is not None:
|
||||
raise _invalid("Only delegated or acting-for assignments can reference a source assignment.")
|
||||
if item.acting_for_account_id is not None:
|
||||
raise _invalid("Only acting-for assignments can set an acting-for account.")
|
||||
_ensure_delegated_assignment(item, function=function, base=base)
|
||||
return
|
||||
if item.source == "acting_for":
|
||||
_ensure_acting_for_assignment(item, function=function, base=base, account_linked_to_identity=account_linked_to_identity)
|
||||
return
|
||||
_ensure_direct_assignment_shape(item)
|
||||
|
||||
|
||||
def _ensure_delegated_assignment(
|
||||
item: IdmOrganizationFunctionAssignment,
|
||||
*,
|
||||
function: OrganizationFunction,
|
||||
base: IdmOrganizationFunctionAssignment | None,
|
||||
) -> None:
|
||||
if base is None:
|
||||
raise _invalid("Delegated assignments require a source assignment.")
|
||||
if not function.delegable:
|
||||
raise _invalid("This organization function does not allow delegation.")
|
||||
if item.acting_for_account_id is not None:
|
||||
raise _invalid("Delegated assignments cannot set an acting-for account.")
|
||||
if item.identity_id == base.identity_id and (item.account_id or "") == (base.account_id or ""):
|
||||
raise _invalid("A delegated assignment must target another identity or account.")
|
||||
|
||||
|
||||
def _ensure_acting_for_assignment(
|
||||
item: IdmOrganizationFunctionAssignment,
|
||||
*,
|
||||
function: OrganizationFunction,
|
||||
base: IdmOrganizationFunctionAssignment | None,
|
||||
account_linked_to_identity: Callable[[str, str], bool],
|
||||
) -> None:
|
||||
if base is None:
|
||||
raise _invalid("Acting-for assignments require a source assignment.")
|
||||
if not function.act_in_place_allowed:
|
||||
raise _invalid("This organization function does not allow acting in place.")
|
||||
if item.acting_for_account_id is None:
|
||||
raise _invalid("Acting-for assignments require an acting-for account.")
|
||||
if base.account_id is not None and item.acting_for_account_id != base.account_id:
|
||||
raise _invalid("Acting-for account must match the source assignment account.")
|
||||
if base.account_id is None and not account_linked_to_identity(base.identity_id, item.acting_for_account_id):
|
||||
raise _invalid("Acting-for account must belong to the source assignment identity.")
|
||||
if item.account_id == item.acting_for_account_id:
|
||||
raise _invalid("The acting account and acting-for account must be different.")
|
||||
|
||||
|
||||
def _ensure_direct_assignment_shape(item: IdmOrganizationFunctionAssignment) -> None:
|
||||
if item.delegated_from_assignment_id is not None:
|
||||
raise _invalid("Only delegated or acting-for assignments can reference a source assignment.")
|
||||
if item.acting_for_account_id is not None:
|
||||
raise _invalid("Only acting-for assignments can set an acting-for account.")
|
||||
|
||||
|
||||
def _requires_assignment_change_request(session: Session, tenant_id: str) -> bool:
|
||||
|
||||
@@ -102,7 +102,7 @@ def _idm_directory(context: ModuleContext) -> object:
|
||||
manifest = ModuleManifest(
|
||||
id="idm",
|
||||
name="IDM",
|
||||
version="0.1.6",
|
||||
version="0.1.8",
|
||||
dependencies=("identity", "organizations"),
|
||||
optional_dependencies=("access", "audit"),
|
||||
required_capabilities=(CAPABILITY_AUTH_PRINCIPAL_RESOLVER, CAPABILITY_AUTH_PERMISSION_EVALUATOR),
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""IDM migration revisions."""
|
||||
@@ -0,0 +1,65 @@
|
||||
"""v0.1.7 idm baseline
|
||||
|
||||
Revision ID: 8f9a0b1c2d3e
|
||||
Revises: None
|
||||
Create Date: 2026-07-11 00:00:00.000000
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '8f9a0b1c2d3e'
|
||||
down_revision = None
|
||||
branch_labels = None
|
||||
depends_on = ('5c6d7e8f9a10', '6d7e8f9a0b1c')
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table('idm_tenant_settings',
|
||||
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('require_assignment_change_requests', sa.Boolean(), nullable=False),
|
||||
sa.Column('audit_detail_level', sa.String(length=20), nullable=False),
|
||||
sa.Column('change_retention_days', sa.Integer(), nullable=True),
|
||||
sa.Column('settings', sa.JSON(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.PrimaryKeyConstraint('tenant_id', name=op.f('pk_idm_tenant_settings'))
|
||||
)
|
||||
op.create_table('idm_organization_function_assignments',
|
||||
sa.Column('id', sa.String(length=36), nullable=False),
|
||||
sa.Column('tenant_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('identity_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('account_id', sa.String(length=36), nullable=True),
|
||||
sa.Column('function_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('organization_unit_id', sa.String(length=36), nullable=False),
|
||||
sa.Column('applies_to_subunits', sa.Boolean(), nullable=False),
|
||||
sa.Column('source', sa.String(length=50), nullable=False),
|
||||
sa.Column('delegated_from_assignment_id', sa.String(length=36), nullable=True),
|
||||
sa.Column('acting_for_account_id', sa.String(length=36), nullable=True),
|
||||
sa.Column('valid_from', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('valid_until', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=False),
|
||||
sa.Column('settings', sa.JSON(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.ForeignKeyConstraint(['delegated_from_assignment_id'], ['idm_organization_function_assignments.id'], name=op.f('fk_idm_organization_function_assignments_delegated_from_assignment_id_idm_organization_function_assignments'), ondelete='SET NULL'),
|
||||
sa.ForeignKeyConstraint(['function_id'], ['organizations_functions.id'], name=op.f('fk_idm_organization_function_assignments_function_id_organizations_functions'), ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['identity_id'], ['identity_identities.id'], name=op.f('fk_idm_organization_function_assignments_identity_id_identity_identities'), ondelete='CASCADE'),
|
||||
sa.ForeignKeyConstraint(['organization_unit_id'], ['organizations_units.id'], name=op.f('fk_idm_organization_function_assignments_organization_unit_id_organizations_units'), ondelete='CASCADE'),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_idm_organization_function_assignments')),
|
||||
sa.UniqueConstraint('tenant_id', 'identity_id', 'function_id', 'organization_unit_id', name='uq_idm_org_function_assignments_identity_scope')
|
||||
)
|
||||
op.create_index(op.f('ix_idm_organization_function_assignments_account_id'), 'idm_organization_function_assignments', ['account_id'], unique=False)
|
||||
op.create_index(op.f('ix_idm_organization_function_assignments_acting_for_account_id'), 'idm_organization_function_assignments', ['acting_for_account_id'], unique=False)
|
||||
op.create_index(op.f('ix_idm_organization_function_assignments_delegated_from_assignment_id'), 'idm_organization_function_assignments', ['delegated_from_assignment_id'], unique=False)
|
||||
op.create_index(op.f('ix_idm_organization_function_assignments_function_id'), 'idm_organization_function_assignments', ['function_id'], unique=False)
|
||||
op.create_index(op.f('ix_idm_organization_function_assignments_identity_id'), 'idm_organization_function_assignments', ['identity_id'], unique=False)
|
||||
op.create_index(op.f('ix_idm_organization_function_assignments_organization_unit_id'), 'idm_organization_function_assignments', ['organization_unit_id'], unique=False)
|
||||
op.create_index(op.f('ix_idm_organization_function_assignments_tenant_id'), 'idm_organization_function_assignments', ['tenant_id'], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table('idm_organization_function_assignments')
|
||||
op.drop_table('idm_tenant_settings')
|
||||
132
tests/test_assignment_workflow.py
Normal file
132
tests/test_assignment_workflow.py
Normal file
@@ -0,0 +1,132 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from types import SimpleNamespace
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from govoplan_idm.backend.api.v1.routes import _ensure_assignment_shape, _ensure_assignment_source_rules
|
||||
|
||||
|
||||
def assignment(**overrides: object) -> SimpleNamespace:
|
||||
values = {
|
||||
"id": "assignment-1",
|
||||
"identity_id": "identity-1",
|
||||
"account_id": "account-1",
|
||||
"function_id": "function-1",
|
||||
"source": "direct",
|
||||
"delegated_from_assignment_id": None,
|
||||
"acting_for_account_id": None,
|
||||
"valid_from": None,
|
||||
"valid_until": None,
|
||||
}
|
||||
values.update(overrides)
|
||||
return SimpleNamespace(**values)
|
||||
|
||||
|
||||
def function(**overrides: object) -> SimpleNamespace:
|
||||
values = {"delegable": True, "act_in_place_allowed": True}
|
||||
values.update(overrides)
|
||||
return SimpleNamespace(**values)
|
||||
|
||||
|
||||
class AssignmentWorkflowTests(unittest.TestCase):
|
||||
def assert_invalid(self, expected_detail: str, callback: object) -> None:
|
||||
with self.assertRaises(HTTPException) as captured:
|
||||
callback()
|
||||
self.assertEqual(captured.exception.detail, expected_detail)
|
||||
|
||||
def test_direct_assignment_accepts_plain_shape(self) -> None:
|
||||
item = assignment()
|
||||
|
||||
_ensure_assignment_shape(item) # type: ignore[arg-type]
|
||||
_ensure_assignment_source_rules( # type: ignore[arg-type]
|
||||
item,
|
||||
function=function(),
|
||||
base=None,
|
||||
account_linked_to_identity=lambda _identity_id, _account_id: False,
|
||||
)
|
||||
|
||||
def test_shape_rejects_backwards_validity_window(self) -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
item = assignment(valid_from=now, valid_until=now - timedelta(days=1))
|
||||
|
||||
self.assert_invalid("Valid until must be after valid from.", lambda: _ensure_assignment_shape(item)) # type: ignore[arg-type]
|
||||
|
||||
def test_shape_rejects_self_delegation(self) -> None:
|
||||
item = assignment(id="assignment-1", delegated_from_assignment_id="assignment-1")
|
||||
|
||||
self.assert_invalid("A function assignment cannot delegate from itself.", lambda: _ensure_assignment_shape(item)) # type: ignore[arg-type]
|
||||
|
||||
def test_delegated_assignment_accepts_different_target(self) -> None:
|
||||
base = assignment(id="source-1", identity_id="identity-1", account_id="account-1")
|
||||
item = assignment(
|
||||
id="assignment-2",
|
||||
identity_id="identity-2",
|
||||
account_id="account-2",
|
||||
source="delegated",
|
||||
delegated_from_assignment_id="source-1",
|
||||
)
|
||||
|
||||
_ensure_assignment_source_rules( # type: ignore[arg-type]
|
||||
item,
|
||||
function=function(delegable=True),
|
||||
base=base,
|
||||
account_linked_to_identity=lambda _identity_id, _account_id: False,
|
||||
)
|
||||
|
||||
def test_delegated_assignment_rejects_same_target(self) -> None:
|
||||
base = assignment(id="source-1", identity_id="identity-1", account_id="account-1")
|
||||
item = assignment(source="delegated", delegated_from_assignment_id="source-1")
|
||||
|
||||
self.assert_invalid(
|
||||
"A delegated assignment must target another identity or account.",
|
||||
lambda: _ensure_assignment_source_rules( # type: ignore[arg-type]
|
||||
item,
|
||||
function=function(delegable=True),
|
||||
base=base,
|
||||
account_linked_to_identity=lambda _identity_id, _account_id: False,
|
||||
),
|
||||
)
|
||||
|
||||
def test_acting_for_assignment_accepts_identity_linked_account(self) -> None:
|
||||
base = assignment(id="source-1", identity_id="identity-1", account_id=None)
|
||||
item = assignment(
|
||||
id="assignment-2",
|
||||
source="acting_for",
|
||||
delegated_from_assignment_id="source-1",
|
||||
account_id="acting-account",
|
||||
acting_for_account_id="represented-account",
|
||||
)
|
||||
|
||||
_ensure_assignment_source_rules( # type: ignore[arg-type]
|
||||
item,
|
||||
function=function(act_in_place_allowed=True),
|
||||
base=base,
|
||||
account_linked_to_identity=lambda identity_id, account_id: identity_id == "identity-1" and account_id == "represented-account",
|
||||
)
|
||||
|
||||
def test_acting_for_assignment_rejects_unlinked_represented_account(self) -> None:
|
||||
base = assignment(id="source-1", identity_id="identity-1", account_id=None)
|
||||
item = assignment(
|
||||
id="assignment-2",
|
||||
source="acting_for",
|
||||
delegated_from_assignment_id="source-1",
|
||||
account_id="acting-account",
|
||||
acting_for_account_id="represented-account",
|
||||
)
|
||||
|
||||
self.assert_invalid(
|
||||
"Acting-for account must belong to the source assignment identity.",
|
||||
lambda: _ensure_assignment_source_rules( # type: ignore[arg-type]
|
||||
item,
|
||||
function=function(act_in_place_allowed=True),
|
||||
base=base,
|
||||
account_linked_to_identity=lambda _identity_id, _account_id: False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@govoplan/idm-webui",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
@@ -14,7 +14,7 @@
|
||||
"./styles/idm.css": "./src/styles/idm.css"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.6",
|
||||
"@govoplan/core-webui": "^0.1.8",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": "^19.0.0",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react";
|
||||
import { Edit3, RefreshCw, Save, XCircle } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from "react";
|
||||
import { Edit3, Plus, RefreshCw } from "lucide-react";
|
||||
import {
|
||||
AdminIconButton,
|
||||
ApiError,
|
||||
Button,
|
||||
Card,
|
||||
DataGrid,
|
||||
Dialog,
|
||||
DismissibleAlert,
|
||||
FormField,
|
||||
LoadingFrame,
|
||||
@@ -219,6 +221,15 @@ function sourceLabel(value: string): string {
|
||||
return SOURCE_OPTIONS.find((item) => item.value === value)?.label ?? value;
|
||||
}
|
||||
|
||||
function idmInitialQuery(): { assignmentId: string; functionId: string } {
|
||||
if (typeof window === "undefined") return { assignmentId: "", functionId: "" };
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return {
|
||||
assignmentId: params.get("assignment_id") || "",
|
||||
functionId: params.get("function_id") || ""
|
||||
};
|
||||
}
|
||||
|
||||
export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
const [model, setModel] = useState<OrganizationModel>(EMPTY_MODEL);
|
||||
const [assignments, setAssignments] = useState<OrganizationFunctionAssignmentItem[]>([]);
|
||||
@@ -233,10 +244,14 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
const [assignmentDraft, setAssignmentDraft] = useState<AssignmentDraft>(() => emptyAssignmentDraft());
|
||||
const [settingsDraft, setSettingsDraft] = useState<SettingsDraft>(() => settingsDraftFrom(null));
|
||||
const [editingAssignmentId, setEditingAssignmentId] = useState<string | null>(null);
|
||||
const [assignmentEditorOpen, setAssignmentEditorOpen] = useState(false);
|
||||
const [assignmentChangeRequestId, setAssignmentChangeRequestId] = useState("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
const initialQuery = useMemo(() => idmInitialQuery(), []);
|
||||
const appliedInitialQueryRef = useRef(false);
|
||||
|
||||
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");
|
||||
@@ -286,11 +301,8 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
if (assignmentDraft.acting_for_account_id) values.add(assignmentDraft.acting_for_account_id);
|
||||
return Array.from(values).sort((left, right) => left.localeCompare(right));
|
||||
}, [actingForOptionById, actingForOptions, assignmentDraft.acting_for_account_id, identityOptionById, sourceAssignment]);
|
||||
const initialFunctionFilter = useMemo(() => {
|
||||
if (typeof window === "undefined") return "";
|
||||
return new URLSearchParams(window.location.search).get("function_id") || "";
|
||||
}, []);
|
||||
const hasDirtyAssignmentDraft = isAssignmentDirty(assignmentDraft);
|
||||
const initialFunctionFilter = initialQuery.functionId;
|
||||
const hasDirtyAssignmentDraft = assignmentEditorOpen && isAssignmentDirty(assignmentDraft);
|
||||
const hasDirtySettingsDraft = canReadSettings && isSettingsDirty(settingsDraft, idmSettings);
|
||||
const hasDirtyDraft = hasDirtyAssignmentDraft || hasDirtySettingsDraft;
|
||||
|
||||
@@ -307,8 +319,14 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
setAssignments(nextAssignments.assignments);
|
||||
setIdmSettings(nextSettings);
|
||||
if (nextSettings) setSettingsDraft(settingsDraftFrom(nextSettings));
|
||||
if (initialFunctionFilter && nextModel.functions.some((item) => item.id === initialFunctionFilter)) {
|
||||
setAssignmentDraft((draft) => draft.function_id ? draft : { ...draft, function_id: initialFunctionFilter });
|
||||
if (!appliedInitialQueryRef.current && initialQuery.assignmentId) {
|
||||
const initialAssignment = nextAssignments.assignments.find((item) => item.id === initialQuery.assignmentId);
|
||||
if (initialAssignment) {
|
||||
appliedInitialQueryRef.current = true;
|
||||
setEditingAssignmentId(initialAssignment.id);
|
||||
setAssignmentDraft(assignmentDraftFrom(initialAssignment));
|
||||
setAssignmentEditorOpen(true);
|
||||
}
|
||||
}
|
||||
if (canSearchIdentities) {
|
||||
try {
|
||||
@@ -326,7 +344,7 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canReadSettings, canSearchIdentities, initialFunctionFilter, settings]);
|
||||
}, [canReadSettings, canSearchIdentities, initialFunctionFilter, initialQuery.assignmentId, settings]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadData();
|
||||
@@ -393,6 +411,8 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
const discardAssignmentDraft = useCallback(() => {
|
||||
setAssignmentDraft(emptyAssignmentDraft());
|
||||
setEditingAssignmentId(null);
|
||||
setAssignmentEditorOpen(false);
|
||||
setAssignmentChangeRequestId("");
|
||||
}, []);
|
||||
|
||||
const discardDrafts = useCallback(() => {
|
||||
@@ -427,14 +447,15 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
setError("i18n:govoplan-idm.function_is_required.5cce5b41");
|
||||
return false;
|
||||
}
|
||||
const payload = assignmentPayload(assignmentDraft);
|
||||
const requestId = textOrNull(assignmentChangeRequestId);
|
||||
const payload = requestId ? { ...assignmentPayload(assignmentDraft), change_request_id: requestId } : assignmentPayload(assignmentDraft);
|
||||
const ok = await runAction(
|
||||
() => editingAssignmentId ? patchOrganizationFunctionAssignment(settings, editingAssignmentId, payload) : createOrganizationFunctionAssignment(settings, payload),
|
||||
editingAssignmentId ? "i18n:govoplan-idm.assignment_updated.fbbf9bd6" : "i18n:govoplan-idm.assignment_added.91f1ee42"
|
||||
);
|
||||
if (ok) discardAssignmentDraft();
|
||||
return ok;
|
||||
}, [assignmentDraft, canManage, discardAssignmentDraft, editingAssignmentId, runAction, settings]);
|
||||
}, [assignmentChangeRequestId, assignmentDraft, canManage, discardAssignmentDraft, editingAssignmentId, runAction, settings]);
|
||||
|
||||
const saveDrafts = useCallback(async (): Promise<boolean> => {
|
||||
if (hasDirtyAssignmentDraft && !(await submitAssignment())) return false;
|
||||
@@ -450,15 +471,20 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
message: "i18n:govoplan-core.this_page_has_unsaved_changes_save_them_before_l.419a9d8b"
|
||||
});
|
||||
|
||||
const openCreateAssignment = useCallback(() => {
|
||||
setEditingAssignmentId(null);
|
||||
setAssignmentDraft({ ...emptyAssignmentDraft(), function_id: initialFunctionFilter && functionById.has(initialFunctionFilter) ? initialFunctionFilter : "" });
|
||||
setAssignmentChangeRequestId("");
|
||||
setAssignmentEditorOpen(true);
|
||||
}, [functionById, initialFunctionFilter]);
|
||||
|
||||
const editAssignment = useCallback((item: OrganizationFunctionAssignmentItem) => {
|
||||
setEditingAssignmentId(item.id);
|
||||
setAssignmentDraft(assignmentDraftFrom(item));
|
||||
setAssignmentChangeRequestId("");
|
||||
setAssignmentEditorOpen(true);
|
||||
}, []);
|
||||
|
||||
const toggleAssignment = useCallback((item: OrganizationFunctionAssignmentItem) => {
|
||||
void runAction(() => patchOrganizationFunctionAssignment(settings, item.id, { is_active: !item.is_active }));
|
||||
}, [runAction, settings]);
|
||||
|
||||
function onIdentityChange(identityId: string) {
|
||||
const identity = identityOptionById.get(identityId);
|
||||
setAssignmentDraft({
|
||||
@@ -534,16 +560,11 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
{
|
||||
id: "actions",
|
||||
header: "",
|
||||
width: 220,
|
||||
width: 88,
|
||||
sticky: "end",
|
||||
render: (row) => (
|
||||
<div className="idm-row-actions">
|
||||
<Button type="button" variant="ghost" disabled={!canManage || busy} onClick={() => editAssignment(row)} title="i18n:govoplan-idm.edit.a5a0f3cc">
|
||||
<Edit3 size={16} aria-hidden="true" /> i18n:govoplan-idm.edit.a5a0f3cc
|
||||
</Button>
|
||||
<Button type="button" variant={row.is_active ? "secondary" : "primary"} disabled={!canManage || busy} onClick={() => toggleAssignment(row)}>
|
||||
{row.is_active ? "i18n:govoplan-idm.deactivate.585777c8" : "i18n:govoplan-idm.reactivate.e4871a43"}
|
||||
</Button>
|
||||
<AdminIconButton label="i18n:govoplan-idm.edit.a5a0f3cc" icon={<Edit3 size={16} aria-hidden="true" />} disabled={!canManage || busy} onClick={() => editAssignment(row)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -560,16 +581,6 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
<Button type="button" onClick={() => void loadData()} disabled={loading || busy} title="i18n:govoplan-idm.reload.870ca3ec">
|
||||
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-idm.reload.870ca3ec
|
||||
</Button>
|
||||
{hasDirtyDraft && (
|
||||
<>
|
||||
<Button type="button" onClick={() => void saveDrafts()} disabled={busy} title="i18n:govoplan-idm.save_drafts.32a0d60a">
|
||||
<Save size={16} aria-hidden="true" /> i18n:govoplan-idm.save_drafts.32a0d60a
|
||||
</Button>
|
||||
<Button type="button" variant="ghost" onClick={discardDrafts} disabled={busy} title="i18n:govoplan-idm.discard_drafts.c0a86816">
|
||||
<XCircle size={16} aria-hidden="true" /> i18n:govoplan-idm.discard_drafts.c0a86816
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -578,98 +589,9 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
{!canManage && <DismissibleAlert tone="warning" dismissible={false}>i18n:govoplan-idm.write_permission_required.c7dde7c6</DismissibleAlert>}
|
||||
|
||||
<LoadingFrame loading={loading || busy} label="i18n:govoplan-idm.loading_idm_assignments.0b1501bd">
|
||||
<div className="idm-layout">
|
||||
<div className="idm-editor-stack">
|
||||
<Card title="i18n:govoplan-idm.assignment_editor.e20598e7">
|
||||
<form className="idm-form-grid" onSubmit={(event) => void submitAssignment(event)}>
|
||||
<FormField label="i18n:govoplan-idm.identity_search.d3460fcf">
|
||||
<input
|
||||
value={identitySearch}
|
||||
placeholder="i18n:govoplan-idm.search_identities.88a9ef15"
|
||||
disabled={!canSearchIdentities || busy}
|
||||
onChange={(event) => setIdentitySearch(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.select_identity.91d31615">
|
||||
<select value={assignmentDraft.identity_id} disabled={!canManage || busy} onChange={(event) => onIdentityChange(event.target.value)}>
|
||||
<option value="">i18n:govoplan-idm.select_identity.91d31615</option>
|
||||
{identitySelectOptions.map((item) => (
|
||||
<option key={item.id} value={item.id}>{item.display_name || item.external_subject || item.id}</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.account.2b2936f8">
|
||||
<select value={assignmentDraft.account_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, account_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-idm.none.2baf5c66</option>
|
||||
{accountIds.map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.select_function.2bec86e0">
|
||||
<select value={assignmentDraft.function_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, function_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-idm.select_function.2bec86e0</option>
|
||||
{model.functions.map((item) => <option key={item.id} value={item.id}>{functionLabel(item, unitById)}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.source.d15b50c9">
|
||||
<select value={assignmentDraft.source} disabled={!canManage || busy} onChange={(event) => onSourceChange(event.target.value)}>
|
||||
{SOURCE_OPTIONS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
{(assignmentDraft.source === "delegated" || assignmentDraft.source === "acting_for") && (
|
||||
<FormField label="i18n:govoplan-idm.delegated_from_assignment_id.20d4a548">
|
||||
<select value={assignmentDraft.delegated_from_assignment_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, delegated_from_assignment_id: event.target.value, acting_for_account_id: "" })}>
|
||||
<option value="">i18n:govoplan-idm.none.2baf5c66</option>
|
||||
{assignmentOptions.map((item) => (
|
||||
<option key={item.id} value={item.id}>{assignmentOptionLabel(item, identityOptionById, functionById, unitById)}</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
)}
|
||||
{assignmentDraft.source === "acting_for" && (
|
||||
<>
|
||||
<FormField label="i18n:govoplan-idm.acting_for_search.b7c526c7">
|
||||
<input
|
||||
value={actingForSearch}
|
||||
placeholder="i18n:govoplan-idm.search_identities.88a9ef15"
|
||||
disabled={!canSearchIdentities || busy}
|
||||
onChange={(event) => setActingForSearch(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.acting_for_account_id.5d7ade5b">
|
||||
<select value={assignmentDraft.acting_for_account_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, acting_for_account_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-idm.select_account.982ee1ad</option>
|
||||
{actingForAccountIds.map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
<div className="idm-check-list">
|
||||
<label>
|
||||
<input type="checkbox" checked={assignmentDraft.applies_to_subunits} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, applies_to_subunits: event.target.checked })} />
|
||||
<span>i18n:govoplan-idm.applies_to_subunits.2e31b50b</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" checked={assignmentDraft.is_active} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, is_active: event.target.checked })} />
|
||||
<span>i18n:govoplan-idm.active.7bd0e9f8</span>
|
||||
</label>
|
||||
</div>
|
||||
{!identityLookupAvailable && <p className="idm-muted wide">i18n:govoplan-idm.identity_lookup_unavailable.b76f7714</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>}
|
||||
{!model.functions.length && <p className="idm-muted wide">i18n:govoplan-idm.no_functions_available.51ba08eb</p>}
|
||||
<div className="idm-form-actions wide">
|
||||
<Button type="submit" variant="primary" disabled={!canManage || busy}>
|
||||
{editingAssignmentId ? "i18n:govoplan-idm.update_assignment.e20f52aa" : "i18n:govoplan-idm.add_assignment.08f2a0d5"}
|
||||
</Button>
|
||||
{editingAssignmentId && (
|
||||
<Button type="button" variant="ghost" onClick={discardAssignmentDraft} disabled={busy}>i18n:govoplan-idm.cancel_edit.ea4781e0</Button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
{canReadSettings && (
|
||||
<Card title="i18n:govoplan-idm.idm_governance.6e4f3251">
|
||||
<div className="idm-table-stack">
|
||||
{canReadSettings && (
|
||||
<Card title="i18n:govoplan-idm.idm_governance.6e4f3251" collapsible collapseKey="idm.governance">
|
||||
<form className="idm-form-grid" onSubmit={(event) => { event.preventDefault(); void submitSettings(); }}>
|
||||
<div className="idm-check-list wide">
|
||||
<label>
|
||||
@@ -710,9 +632,8 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
</form>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Card title="i18n:govoplan-idm.assignments.a0d19ec5">
|
||||
<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} />}>
|
||||
<DataGrid
|
||||
id="idm-organization-function-assignments"
|
||||
rows={assignments}
|
||||
@@ -725,6 +646,112 @@ export default function IdmPage({ settings, auth }: IdmPageProps) {
|
||||
</Card>
|
||||
</div>
|
||||
</LoadingFrame>
|
||||
{renderAssignmentDialog()}
|
||||
</div>
|
||||
);
|
||||
|
||||
function renderAssignmentDialog() {
|
||||
const formId = "idm-assignment-editor";
|
||||
return (
|
||||
<Dialog
|
||||
open={assignmentEditorOpen}
|
||||
title={editingAssignmentId ? "i18n:govoplan-idm.update_assignment.e20f52aa" : "i18n:govoplan-idm.add_assignment.08f2a0d5"}
|
||||
onClose={() => !busy && discardAssignmentDraft()}
|
||||
closeDisabled={busy}
|
||||
className="admin-dialog admin-dialog-wide idm-editor-dialog"
|
||||
footer={(
|
||||
<>
|
||||
<Button type="button" onClick={discardAssignmentDraft} disabled={busy}>i18n:govoplan-idm.cancel_edit.ea4781e0</Button>
|
||||
<Button type="submit" form={formId} variant="primary" disabled={!canManage || busy || !assignmentDraft.identity_id || !assignmentDraft.function_id}>
|
||||
{editingAssignmentId ? "i18n:govoplan-idm.update_assignment.e20f52aa" : "i18n:govoplan-idm.add_assignment.08f2a0d5"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<form id={formId} className="idm-form-grid" onSubmit={(event) => void submitAssignment(event)}>
|
||||
<FormField label="i18n:govoplan-idm.identity_search.d3460fcf">
|
||||
<input
|
||||
value={identitySearch}
|
||||
placeholder="i18n:govoplan-idm.search_identities.88a9ef15"
|
||||
disabled={!canSearchIdentities || busy}
|
||||
onChange={(event) => setIdentitySearch(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.select_identity.91d31615">
|
||||
<select value={assignmentDraft.identity_id} disabled={!canManage || busy} onChange={(event) => onIdentityChange(event.target.value)}>
|
||||
<option value="">i18n:govoplan-idm.select_identity.91d31615</option>
|
||||
{identitySelectOptions.map((item) => (
|
||||
<option key={item.id} value={item.id}>{item.display_name || item.external_subject || item.id}</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.account.2b2936f8">
|
||||
<select value={assignmentDraft.account_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, account_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-idm.none.2baf5c66</option>
|
||||
{accountIds.map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.select_function.2bec86e0">
|
||||
<select value={assignmentDraft.function_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, function_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-idm.select_function.2bec86e0</option>
|
||||
{model.functions.map((item) => <option key={item.id} value={item.id}>{functionLabel(item, unitById)}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.source.d15b50c9">
|
||||
<select value={assignmentDraft.source} disabled={!canManage || busy} onChange={(event) => onSourceChange(event.target.value)}>
|
||||
{SOURCE_OPTIONS.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
{(assignmentDraft.source === "delegated" || assignmentDraft.source === "acting_for") && (
|
||||
<FormField label="i18n:govoplan-idm.delegated_from_assignment_id.20d4a548">
|
||||
<select value={assignmentDraft.delegated_from_assignment_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, delegated_from_assignment_id: event.target.value, acting_for_account_id: "" })}>
|
||||
<option value="">i18n:govoplan-idm.none.2baf5c66</option>
|
||||
{assignmentOptions.map((item) => (
|
||||
<option key={item.id} value={item.id}>{assignmentOptionLabel(item, identityOptionById, functionById, unitById)}</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
)}
|
||||
{assignmentDraft.source === "acting_for" && (
|
||||
<>
|
||||
<FormField label="i18n:govoplan-idm.acting_for_search.b7c526c7">
|
||||
<input
|
||||
value={actingForSearch}
|
||||
placeholder="i18n:govoplan-idm.search_identities.88a9ef15"
|
||||
disabled={!canSearchIdentities || busy}
|
||||
onChange={(event) => setActingForSearch(event.target.value)}
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="i18n:govoplan-idm.acting_for_account_id.5d7ade5b">
|
||||
<select value={assignmentDraft.acting_for_account_id} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, acting_for_account_id: event.target.value })}>
|
||||
<option value="">i18n:govoplan-idm.select_account.982ee1ad</option>
|
||||
{actingForAccountIds.map((accountId) => <option key={accountId} value={accountId}>{accountId}</option>)}
|
||||
</select>
|
||||
</FormField>
|
||||
</>
|
||||
)}
|
||||
<div className="idm-check-list">
|
||||
<label>
|
||||
<input type="checkbox" checked={assignmentDraft.applies_to_subunits} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, applies_to_subunits: event.target.checked })} />
|
||||
<span>i18n:govoplan-idm.applies_to_subunits.2e31b50b</span>
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" checked={assignmentDraft.is_active} disabled={!canManage || busy} onChange={(event) => setAssignmentDraft({ ...assignmentDraft, is_active: event.target.checked })} />
|
||||
<span>i18n:govoplan-idm.active.7bd0e9f8</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="wide idm-dialog-change-request">
|
||||
<FormField label="i18n:govoplan-idm.change_request_id.b7d816db">
|
||||
<input value={assignmentChangeRequestId} onChange={(event) => setAssignmentChangeRequestId(event.target.value)} placeholder="cfgreq-..." disabled={busy} />
|
||||
</FormField>
|
||||
<p className="idm-muted">i18n:govoplan-idm.change_request_id_help.cc7de508</p>
|
||||
</div>
|
||||
{!identityLookupAvailable && <p className="idm-muted wide">i18n:govoplan-idm.identity_lookup_unavailable.b76f7714</p>}
|
||||
{identityLoading && <p className="idm-muted wide">i18n:govoplan-idm.loading_identities.f3b84693</p>}
|
||||
{actingForLoading && <p className="idm-muted wide">i18n:govoplan-idm.loading_acting_for_accounts.c9894b1e</p>}
|
||||
{!model.functions.length && <p className="idm-muted wide">i18n:govoplan-idm.no_functions_available.51ba08eb</p>}
|
||||
</form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-idm.assignments.a0d19ec5": "Assignments",
|
||||
"i18n:govoplan-idm.cancel_edit.ea4781e0": "Cancel edit",
|
||||
"i18n:govoplan-idm.audit_detail_level.eb2e6fd2": "Audit detail level",
|
||||
"i18n:govoplan-idm.change_request_id.b7d816db": "Change request ID",
|
||||
"i18n:govoplan-idm.change_request_id_help.cc7de508": "Only required when IDM assignment governance requires an approved recorded change request.",
|
||||
"i18n:govoplan-idm.change_retention_days.4a91f7d3": "Change retention days",
|
||||
"i18n:govoplan-idm.deactivate.585777c8": "Deactivate",
|
||||
"i18n:govoplan-idm.delegated.b189f4b6": "delegated",
|
||||
@@ -76,6 +78,8 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-idm.assignments.a0d19ec5": "Zuordnungen",
|
||||
"i18n:govoplan-idm.cancel_edit.ea4781e0": "Bearbeitung abbrechen",
|
||||
"i18n:govoplan-idm.audit_detail_level.eb2e6fd2": "Audit-Detailgrad",
|
||||
"i18n:govoplan-idm.change_request_id.b7d816db": "Änderungsantrags-ID",
|
||||
"i18n:govoplan-idm.change_request_id_help.cc7de508": "Nur erforderlich, wenn die IDM-Zuordnungs-Governance einen genehmigten erfassten Änderungsantrag verlangt.",
|
||||
"i18n:govoplan-idm.change_retention_days.4a91f7d3": "Änderungsaufbewahrung in Tagen",
|
||||
"i18n:govoplan-idm.deactivate.585777c8": "Deaktivieren",
|
||||
"i18n:govoplan-idm.delegated.b189f4b6": "delegiert",
|
||||
|
||||
@@ -17,16 +17,14 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.idm-layout {
|
||||
.idm-table-stack {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(300px, 420px) minmax(0, 1fr);
|
||||
gap: 18px;
|
||||
align-items: start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.idm-editor-stack {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
.idm-table-stack > .card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.idm-form-grid {
|
||||
@@ -57,8 +55,8 @@
|
||||
.idm-row-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.idm-row-actions {
|
||||
@@ -87,10 +85,8 @@
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.idm-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.idm-dialog-change-request {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
|
||||
Reference in New Issue
Block a user