1 Commits
Author SHA1 Message Date
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
8 changed files with 42 additions and 24 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/organizations-webui",
"version": "0.1.20",
"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.20"
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.20"
__version__ = "0.1.21"
+16 -1
View File
@@ -144,7 +144,7 @@ def _organizations_dsar_provider(context: ModuleContext) -> object:
manifest = ModuleManifest(
id="organizations",
name="Organizations",
version="0.1.20",
version="0.1.21",
required_capabilities=(
CAPABILITY_AUTH_PRINCIPAL_RESOLVER,
CAPABILITY_AUTH_PERMISSION_EVALUATOR,
@@ -265,6 +265,21 @@ manifest = ModuleManifest(
),
},
documentation=(
DocumentationTopic(
id="organizations.workspace-layout",
title="Organizations workspace layout",
summary="Find workspace actions and read consistently arranged content.",
body="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": "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",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@govoplan/organizations-webui",
"version": "0.1.20",
"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>
@@ -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;
}) {
@@ -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>