From 039ce35e7888e2e90dd23d2ee51a1a0d7b5fac20 Mon Sep 17 00:00:00 2001
From: Albrecht Degering
Date: Wed, 19 Aug 2026 14:26:26 +0200
Subject: [PATCH] Adopt semantic campaign page actions
---
.../backend/documentation.py | 4 ++--
.../campaigns/AttachmentsDataPage.tsx | 18 ++++++++-------
.../features/campaigns/CampaignAuditPage.tsx | 11 +++++----
.../features/campaigns/CampaignFieldsPage.tsx | 16 +++++++------
.../features/campaigns/CampaignJsonView.tsx | 15 ++++++------
.../features/campaigns/CampaignListPage.tsx | 17 +++++++-------
.../campaigns/CampaignOverviewPage.tsx | 20 ++++++++++------
.../features/campaigns/CampaignReportPage.tsx | 15 +++++++-----
.../features/campaigns/GlobalSettingsPage.tsx | 16 +++++++------
.../features/campaigns/MailSettingsPage.tsx | 23 ++++++++++++++-----
.../src/features/campaigns/ReviewSendPage.tsx | 17 +++++++-------
.../features/campaigns/TemplateDataPage.tsx | 18 ++++++++-------
.../components/CampaignDraftPageScaffold.tsx | 16 +++++++------
.../campaigns/wizard/WizardDirectoryPage.tsx | 8 +++----
.../features/operator/OperatorQueuePage.tsx | 11 +++++----
.../features/reports/AggregateReportsPage.tsx | 12 ++++++----
.../src/features/templates/TemplatesPage.tsx | 16 ++++++-------
17 files changed, 147 insertions(+), 106 deletions(-)
diff --git a/src/govoplan_campaign/backend/documentation.py b/src/govoplan_campaign/backend/documentation.py
index c5c2607..aebb628 100644
--- a/src/govoplan_campaign/backend/documentation.py
+++ b/src/govoplan_campaign/backend/documentation.py
@@ -119,7 +119,7 @@ CAMPAIGN_USER_DOCUMENTATION = (
topic_id="campaigns.workflow.create-campaign",
title="Create a campaign",
summary="Start a governed campaign as an editable draft and complete its purpose and ownership before adding delivery data.",
- body="A new campaign starts with one editable working version. Creating it does not grant access to Mail profiles, managed files, address sources, or delivery actions; those remain separately authorized.",
+ body="A new campaign starts with one editable working version. Campaign editors report saved, unsaved, and saving state in the page action bar; Discard remains immediately before Save, and leaving a dirty draft invokes the shared save-or-discard guard. Destructive campaign lifecycle actions are visually separated from ordinary actions. Creating a campaign does not grant access to Mail profiles, managed files, address sources, or delivery actions; those remain separately authorized.",
order=30,
audience=("campaign_manager", "campaign_author"),
required_scopes=("campaigns:campaign:read", "campaigns:campaign:create"),
@@ -131,7 +131,7 @@ CAMPAIGN_USER_DOCUMENTATION = (
"Open Campaigns and select New campaign.",
"Use the creation wizard to enter a clear name, identifier, and purpose.",
"Open the new campaign and confirm its owner before adding recipient or delivery data.",
- "Continue through the preparation sections and save the editable working version.",
+ "Continue through the preparation sections; use the stable Discard and Save actions while the bar reports the draft state.",
),
outcome="An owned campaign draft with an editable working version.",
verification="The Campaign overview shows the new campaign as a draft and identifies its current working version.",
diff --git a/webui/src/features/campaigns/AttachmentsDataPage.tsx b/webui/src/features/campaigns/AttachmentsDataPage.tsx
index aff90f2..74a5bd5 100644
--- a/webui/src/features/campaigns/AttachmentsDataPage.tsx
+++ b/webui/src/features/campaigns/AttachmentsDataPage.tsx
@@ -5,7 +5,7 @@ import { useGuardedNavigate, usePlatformModuleInstalled, usePlatformUiCapability
import type { ApiSettings } from "../../types";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
-import { PageLayout } from "@govoplan/core-webui";
+import { PageActionBar, PageLayout } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { MetricCard } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
@@ -249,18 +249,20 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
return (
}
headerLoading={loading}
error={error}
- actions={
- <>
- {filesModuleInstalled && }
-
-
- >
- }
+ actions={ navigate("/files")}>i18n:govoplan-campaign.manage_files.90a419f7 : undefined}
+ discardAction={{ label: "i18n:govoplan-campaign.discard.36fff63c", onClick: () => void discardDraft() }}
+ saveAction={{ label: "i18n:govoplan-campaign.save.efc007a3", onClick: () => saveDraft("manual"), disabled: !canSave && dirty, disabledReason: !canSave && dirty ? "Resolve the current editor blocker before saving." : undefined }}
+ />}
notices={(localError || locked) ? <>
{localError && {localError}}
{locked && }
diff --git a/webui/src/features/campaigns/CampaignAuditPage.tsx b/webui/src/features/campaigns/CampaignAuditPage.tsx
index 755bdc0..c4610e0 100644
--- a/webui/src/features/campaigns/CampaignAuditPage.tsx
+++ b/webui/src/features/campaigns/CampaignAuditPage.tsx
@@ -2,7 +2,7 @@ import type { ApiSettings } from "../../types";
import { Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import VersionLine from "./components/VersionLine";
-import { ActionBlockerHint, DocumentationHelpLink, PageLayout } from "@govoplan/core-webui";
+import { ActionBlockerHint, DocumentationHelpLink, PageActionBar, PageLayout } from "@govoplan/core-webui";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
export default function CampaignAuditPage({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) {
@@ -11,15 +11,18 @@ export default function CampaignAuditPage({ settings, campaignId }: {settings: A
return (
}
loading={loading}
loadingLabel="i18n:govoplan-campaign.loading_audit_data.af52b968"
error={error}
- actions={
-
- }
+ actions={ void reload({ force: true })} disabled={loading}>i18n:govoplan-campaign.reload.cce71553}
+ />}
>
}
headerLoading={loading}
error={error}
- actions={
- <>
-
-
- >
- }
+ actions={ void discardDraft() }}
+ saveAction={{ label: "i18n:govoplan-campaign.save.efc007a3", onClick: saveFields, disabled: !canSave && dirty, disabledReason: !canSave && dirty ? "Resolve the current field-definition blocker before saving." : undefined }}
+ />}
notices={(localError || fieldNameWarning || locked) ? <>
{localError && {localError}}
{fieldNameWarning && {fieldNameWarning}}
diff --git a/webui/src/features/campaigns/CampaignJsonView.tsx b/webui/src/features/campaigns/CampaignJsonView.tsx
index 2f047a6..c889bf4 100644
--- a/webui/src/features/campaigns/CampaignJsonView.tsx
+++ b/webui/src/features/campaigns/CampaignJsonView.tsx
@@ -1,7 +1,7 @@
import type { ApiSettings } from "../../types";
import { Card } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui";
-import { DismissibleAlert, PageLayout } from "@govoplan/core-webui";
+import { DismissibleAlert, PageActionBar, PageLayout } from "@govoplan/core-webui";
import VersionLine from "./components/VersionLine";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { asRecord, getCampaignJson } from "./utils/campaignView";
@@ -16,18 +16,19 @@ export default function CampaignJsonView({ settings, campaignId }: {settings: Ap
return (
}
loading={loading}
loadingLabel="i18n:govoplan-campaign.loading_json.812c7a50"
error={error}
- actions={
- <>
-
-
- >
- }
+ actions={ void reload({ force: true })} disabled={loading}>i18n:govoplan-campaign.reload.cce71553}
+ primaryActions={}
+ />}
notices={
This expert view contains the complete authorized campaign configuration,
diff --git a/webui/src/features/campaigns/CampaignListPage.tsx b/webui/src/features/campaigns/CampaignListPage.tsx
index 8931400..fc861f3 100644
--- a/webui/src/features/campaigns/CampaignListPage.tsx
+++ b/webui/src/features/campaigns/CampaignListPage.tsx
@@ -7,7 +7,7 @@ import { Card } from "@govoplan/core-webui";
import { Button } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
-import { PageLayout, TableActionGroup, i18nMessage, useGuardedNavigate } from "@govoplan/core-webui";
+import { PageActionBar, PageLayout, TableActionGroup, i18nMessage, useGuardedNavigate } from "@govoplan/core-webui";
import { DataGrid, type DataGridColumn } from "@govoplan/core-webui";
import { createNewCampaign, listCampaignsDelta, type CampaignDeltaResponse } from "../../api/campaigns";
import type { CampaignListItem } from "../../types";
@@ -143,20 +143,21 @@ export default function CampaignListPage({ settings }: {settings: ApiSettings;})
return (
{lastLoadedAt ? i18nMessage("i18n:govoplan-campaign.last_loaded_value.35ef046a", { value0: lastLoadedAt }) : "i18n:govoplan-campaign.not_loaded_yet.9968c191"}
}
headerLoading={loading}
error={error}
- actions={
- <>
-
- }
+ createAction={
- >
- }
+ }
+ />}
>
diff --git a/webui/src/features/campaigns/CampaignOverviewPage.tsx b/webui/src/features/campaigns/CampaignOverviewPage.tsx
index 761db7f..25ca1a9 100644
--- a/webui/src/features/campaigns/CampaignOverviewPage.tsx
+++ b/webui/src/features/campaigns/CampaignOverviewPage.tsx
@@ -10,7 +10,7 @@ import { Dialog } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { MetricCard } from "@govoplan/core-webui";
-import { PageLayout } from "@govoplan/core-webui";
+import { PageActionBar, PageLayout } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import { ToggleSwitch } from "@govoplan/core-webui";
import { DismissibleAlert, TableActionGroup, hasScope, i18nMessage, useGuardedNavigate, useUnsavedDraftGuard } from "@govoplan/core-webui";
@@ -334,14 +334,18 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
return (
i18n:govoplan-campaign.campaign_overview_version_independent_identity_a.ebaf1113}
headerLoading={loading}
error={error}
success={message}
- actions={
- <>
+ actions={
{canCopy && data.currentVersion && }
+ >}
+ destructiveActions={<>
{canDelete && }
-
-
- >
- }
+ >}
+ discardAction={{ label: "i18n:govoplan-campaign.discard.36fff63c", onClick: () => void discardOverview(), disabled: loading || lockBusy }}
+ saveAction={{ label: "i18n:govoplan-campaign.save.efc007a3", onClick: () => void saveIdentity(), disabled: !campaign && identityDirty, disabledReason: !campaign && identityDirty ? "The campaign identity is not available." : undefined }}
+ />}
>
diff --git a/webui/src/features/campaigns/CampaignReportPage.tsx b/webui/src/features/campaigns/CampaignReportPage.tsx
index b01c44c..edd5e82 100644
--- a/webui/src/features/campaigns/CampaignReportPage.tsx
+++ b/webui/src/features/campaigns/CampaignReportPage.tsx
@@ -22,7 +22,7 @@ import { DataGrid, type DataGridColumn, type DataGridListOption, type DataGridQu
import { Dialog } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
-import { PageLayout } from "@govoplan/core-webui";
+import { PageActionBar, PageLayout } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import VersionLine from "./components/VersionLine";
import { LoadingFrame, TableActionGroup, ToggleSwitch, i18nMessage } from "@govoplan/core-webui";
@@ -447,19 +447,22 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
return (
}
headerLoading={loading}
error={error || actionError}
success={actionMessage}
- actions={
- <>
+ actions={ void reloadAll()} disabled={loading || jobsLoading}>i18n:govoplan-campaign.reload.cce71553}
+ primaryActions={<>
-
- >
- }
+ >}
+ />}
>
diff --git a/webui/src/features/campaigns/GlobalSettingsPage.tsx b/webui/src/features/campaigns/GlobalSettingsPage.tsx
index 7832a45..9a0cef9 100644
--- a/webui/src/features/campaigns/GlobalSettingsPage.tsx
+++ b/webui/src/features/campaigns/GlobalSettingsPage.tsx
@@ -10,7 +10,7 @@ import { FormGrid, ContentGrid, Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
-import { PageLayout } from "@govoplan/core-webui";
+import { PageActionBar, PageLayout } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { PolicyRow } from "@govoplan/core-webui";
import { PolicyTable } from "@govoplan/core-webui";
@@ -179,17 +179,19 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
return (
}
headerLoading={loading}
error={error}
- actions={
- <>
-
-
- >
- }
+ actions={ void discardDraft() }}
+ saveAction={{ label: "i18n:govoplan-campaign.save.efc007a3", onClick: () => saveDraft("manual"), disabled: (locked || !draft) && dirty, disabledReason: locked && dirty ? "This campaign version is locked." : !draft && dirty ? "The campaign draft is not available." : undefined }}
+ />}
notices={(localError || locked) ? <>
{localError && {localError}}
{locked && }
diff --git a/webui/src/features/campaigns/MailSettingsPage.tsx b/webui/src/features/campaigns/MailSettingsPage.tsx
index 8f2e0ce..b8d60b0 100644
--- a/webui/src/features/campaigns/MailSettingsPage.tsx
+++ b/webui/src/features/campaigns/MailSettingsPage.tsx
@@ -9,6 +9,7 @@ import { FormGrid,
LoadingFrame,
MailServerFolderLookupResultView,
MetricCard,
+ PageActionBar,
PageLayout,
PasswordField,
ToggleSwitch,
@@ -328,17 +329,27 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
return (
}
headerLoading={loading}
error={error}
- actions={
- <>
- {!isPolicyView && }
-
- >
- }
+ actions={isPolicyView ? (
+ void reload({ force: true })} disabled={loading}>i18n:govoplan-campaign.reload.cce71553}
+ />
+ ) : (
+ void discardDraft() }}
+ saveAction={{ label: "i18n:govoplan-campaign.save.efc007a3", onClick: () => void saveDraft("manual"), disabled: !canSave && dirty, disabledReason: !canSave && dirty ? "Resolve the current mail-settings blocker before saving." : undefined }}
+ />
+ )}
notices={(localError || locked) ? <>
{localError && {localError}}
{locked && }
diff --git a/webui/src/features/campaigns/ReviewSendPage.tsx b/webui/src/features/campaigns/ReviewSendPage.tsx
index 93945f9..0330e09 100644
--- a/webui/src/features/campaigns/ReviewSendPage.tsx
+++ b/webui/src/features/campaigns/ReviewSendPage.tsx
@@ -43,7 +43,7 @@ import { DataGrid, type DataGridQueryState } from "@govoplan/core-webui";
import { DismissibleAlert } from "@govoplan/core-webui";
import { ConfirmDialog } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
-import { PageLayout } from "@govoplan/core-webui";
+import { PageActionBar, PageLayout } from "@govoplan/core-webui";
import { StatusBadge } from "@govoplan/core-webui";
import { ToggleSwitch } from "@govoplan/core-webui";
import { i18nMessage } from "@govoplan/core-webui";
@@ -1365,20 +1365,21 @@ export default function ReviewSendPage({
return (
}
headerLoading={loading || Boolean(busy)}
error={error}
- actions={
- <>
- }
+ primaryActions={
-
- >
- }
+ }
+ />}
notices={(message || (version && (historicalVersion || readyForDelivery || userLockedVersion || finalVersion))) ? <>
{message && {message}}
{version && (historicalVersion || readyForDelivery || userLockedVersion || finalVersion) &&
diff --git a/webui/src/features/campaigns/TemplateDataPage.tsx b/webui/src/features/campaigns/TemplateDataPage.tsx
index a0b79cf..f7b5bbc 100644
--- a/webui/src/features/campaigns/TemplateDataPage.tsx
+++ b/webui/src/features/campaigns/TemplateDataPage.tsx
@@ -16,7 +16,7 @@ import { Card } from "@govoplan/core-webui";
import { Dialog } from "@govoplan/core-webui";
import { FormField } from "@govoplan/core-webui";
import { FieldLabel } from "@govoplan/core-webui";
-import { PageLayout } from "@govoplan/core-webui";
+import { PageActionBar, PageLayout } from "@govoplan/core-webui";
import { LoadingFrame } from "@govoplan/core-webui";
import { DismissibleAlert, SegmentedControl, ToggleSwitch, i18nMessage } from "@govoplan/core-webui";
import { WysiwygEditor, type WysiwygEditorHandle } from "@govoplan/core-webui/wysiwyg";
@@ -412,19 +412,21 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
return (
}
headerLoading={loading}
error={error}
success={contentLibraryNotice}
- actions={
- <>
-
-
-
- >
- }
+ actions={ window.location.assign("/templates")}>i18n:govoplan-campaign.manage_templates.23688071}
+ discardAction={{ label: "i18n:govoplan-campaign.discard.36fff63c", onClick: () => void discardDraft() }}
+ saveAction={{ label: "i18n:govoplan-campaign.save.efc007a3", onClick: () => saveDraft("manual"), disabled: (locked || !draft) && dirty, disabledReason: locked && dirty ? "This campaign version is locked." : !draft && dirty ? "The campaign draft is not available." : undefined }}
+ />}
notices={(localError || locked) ? <>
{localError && {localError}}
{locked && }
diff --git a/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx b/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx
index 84008d7..716681f 100644
--- a/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx
+++ b/webui/src/features/campaigns/components/CampaignDraftPageScaffold.tsx
@@ -1,5 +1,5 @@
import type { ReactNode } from "react";
-import { Button, DismissibleAlert, PageLayout } from "@govoplan/core-webui";
+import { DismissibleAlert, PageActionBar, PageLayout } from "@govoplan/core-webui";
import type { ApiSettings } from "../../../types";
import type { CampaignVersionDetail, CampaignVersionListItem } from "../../../api/campaigns";
import LockedVersionNotice from "./LockedVersionNotice";
@@ -44,18 +44,20 @@ export default function CampaignDraftPageScaffold({
}: CampaignDraftPageScaffoldProps) {
return (
}
loading={loading || !draft}
loadingLabel="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50"
error={error || ""}
- actions={
- <>
-
-
- >
- }
+ actions={}
notices={(localError || locked) ?
<>
{localError && {localError}}
diff --git a/webui/src/features/campaigns/wizard/WizardDirectoryPage.tsx b/webui/src/features/campaigns/wizard/WizardDirectoryPage.tsx
index 12bf390..d17be53 100644
--- a/webui/src/features/campaigns/wizard/WizardDirectoryPage.tsx
+++ b/webui/src/features/campaigns/wizard/WizardDirectoryPage.tsx
@@ -1,5 +1,6 @@
import { Link } from "react-router";
import {
+ PageActionBar,
PageLayout,
WizardDirectory,
usePlatformUiCapabilities,
@@ -39,13 +40,12 @@ export default function WizardDirectoryPage({
return (
+ actions={
i18n:govoplan-campaign.back_to_overview.ec986cba
-
- }
+ } />}
>
void refreshAll()} disabled={loading || jobsLoading}>i18n:govoplan-campaign.refresh.56e3badc
- }
+ actions={ void refreshAll()} disabled={loading || jobsLoading}>i18n:govoplan-campaign.refresh.56e3badc}
+ />}
notices={jobsError ? {jobsError} : undefined}
>
diff --git a/webui/src/features/reports/AggregateReportsPage.tsx b/webui/src/features/reports/AggregateReportsPage.tsx
index a2e7040..e80e384 100644
--- a/webui/src/features/reports/AggregateReportsPage.tsx
+++ b/webui/src/features/reports/AggregateReportsPage.tsx
@@ -8,6 +8,7 @@ import { ContentGrid,
DataGrid,
LoadingFrame,
MetricCard,
+ PageActionBar,
PageLayout,
StatusBadge,
TableActionGroup,
@@ -167,19 +168,22 @@ export default function AggregateReportsPage({ settings }: {settings: ApiSetting
return (
void Promise.all([loadCampaigns(), selectedFromUrl ? loadReport(selectedFromUrl) : Promise.resolve()])}
disabled={listLoading || reportLoading}
>
i18n:govoplan-campaign.refresh.56e3badc
-
- }
+ }
+ />}
>
diff --git a/webui/src/features/templates/TemplatesPage.tsx b/webui/src/features/templates/TemplatesPage.tsx
index a4efa15..4ffc62e 100644
--- a/webui/src/features/templates/TemplatesPage.tsx
+++ b/webui/src/features/templates/TemplatesPage.tsx
@@ -3,7 +3,7 @@ import { useMemo, useState } from "react";
import { ExternalLink } from "lucide-react";
import { FormGrid, ContentGrid, Button } from "@govoplan/core-webui";
import { Card } from "@govoplan/core-webui";
-import { PageLayout, WorkspaceLayout } from "@govoplan/core-webui";
+import { PageActionBar, PageLayout, WorkspaceLayout } from "@govoplan/core-webui";
import { FieldLabel } from "@govoplan/core-webui";
import { StatusBadge, TableActionGroup, i18nMessage } from "@govoplan/core-webui";
import { usePlatformLanguage } from "@govoplan/core-webui";
@@ -146,15 +146,14 @@ export default function TemplatesPage() {
helpModuleId="campaign"
>
+ actions={
- >
- }
+ >} />}
>
{active === "overview" && }
@@ -170,15 +169,14 @@ export default function TemplatesPage() {
return (
+ actions={
- >
- }
+ >} />}
>