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} >