From 11598b7b5b89e1794c2d4e6c9f1fa547b77be822 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Wed, 9 Sep 2026 02:03:36 +0200 Subject: [PATCH] fix(ui): align campaign tables, recipient sizing and contextual help 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. --- src/govoplan_campaign/backend/manifest.py | 62 +++++++++++++++++++ tests/test_documentation_contract.py | 12 ++++ .../campaigns/AttachmentsDataPage.tsx | 8 ++- .../features/campaigns/CampaignAuditPage.tsx | 15 +++-- .../features/campaigns/RecipientDataPage.tsx | 11 ++-- .../recipients/recipientProfileColumns.tsx | 11 ++-- .../features/reports/AggregateReportsPage.tsx | 2 +- .../aggregate-report-ui-structure.test.mjs | 2 + .../recipient-search-ui-structure.test.mjs | 10 +++ 9 files changed, 108 insertions(+), 25 deletions(-) diff --git a/src/govoplan_campaign/backend/manifest.py b/src/govoplan_campaign/backend/manifest.py index 61e1950..8cc95d2 100644 --- a/src/govoplan_campaign/backend/manifest.py +++ b/src/govoplan_campaign/backend/manifest.py @@ -836,6 +836,68 @@ manifest = ModuleManifest( ), ), documentation=localize_documentation_topics(( + DocumentationTopic( + id="campaigns.module-navigation-and-table-layout", + title="Distinguish module reports from one campaign's report", + summary="Use accurate module breadcrumbs and shared edge-to-edge report and attachment tables.", + body=( + "The Campaign audit documentation book sits immediately to the right of the Recent audit " + "events heading. " + "The Campaigns module has separate Reports (/campaigns/reports) and Operator queue (/campaigns/queue) views. " + "Their breadcrumbs name the module section, not a campaign record. A selected campaign's own report remains under " + "/campaigns/{campaign_id}/report (including its reports alias) with the singular Campaign and Report context. " + "The legacy /operator link still redirects to the queue; report selection query parameters, editor deep links and " + "Quick Access return-to-origin history behavior remain unchanged. These labels grant no extra report or delivery permissions. " + "The Campaign reports available to you and Global Attachments cards use the shared table-body layout: tables reach " + "the card boundary, including while loading, without module-local negative margins. Global Attachments remains collapsible; " + "chooser warnings, row actions, empty-state actions and existing filters remain available. Layout changes do not change " + "recipients, attachment rules or bytes, report privacy suppression, saved data, exports or delivery state. " + "In Recipient data, automatic fitting prefers at most 640/480 pixels for Recipient(s)/Delivery and 360 pixels " + "for configurable fields before distributing spare space. These are starting-layout preferences, not manual " + "resize limits: all three kinds of column can be widened further and reduced again. " + "The table scrolls horizontally while manual resizing keeps the non-resizable Active and Attachments columns " + "at their current widths. These are personal " + "browser layout changes, not campaign autosaves. Recipient and global-attachment grids wait for the initial draft " + "before restoring their saved column layout, so a reload does not replace personal widths with temporary loading columns." + ), + layer="available", + documentation_types=("user", "admin"), + audience=("campaign_manager", "campaign_operator", "campaign_admin"), + order=19, + conditions=(DocumentationCondition(required_modules=("campaigns",), any_scopes=CAMPAIGN_MODULE_REQUIRED_ANY),), + links=( + DocumentationLink(label="Campaigns", href="/campaigns", kind="runtime"), + DocumentationLink(label="Reports", href="/campaigns/reports", kind="runtime"), + DocumentationLink(label="Operator queue", href="/campaigns/queue", kind="runtime"), + ), + metadata={"kind": "reference", "related_topic_ids": ["campaigns.workflow.prepare-validate-and-build"]}, + translations={"de": { + "title": "Modulberichte vom Bericht einer einzelnen Kampagne unterscheiden", + "summary": "Eindeutige Modul-Breadcrumbs sowie gemeinsame, bündige Berichts- und Anhangstabellen verwenden.", + "body": ( + "Das Dokumentationsbuch im Kampagnen-Audit steht unmittelbar rechts neben der Überschrift " + "Letzte Audit-Ereignisse. " + "Das Modul Kampagnen besitzt getrennte Ansichten für Berichte (/campaigns/reports) und die Operator-Warteschlange " + "(/campaigns/queue). Ihre Breadcrumbs benennen den Modulbereich und keinen Kampagnendatensatz. Der Bericht einer " + "ausgewählten Kampagne bleibt unter /campaigns/{campaign_id}/report (einschließlich des reports-Alias) im Kontext " + "Kampagne und Bericht. Der bisherige Link /operator leitet weiterhin zur Warteschlange um; Auswahlparameter für " + "Berichte, Editor-Deep-Links und die Rückkehr zum Ursprung über Quick Access bleiben unverändert. Die Beschriftung " + "vergibt keine zusätzlichen Berichts- oder Versandberechtigungen. Die Karten für verfügbare Kampagnenberichte und " + "globale Anhänge verwenden das gemeinsame Tabellenlayout: Tabellen reichen auch beim Laden bis an den Kartenrand, " + "ohne negative modulspezifische Abstände. Globale Anhänge bleiben einklappbar; Auswahlwarnungen, Zeilenaktionen, " + "Aktionen für leere Tabellen und vorhandene Filter bleiben verfügbar. Das Layout ändert weder Empfänger, " + "Anhangsregeln oder Bytes noch Datenschutzunterdrückung, gespeicherte Daten, Exporte oder Versandzustand. " + "In den Empfängerdaten bevorzugt die automatische Anpassung zunächst höchstens 640/480 Pixel für Empfänger/Zustellung " + "und 360 Pixel für konfigurierbare Felder, bevor sie freien Platz verteilt. Diese Werte bestimmen nur das " + "Ausgangslayout und begrenzen nicht die manuelle Größenänderung: Alle drei Spaltenarten lassen sich weiter " + "verbreitern und wieder verkleinern. Die Tabelle wird horizontal scrollbar; bei der manuellen Größenänderung " + "behalten die nicht verstellbaren Spalten Aktiv und Anhänge ihre aktuellen Breiten. Dies sind persönliche " + "Browser-Einstellungen und keine automatische Speicherung der Kampagne. " + "Empfänger- und globale Anhangstabellen warten beim ersten Laden auf den Entwurf, bevor sie gespeicherte " + "Spaltenbreiten wiederherstellen. Vorläufige Ladespalten überschreiben dadurch beim Neuladen keine persönlichen Breiten." + ), + }}, + ), DocumentationTopic( id="campaigns.attachment-filename-fidelity", title="Deterministic attachment and ZIP names", diff --git a/tests/test_documentation_contract.py b/tests/test_documentation_contract.py index fb39357..b050c85 100644 --- a/tests/test_documentation_contract.py +++ b/tests/test_documentation_contract.py @@ -5,3 +5,15 @@ def test_static_documentation_has_complete_german_reference_copy() -> None: for topic in get_manifest().documentation: german = topic.translations.get("de", {}) assert all(german.get(field, "").strip() for field in ("title", "summary", "body")), topic.id + + +def test_module_breadcrumb_and_table_layout_contract_is_bilingual_and_non_mutating() -> None: + topic = next(item for item in get_manifest().documentation if item.id == "campaigns.module-navigation-and-table-layout") + assert set(topic.documentation_types) == {"user", "admin"} + assert topic.layer == "available" + for body in (topic.body, topic.translations["de"]["body"]): + for route in ("/campaigns/reports", "/campaigns/queue", "/campaigns/{campaign_id}/report", "/operator"): + assert route in body + assert "Quick Access" in body + assert "report privacy suppression" in topic.body + assert "without module-local negative margins" in topic.body diff --git a/webui/src/features/campaigns/AttachmentsDataPage.tsx b/webui/src/features/campaigns/AttachmentsDataPage.tsx index ef2d58d..0dfddc0 100644 --- a/webui/src/features/campaigns/AttachmentsDataPage.tsx +++ b/webui/src/features/campaigns/AttachmentsDataPage.tsx @@ -444,8 +444,10 @@ export default function AttachmentsDataPage({ settings, auth, campaignId }: {set } - - + {/* The loading draft has no ZIP columns yet. Do not let that + temporary signature overwrite the user's saved table layout. */} + {version && draft && patch(["attachments", "global"], rules)} /> + onChange={(rules) => patch(["attachments", "global"], rules)} />} diff --git a/webui/src/features/campaigns/CampaignAuditPage.tsx b/webui/src/features/campaigns/CampaignAuditPage.tsx index 68baf32..16f2cbe 100644 --- a/webui/src/features/campaigns/CampaignAuditPage.tsx +++ b/webui/src/features/campaigns/CampaignAuditPage.tsx @@ -24,7 +24,13 @@ export default function CampaignAuditPage({ settings, campaignId }: {settings: A reloadAction={{ onReload: () => void reload({ force: true }), loading }} />} > - + }> - ); diff --git a/webui/src/features/campaigns/RecipientDataPage.tsx b/webui/src/features/campaigns/RecipientDataPage.tsx index c5fe01a..ff9df4b 100644 --- a/webui/src/features/campaigns/RecipientDataPage.tsx +++ b/webui/src/features/campaigns/RecipientDataPage.tsx @@ -5,7 +5,6 @@ import { getCampaignPostboxCatalog, listCampaignRecipientAddressSources, listCampaignRecipientDistributionLists, - snapshotCampaignRecipientAddressSource, type CampaignDistributionListExpansion, type CampaignDistributionListSource, type CampaignPostboxCatalog, @@ -41,7 +40,7 @@ import { createAddressSourceImportProvenance } from "./utils/addressSourceImport"; import { addressesFromValue, type MailboxAddress } from "@govoplan/core-webui"; -import { i18nMessage, insertAfter, moveArrayItem, useGuardedNavigate, usePlatformLanguage } from "@govoplan/core-webui"; +import { i18nMessage, insertAfter, moveArrayItem, usePlatformLanguage } from "@govoplan/core-webui"; import AddressSourceImportDialog from "./recipients/AddressSourceImportDialog"; import DistributionListImportDialog from "./recipients/DistributionListImportDialog"; import { @@ -56,11 +55,7 @@ import { entryWithAddressValues, formatAddressCollectionForClipboard, getAddressColumn, - getEntryAddresses, headerAddressValues, - hiddenRecipientAddressMatch, - recipientAddressFilterValue, - recipientAddressSummary, recipientHeaderRows, type AddressFieldKey, type EntryAddressColumn, @@ -563,7 +558,9 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A } - {!source.type && + {/* Mount with the real draft's delivery/attachment/field columns; + an empty loading signature would erase personal column widths. */} + {version && draft && !source.type &&
{ @@ -114,7 +113,7 @@ export function recipientProfileColumns({ settings, campaignId, draft, locked, f id: "delivery", header: "Delivery", width: "minmax(260px, 0.9fr)", - maxWidth: 480, + preferredMaxWidth: 480, resizable: true, filterable: true, render: (entry, index) => { @@ -151,7 +150,7 @@ export function recipientProfileColumns({ settings, campaignId, draft, locked, f Postboxes ({targets.length}) )} - {printTarget.target && ( + {Boolean(printTarget.target) && ( {printTarget.channel === "internal_mail" ? "Internal mail" : "Postal"}: {String(printTarget.target)} @@ -185,13 +184,13 @@ export function recipientProfileColumns({ settings, campaignId, draft, locked, f }, value: (entry) => normalizeAttachmentRules(entry.attachments).map((rule) => `${rule.label ?? ""} ${rule.file_filter ?? ""}`).join(", ") - }] : []), + } satisfies DataGridColumn>] : []), ...fieldDefinitions.filter((field) => field.can_override !== false).map((field): DataGridColumn> => ({ id: `field-${field.name}`, header: field.label || field.name, width: 190, minWidth: 160, - maxWidth: 360, + preferredMaxWidth: 360, resizable: true, sortable: true, filterable: true, diff --git a/webui/src/features/reports/AggregateReportsPage.tsx b/webui/src/features/reports/AggregateReportsPage.tsx index 1fcd7c8..d6d9c38 100644 --- a/webui/src/features/reports/AggregateReportsPage.tsx +++ b/webui/src/features/reports/AggregateReportsPage.tsx @@ -183,7 +183,7 @@ export default function AggregateReportsPage({ settings }: {settings: ApiSetting />} > - + \s*]*bodyLayout="table"[^>]*collapsible>[\s\S]*?\{version && draft &&