From 1409dbf94dbcec52a2aa73120ba3368dd882786e Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Mon, 3 Aug 2026 11:01:22 +0200 Subject: [PATCH] Migrate Access administration patterns --- README.md | 3 + docs/INTERFACE_PATTERN_MIGRATION.md | 53 +++++++++++++ src/govoplan_access/backend/manifest.py | 19 +++++ .../test_interface_documentation_contract.py | 65 +++++++++++++++ webui/package.json | 3 + .../test-interface-pattern-language.mjs | 50 ++++++++++++ webui/src/features/admin/AdminPage.tsx | 59 ++++++++++---- webui/src/features/admin/ApiKeysPanel.tsx | 9 ++- .../admin/CredentialEnvelopesPanel.tsx | 26 +++--- .../ExternalFunctionRoleMappingsPanel.tsx | 16 ++-- .../features/admin/FileConnectorsPanel.tsx | 24 +++++- webui/src/features/admin/GroupsPanel.tsx | 15 ++-- .../src/features/admin/MailProfilesPanel.tsx | 25 ++++-- webui/src/features/admin/RolesPanel.tsx | 11 +-- webui/src/features/admin/SystemRolesPanel.tsx | 11 +-- webui/src/features/admin/SystemUsersPanel.tsx | 11 +-- webui/src/features/admin/UsersPanel.tsx | 17 ++-- webui/src/features/admin/interfacePatterns.ts | 79 +++++++++++++++++++ webui/src/i18n/generatedTranslations.ts | 60 ++++++++++++++ webui/src/module.ts | 26 +++--- 20 files changed, 498 insertions(+), 84 deletions(-) create mode 100644 docs/INTERFACE_PATTERN_MIGRATION.md create mode 100644 tests/test_interface_documentation_contract.py create mode 100644 webui/scripts/test-interface-pattern-language.mjs create mode 100644 webui/src/features/admin/interfacePatterns.ts diff --git a/README.md b/README.md index 55d8a42..83216a6 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,9 @@ instead of importing access ORM models or backend dependency internals. The detailed module boundary and serialization fields are documented in [docs/ACCESS_MODULE_BOUNDARY.md](docs/ACCESS_MODULE_BOUNDARY.md). +The Access-owned administration surfaces, consequence classes, shared control +contract, contextual-help references, and verification evidence are recorded +in [docs/INTERFACE_PATTERN_MIGRATION.md](docs/INTERFACE_PATTERN_MIGRATION.md). For scheduled and event-driven work, Access provides `auth.automationPrincipalProvider`. Automation records store only an owner diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md new file mode 100644 index 0000000..a6dc725 --- /dev/null +++ b/docs/INTERFACE_PATTERN_MIGRATION.md @@ -0,0 +1,53 @@ +# Access interface pattern migration + +This document records the Access-owned surfaces covered by the platform +interface pattern language. Shared primitives remain owned by Core and +optional Mail, Files, Organizations, IDM, and Docs behavior is consumed only +through declared capabilities or metadata. + +## Surface inventory + +| Surface | Archetype | Authority and state model | +| --- | --- | --- | +| `/admin` tree and unavailable state | Tree-navigated administration workspace | The effective principal and active View determine which branches exist. A missing administration grant is an explained blocker, not an empty route. | +| System and tenant users | Server-authoritative directory plus list/detail editor | Delta reads refresh accounts and memberships. Create, update, assignment, suspension, and final-owner safeguards remain independent permissions. | +| System and tenant roles | Governed definition directory | Built-in and system-managed definitions remain visible but immutable. Assigned roles cannot be deleted. | +| Tenant groups | Governed definition and membership editor | Definition, membership, and role-assignment rights remain independent. Required system groups cannot be deactivated. | +| Tenant API keys | Immutable-secret lifecycle directory | A key is created once, its secret is shown once, and revocation is consequential and confirmed. | +| Function mappings | Governed cross-module mapping editor | Organizations supplies function choices, IDM supplies accepted facts, and Access maps facts to assignable roles. | +| Credential scopes | Adaptive configuration panel | Core owns the reusable credential manager. Access supplies system, tenant, group, and user ownership choices. | +| Mail and Files scope panels | Optional capability host | Access supplies owner selection; the owning module supplies configuration UI. A missing capability names the required module, actor, and destination. | + +## Consequence classes + +- Reload, inspect, filter, select, and open-help actions are reversible. +- User, group, role, mapping, and credential edits are governed mutations and + expose permission or validation blockers before submission. +- Account or membership deactivation, group deactivation, role deletion, + mapping deletion, API-key revocation, and credential deletion are + consequential actions and use the shared confirmation contract. +- Secret values and temporary passwords are never placed in list rows or + persistent notices. One-time values remain inside dedicated dialogs. + +## Interaction evidence + +- `AdminPageLayout`, `TreeSubnav`, `DataGrid`, `Dialog`, `ConfirmDialog`, + `TableActionGroup`, `PasswordField`, `ActionBlockerHint`, and + `DocumentationHelpLink` come from Core. +- Dialog focus trapping and restoration, disabled-action tooltips, keyboard + ordering, responsive grid overflow, and alert semantics therefore inherit + the tested Core behavior. +- All Access-owned labels added by this migration are present in the English + and German module catalogs. +- The WebUI structural test rejects browser-native confirmation calls, private + sibling imports, missing contextual-help references, and unexplained + optional-module blockers. + +## Documentation contexts + +- `access.workflow.grant-user-access` covers the user, group, and role path. +- `access.reference.admin-access-fields` covers accounts, roles, API keys, and + reusable credentials. +- `access.reference.external-function-role-mappings` explains the + Organizations, IDM, and Access responsibility split. +- Files and Mail blockers link to documentation supplied by the owning module. diff --git a/src/govoplan_access/backend/manifest.py b/src/govoplan_access/backend/manifest.py index 8716037..0f73118 100644 --- a/src/govoplan_access/backend/manifest.py +++ b/src/govoplan_access/backend/manifest.py @@ -300,6 +300,12 @@ ACCESS_DOCUMENTATION: tuple[DocumentationTopic, ...] = ( configuration_keys=("access_governance",), metadata={ "kind": "workflow", + "help_contexts": [ + "access.admin.users", + "access.admin.groups", + "access.admin.roles", + "access.admin.blocked", + ], "outcome": "A person can sign in to the tenant and receives the intended access through groups and roles.", "prerequisites": [ "You can open Admin.", @@ -362,6 +368,15 @@ ACCESS_DOCUMENTATION: tuple[DocumentationTopic, ...] = ( configuration_keys=("access_governance",), metadata={ "kind": "reference", + "help_contexts": [ + "access.admin.system-users", + "access.admin.system-roles", + "access.admin.tenant-users", + "access.admin.tenant-groups", + "access.admin.tenant-roles", + "access.admin.api-keys", + "access.credentials", + ], "route": "/admin", "screen": "Admin", "section": "Users, groups, roles, and API keys", @@ -458,6 +473,10 @@ ACCESS_DOCUMENTATION: tuple[DocumentationTopic, ...] = ( ), metadata={ "kind": "reference", + "help_contexts": [ + "access.admin.function-mappings", + "access.explanation", + ], "route": "/admin", "api_path": "/api/v1/admin/external-function-role-mappings", "explanation_api_path": "/api/v1/admin/users/{user_id}/access-explanation", diff --git a/tests/test_interface_documentation_contract.py b/tests/test_interface_documentation_contract.py new file mode 100644 index 0000000..c23228f --- /dev/null +++ b/tests/test_interface_documentation_contract.py @@ -0,0 +1,65 @@ +from __future__ import annotations + +import unittest + +from govoplan_access.backend.manifest import manifest + + +class InterfaceDocumentationContractTests(unittest.TestCase): + def test_access_admin_topics_publish_stable_help_contexts(self) -> None: + topics = {topic.id: topic for topic in manifest.documentation} + + expected_contexts = { + "access.workflow.grant-user-access": { + "access.admin.users", + "access.admin.groups", + "access.admin.roles", + "access.admin.blocked", + }, + "access.reference.admin-access-fields": { + "access.admin.system-users", + "access.admin.system-roles", + "access.admin.tenant-users", + "access.admin.tenant-groups", + "access.admin.tenant-roles", + "access.admin.api-keys", + "access.credentials", + }, + "access.reference.external-function-role-mappings": { + "access.admin.function-mappings", + "access.explanation", + }, + } + + for topic_id, expected in expected_contexts.items(): + self.assertIn(topic_id, topics) + metadata = topics[topic_id].metadata or {} + self.assertTrue( + expected.issubset(set(metadata.get("help_contexts", ()))), + topic_id, + ) + + def test_access_admin_surfaces_remain_declared(self) -> None: + surface_ids = { + surface.id for surface in manifest.frontend.view_surfaces + } + self.assertTrue( + { + "access.admin.system-roles", + "access.admin.system-users", + "access.admin.system-credentials", + "access.admin.tenant-roles", + "access.admin.tenant-function-mappings", + "access.admin.tenant-groups", + "access.admin.tenant-users", + "access.admin.tenant-credentials", + "access.admin.tenant-api-keys", + "access.admin.group-credentials", + "access.admin.user-credentials", + "access.settings.credentials", + }.issubset(surface_ids) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/webui/package.json b/webui/package.json index 215e265..d2e205e 100644 --- a/webui/package.json +++ b/webui/package.json @@ -3,6 +3,9 @@ "version": "0.1.11", "private": true, "type": "module", + "scripts": { + "test:interface-patterns": "node scripts/test-interface-pattern-language.mjs" + }, "main": "src/index.ts", "module": "src/index.ts", "types": "src/index.ts", diff --git a/webui/scripts/test-interface-pattern-language.mjs b/webui/scripts/test-interface-pattern-language.mjs new file mode 100644 index 0000000..5b60b6d --- /dev/null +++ b/webui/scripts/test-interface-pattern-language.mjs @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const root = resolve(import.meta.dirname, ".."); +const read = (path) => readFileSync(resolve(root, path), "utf8"); + +const adminPage = read("src/features/admin/AdminPage.tsx"); +const users = read("src/features/admin/UsersPanel.tsx"); +const groups = read("src/features/admin/GroupsPanel.tsx"); +const roles = read("src/features/admin/RolesPanel.tsx"); +const systemUsers = read("src/features/admin/SystemUsersPanel.tsx"); +const systemRoles = read("src/features/admin/SystemRolesPanel.tsx"); +const apiKeys = read("src/features/admin/ApiKeysPanel.tsx"); +const mappings = read("src/features/admin/ExternalFunctionRoleMappingsPanel.tsx"); +const credentials = read("src/features/admin/CredentialEnvelopesPanel.tsx"); +const files = read("src/features/admin/FileConnectorsPanel.tsx"); +const mail = read("src/features/admin/MailProfilesPanel.tsx"); +const moduleSource = read("src/module.ts"); +const surfaces = [users, groups, roles, systemUsers, systemRoles, apiKeys, mappings]; +const allAdminSource = [adminPage, credentials, files, mail, ...surfaces].join("\n"); + +assert.match(adminPage, /TreeSubnav/); +assert.match(adminPage, /ActionBlockerHint/); +assert.match(adminPage, /ACCESS_WORKFLOW_DOCUMENTATION/); + +for (const source of surfaces) { + assert.match(source, /AdminPageLayout/); + assert.match(source, /DataGrid/); + assert.match(source, /DocumentationHelpLink/); + assert.match(source, /disabledReason/); +} + +for (const source of [users, groups, roles, systemUsers, systemRoles, apiKeys, mappings]) { + assert.match(source, /ConfirmDialog/); +} + +assert.match(credentials, /CredentialEnvelopeManager/); +assert.match(credentials, /DocumentationHelpLink/); +assert.match(files, /usePlatformUiCapability/); +assert.match(files, /ActionBlockerHint/); +assert.match(mail, /usePlatformUiCapability/); +assert.match(mail, /ActionBlockerHint/); +assert.match(moduleSource, /translations,/); +assert.match(moduleSource, /version: "0\.1\.11"/); + +assert.doesNotMatch(allAdminSource, /window\.(alert|confirm|prompt)\s*\(/); +assert.doesNotMatch(allAdminSource, /@govoplan\/(files|mail|organizations|idm)-webui\//); + +console.log("Access interface pattern-language checks passed."); diff --git a/webui/src/features/admin/AdminPage.tsx b/webui/src/features/admin/AdminPage.tsx index 7f7f1eb..0623099 100644 --- a/webui/src/features/admin/AdminPage.tsx +++ b/webui/src/features/admin/AdminPage.tsx @@ -11,7 +11,7 @@ import type { OrganizationFunctionPickerUiCapability } from "@govoplan/core-webui"; import { fetchShellAuth } from "@govoplan/core-webui"; -import { Card } from "@govoplan/core-webui"; +import { ActionBlockerHint } from "@govoplan/core-webui"; import { PageScrollViewport } from "@govoplan/core-webui"; import { TreeSubnav, @@ -28,6 +28,10 @@ import ApiKeysPanel from "./ApiKeysPanel"; import FileConnectorsPanel from "./FileConnectorsPanel"; import MailProfilesPanel from "./MailProfilesPanel"; import CredentialEnvelopesPanel from "./CredentialEnvelopesPanel"; +import { + ACCESS_INTERFACE_I18N, + ACCESS_WORKFLOW_DOCUMENTATION +} from "./interfacePatterns"; import { isViewSurfaceVisible, useEffectiveView, @@ -45,6 +49,7 @@ type OrderedAdminNavItem = { kind?: "management" | "settings"; }; type AdminNavGroup = { + id: string; title: string; items: OrderedAdminNavItem[]; }; @@ -221,18 +226,32 @@ export default function AdminPage({ if (!hasAnyScope(auth, adminReadScopes)) { return ( -
- -

