2 Commits
Author SHA1 Message Date
zemion ecfcdd1fea fix(webui): bind record lifecycle controls to help
Module Package Release / publish-packages (push) Successful in 12s
2026-08-24 11:36:43 +02:00
zemion 312a8e281e docs(records): complete German reference coverage
Module Package Release / publish-packages (push) Successful in 11s
2026-08-23 19:31:24 +02:00
7 changed files with 47 additions and 9 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/records", "name": "@govoplan/records",
"version": "0.1.20", "version": "0.1.22",
"private": true, "private": true,
"description": "GovOPlaN eAkte and institutional records module.", "description": "GovOPlaN eAkte and institutional records module.",
"type": "module", "type": "module",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "govoplan-records" name = "govoplan-records"
version = "0.1.20" version = "0.1.22"
description = "GovOPlaN eAkte and institutional records module." description = "GovOPlaN eAkte and institutional records module."
readme = "README.md" readme = "README.md"
requires-python = ">=3.12" requires-python = ">=3.12"
+25 -1
View File
@@ -16,6 +16,7 @@ from govoplan_core.core.module_guards import (
) )
from govoplan_core.core.modules import ( from govoplan_core.core.modules import (
CapabilityDocumentation, CapabilityDocumentation,
DocumentationCondition,
DocumentationLink, DocumentationLink,
DocumentationTopic, DocumentationTopic,
FrontendModule, FrontendModule,
@@ -52,7 +53,7 @@ from govoplan_records.backend.archive import (
MODULE_ID = "records" MODULE_ID = "records"
MODULE_NAME = "Records" MODULE_NAME = "Records"
MODULE_VERSION = "0.1.20" MODULE_VERSION = "0.1.22"
READ_SCOPE = "records:workspace:read" READ_SCOPE = "records:workspace:read"
WRITE_SCOPE = "records:workspace:write" WRITE_SCOPE = "records:workspace:write"
ADMIN_SCOPE = "records:workspace:admin" ADMIN_SCOPE = "records:workspace:admin"
@@ -222,11 +223,31 @@ DOCUMENTATION = (
), ),
), ),
metadata={ metadata={
"kind": "reference",
"help_contexts": [ "help_contexts": [
"records.data-subject-requests", "records.data-subject-requests",
"records.lifecycle", "records.lifecycle",
], ],
}, },
translations={
"de": {
"title": "Datenschutzanfragen zur eAkte",
"summary": (
"Minimierte Aktenmetadaten ausgeben und dabei Aufbewahrungs-, Sperr-, Aussonderungs- und Archivnachweise bewahren."
),
"body": (
"Records gleicht innerhalb eines Mandanten exakte Verweise auf Akte, Revision, Eintrag, Band, Chronologie, "
"Sperre, Aussonderung, Übergabepaket und anbietergeführte Quellen ab. Konto-, Identitäts- und "
"Mitgliedschaftsselektoren zeigen Verantwortungszuordnungen von Beschäftigten, bedeuten aber niemals, dass die "
"beschäftigte Person Gegenstand einer Akte ist. Eine Auskunft zur aktenbetroffenen Person erfordert deshalb eine "
"führende Akten- oder Quellverknüpfung. Die Ausgabe enthält begrenzte Metadaten zu Akte, Veraktung, Chronologie, "
"Sperre, Aussonderung und Übergabe. Quellinhalt, Schnappschüsse, Suchtext, undurchsichtige Kontexte und Nutzdaten, "
"Prüfsummen, Wiederholungsschlüssel, Start-URLs, Archivmanifeste und -belege, Genehmigungskennungen und fremde "
"Akten bleiben ausgeschlossen. Unveränderliche Nachweise werden aufbewahrt. Der aktuelle Aktenfakt erhält eine "
"nicht ausführbare manuelle Prüfung und kann nur über den gesteuerten eAkte-Lebenszyklus verändert werden."
),
}
},
), ),
DocumentationTopic( DocumentationTopic(
id="records.workspace", id="records.workspace",
@@ -243,6 +264,7 @@ DOCUMENTATION = (
layer="configured", layer="configured",
documentation_types=("admin", "user"), documentation_types=("admin", "user"),
audience=("user", "records_manager", "operator", "module_admin", "auditor"), audience=("user", "records_manager", "operator", "module_admin", "auditor"),
conditions=(DocumentationCondition(required_scopes=(READ_SCOPE,)),),
order=100, order=100,
related_modules=OPTIONAL_DEPENDENCIES, related_modules=OPTIONAL_DEPENDENCIES,
links=( links=(
@@ -267,6 +289,7 @@ DOCUMENTATION = (
} }
}, },
metadata={ metadata={
"kind": "workflow",
"help_contexts": [ "help_contexts": [
"records.workspace", "records.workspace",
"records.file-plan", "records.file-plan",
@@ -329,6 +352,7 @@ DOCUMENTATION = (
} }
}, },
metadata={ metadata={
"kind": "reference",
"help_contexts": [ "help_contexts": [
"records.action.file", "records.action.file",
"records.field.source-module", "records.field.source-module",
+14
View File
@@ -0,0 +1,14 @@
from __future__ import annotations
from govoplan_records.backend.manifest import manifest
def test_records_german_workflow_and_reference_are_complete() -> None:
topics = {topic.id: topic for topic in manifest.documentation}
assert len(topics) == 5
for topic in topics.values():
assert all(topic.translations["de"].get(key) for key in ("title", "summary", "body"))
workflow = topics["records.workspace"]
assert workflow.metadata["kind"] == "workflow"
assert workflow.conditions
assert topics["records.filing"].metadata["kind"] == "reference"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@govoplan/records-webui", "name": "@govoplan/records-webui",
"version": "0.1.20", "version": "0.1.22",
"private": true, "private": true,
"type": "module", "type": "module",
"main": "src/index.ts", "main": "src/index.ts",
@@ -162,7 +162,7 @@ export function RecordCatalogDialog({
<FormField label={kind === "file-plan" ? "Node ID" : "Class ID"}><input value={identifier} onChange={(event) => setIdentifier(event.target.value)} disabled={Boolean(selectedId)} required /></FormField> <FormField label={kind === "file-plan" ? "Node ID" : "Class ID"}><input value={identifier} onChange={(event) => setIdentifier(event.target.value)} disabled={Boolean(selectedId)} required /></FormField>
<FormField label={kind === "file-plan" ? "Code" : "Key"}><input value={code} onChange={(event) => setCode(event.target.value)} required /></FormField> <FormField label={kind === "file-plan" ? "Code" : "Key"}><input value={code} onChange={(event) => setCode(event.target.value)} required /></FormField>
<FormField label="Label"><input value={label} onChange={(event) => setLabel(event.target.value)} required /></FormField> <FormField label="Label"><input value={label} onChange={(event) => setLabel(event.target.value)} required /></FormField>
{kind === "file-plan" ? <FormField label="Parent node"><select value={parentId} onChange={(event) => setParentId(event.target.value)}><option value="">Top level</option>{catalog.file_plan.filter((item) => item.node_id !== identifier).map((item) => <option key={item.node_id} value={item.node_id}>{item.code} · {item.label}</option>)}</select></FormField> : <><FormField label="File-plan node"><select value={filePlanNodeId} onChange={(event) => setFilePlanNodeId(event.target.value)} required>{catalog.file_plan.map((item) => <option key={item.node_id} value={item.node_id}>{item.code} · {item.label}</option>)}</select></FormField><FormField label="Retention period (days)"><input type="number" min="0" value={retentionDays} onChange={(event) => setRetentionDays(event.target.value)} /></FormField><FormField label="Retention trigger"><select value={closureTrigger} onChange={(event) => setClosureTrigger(event.target.value)}><option value="record_closed">Record closure</option><option value="calendar_month_end">End of closure month</option><option value="calendar_year_end">End of closure year</option><option value="explicit">Explicit date at closure</option></select></FormField></>} {kind === "file-plan" ? <FormField label="Parent node"><select value={parentId} onChange={(event) => setParentId(event.target.value)}><option value="">Top level</option>{catalog.file_plan.filter((item) => item.node_id !== identifier).map((item) => <option key={item.node_id} value={item.node_id}>{item.code} · {item.label}</option>)}</select></FormField> : <><FormField label="File-plan node"><select value={filePlanNodeId} onChange={(event) => setFilePlanNodeId(event.target.value)} required>{catalog.file_plan.map((item) => <option key={item.node_id} value={item.node_id}>{item.code} · {item.label}</option>)}</select></FormField><FormField label="Retention period (days)" helpContextId="records.field.retention-trigger" helpModuleId="records"><input type="number" min="0" value={retentionDays} onChange={(event) => setRetentionDays(event.target.value)} /></FormField><FormField label="Retention trigger" helpContextId="records.field.retention-trigger" helpModuleId="records"><select value={closureTrigger} onChange={(event) => setClosureTrigger(event.target.value)}><option value="record_closed">Record closure</option><option value="calendar_month_end">End of closure month</option><option value="calendar_year_end">End of closure year</option><option value="explicit">Explicit date at closure</option></select></FormField></>}
<FormField label="Description"><textarea rows={4} value={description} onChange={(event) => setDescription(event.target.value)} /></FormField> <FormField label="Description"><textarea rows={4} value={description} onChange={(event) => setDescription(event.target.value)} /></FormField>
</DialogForm> </DialogForm>
</Dialog> </Dialog>
@@ -258,10 +258,10 @@ export function RecordLifecyclePanel({
{availableActions.canReopen && <Button type="button" variant="ghost" onClick={() => openAction("reopen")}><RotateCcw size={16} aria-hidden="true" /> Reopen</Button>} {availableActions.canReopen && <Button type="button" variant="ghost" onClick={() => openAction("reopen")}><RotateCcw size={16} aria-hidden="true" /> Reopen</Button>}
{availableActions.canAppraise && <Button type="button" variant="ghost" onClick={() => openAction("appraise")}><CheckCheck size={16} aria-hidden="true" /> Appraise</Button>} {availableActions.canAppraise && <Button type="button" variant="ghost" onClick={() => openAction("appraise")}><CheckCheck size={16} aria-hidden="true" /> Appraise</Button>}
<Button type="button" variant="ghost" onClick={() => openAction("hold")} disabledReason={!canWrite ? "Your account may not apply a hold." : undefined}><LockKeyhole size={16} aria-hidden="true" /> Apply hold</Button> <Button type="button" variant="ghost" onClick={() => openAction("hold")} disabledReason={!canWrite ? "Your account may not apply a hold." : undefined}><LockKeyhole size={16} aria-hidden="true" /> Apply hold</Button>
{availableActions.canPropose && <Button type="button" variant="primary" onClick={() => openAction("disposition")} disabledReason={activeHolds.length ? "Release all active holds first." : undefined}><ArchiveRestore size={16} aria-hidden="true" /> Propose disposition</Button>} {availableActions.canPropose && <Button type="button" variant="primary" helpContextId="records.lifecycle.disposition" helpModuleId="records" onClick={() => openAction("disposition")} disabledReason={activeHolds.length ? "Release all active holds first." : undefined}><ArchiveRestore size={16} aria-hidden="true" /> Propose disposition</Button>}
{availableActions.canFinalize && <Button type="button" variant="primary" onClick={() => openAction("finalize")} disabledReason={activeHolds.length ? "Release all active holds first." : undefined}><CheckCheck size={16} aria-hidden="true" /> Finalize approved disposition</Button>} {availableActions.canFinalize && <Button type="button" variant="primary" helpContextId="records.lifecycle.finalize" helpModuleId="records" onClick={() => openAction("finalize")} disabledReason={activeHolds.length ? "Release all active holds first." : undefined}><CheckCheck size={16} aria-hidden="true" /> Finalize approved disposition</Button>}
{currentDisposition && ["review_pending", "review_unavailable"].includes(currentDisposition.status) && <Button type="button" variant="ghost" onClick={() => openAction("withdraw")}><Ban size={16} aria-hidden="true" /> Withdraw proposal</Button>} {currentDisposition && ["review_pending", "review_unavailable"].includes(currentDisposition.status) && <Button type="button" variant="ghost" helpContextId="records.lifecycle.disposition" helpModuleId="records" onClick={() => openAction("withdraw")}><Ban size={16} aria-hidden="true" /> Withdraw proposal</Button>}
{availableActions.canPrepare && <Button type="button" variant="primary" onClick={() => openAction("prepare-transfer")} disabledReason={healthyProviders.length === 0 ? "No healthy archive profile is available." : activeHolds.length ? "Release all active holds first." : undefined}><Box size={16} aria-hidden="true" /> Prepare transfer</Button>} {availableActions.canPrepare && <Button type="button" variant="primary" helpContextId="records.lifecycle.prepare-transfer" helpModuleId="records" onClick={() => openAction("prepare-transfer")} disabledReason={healthyProviders.length === 0 ? "No healthy archive profile is available." : activeHolds.length ? "Release all active holds first." : undefined}><Box size={16} aria-hidden="true" /> Prepare transfer</Button>}
{availableActions.canDispatch && <Button type="button" variant="primary" onClick={() => openAction("dispatch-transfer")} disabledReason={!currentPackage?.simulated ? "Real archive dispatch needs a target-specific recovery profile." : activeHolds.length ? "Release all active holds first." : undefined}><Play size={16} aria-hidden="true" /> Run simulation</Button>} {availableActions.canDispatch && <Button type="button" variant="primary" onClick={() => openAction("dispatch-transfer")} disabledReason={!currentPackage?.simulated ? "Real archive dispatch needs a target-specific recovery profile." : activeHolds.length ? "Release all active holds first." : undefined}><Play size={16} aria-hidden="true" /> Run simulation</Button>}
{canAdmin && <Button type="button" variant="ghost" onClick={openRecovery}><ArchiveRestore size={16} aria-hidden="true" /> Recovery evidence</Button>} {canAdmin && <Button type="button" variant="ghost" onClick={openRecovery}><ArchiveRestore size={16} aria-hidden="true" /> Recovery evidence</Button>}
</div> </div>