3 Commits
Author SHA1 Message Date
zemion c067be36c1 fix(ui): align contextual documentation with headings
Verified with the coordinated workspace changes by devkit full run
2026-09-08T225814-186389-0000-3e3ed7cd (all seven phases passed).
This shared UI pass does not mark the individual module reviews complete.
2026-09-09 02:03:53 +02:00
zemion 58ebf2c7e6 Release govoplan-organizations v0.1.21: unify interface contracts and documentation
Module Package Release / publish-packages (push) Successful in 12s
2026-09-08 01:32:48 +02:00
zemion b23e6e4eee fix(webui): bind organization retention to help
Module Package Release / publish-packages (push) Successful in 11s
2026-08-24 11:36:41 +02:00
10 changed files with 63 additions and 33 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/organizations-webui",
"version": "0.1.19",
"version": "0.1.21",
"private": true,
"type": "module",
"main": "webui/src/index.ts",
@@ -19,7 +19,7 @@
"LICENSE"
],
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"@govoplan/core-webui": "^0.1.45",
"@vitejs/plugin-react": "^5.2.0",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
+2 -2
View File
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
[project]
name = "govoplan-organizations"
version = "0.1.19"
version = "0.1.21"
description = "GovOPlaN organizational model module."
readme = "README.md"
requires-python = ">=3.12"
authors = [{ name = "GovOPlaN" }]
dependencies = [
"govoplan-core>=0.1.18",
"govoplan-core>=0.1.45",
"govoplan-tenancy>=0.1.18",
]
+1 -1
View File
@@ -1,3 +1,3 @@
"""GovOPlaN organizations module."""
__version__ = "0.1.19"
__version__ = "0.1.21"
+22 -1
View File
@@ -144,7 +144,7 @@ def _organizations_dsar_provider(context: ModuleContext) -> object:
manifest = ModuleManifest(
id="organizations",
name="Organizations",
version="0.1.19",
version="0.1.21",
required_capabilities=(
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
@@ -265,6 +265,27 @@ manifest = ModuleManifest(
),
},
documentation=(
DocumentationTopic(
id="organizations.workspace-layout",
title="Organizations workspace layout",
summary="Find workspace actions and read consistently arranged content.",
body="Documentation books sit immediately beside the visible heading or contextual label for "
"Organizations, organization settings and record dialogs, not among operational action "
"buttons. Field help remains beside its label. "
"Organization model tables use the complete card width; headings and creation actions stay in the card header, while the shared DataGrid owns filtering, sizing, pagination, and row actions. Unit types, structures, relation types, function types, units, relations, and functions follow the same layout. Model upgrades keep their meaningful version and decision context separate from the result table. Administrators and configurators must use Core's table-card layout instead of local padding or width overrides. The change does not modify model permissions, effective dates, or template-copy semantics.",
layer="static",
documentation_types=("user", "admin"),
audience=("user", "module_admin", "operator"),
order=5,
translations={"de": {
"title": "Organisationen: Aufbau des Arbeitsbereichs",
"summary": "Arbeitsbereichsaktionen finden und einheitlich angeordnete Inhalte lesen.",
"body": "Dokumentationsbücher stehen unmittelbar neben der sichtbaren Überschrift oder "
"Kontextbezeichnung für Organisationen, Organisationseinstellungen und Eintragsdialoge, nicht "
"zwischen ausführbaren Aktionsschaltflächen. Feldhilfe bleibt neben der Feldbezeichnung. "
"Tabellen des Organisationsmodells nutzen die gesamte Kartenbreite. Überschrift und Anlegen-Aktionen bleiben im Kartenkopf; das gemeinsame DataGrid übernimmt Filter, Größe, Seitennavigation und Zeilenaktionen. Einheitentypen, Strukturen, Beziehungstypen, Funktionstypen, Einheiten, Beziehungen und Funktionen folgen demselben Layout. Modell-Upgrades behalten ihren relevanten Versions- und Entscheidungskontext getrennt von der Ergebnistabelle. Administratoren und Konfiguratoren verwenden Cores Tabellenkartenlayout statt lokaler Innenabstands- oder Breitenregeln. Rechte, Gültigkeitsdaten und das Kopierprinzip der Vorlagen ändern sich dadurch nicht.",
}},
),
DocumentationTopic(
id="organizations.privacy.data-subject-requests",
title="Review Organizations data in a data-subject request",
@@ -45,7 +45,13 @@ class OrganizationsInterfaceDocumentationContractTests(unittest.TestCase):
def test_german_reference_documentation_is_complete(self) -> None:
topics = manifest.documentation
self.assertEqual(4, len(topics))
self.assertEqual({
"organizations.workspace-layout",
"organizations.privacy.data-subject-requests",
"organizations.template-upgrades",
"organizations.model",
"organizations.reference.fields-and-consequences",
}, {topic.id for topic in topics})
for topic in topics:
translation = topic.translations.get("de", {})
self.assertTrue(translation.get("title"), topic.id)
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/organizations-webui",
"version": "0.1.19",
"version": "0.1.21",
"private": true,
"type": "module",
"main": "src/index.ts",
@@ -19,7 +19,7 @@
"./styles/organizations.css": "./src/styles/organizations.css"
},
"peerDependencies": {
"@govoplan/core-webui": "^0.1.18",
"@govoplan/core-webui": "^0.1.45",
"@vitejs/plugin-react": "^5.2.0",
"lucide-react": "^1.23.0",
"react": ">=19.2.7 <20",
@@ -17,7 +17,8 @@ const translations = source("../src/i18n/generatedTranslations.ts");
assert(page.includes("DocumentationHelpLink") && settings.includes("DocumentationHelpLink"), "Workspace and tenant settings expose contextual documentation");
assert(page.includes("ActionBlockerHint") && settings.includes("ActionBlockerHint"), "Read-only states identify action, actor, and destination");
assert(page.includes("disabledReason") && settings.includes("disabledReason"), "Unavailable organization actions explain their state");
assert(page.includes("requestDiscard(() => void loadModel())") && settings.includes("requestDiscard(() => void load())"), "Reload preserves dirty organization drafts and settings");
assert(page.includes("<PageActionBar") && page.includes("reloadAction={{") && page.includes("onReload: () => void loadModel()") && settings.includes("requestDiscard(() => void load())"), "Reload uses the guarded semantic page action or the settings draft guard");
assert((page.match(/<Card bodyLayout="table"/g) ?? []).length === 7, "All seven organization collection tables use the shared edge-to-edge card body");
assert(page.includes("hasDirtyDraft ? requestDiscard(discardDrafts)"), "Editor close uses the shared unsaved-change guard");
assert(page.includes("ORGANIZATIONS_FIELD_DOCUMENTATION"), "Model fields link to stable consequence documentation");
assert(patterns.includes('topicId: "organizations.model"') && patterns.includes('topicId: "organizations.reference.fields-and-consequences"'), "Organizations uses manifest-backed help references");
@@ -178,8 +178,9 @@ export default function OrganizationTemplateUpgradePanel({ settings, auth }: { s
return <>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{success && <DismissibleAlert tone="success" resetKey={success}>{success}</DismissibleAlert>}
<Card title="Organization template upgrades" actions={<Button onClick={() => void load()} disabled={loading || busy}><RefreshCw aria-hidden="true" /> Reload</Button>}>
<Card bodyLayout="table" title="Organization template upgrades" actions={<Button onClick={() => void load()} disabled={loading || busy}><RefreshCw aria-hidden="true" /> Reload</Button>}>
<LoadingFrame loading={loading} label="Loading organization template upgrade state">
<div className="content-pad">
<MetricGrid density="compact">
<MetricCard label="Applied version" value={currentVersion?.version ?? "Custom model"} tone="info" />
<MetricCard label="Available upgrades" value={targets.length} tone={targets.length ? "info" : "good"} />
@@ -191,6 +192,7 @@ export default function OrganizationTemplateUpgradePanel({ settings, auth }: { s
<FormField label="Published target version"><select value={targetVersionId} disabled={!canWrite || busy || !targets.length} onChange={(event) => setTargetVersionId(event.target.value)}>{targets.length ? targets.map((version) => <option key={version.id} value={version.id}>{templateVersionLabel(templates, version)}</option>) : <option value="">No newer published version</option>}</select></FormField>
<Button variant="primary" onClick={() => void createPreview()} disabled={!canWrite || busy || !targetVersionId}><GitCompareArrows aria-hidden="true" /> Create preview</Button>
</ActionToolbar>
</div>
<div className="organization-upgrade-table"><DataGrid id="organization-model-upgrades" rows={upgrades} columns={upgradeColumns} initialFit="container" getRowKey={(row) => row.id} emptyText={current ? "No organization template upgrades have been previewed." : "This tenant model was not instantiated from a system template."} /></div>
</LoadingFrame>
</Card>
@@ -111,7 +111,7 @@ export default function OrganizationsAdminPanel({ settings, auth }: { settings:
return (
<AdminPageLayout
title="i18n:govoplan-organizations.organization_settings.c9ab9829"
title="i18n:govoplan-organizations.organization_settings.c9ab9829" titleHelp={<DocumentationHelpLink reference={ORGANIZATIONS_DOCUMENTATION} />}
description="i18n:govoplan-organizations.organization_settings_description.35dc9f10"
loading={loading}
loadingLabel="i18n:govoplan-organizations.loading_organization_settings.c6008db8"
@@ -119,7 +119,7 @@ export default function OrganizationsAdminPanel({ settings, auth }: { settings:
success={success}
actions={(
<>
<DocumentationHelpLink reference={ORGANIZATIONS_DOCUMENTATION} />
<Button
onClick={() => requestDiscard(() => void load())}
disabled={Boolean(reloadDisabledReason)}
@@ -190,6 +190,8 @@ export default function OrganizationsAdminPanel({ settings, auth }: { settings:
label="i18n:govoplan-organizations.change_retention_days.71bbd140"
help={organizationWriteReason(canWrite, busy, "settings")}
documentation={ORGANIZATIONS_FIELD_DOCUMENTATION}
helpContextId="organizations.field.audit-retention"
helpModuleId="organizations"
>
<input
type="number"
@@ -1,5 +1,5 @@
import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent } from "react";
import { Edit3, Plus, RefreshCw } from "lucide-react";
import { useCallback, useEffect, useMemo, useRef, useState, type FormEvent, type JSX } from "react";
import { Edit3, Plus } from "lucide-react";
import { FormLayout, ActionToolbar,
ActionBlockerHint,
AdminIconButton,
@@ -282,7 +282,7 @@ function mapById<T extends { id: string }>(items: T[]): Map<string, T> {
function sectionGroupsFor(sections: ReadonlySet<OrganizationSection>): ModuleSubnavGroup<OrganizationSection>[] {
return SECTION_GROUPS.map((group) => ({
...group,
items: group.items.filter((item) => sections.has(item.id))
items: group.items.filter((item) => "id" in item && sections.has(item.id))
})).filter((group) => group.items.length > 0);
}
@@ -309,14 +309,14 @@ function activeStatus(active: boolean): JSX.Element {
return <StatusBadge status={active ? "success" : "inactive"} label={active ? "i18n:govoplan-organizations.active.a733b809" : "i18n:govoplan-organizations.inactive.09af574c"} />;
}
function SluggedFields({
function SluggedFields<T extends SluggedDraft>({
draft,
onChange,
disabled,
disabledReason
}: {
draft: SluggedDraft;
onChange: (next: SluggedDraft) => void;
draft: T;
onChange: (next: T) => void;
disabled: boolean;
disabledReason?: string;
}) {
@@ -1002,7 +1002,7 @@ export default function OrganizationsPage({
<>
<PageLayout
archetype="workspace"
title={title}
title={title} titleHelp={<DocumentationHelpLink reference={ORGANIZATIONS_DOCUMENTATION} />}
description={description}
actions={<PageActionBar
variant="workspace"
@@ -1013,7 +1013,7 @@ export default function OrganizationsPage({
disabledReason: reloadDisabledReason,
title: "i18n:govoplan-organizations.reload.cce71553"
}}
helpAction={<DocumentationHelpLink reference={ORGANIZATIONS_DOCUMENTATION} />}
/>}
mode={mode === "workspace" ? "workspace" : "embedded"}
headerLoading={loading}
@@ -1066,7 +1066,7 @@ export default function OrganizationsPage({
)}
primaryLabel="i18n:govoplan-organizations.organization_model.5945c48a"
contentLabel={title}
documentationType={mode === "admin" ? "admin" : "user"}
documentationType="user"
>
{content}
</WorkspaceLayout>
@@ -1075,16 +1075,16 @@ export default function OrganizationsPage({
function renderModelSection() {
return (
<div className="organizations-table-stack">
<Card title="i18n:govoplan-organizations.unit_types.c7afc174" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit_type.58f2c05a" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} onClick={openUnitTypeCreate} />}>
<Card bodyLayout="table" title="i18n:govoplan-organizations.unit_types.c7afc174" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit_type.58f2c05a" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} onClick={openUnitTypeCreate} />}>
<DataGrid id="organizations-unit-types" rows={model.unit_types} columns={unitTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_unit_types_found.c81fb2a7" initialFit="container" />
</Card>
<Card title="i18n:govoplan-organizations.structures.f9b7f3b4" actions={<AdminIconButton label="i18n:govoplan-organizations.add_structure.b722042a" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} onClick={openStructureCreate} />}>
<Card bodyLayout="table" title="i18n:govoplan-organizations.structures.f9b7f3b4" actions={<AdminIconButton label="i18n:govoplan-organizations.add_structure.b722042a" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} onClick={openStructureCreate} />}>
<DataGrid id="organizations-structures" rows={model.structures} columns={structureColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_structures_found.20b382d0" initialFit="container" />
</Card>
<Card title="i18n:govoplan-organizations.relation_types.e5890528" actions={<AdminIconButton label="i18n:govoplan-organizations.add_relation_type.2ad19d03" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} onClick={openRelationTypeCreate} />}>
<Card bodyLayout="table" title="i18n:govoplan-organizations.relation_types.e5890528" actions={<AdminIconButton label="i18n:govoplan-organizations.add_relation_type.2ad19d03" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} onClick={openRelationTypeCreate} />}>
<DataGrid id="organizations-relation-types" rows={model.relation_types} columns={relationTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_relation_types_found.6f90bb81" initialFit="container" />
</Card>
<Card title="i18n:govoplan-organizations.function_types.172c01fe" actions={<AdminIconButton label="i18n:govoplan-organizations.add_function_type.90d793f1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} onClick={openFunctionTypeCreate} />}>
<Card bodyLayout="table" title="i18n:govoplan-organizations.function_types.172c01fe" actions={<AdminIconButton label="i18n:govoplan-organizations.add_function_type.90d793f1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteModel || busy} disabledReason={organizationWriteReason(canWriteModel, busy, "model")} onClick={openFunctionTypeCreate} />}>
<DataGrid id="organizations-function-types" rows={model.function_types} columns={functionTypeColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_function_types_found.5eef31b1" initialFit="container" />
</Card>
</div>
@@ -1129,7 +1129,7 @@ export default function OrganizationsPage({
/>
) : <p className="muted small-note">i18n:govoplan-organizations.no_units_found.eea2dd0c</p>}
</Card>
<Card title="i18n:govoplan-organizations.units.e14d0d92" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit.8fa12fb1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} disabledReason={organizationWriteReason(canWriteUnits, busy, "unit")} onClick={() => openUnitCreate()} />}>
<Card bodyLayout="table" title="i18n:govoplan-organizations.units.e14d0d92" actions={<AdminIconButton label="i18n:govoplan-organizations.add_unit.8fa12fb1" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} disabledReason={organizationWriteReason(canWriteUnits, busy, "unit")} onClick={() => openUnitCreate()} />}>
<DataGrid id="organizations-units" rows={model.units} columns={unitColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_units_found.eea2dd0c" initialFit="container" />
</Card>
</div>
@@ -1139,7 +1139,7 @@ export default function OrganizationsPage({
function renderRelationsSection() {
return (
<div className="organizations-table-stack">
<Card title="i18n:govoplan-organizations.relations.1c796711" actions={<AdminIconButton label="i18n:govoplan-organizations.add_relation.6b6e67ea" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} disabledReason={organizationWriteReason(canWriteUnits, busy, "unit")} onClick={openRelationCreate} />}>
<Card bodyLayout="table" title="i18n:govoplan-organizations.relations.1c796711" actions={<AdminIconButton label="i18n:govoplan-organizations.add_relation.6b6e67ea" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteUnits || busy} disabledReason={organizationWriteReason(canWriteUnits, busy, "unit")} onClick={openRelationCreate} />}>
<DataGrid id="organizations-relations" rows={model.relations} columns={relationColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_relations_found.4e75b11d" initialFit="container" />
</Card>
</div>
@@ -1149,7 +1149,7 @@ export default function OrganizationsPage({
function renderFunctionsSection() {
return (
<div className="organizations-table-stack">
<Card title="i18n:govoplan-organizations.functions.805dc49b" actions={<AdminIconButton label="i18n:govoplan-organizations.add_function.6abafee0" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteFunctions || busy} disabledReason={organizationWriteReason(canWriteFunctions, busy, "function")} onClick={() => openFunctionCreate()} />}>
<Card bodyLayout="table" title="i18n:govoplan-organizations.functions.805dc49b" actions={<AdminIconButton label="i18n:govoplan-organizations.add_function.6abafee0" icon={<Plus size={16} aria-hidden="true" />} variant="primary" disabled={!canWriteFunctions || busy} disabledReason={organizationWriteReason(canWriteFunctions, busy, "function")} onClick={() => openFunctionCreate()} />}>
<DataGrid id="organizations-functions" rows={model.functions} columns={functionColumns} getRowKey={(row) => row.id} emptyText="i18n:govoplan-organizations.no_functions_found.54e759a0" initialFit="container" />
</Card>
</div>
@@ -1226,7 +1226,7 @@ export default function OrganizationsPage({
return (
<Dialog variant="administration" size="wide"
open={Boolean(activeEditor)}
title={editorTitle()}
title={editorTitle()} titleHelp={<DocumentationHelpLink reference={ORGANIZATIONS_FIELD_DOCUMENTATION} />}
onClose={() => {
if (busy) return;
if (hasDirtyDraft) requestDiscard(discardDrafts);
@@ -1256,9 +1256,7 @@ export default function OrganizationsPage({
</>
)}
>
<div className="button-row organizations-dialog-help">
<DocumentationHelpLink reference={ORGANIZATIONS_FIELD_DOCUMENTATION} />
</div>
<FormLayout columns={2} gap="small" collapseAt="workspace" id={formId} className="" onSubmit={(event) => void submit(event)}>
{renderEditorFields()}
</FormLayout>