feat(campaign): expose governed archive action
This commit is contained in:
@@ -569,14 +569,13 @@ CAMPAIGN_USER_DOCUMENTATION = (
|
||||
steps=(
|
||||
"Open Report and resolve every active or outcome-unknown delivery state.",
|
||||
"Confirm that the campaign should leave active work while its evidence remains retained.",
|
||||
"Invoke the authorized Archive action from a supporting client.",
|
||||
"Open the campaign overview, choose Archive campaign, and confirm the retained-evidence consequence.",
|
||||
"Reopen or query the campaign and confirm its state is Archived.",
|
||||
),
|
||||
outcome="An archived campaign whose versions, reports, and audit evidence remain preserved.",
|
||||
verification="The campaign state is Archived and its report remains available. Ask an authorized audit reader to verify the platform audit event.",
|
||||
related_topic_ids=("campaigns.workflow.view-delivery-report", "campaigns.workflow.delete-untouched-draft"),
|
||||
limitations=(
|
||||
"The current Campaign Web UI does not yet expose the archive action; use an authorized supporting client or API.",
|
||||
"The Campaign-local Audit page is not integrated yet; audit verification uses the platform audit surface or API.",
|
||||
),
|
||||
),
|
||||
|
||||
+2
-1
@@ -34,7 +34,8 @@
|
||||
"test:operator-queue": "node --experimental-strip-types --test tests/operator-queue-model.test.ts && node tests/operator-queue-ui-structure.test.mjs",
|
||||
"test:aggregate-report": "tsc -p tsconfig.aggregate-report-tests.json && node tests/aggregate-report-ui-structure.test.mjs",
|
||||
"test:wizards": "node tests/wizard-directory-ui-structure.test.mjs",
|
||||
"test:accessibility-contract": "node tests/accessibility-contract.test.mjs"
|
||||
"test:accessibility-contract": "node tests/accessibility-contract.test.mjs",
|
||||
"test:campaign-lifecycle": "node tests/campaign-lifecycle-ui-structure.test.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2"
|
||||
|
||||
@@ -1002,6 +1002,15 @@ payload: CampaignUpdatePayload)
|
||||
});
|
||||
}
|
||||
|
||||
export async function archiveCampaign(
|
||||
settings: ApiSettings,
|
||||
campaignId: string)
|
||||
: Promise<CampaignListItem> {
|
||||
return apiFetch<CampaignListItem>(settings, `/api/v1/campaigns/${campaignId}/archive`, {
|
||||
method: "POST"
|
||||
});
|
||||
}
|
||||
|
||||
export async function createNewCampaign(
|
||||
settings: ApiSettings,
|
||||
overrides: CampaignCreateMinimalPayload = {})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { ExternalLink, LockKeyhole, LockOpen } from "lucide-react";
|
||||
import { Archive, ExternalLink, LockKeyhole, LockOpen } from "lucide-react";
|
||||
import { Link } from "react-router";
|
||||
import type { ApiSettings } from "../../types";
|
||||
import type { ApiSettings, AuthInfo } from "../../types";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
import { Card } from "@govoplan/core-webui";
|
||||
import { ConfirmDialog } from "@govoplan/core-webui";
|
||||
@@ -10,9 +10,10 @@ import { LoadingFrame } from "@govoplan/core-webui";
|
||||
import { MetricCard } from "@govoplan/core-webui";
|
||||
import { PageTitle } from "@govoplan/core-webui";
|
||||
import { StatusBadge } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert, TableActionGroup, i18nMessage, useGuardedNavigate, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert, TableActionGroup, hasScope, i18nMessage, useGuardedNavigate, useUnsavedDraftGuard } from "@govoplan/core-webui";
|
||||
import { DataGrid, type DataGridColumn } from "@govoplan/core-webui";
|
||||
import {
|
||||
archiveCampaign,
|
||||
lockCampaignVersionPermanently,
|
||||
lockCampaignVersionTemporarily,
|
||||
unlockCampaignVersionUserLock,
|
||||
@@ -39,7 +40,7 @@ const campaignModeOptions = ["draft", "test", "send"];
|
||||
type LockAction = "temporary" | "unlock" | "permanent";
|
||||
type PendingLockAction = {version: CampaignVersionListItem;action: LockAction;} | null;
|
||||
|
||||
export default function CampaignOverviewPage({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) {
|
||||
export default function CampaignOverviewPage({ settings, auth, campaignId }: {settings: ApiSettings;auth: AuthInfo;campaignId: string;}) {
|
||||
const navigate = useGuardedNavigate();
|
||||
const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId, { includeSummary: true });
|
||||
const campaign = data.campaign;
|
||||
@@ -49,8 +50,11 @@ export default function CampaignOverviewPage({ settings, campaignId }: {settings
|
||||
const [savingIdentity, setSavingIdentity] = useState(false);
|
||||
const [pendingLockAction, setPendingLockAction] = useState<PendingLockAction>(null);
|
||||
const [lockBusy, setLockBusy] = useState(false);
|
||||
const [archiveDialogOpen, setArchiveDialogOpen] = useState(false);
|
||||
const [archiving, setArchiving] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
const versionMetrics = useMemo(() => campaignVersionMetrics(data.currentVersion), [data.currentVersion]);
|
||||
const canArchive = Boolean(campaign) && campaign?.status !== "archived" && hasScope(auth, "campaigns:campaign:archive");
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty: identityDirty,
|
||||
@@ -145,6 +149,23 @@ export default function CampaignOverviewPage({ settings, campaignId }: {settings
|
||||
await reload({ force: true });
|
||||
}
|
||||
|
||||
async function applyArchive() {
|
||||
if (!campaign || archiving) return;
|
||||
setArchiving(true);
|
||||
setError("");
|
||||
setMessage("");
|
||||
try {
|
||||
await archiveCampaign(settings, campaign.id);
|
||||
setArchiveDialogOpen(false);
|
||||
setMessage("i18n:govoplan-campaign.campaign_archived.3f0ca2b7");
|
||||
await reload({ force: true });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setArchiving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="content-pad workspace-data-page">
|
||||
<div className="page-heading split workspace-heading">
|
||||
@@ -153,6 +174,14 @@ export default function CampaignOverviewPage({ settings, campaignId }: {settings
|
||||
<p className="mono-small">i18n:govoplan-campaign.campaign_overview_version_independent_identity_a.ebaf1113</p>
|
||||
</div>
|
||||
<div className="button-row compact-actions">
|
||||
{canArchive && <Button
|
||||
variant="danger"
|
||||
onClick={() => setArchiveDialogOpen(true)}
|
||||
disabled={loading || savingIdentity || lockBusy || identityDirty}
|
||||
disabledReason={identityDirty ? "i18n:govoplan-campaign.save_or_discard_overview_changes_before_archiving.413ff9e0" : undefined}>
|
||||
<Archive size={16} aria-hidden="true" />
|
||||
i18n:govoplan-campaign.archive_campaign.26dcfb8a
|
||||
</Button>}
|
||||
<Button onClick={() => void discardOverview()} disabled={loading || savingIdentity || lockBusy}>Discard</Button>
|
||||
<Button variant="primary" onClick={() => void saveIdentity()} disabled={!campaign || !identityDirty || savingIdentity}>{savingIdentity ? "i18n:govoplan-campaign.saving.56a2285c" : "i18n:govoplan-campaign.save.efc007a3"}</Button>
|
||||
</div>
|
||||
@@ -226,6 +255,16 @@ export default function CampaignOverviewPage({ settings, campaignId }: {settings
|
||||
</Card>
|
||||
</LoadingFrame>
|
||||
|
||||
<ConfirmDialog
|
||||
open={archiveDialogOpen}
|
||||
title="i18n:govoplan-campaign.archive_campaign.26dcfb8a"
|
||||
message="i18n:govoplan-campaign.archive_campaign_confirmation.c0cc62e1"
|
||||
confirmLabel="i18n:govoplan-campaign.archive_campaign.26dcfb8a"
|
||||
tone="danger"
|
||||
busy={archiving}
|
||||
onCancel={() => setArchiveDialogOpen(false)}
|
||||
onConfirm={() => void applyArchive()} />
|
||||
|
||||
<ConfirmDialog
|
||||
open={Boolean(pendingLockAction)}
|
||||
title={lockDialogTitle(pendingLockAction)}
|
||||
|
||||
@@ -89,7 +89,7 @@ function CampaignWorkspaceInner({ settings, auth }: { settings: ApiSettings; aut
|
||||
<SectionSidebar active={active} onSelect={select} />
|
||||
<section className="workspace-content">
|
||||
<Routes>
|
||||
<Route index element={<CampaignOverviewPage settings={settings} campaignId={campaignId || ""} />} />
|
||||
<Route index element={<CampaignOverviewPage settings={settings} auth={auth} campaignId={campaignId || ""} />} />
|
||||
<Route path="data" element={<Navigate to="../recipients" replace />} />
|
||||
<Route path="fields" element={<CampaignFieldsPage settings={settings} campaignId={campaignId || ""} />} />
|
||||
<Route path="recipients" element={<RecipientDataPage settings={settings} campaignId={campaignId || ""} />} />
|
||||
|
||||
@@ -251,9 +251,12 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-campaign.archive_filename.de958cbd": "archive filename",
|
||||
"i18n:govoplan-campaign.archive_filenames_must_be_present_and_unique.aa2fb639": "Archive filenames must be present and unique.",
|
||||
"i18n:govoplan-campaign.archive_filenames_must_not_be_empty.42e91427": "Archive filenames must not be empty",
|
||||
"i18n:govoplan-campaign.archive_campaign.26dcfb8a": "Archive campaign",
|
||||
"i18n:govoplan-campaign.archive_campaign_confirmation.c0cc62e1": "This removes the campaign from active work while preserving versions, delivery results, and audit evidence. Active or outcome-unknown delivery must be resolved first.",
|
||||
"i18n:govoplan-campaign.archive_name.6310f9e1": "Archive name",
|
||||
"i18n:govoplan-campaign.archive_names_support_recipient_and_campaign_fie.d5b1b2d1": "Archive names support recipient and campaign fields. Use the pencil action to edit the filename and insert placeholders. Password fields are intentionally not offered for filenames. The campaign standard is used by attachment rows set to Campaign standard.",
|
||||
"i18n:govoplan-campaign.archived.eddc813f": "Archived",
|
||||
"i18n:govoplan-campaign.campaign_archived.3f0ca2b7": "Campaign archived.",
|
||||
"i18n:govoplan-campaign.as_a_campaign_field_remove_this_placeholder_or_c.6bf2dea8": "as a campaign field, remove this placeholder, or continue editing.",
|
||||
"i18n:govoplan-campaign.attach_job_csv.adb76197": "Attach job CSV",
|
||||
"i18n:govoplan-campaign.attach_json_report.d70883b5": "Attach JSON report",
|
||||
@@ -1042,6 +1045,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-campaign.save.efc007a3": "Save",
|
||||
"i18n:govoplan-campaign.saved.c0ae8f6e": "Saved",
|
||||
"i18n:govoplan-campaign.saving.56a2285c": "Saving…",
|
||||
"i18n:govoplan-campaign.save_or_discard_overview_changes_before_archiving.413ff9e0": "Save or discard overview changes before archiving.",
|
||||
"i18n:govoplan-campaign.scope_field_value": "{value0} field \"{value1}\"",
|
||||
"i18n:govoplan-campaign.scope.4651a34e": "Scope",
|
||||
"i18n:govoplan-campaign.search_recipient_subject_or_entry_id.6d6544f5": "Search recipient, subject or entry ID",
|
||||
@@ -1580,9 +1584,12 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-campaign.archive_filename.de958cbd": "archive filename",
|
||||
"i18n:govoplan-campaign.archive_filenames_must_be_present_and_unique.aa2fb639": "Archive filenames must be present and unique.",
|
||||
"i18n:govoplan-campaign.archive_filenames_must_not_be_empty.42e91427": "Archive filenames must not be empty",
|
||||
"i18n:govoplan-campaign.archive_campaign.26dcfb8a": "Kampagne archivieren",
|
||||
"i18n:govoplan-campaign.archive_campaign_confirmation.c0cc62e1": "Dadurch wird die Kampagne aus der aktiven Arbeit entfernt. Versionen, Versandergebnisse und Pruefnachweise bleiben erhalten. Aktive oder ungeklaerte Versandvorgaenge muessen zuerst abgeschlossen werden.",
|
||||
"i18n:govoplan-campaign.archive_name.6310f9e1": "Archive name",
|
||||
"i18n:govoplan-campaign.archive_names_support_recipient_and_campaign_fie.d5b1b2d1": "Archive names support recipient and campaign fields. Use the pencil action to edit the filename and insert placeholders. Password fields are intentionally not offered for filenames. The campaign standard is used by attachment rows set to Campaign standard.",
|
||||
"i18n:govoplan-campaign.archived.eddc813f": "Archived",
|
||||
"i18n:govoplan-campaign.campaign_archived.3f0ca2b7": "Kampagne archiviert.",
|
||||
"i18n:govoplan-campaign.as_a_campaign_field_remove_this_placeholder_or_c.6bf2dea8": "as a campaign field, remove this placeholder, or continue editing.",
|
||||
"i18n:govoplan-campaign.attach_job_csv.adb76197": "Attach job CSV",
|
||||
"i18n:govoplan-campaign.attach_json_report.d70883b5": "Attach JSON report",
|
||||
@@ -2371,6 +2378,7 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-campaign.save.efc007a3": "Speichern",
|
||||
"i18n:govoplan-campaign.saved.c0ae8f6e": "Gespeichert",
|
||||
"i18n:govoplan-campaign.saving.56a2285c": "Saving…",
|
||||
"i18n:govoplan-campaign.save_or_discard_overview_changes_before_archiving.413ff9e0": "Speichern oder verwerfen Sie die Aenderungen an der Uebersicht vor dem Archivieren.",
|
||||
"i18n:govoplan-campaign.scope_field_value": "{value0}-Feld \"{value1}\"",
|
||||
"i18n:govoplan-campaign.scope.4651a34e": "Geltungsbereich",
|
||||
"i18n:govoplan-campaign.search_recipient_subject_or_entry_id.6d6544f5": "Search recipient, subject or entry ID",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const workspace = readFileSync("src/features/campaigns/CampaignWorkspace.tsx", "utf8");
|
||||
const overview = readFileSync("src/features/campaigns/CampaignOverviewPage.tsx", "utf8");
|
||||
const api = readFileSync("src/api/campaigns.ts", "utf8");
|
||||
|
||||
assert.match(workspace, /<CampaignOverviewPage settings=\{settings\} auth=\{auth\}/);
|
||||
assert.match(overview, /hasScope\(auth, "campaigns:campaign:archive"\)/);
|
||||
assert.match(overview, /archiveDialogOpen/);
|
||||
assert.match(overview, /archive_campaign_confirmation/);
|
||||
assert.match(overview, /await archiveCampaign\(settings, campaign\.id\)/);
|
||||
assert.match(api, /\/api\/v1\/campaigns\/\$\{campaignId\}\/archive/);
|
||||
|
||||
console.log("Campaign lifecycle UI structural contract passed.");
|
||||
Reference in New Issue
Block a user