feat(campaign): expose governed archive action

This commit is contained in:
2026-08-03 20:39:06 +02:00
parent d9195a2d2b
commit 3f3545f080
7 changed files with 79 additions and 8 deletions
@@ -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 || ""} />} />