i18n:govoplan-access.your_current_roles_do_not_grant_administrative_a.6eafee69

-
-
+
+ +
); } const adminNavGroups: AdminNavGroup[] = [ { - title: "ADMINISTRATION", + id: "administration", + title: "i18n:govoplan-access.admin.4e7afebc", items: sortNavItems([ ...contributedNavItems(contributedSections, available, "ROOT"), visibleNavItem(available, "system-modules", "i18n:govoplan-access.modules.04e9462c", 10), @@ -243,7 +262,8 @@ export default function AdminPage({ ]) }, { - title: "GLOBAL", + id: "global", + title: "i18n:govoplan-access.global", items: sortNavItems([ visibleNavItem(available, "system-roles", "i18n:govoplan-access.system_roles.a9461aa6", 20), visibleNavItem(available, "system-role-templates", "i18n:govoplan-access.tenant_role_templates", 30), @@ -257,7 +277,8 @@ export default function AdminPage({ ]) }, { - title: "TENANT", + id: "tenant", + title: "i18n:govoplan-access.tenant.3ca93c78", items: sortNavItems([ visibleNavItem(available, "tenant-roles", "i18n:govoplan-access.roles.47dcc27d", 10), visibleNavItem(available, "tenant-function-role-mappings", "i18n:govoplan-access.function_role_mappings.2b64e9c3", 20), @@ -271,7 +292,8 @@ export default function AdminPage({ ]) }, { - title: "GROUP", + id: "group", + title: "i18n:govoplan-access.group.171a0606", items: sortNavItems([ visibleNavItem(available, "tenant-group-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10), visibleNavItem(available, "tenant-group-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20), @@ -280,7 +302,8 @@ export default function AdminPage({ ]) }, { - title: "USER", + id: "user", + title: "i18n:govoplan-access.user.9f8a2389", items: sortNavItems([ visibleNavItem(available, "tenant-user-file-connectors", "i18n:govoplan-access.file_connections.1e362326", 10), visibleNavItem(available, "tenant-user-mail-servers", "i18n:govoplan-access.mail_servers.d627326a", 20), @@ -398,13 +421,13 @@ function adminNavigationTree( byModule.set(moduleId, [...(byModule.get(moduleId) ?? []), item]); } children.push({ - branchId: `admin-${group.title.toLowerCase()}-settings`, + branchId: `admin-${group.id}-settings`, label: "i18n:govoplan-core.settings.c7f73bb5", defaultExpanded: false, children: [...byModule.entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([moduleId, items]) => ({ - branchId: `admin-${group.title.toLowerCase()}-settings-${moduleId}`, + branchId: `admin-${group.id}-settings-${moduleId}`, label: moduleLabel(moduleId), defaultExpanded: items.some( (item) => item.id === "system-settings" @@ -414,16 +437,20 @@ function adminNavigationTree( }); } return { - branchId: `admin-${group.title.toLowerCase()}`, + branchId: `admin-${group.id}`, label: group.title, - defaultExpanded: group.title === "ADMINISTRATION", + defaultExpanded: group.id === "administration", children }; }); } function moduleLabel(moduleId: string): string { - if (moduleId === "platform") return "Platform"; + if (moduleId === "platform") return "i18n:govoplan-access.platform_administration"; + if (moduleId === "access") return "i18n:govoplan-access.access.2f81a22d"; + if (moduleId === "admin") return "i18n:govoplan-access.admin.4e7afebc"; + if (moduleId === "files") return "i18n:govoplan-access.files.6ce6c512"; + if (moduleId === "mail") return "i18n:govoplan-access.mail_servers.d627326a"; return moduleId .split(/[-_]/) .filter(Boolean) diff --git a/webui/src/features/admin/ApiKeysPanel.tsx b/webui/src/features/admin/ApiKeysPanel.tsx index 99c0a1f..d30ae43 100644 --- a/webui/src/features/admin/ApiKeysPanel.tsx +++ b/webui/src/features/admin/ApiKeysPanel.tsx @@ -10,9 +10,10 @@ import { DateTimeField } from "@govoplan/core-webui"; import { StatusBadge } from "@govoplan/core-webui"; import { ToggleSwitch } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; -import { AdminIconButton, AdminPageLayout, AdminSelectionList, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime, useDeltaWatermarks } from "@govoplan/core-webui"; +import { AdminIconButton, AdminPageLayout, AdminSelectionList, DocumentationHelpLink, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime, useDeltaWatermarks } from "@govoplan/core-webui"; import { scopeGrants, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { loadDeltaRows } from "./utils/deltaRows"; +import { ACCESS_INTERFACE_I18N, ACCESS_REFERENCE_DOCUMENTATION, saveDisabledReason } from "./interfacePatterns"; function defaultDraft(userId: string) { return { name: "", userId, scopes: ["campaign:read"], expiresAt: "" }; @@ -102,7 +103,7 @@ export default function ApiKeysPanel({ settings, auth, canCreate, canRevoke }: { { id: "expires", header: "i18n:govoplan-access.expires.a99be3da", width: 180, minWidth: 150, resizable: true, sortable: true, value: (row) => row.expires_at || "", render: (row) => row.expires_at ? formatDateTime(row.expires_at) : "i18n:govoplan-access.no_expiry.39d436aa" }, { id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 108, sticky: "end", resizable: false, align: "right", render: (row) => , onClick: () => setViewing(row) }, - { id: "revoke", label: i18nMessage("i18n:govoplan-access.revoke_value.34640d6a", { value0: row.name }), icon: , variant: "danger", applicable: !row.revoked_at, disabled: !canRevoke, onClick: () => setRevoking(row) } + { id: "revoke", label: i18nMessage("i18n:govoplan-access.revoke_value.34640d6a", { value0: row.name }), icon: , variant: "danger", applicable: !row.revoked_at, disabled: !canRevoke, disabledReason: row.revoked_at ? "i18n:govoplan-access.revoked.85f17ac0" : !canRevoke ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined, onClick: () => setRevoking(row) } ]} /> }], [canRevoke]); @@ -151,11 +152,11 @@ export default function ApiKeysPanel({ settings, auth, canCreate, canRevoke }: { return ( <> - } variant="primary" onClick={openCreate} disabled={!canCreate || !users.length} />}> + } variant="primary" onClick={openCreate} disabled={!canCreate || !users.length} disabledReason={!canCreate ? ACCESS_INTERFACE_I18N.createPermissionRequired : !users.length ? ACCESS_INTERFACE_I18N.selectUserAndScopes : undefined} />}>
row.id} emptyText="i18n:govoplan-access.no_api_keys_found.1f377128" />
- !busy && setCreating(false)} className="admin-dialog admin-dialog-wide" footer={<>}> + !busy && setCreating(false)} className="admin-dialog admin-dialog-wide" footer={<>}>
setDraft({ ...draft, name: event.target.value })} /> diff --git a/webui/src/features/admin/CredentialEnvelopesPanel.tsx b/webui/src/features/admin/CredentialEnvelopesPanel.tsx index b41258a..4a7e005 100644 --- a/webui/src/features/admin/CredentialEnvelopesPanel.tsx +++ b/webui/src/features/admin/CredentialEnvelopesPanel.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"; import { AdminPageLayout, CredentialEnvelopeManager, + DocumentationHelpLink, adminErrorMessage, useDeltaWatermarks, type ApiSettings, @@ -15,6 +16,10 @@ import { type UserAdminItem } from "../../api/admin"; import { loadDeltaRows } from "./utils/deltaRows"; +import { + ACCESS_INTERFACE_I18N, + CREDENTIAL_DOCUMENTATION +} from "./interfacePatterns"; type ScopeType = Extract; @@ -116,13 +121,14 @@ export default function CredentialEnvelopesPanel({ description={scopeDescription(scopeType)} loading={loadingTargets} error={targetError} + actions={} > @@ -130,18 +136,20 @@ export default function CredentialEnvelopesPanel({ } function scopeTitle(scopeType: ScopeType): string { - if (scopeType === "system") return "System credentials"; - if (scopeType === "tenant") return "Tenant credentials"; - if (scopeType === "group") return "Group credentials"; - return "User credentials"; + if (scopeType === "system") return ACCESS_INTERFACE_I18N.systemCredentials; + if (scopeType === "tenant") return ACCESS_INTERFACE_I18N.tenantCredentials; + if (scopeType === "group") return ACCESS_INTERFACE_I18N.groupCredentials; + return ACCESS_INTERFACE_I18N.userCredentials; } function scopeDescription(scopeType: ScopeType): string { if (scopeType === "system") { - return "Instance credentials that can be inherited by tenants and limited to selected modules or servers."; + return ACCESS_INTERFACE_I18N.systemCredentialDescription; } if (scopeType === "tenant") { - return "Tenant credentials shared by Mail, Files, Calendar, Addresses, and other permitted modules."; + return ACCESS_INTERFACE_I18N.tenantCredentialDescription; } - return `Reusable credentials owned by the selected ${scopeType}.`; + return scopeType === "group" + ? ACCESS_INTERFACE_I18N.groupCredentialDescription + : ACCESS_INTERFACE_I18N.userCredentialDescription; } diff --git a/webui/src/features/admin/ExternalFunctionRoleMappingsPanel.tsx b/webui/src/features/admin/ExternalFunctionRoleMappingsPanel.tsx index 45e2667..d2f3d5d 100644 --- a/webui/src/features/admin/ExternalFunctionRoleMappingsPanel.tsx +++ b/webui/src/features/admin/ExternalFunctionRoleMappingsPanel.tsx @@ -15,9 +15,10 @@ import { DataGrid, type DataGridColumn } from "@govoplan/core-webui"; import { Dialog } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; -import { AdminIconButton, AdminPageLayout, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime } from "@govoplan/core-webui"; +import { AdminIconButton, AdminPageLayout, DocumentationHelpLink, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime } from "@govoplan/core-webui"; import { i18nMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { loadDeltaRows } from "./utils/deltaRows"; +import { ACCESS_INTERFACE_I18N, FUNCTION_MAPPING_DOCUMENTATION, saveDisabledReason } from "./interfacePatterns"; const emptyDraft = { sourceModule: "organizations", @@ -258,8 +259,8 @@ export default function ExternalFunctionRoleMappingsPanel({ resizable: false, align: "right", render: (row) => , disabled: !canWrite, onClick: () => openEdit(row) }, - { id: "delete", label: i18nMessage("i18n:govoplan-access.delete_value.4d18989e", { value0: row.function_id }), icon: , variant: "danger", disabled: !canWrite, onClick: () => setDeleting(row) } + { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.function_id }), icon: , disabled: !canWrite, disabledReason: !canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined, onClick: () => openEdit(row) }, + { id: "delete", label: i18nMessage("i18n:govoplan-access.delete_value.4d18989e", { value0: row.function_id }), icon: , variant: "danger", disabled: !canWrite, disabledReason: !canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined, onClick: () => setDeleting(row) } ]} /> } ], @@ -276,8 +277,9 @@ export default function ExternalFunctionRoleMappingsPanel({ success={success} actions={ <> - - } variant="primary" onClick={openCreate} disabled={!canWrite || !assignableRoles.length} /> + + + } variant="primary" onClick={openCreate} disabled={!canWrite || !assignableRoles.length} disabledReason={!canWrite ? ACCESS_INTERFACE_I18N.createPermissionRequired : !assignableRoles.length ? ACCESS_INTERFACE_I18N.selectAssignableRole : undefined} /> } > @@ -300,8 +302,8 @@ export default function ExternalFunctionRoleMappingsPanel({ className="admin-dialog" footer={ <> - - + diff --git a/webui/src/features/admin/FileConnectorsPanel.tsx b/webui/src/features/admin/FileConnectorsPanel.tsx index 59a43db..2776c2c 100644 --- a/webui/src/features/admin/FileConnectorsPanel.tsx +++ b/webui/src/features/admin/FileConnectorsPanel.tsx @@ -1,8 +1,12 @@ import { useEffect, useRef, useState } from "react"; import type { ApiSettings, FileConnectorScope, FileConnectorTargetOption, FilesConnectorsUiCapability } from "@govoplan/core-webui"; -import { AdminPageLayout, Card, adminErrorMessage, useDeltaWatermarks, usePlatformUiCapability } from "@govoplan/core-webui"; +import { ActionBlockerHint, AdminPageLayout, adminErrorMessage, useDeltaWatermarks, usePlatformUiCapability } from "@govoplan/core-webui"; import { fetchGroupsDelta, fetchUsersDelta, type GroupSummary, type UserAdminItem } from "../../api/admin"; import { loadDeltaRows } from "./utils/deltaRows"; +import { + ACCESS_INTERFACE_I18N, + FILE_CONNECTOR_DOCUMENTATION +} from "./interfacePatterns"; type Props = { settings: ApiSettings; @@ -82,9 +86,21 @@ export default function FileConnectorsPanel({ settings, scopeType, canWrite }: P if (!FileConnectorScopeManager) { return ( - -

i18n:govoplan-access.install_and_enable_the_files_module_to_manage_fi.f842c153

-
+
); } diff --git a/webui/src/features/admin/GroupsPanel.tsx b/webui/src/features/admin/GroupsPanel.tsx index f86e71c..d9c3841 100644 --- a/webui/src/features/admin/GroupsPanel.tsx +++ b/webui/src/features/admin/GroupsPanel.tsx @@ -8,8 +8,13 @@ import { Dialog } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; import { StatusBadge } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; -import { AdminIconButton, AdminPageLayout, AdminSelectionList, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime, joinLabels, i18nMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; +import { AdminIconButton, AdminPageLayout, AdminSelectionList, DocumentationHelpLink, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime, joinLabels, i18nMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { loadDeltaRows } from "./utils/deltaRows"; +import { + ACCESS_INTERFACE_I18N, + ACCESS_WORKFLOW_DOCUMENTATION, + saveDisabledReason +} from "./interfacePatterns"; const emptyDraft = { slug: "", name: "", description: "", isActive: true, memberIds: [] as string[], roleIds: [] as string[] }; @@ -133,18 +138,18 @@ export default function GroupsPanel({ settings, auth, canDefine, canManageMember { id: "status", header: "i18n:govoplan-access.status.bae7d5be", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.is_active ? "active" : "inactive", render: (row) => }, { id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right", render: (row) => , onClick: () => setViewing(row) }, - { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.name }), icon: , disabled: !(canDefine || canManageMembers || canAssignRoles), onClick: () => openEdit(row) }, - { id: "deactivate", label: i18nMessage("i18n:govoplan-access.deactivate_value.a276a667", { value0: row.name }), icon: , variant: "danger", applicable: row.is_active, disabled: !canDefine || Boolean(row.system_required), onClick: () => setDeactivating(row) } + { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.name }), icon: , disabled: !(canDefine || canManageMembers || canAssignRoles), disabledReason: !(canDefine || canManageMembers || canAssignRoles) ? ACCESS_INTERFACE_I18N.updatePermissionRequired : undefined, onClick: () => openEdit(row) }, + { id: "deactivate", label: i18nMessage("i18n:govoplan-access.deactivate_value.a276a667", { value0: row.name }), icon: , variant: "danger", applicable: row.is_active, disabled: !canDefine || Boolean(row.system_required), disabledReason: !row.is_active ? "i18n:govoplan-access.inactive.09af574c" : !canDefine ? ACCESS_INTERFACE_I18N.writePermissionRequired : row.system_required ? ACCESS_INTERFACE_I18N.systemManagedObject : undefined, onClick: () => setDeactivating(row) } ]} /> }], [canAssignRoles, canDefine, canManageMembers]); return ( <> - } variant="primary" onClick={openCreate} disabled={!canDefine} />}> + } variant="primary" onClick={openCreate} disabled={!canDefine} disabledReason={!canDefine ? ACCESS_INTERFACE_I18N.createPermissionRequired : undefined} />}>
row.id} emptyText="i18n:govoplan-access.no_groups_found.627ca913" />
- !busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}> + !busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}> {editing !== "new" && editing?.system_template_id &&

i18n:govoplan-access.this_group_definition_is_managed_by_the_system_n.640b235e

}
setDraft({ ...draft, name: event.target.value })} /> diff --git a/webui/src/features/admin/MailProfilesPanel.tsx b/webui/src/features/admin/MailProfilesPanel.tsx index b110fc4..de2e039 100644 --- a/webui/src/features/admin/MailProfilesPanel.tsx +++ b/webui/src/features/admin/MailProfilesPanel.tsx @@ -1,10 +1,13 @@ import { useEffect, useRef, useState } from "react"; import type { ApiSettings, MailProfileScope, MailProfilesUiCapability, MailProfileTargetOption } from "@govoplan/core-webui"; import { fetchGroupsDelta, fetchUsersDelta, type GroupSummary, type UserAdminItem } from "../../api/admin"; -import { Card } from "@govoplan/core-webui"; -import { AdminPageLayout, adminErrorMessage, useDeltaWatermarks } from "@govoplan/core-webui"; +import { ActionBlockerHint, AdminPageLayout, adminErrorMessage, useDeltaWatermarks } from "@govoplan/core-webui"; import { usePlatformUiCapability } from "@govoplan/core-webui"; import { loadDeltaRows } from "./utils/deltaRows"; +import { + ACCESS_INTERFACE_I18N, + MAIL_PROFILE_DOCUMENTATION +} from "./interfacePatterns"; type Props = { settings: ApiSettings; @@ -106,9 +109,21 @@ export default function MailProfilesPanel({ settings, scopeType, canWriteProfile if (!MailProfileScopeManager) { return ( - -

i18n:govoplan-access.install_and_enable_the_mail_module_to_manage_mai.a8ad5b3a

-
+
); } diff --git a/webui/src/features/admin/RolesPanel.tsx b/webui/src/features/admin/RolesPanel.tsx index 56ffa01..98c9656 100644 --- a/webui/src/features/admin/RolesPanel.tsx +++ b/webui/src/features/admin/RolesPanel.tsx @@ -8,9 +8,10 @@ import { Dialog } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; import { StatusBadge } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; -import { AdminIconButton, AdminPageLayout, AdminSelectionList, TableActionGroup, adminErrorMessage } from "@govoplan/core-webui"; +import { AdminIconButton, AdminPageLayout, AdminSelectionList, DocumentationHelpLink, TableActionGroup, adminErrorMessage } from "@govoplan/core-webui"; import { hasTenantWildcard, i18nMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { loadDeltaRows } from "./utils/deltaRows"; +import { ACCESS_INTERFACE_I18N, ACCESS_WORKFLOW_DOCUMENTATION, saveDisabledReason } from "./interfacePatterns"; const emptyDraft = { slug: "", name: "", description: "", permissions: [] as string[], isAssignable: true }; @@ -126,18 +127,18 @@ export default function RolesPanel({ settings, auth, canDefine, onAuthRefresh }: { id: "type", header: "i18n:govoplan-access.type.3deb7456", width: 140, resizable: false, sortable: true, filterable: true, value: (row) => row.is_builtin ? "built-in" : row.system_template_id ? "system-managed" : "custom", render: (row) => }, { id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right", render: (row) => , onClick: () => setViewing(row) }, - { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.name }), icon: , applicable: !row.is_builtin && !row.system_template_id, disabled: !canDefine, onClick: () => openEdit(row) }, - { id: "delete", label: i18nMessage("i18n:govoplan-access.delete_value.4d18989e", { value0: row.name }), icon: , variant: "danger", applicable: !row.is_builtin && !row.system_template_id, disabled: !canDefine || row.user_assignments + row.group_assignments > 0, onClick: () => setDeleting(row) } + { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.name }), icon: , applicable: !row.is_builtin && !row.system_template_id, disabled: !canDefine, disabledReason: row.is_builtin || row.system_template_id ? ACCESS_INTERFACE_I18N.systemManagedObject : !canDefine ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined, onClick: () => openEdit(row) }, + { id: "delete", label: i18nMessage("i18n:govoplan-access.delete_value.4d18989e", { value0: row.name }), icon: , variant: "danger", applicable: !row.is_builtin && !row.system_template_id, disabled: !canDefine || row.user_assignments + row.group_assignments > 0, disabledReason: row.is_builtin || row.system_template_id ? ACCESS_INTERFACE_I18N.systemManagedObject : !canDefine ? ACCESS_INTERFACE_I18N.writePermissionRequired : row.user_assignments + row.group_assignments > 0 ? ACCESS_INTERFACE_I18N.assignedObjectCannotBeDeleted : undefined, onClick: () => setDeleting(row) } ]} /> }], [canDefine, permissions]); return ( <> - } variant="primary" onClick={openCreate} disabled={!canDefine} />}> + } variant="primary" onClick={openCreate} disabled={!canDefine} disabledReason={!canDefine ? ACCESS_INTERFACE_I18N.createPermissionRequired : undefined} />}>
row.id} emptyText="i18n:govoplan-access.no_roles_found.70f7c0c9" />
- !busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}> + !busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}>
setDraft({ ...draft, name: event.target.value })} /> setDraft({ ...draft, slug: event.target.value })} /> diff --git a/webui/src/features/admin/SystemRolesPanel.tsx b/webui/src/features/admin/SystemRolesPanel.tsx index 87093a8..7887c9b 100644 --- a/webui/src/features/admin/SystemRolesPanel.tsx +++ b/webui/src/features/admin/SystemRolesPanel.tsx @@ -16,8 +16,9 @@ import { DataGrid, type DataGridColumn } from "@govoplan/core-webui"; import { Dialog } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; import { StatusBadge } from "@govoplan/core-webui"; -import { AdminIconButton, AdminPageLayout, AdminSelectionList, TableActionGroup, adminErrorMessage, joinLabels, i18nMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; +import { AdminIconButton, AdminPageLayout, AdminSelectionList, DocumentationHelpLink, TableActionGroup, adminErrorMessage, joinLabels, i18nMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { loadDeltaRows } from "./utils/deltaRows"; +import { ACCESS_INTERFACE_I18N, ACCESS_REFERENCE_DOCUMENTATION, saveDisabledReason } from "./interfacePatterns"; const emptyDraft = { slug: "", @@ -225,8 +226,8 @@ export default function SystemRolesPanel({ const protectedOwner = row.slug === "system_owner"; return , onClick: () => setViewing(row) }, - { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.name }), icon: , applicable: !protectedOwner, disabled: !canWrite, onClick: () => openEdit(row) }, - { id: "delete", label: i18nMessage("i18n:govoplan-access.delete_value.4d18989e", { value0: row.name }), icon: , variant: "danger", applicable: !protectedOwner, disabled: !canWrite || row.user_assignments > 0, onClick: () => setDeleting(row) } + { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.name }), icon: , applicable: !protectedOwner, disabled: !canWrite, disabledReason: protectedOwner ? ACCESS_INTERFACE_I18N.systemManagedObject : !canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : undefined, onClick: () => openEdit(row) }, + { id: "delete", label: i18nMessage("i18n:govoplan-access.delete_value.4d18989e", { value0: row.name }), icon: , variant: "danger", applicable: !protectedOwner, disabled: !canWrite || row.user_assignments > 0, disabledReason: protectedOwner ? ACCESS_INTERFACE_I18N.systemManagedObject : !canWrite ? ACCESS_INTERFACE_I18N.writePermissionRequired : row.user_assignments > 0 ? ACCESS_INTERFACE_I18N.assignedObjectCannotBeDeleted : undefined, onClick: () => setDeleting(row) } ]} />; } }], @@ -240,7 +241,7 @@ export default function SystemRolesPanel({ loading={loading} error={error} success={success} - actions={<>} variant="primary" onClick={openCreate} disabled={!canWrite} />}> + actions={<>} variant="primary" onClick={openCreate} disabled={!canWrite} disabledReason={!canWrite ? ACCESS_INTERFACE_I18N.createPermissionRequired : undefined} />}>
row.id} emptyText="i18n:govoplan-access.no_system_roles_found.051cf727" /> @@ -252,7 +253,7 @@ export default function SystemRolesPanel({ title={editing === "new" ? "i18n:govoplan-access.create_system_role.a1e40b25" : "i18n:govoplan-access.edit_system_role.6ebb7cb0"} onClose={() => !busy && setEditing(null)} className="admin-dialog admin-dialog-wide" - footer={<>}> + footer={<>}>
setDraft({ ...draft, name: event.target.value })} /> diff --git a/webui/src/features/admin/SystemUsersPanel.tsx b/webui/src/features/admin/SystemUsersPanel.tsx index ce66c48..4d6d5d1 100644 --- a/webui/src/features/admin/SystemUsersPanel.tsx +++ b/webui/src/features/admin/SystemUsersPanel.tsx @@ -20,7 +20,8 @@ import { type SystemMembershipDraft, type TenantAdminItem } from "../../api/admin"; -import { AdminIconButton, AdminPageLayout, AdminSelectionList, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime, joinLabels, i18nMessage, mergeDeltaRows, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; +import { AdminIconButton, AdminPageLayout, AdminSelectionList, DocumentationHelpLink, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime, joinLabels, i18nMessage, mergeDeltaRows, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; +import { ACCESS_INTERFACE_I18N, ACCESS_REFERENCE_DOCUMENTATION, saveDisabledReason } from "./interfacePatterns"; const emptyDraft = { email: "", @@ -214,8 +215,8 @@ export default function SystemUsersPanel({ { id: "last_login", header: "i18n:govoplan-access.last_login.43dab84f", width: 180, minWidth: 150, resizable: true, sortable: true, value: (row) => row.last_login_at || "", render: (row) => formatDateTime(row.last_login_at) }, { id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right", render: (row) => , onClick: () => setViewing(row) }, - { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.email }), icon: , disabled: !(canUpdate || canSuspend || canAssignRoles || canManageMemberships), onClick: () => openEdit(row) }, - { id: "deactivate", label: i18nMessage("i18n:govoplan-access.deactivate_value.a276a667", { value0: row.email }), icon: , variant: "danger", applicable: row.is_active, disabled: !canSuspend || row.memberships.some((membership) => membership.is_last_active_owner), onClick: () => setDeactivating(row) } + { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.email }), icon: , disabled: !(canUpdate || canSuspend || canAssignRoles || canManageMemberships), disabledReason: !(canUpdate || canSuspend || canAssignRoles || canManageMemberships) ? ACCESS_INTERFACE_I18N.updatePermissionRequired : undefined, onClick: () => openEdit(row) }, + { id: "deactivate", label: i18nMessage("i18n:govoplan-access.deactivate_value.a276a667", { value0: row.email }), icon: , variant: "danger", applicable: row.is_active, disabled: !canSuspend || row.memberships.some((membership) => membership.is_last_active_owner), disabledReason: !row.is_active ? "i18n:govoplan-access.inactive.09af574c" : !canSuspend ? ACCESS_INTERFACE_I18N.updatePermissionRequired : row.memberships.some((membership) => membership.is_last_active_owner) ? ACCESS_INTERFACE_I18N.lastOwnerCannotBeDeactivated : undefined, onClick: () => setDeactivating(row) } ]} /> }], [canAssignRoles, canManageMemberships, canSuspend, canUpdate]); @@ -227,12 +228,12 @@ export default function SystemUsersPanel({ loading={loading} error={error} success={success} - actions={<>} variant="primary" onClick={openCreate} disabled={!canCreate} />}> + actions={<>} variant="primary" onClick={openCreate} disabled={!canCreate} disabledReason={!canCreate ? ACCESS_INTERFACE_I18N.createPermissionRequired : undefined} />}>
row.account_id} emptyText="i18n:govoplan-access.no_global_accounts_found.29d96a9e" />
- !busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}> + !busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}>
setDraft({ ...draft, email: event.target.value })} /> setDraft({ ...draft, displayName: event.target.value })} /> diff --git a/webui/src/features/admin/UsersPanel.tsx b/webui/src/features/admin/UsersPanel.tsx index da3efa6..e8918ec 100644 --- a/webui/src/features/admin/UsersPanel.tsx +++ b/webui/src/features/admin/UsersPanel.tsx @@ -10,9 +10,14 @@ import { PasswordField } from "@govoplan/core-webui"; import { StatusBadge } from "@govoplan/core-webui"; import { ToggleSwitch } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; -import { AdminIconButton, AdminPageLayout, AdminSelectionList, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime, joinLabels } from "@govoplan/core-webui"; +import { AdminIconButton, AdminPageLayout, AdminSelectionList, DocumentationHelpLink, TableActionGroup, adminErrorMessage, formatAdminDateTime as formatDateTime, joinLabels } from "@govoplan/core-webui"; import { hasTenantWildcard, i18nMessage, useDeltaWatermarks, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { loadDeltaRows } from "./utils/deltaRows"; +import { + ACCESS_INTERFACE_I18N, + ACCESS_WORKFLOW_DOCUMENTATION, + saveDisabledReason +} from "./interfacePatterns"; const emptyDraft = { email: "", @@ -190,18 +195,18 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu { id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 190, sticky: "end", resizable: false, align: "right", render: (row) => , onClick: () => setViewing(row) }, { id: "explain", label: i18nMessage("i18n:govoplan-access.explain_access_for_value.3af96e47", { value0: row.email }), icon: , onClick: () => void openAccessExplanation(row) }, - { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.email }), icon: , disabled: !(canUpdate || canSuspend || canManageGroups || canAssignRoles), onClick: () => openEdit(row) }, - { id: "deactivate", label: i18nMessage("i18n:govoplan-access.deactivate_value.a276a667", { value0: row.email }), icon: , variant: "danger", applicable: row.is_active, disabled: !canSuspend || row.is_last_active_owner, onClick: () => setDeactivating(row) } + { id: "edit", label: i18nMessage("i18n:govoplan-access.edit_value.fad75899", { value0: row.email }), icon: , disabled: !(canUpdate || canSuspend || canManageGroups || canAssignRoles), disabledReason: !(canUpdate || canSuspend || canManageGroups || canAssignRoles) ? ACCESS_INTERFACE_I18N.updatePermissionRequired : undefined, onClick: () => openEdit(row) }, + { id: "deactivate", label: i18nMessage("i18n:govoplan-access.deactivate_value.a276a667", { value0: row.email }), icon: , variant: "danger", applicable: row.is_active, disabled: !canSuspend || row.is_last_active_owner, disabledReason: !row.is_active ? "i18n:govoplan-access.inactive.09af574c" : !canSuspend ? ACCESS_INTERFACE_I18N.updatePermissionRequired : row.is_last_active_owner ? ACCESS_INTERFACE_I18N.lastOwnerCannotBeDeactivated : undefined, onClick: () => setDeactivating(row) } ]} /> }], [canAssignRoles, canManageGroups, canSuspend, canUpdate, settings]); return ( <> - } variant="primary" onClick={openCreate} disabled={!canCreate} />}> + } variant="primary" onClick={openCreate} disabled={!canCreate} disabledReason={!canCreate ? ACCESS_INTERFACE_I18N.createPermissionRequired : undefined} />}>
row.id} emptyText="i18n:govoplan-access.no_tenant_users_found.74bb615f" />
- !busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}> + !busy && setEditing(null)} className="admin-dialog admin-dialog-wide" footer={<>}>
setDraft({ ...draft, email: event.target.value })} /> setDraft({ ...draft, displayName: event.target.value })} /> @@ -235,7 +240,7 @@ export default function UsersPanel({ settings, auth, canCreate, canUpdate, canSu }
- { if (!accessExplanationLoading) { setExplaining(null); setAccessExplanation(null); } }} className="admin-dialog admin-dialog-wide" footer={}> + { if (!accessExplanationLoading) { setExplaining(null); setAccessExplanation(null); } }} className="admin-dialog admin-dialog-wide" footer={}> {accessExplanationLoading &&

i18n:govoplan-access.loading_access_explanation.04a7c934

} {accessExplanation && <>
diff --git a/webui/src/features/admin/interfacePatterns.ts b/webui/src/features/admin/interfacePatterns.ts new file mode 100644 index 0000000..d4cc4f4 --- /dev/null +++ b/webui/src/features/admin/interfacePatterns.ts @@ -0,0 +1,79 @@ +import type { DocumentationHelpReference } from "@govoplan/core-webui"; + +export const ACCESS_WORKFLOW_DOCUMENTATION = { + topicId: "access.workflow.grant-user-access", + documentationType: "admin" +} satisfies DocumentationHelpReference; + +export const ACCESS_REFERENCE_DOCUMENTATION = { + topicId: "access.reference.admin-access-fields", + documentationType: "admin" +} satisfies DocumentationHelpReference; + +export const FUNCTION_MAPPING_DOCUMENTATION = { + topicId: "access.reference.external-function-role-mappings", + documentationType: "admin" +} satisfies DocumentationHelpReference; + +export const CREDENTIAL_DOCUMENTATION = { + contextId: "access.credentials", + documentationType: "admin" +} satisfies DocumentationHelpReference; + +export const FILE_CONNECTOR_DOCUMENTATION = { + topicId: "files.governed-connectors-and-provenance", + documentationType: "admin" +} satisfies DocumentationHelpReference; + +export const MAIL_PROFILE_DOCUMENTATION = { + topicId: "mail.profiles-and-policy", + documentationType: "admin" +} satisfies DocumentationHelpReference; + +export const ACCESS_INTERFACE_I18N = { + loading: "i18n:govoplan-access.administration_data_is_loading.4af2c001", + createPermissionRequired: "i18n:govoplan-access.create_permission_is_required.4af2c002", + updatePermissionRequired: "i18n:govoplan-access.update_or_assignment_permission_is_required.4af2c003", + writePermissionRequired: "i18n:govoplan-access.write_permission_is_required.4af2c004", + completeRequiredFields: "i18n:govoplan-access.complete_the_required_fields_before_saving.4af2c005", + selectUserAndScopes: "i18n:govoplan-access.select_an_account_and_at_least_one_scope.4af2c006", + selectAssignableRole: "i18n:govoplan-access.select_an_assignable_role_before_creating_a_mapping.4af2c007", + assignedObjectCannotBeDeleted: "i18n:govoplan-access.remove_existing_assignments_before_deleting.4af2c008", + lastOwnerCannotBeDeactivated: "i18n:govoplan-access.assign_another_operational_owner_before_deactivating.4af2c009", + systemManagedObject: "i18n:govoplan-access.this_definition_is_managed_by_the_system.4af2c010", + operationInProgress: "i18n:govoplan-access.an_access_administration_operation_is_in_progress.4af2c011", + requiredAction: "i18n:govoplan-access.required_action.4af2c012", + actor: "i18n:govoplan-access.who_can_fix_it.4af2c013", + destinationLabel: "i18n:govoplan-access.where_to_go.4af2c014", + requestAdministrationAccess: "i18n:govoplan-access.request_an_administrative_role_for_the_required_scope.4af2c015", + accessAdministrator: "i18n:govoplan-access.a_tenant_owner_or_system_access_administrator.4af2c016", + accessAdministration: "i18n:govoplan-access.admin_users_groups_and_roles.4af2c017", + installFiles: "i18n:govoplan-access.install_and_enable_the_files_module.4af2c018", + installMail: "i18n:govoplan-access.install_and_enable_the_mail_module.4af2c019", + systemModuleAdministrator: "i18n:govoplan-access.a_system_module_administrator.4af2c020", + moduleManagement: "i18n:govoplan-access.admin_modules.4af2c021", + reusableCredentials: "i18n:govoplan-access.reusable_credentials.4af2c022", + systemCredentials: "i18n:govoplan-access.system_credentials.4af2c023", + tenantCredentials: "i18n:govoplan-access.tenant_credentials.4af2c024", + groupCredentials: "i18n:govoplan-access.group_credentials.4af2c025", + userCredentials: "i18n:govoplan-access.user_credentials.4af2c026", + systemCredentialDescription: "i18n:govoplan-access.instance_credentials_can_be_inherited_and_governed.4af2c027", + tenantCredentialDescription: "i18n:govoplan-access.tenant_credentials_can_be_shared_with_permitted_modules.4af2c028", + groupCredentialDescription: "i18n:govoplan-access.reusable_credentials_owned_by_the_selected_group.4af2c029", + userCredentialDescription: "i18n:govoplan-access.reusable_credentials_owned_by_the_selected_user.4af2c030" +} as const; + +export function saveDisabledReason({ + busy, + permitted, + complete +}: { + busy: boolean; + permitted: boolean; + complete: boolean; +}): string | undefined { + if (busy) return ACCESS_INTERFACE_I18N.operationInProgress; + if (!permitted) return ACCESS_INTERFACE_I18N.writePermissionRequired; + if (!complete) return ACCESS_INTERFACE_I18N.completeRequiredFields; + return undefined; +} diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts index 6bfb3d3..304c8e9 100644 --- a/webui/src/i18n/generatedTranslations.ts +++ b/webui/src/i18n/generatedTranslations.ts @@ -2,6 +2,36 @@ import type { PlatformTranslations } from "@govoplan/core-webui"; export const generatedTranslations: PlatformTranslations = { "en": { + "i18n:govoplan-access.administration_data_is_loading.4af2c001": "Administration data is loading.", + "i18n:govoplan-access.create_permission_is_required.4af2c002": "Create permission is required for this action.", + "i18n:govoplan-access.update_or_assignment_permission_is_required.4af2c003": "Update or assignment permission is required for this action.", + "i18n:govoplan-access.write_permission_is_required.4af2c004": "Write permission is required for this action.", + "i18n:govoplan-access.complete_the_required_fields_before_saving.4af2c005": "Complete the required fields before saving.", + "i18n:govoplan-access.select_an_account_and_at_least_one_scope.4af2c006": "Select an account and at least one allowed scope.", + "i18n:govoplan-access.select_an_assignable_role_before_creating_a_mapping.4af2c007": "Create an assignable role before adding a function mapping.", + "i18n:govoplan-access.remove_existing_assignments_before_deleting.4af2c008": "Remove existing assignments before deleting this definition.", + "i18n:govoplan-access.assign_another_operational_owner_before_deactivating.4af2c009": "Assign another operational owner before deactivating this account or membership.", + "i18n:govoplan-access.this_definition_is_managed_by_the_system.4af2c010": "This protected definition is managed by the system.", + "i18n:govoplan-access.an_access_administration_operation_is_in_progress.4af2c011": "An access administration operation is in progress.", + "i18n:govoplan-access.required_action.4af2c012": "Required action", + "i18n:govoplan-access.who_can_fix_it.4af2c013": "Who can fix it", + "i18n:govoplan-access.where_to_go.4af2c014": "Where to go", + "i18n:govoplan-access.request_an_administrative_role_for_the_required_scope.4af2c015": "Request an administrative role for the required scope.", + "i18n:govoplan-access.a_tenant_owner_or_system_access_administrator.4af2c016": "A tenant owner or system access administrator", + "i18n:govoplan-access.admin_users_groups_and_roles.4af2c017": "Admin > Users, Groups, or Roles", + "i18n:govoplan-access.install_and_enable_the_files_module.4af2c018": "Install and enable the Files module.", + "i18n:govoplan-access.install_and_enable_the_mail_module.4af2c019": "Install and enable the Mail module.", + "i18n:govoplan-access.a_system_module_administrator.4af2c020": "A system module administrator", + "i18n:govoplan-access.admin_modules.4af2c021": "Admin > Modules", + "i18n:govoplan-access.reusable_credentials.4af2c022": "Reusable credentials", + "i18n:govoplan-access.system_credentials.4af2c023": "System credentials", + "i18n:govoplan-access.tenant_credentials.4af2c024": "Tenant credentials", + "i18n:govoplan-access.group_credentials.4af2c025": "Group credentials", + "i18n:govoplan-access.user_credentials.4af2c026": "User credentials", + "i18n:govoplan-access.instance_credentials_can_be_inherited_and_governed.4af2c027": "Instance credentials can be inherited by tenants and limited to selected modules or servers.", + "i18n:govoplan-access.tenant_credentials_can_be_shared_with_permitted_modules.4af2c028": "Tenant credentials can be shared with Mail, Files, Calendar, Addresses, and other permitted modules.", + "i18n:govoplan-access.reusable_credentials_owned_by_the_selected_group.4af2c029": "Reusable credentials owned by the selected group.", + "i18n:govoplan-access.reusable_credentials_owned_by_the_selected_user.4af2c030": "Reusable credentials owned by the selected user.", "i18n:govoplan-access.a_role_may_contain_only_permissions_held_by_the_.a7ee5e45": "A role may contain only permissions held by the administrator defining it. The protected system:* wildcard is reserved for System owner.", "i18n:govoplan-access.access_updated_for_value.87f22245": "Access updated for {value0}.", "i18n:govoplan-access.access_explanation.75ee7f62": "Access explanation", @@ -352,6 +382,36 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-access.your_current_roles_do_not_grant_administrative_a.6eafee69": "Your current roles do not grant administrative access." }, "de": { + "i18n:govoplan-access.administration_data_is_loading.4af2c001": "Administrationsdaten werden geladen.", + "i18n:govoplan-access.create_permission_is_required.4af2c002": "Für diese Aktion ist die Berechtigung zum Erstellen erforderlich.", + "i18n:govoplan-access.update_or_assignment_permission_is_required.4af2c003": "Für diese Aktion ist eine Berechtigung zum Ändern oder Zuweisen erforderlich.", + "i18n:govoplan-access.write_permission_is_required.4af2c004": "Für diese Aktion ist eine Schreibberechtigung erforderlich.", + "i18n:govoplan-access.complete_the_required_fields_before_saving.4af2c005": "Füllen Sie vor dem Speichern die Pflichtfelder aus.", + "i18n:govoplan-access.select_an_account_and_at_least_one_scope.4af2c006": "Wählen Sie ein Konto und mindestens einen zulässigen Geltungsbereich aus.", + "i18n:govoplan-access.select_an_assignable_role_before_creating_a_mapping.4af2c007": "Erstellen Sie vor dem Hinzufügen einer Funktionszuordnung eine zuweisbare Rolle.", + "i18n:govoplan-access.remove_existing_assignments_before_deleting.4af2c008": "Entfernen Sie bestehende Zuweisungen, bevor Sie diese Definition löschen.", + "i18n:govoplan-access.assign_another_operational_owner_before_deactivating.4af2c009": "Weisen Sie einen anderen betriebsfähigen Eigentümer zu, bevor Sie dieses Konto oder diese Mitgliedschaft deaktivieren.", + "i18n:govoplan-access.this_definition_is_managed_by_the_system.4af2c010": "Diese geschützte Definition wird vom System verwaltet.", + "i18n:govoplan-access.an_access_administration_operation_is_in_progress.4af2c011": "Eine Zugriffsverwaltungsaktion wird gerade ausgeführt.", + "i18n:govoplan-access.required_action.4af2c012": "Erforderliche Aktion", + "i18n:govoplan-access.who_can_fix_it.4af2c013": "Wer kann das beheben", + "i18n:govoplan-access.where_to_go.4af2c014": "Zuständige Stelle", + "i18n:govoplan-access.request_an_administrative_role_for_the_required_scope.4af2c015": "Fordern Sie eine administrative Rolle für den benötigten Bereich an.", + "i18n:govoplan-access.a_tenant_owner_or_system_access_administrator.4af2c016": "Ein Mandanteneigentümer oder System-Zugriffsadministrator", + "i18n:govoplan-access.admin_users_groups_and_roles.4af2c017": "Administration > Benutzer, Gruppen oder Rollen", + "i18n:govoplan-access.install_and_enable_the_files_module.4af2c018": "Installieren und aktivieren Sie das Dateimodul.", + "i18n:govoplan-access.install_and_enable_the_mail_module.4af2c019": "Installieren und aktivieren Sie das Mailmodul.", + "i18n:govoplan-access.a_system_module_administrator.4af2c020": "Ein System-Moduladministrator", + "i18n:govoplan-access.admin_modules.4af2c021": "Administration > Module", + "i18n:govoplan-access.reusable_credentials.4af2c022": "Wiederverwendbare Zugangsdaten", + "i18n:govoplan-access.system_credentials.4af2c023": "System-Zugangsdaten", + "i18n:govoplan-access.tenant_credentials.4af2c024": "Mandanten-Zugangsdaten", + "i18n:govoplan-access.group_credentials.4af2c025": "Gruppen-Zugangsdaten", + "i18n:govoplan-access.user_credentials.4af2c026": "Benutzer-Zugangsdaten", + "i18n:govoplan-access.instance_credentials_can_be_inherited_and_governed.4af2c027": "Instanzweite Zugangsdaten können von Mandanten geerbt und auf ausgewählte Module oder Server begrenzt werden.", + "i18n:govoplan-access.tenant_credentials_can_be_shared_with_permitted_modules.4af2c028": "Mandanten-Zugangsdaten können mit Mail, Dateien, Kalender, Adressen und anderen erlaubten Modulen geteilt werden.", + "i18n:govoplan-access.reusable_credentials_owned_by_the_selected_group.4af2c029": "Wiederverwendbare Zugangsdaten der ausgewählten Gruppe.", + "i18n:govoplan-access.reusable_credentials_owned_by_the_selected_user.4af2c030": "Wiederverwendbare Zugangsdaten des ausgewählten Benutzers.", "i18n:govoplan-access.a_role_may_contain_only_permissions_held_by_the_.a7ee5e45": "A role may contain only permissions held by the administrator defining it. The protected system:* wildcard is reserved for System owner.", "i18n:govoplan-access.access_updated_for_value.87f22245": "Access updated for {value0}.", "i18n:govoplan-access.access_explanation.75ee7f62": "Zugriffserklaerung", diff --git a/webui/src/module.ts b/webui/src/module.ts index 85792b2..9e2c29e 100644 --- a/webui/src/module.ts +++ b/webui/src/module.ts @@ -12,18 +12,18 @@ const translations = { }; const accessAdminSurfaces = [ - { id: "access.admin.system-roles", moduleId: "access", kind: "section" as const, label: "System roles", order: 20 }, - { id: "access.admin.system-users", moduleId: "access", kind: "section" as const, label: "System users", order: 50 }, - { id: "access.admin.system-credentials", moduleId: "access", kind: "section" as const, label: "System credentials", order: 80 }, - { id: "access.admin.tenant-roles", moduleId: "access", kind: "section" as const, label: "Tenant roles", order: 10 }, - { id: "access.admin.tenant-function-mappings", moduleId: "access", kind: "section" as const, label: "Function mappings", order: 20 }, - { id: "access.admin.tenant-groups", moduleId: "access", kind: "section" as const, label: "Tenant groups", order: 30 }, - { id: "access.admin.tenant-users", moduleId: "access", kind: "section" as const, label: "Tenant users", order: 40 }, - { id: "access.admin.tenant-credentials", moduleId: "access", kind: "section" as const, label: "Tenant credentials", order: 70 }, - { id: "access.admin.tenant-api-keys", moduleId: "access", kind: "section" as const, label: "Tenant API keys", order: 80 }, - { id: "access.admin.group-credentials", moduleId: "access", kind: "section" as const, label: "Group credentials", order: 30 }, - { id: "access.admin.user-credentials", moduleId: "access", kind: "section" as const, label: "User credentials", order: 30 }, - { id: "access.settings.credentials", moduleId: "access", kind: "section" as const, label: "Personal credentials", order: 30 } + { id: "access.admin.system-roles", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.system_roles.a9461aa6", order: 20 }, + { id: "access.admin.system-users", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.central_users.91ac1b51", order: 50 }, + { id: "access.admin.system-credentials", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.system_credentials.4af2c023", order: 80 }, + { id: "access.admin.tenant-roles", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.tenant_roles.51aca82d", order: 10 }, + { id: "access.admin.tenant-function-mappings", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.function_role_mappings.2b64e9c3", order: 20 }, + { id: "access.admin.tenant-groups", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.tenant_groups.47e6cc05", order: 30 }, + { id: "access.admin.tenant-users", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.tenant_users.cb800b38", order: 40 }, + { id: "access.admin.tenant-credentials", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.tenant_credentials.4af2c024", order: 70 }, + { id: "access.admin.tenant-api-keys", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.tenant_api_keys.4b1d81f8", order: 80 }, + { id: "access.admin.group-credentials", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.group_credentials.4af2c025", order: 30 }, + { id: "access.admin.user-credentials", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.user_credentials.4af2c026", order: 30 }, + { id: "access.settings.credentials", moduleId: "access", kind: "section" as const, label: "i18n:govoplan-access.reusable_credentials.4af2c022", order: 30 } ]; function renderAdminRoute({ settings, auth, onAuthChange }: PlatformRouteContext) { @@ -36,7 +36,7 @@ function renderAdminRoute({ settings, auth, onAuthChange }: PlatformRouteContext export const accessModule: PlatformWebModule = { id: "access", label: "i18n:govoplan-access.access.2f81a22d", - version: "1.0.0", + version: "0.1.11", translations, viewSurfaces: accessAdminSurfaces, navItems: [