Add bulk recipient activation controls
This commit is contained in:
@@ -454,6 +454,15 @@ Breaking payload or ownership changes require an interface-version bump and a
|
||||
release-composition alignment gate. Optional absence must be tested physically,
|
||||
not only hidden in navigation.
|
||||
|
||||
### Bulk recipient activation
|
||||
|
||||
Recipient data can activate all currently inactive rows or deactivate all
|
||||
currently active rows. The action displays the exact affected count and
|
||||
requires confirmation. It changes only the local Campaign draft until the
|
||||
operator saves. Saving follows the ordinary versioned Campaign update path, so
|
||||
the resulting recipient state is retained in version and protocol evidence and
|
||||
any stale validation, build, or review evidence is invalidated.
|
||||
|
||||
### Reusable Distribution Lists
|
||||
|
||||
When Distribution Lists is available, Recipient data offers a separate import
|
||||
|
||||
@@ -844,7 +844,7 @@ manifest = ModuleManifest(
|
||||
id="campaigns.workflow.prepare-validate-and-build",
|
||||
title="Prepare, validate, and build a campaign",
|
||||
summary="Turn governed recipient, template, attachment, and Mail-profile inputs into exact built messages for review.",
|
||||
body="Prepare each input in its owning surface, resolve every blocking validation issue, and build exact recipient messages before review. Campaign freezes recipient and attachment evidence for the selected version; later source changes do not silently alter that build. When the Templates module is installed, its single Templates navigation entry owns the reusable library while campaign-specific composition remains in the campaign workspace.",
|
||||
body="Prepare each input in its owning surface, resolve every blocking validation issue, and build exact recipient messages before review. Recipient data can activate or deactivate every currently opposite-state row as one explicitly confirmed draft change; saving it creates the normal Campaign version evidence and invalidates stale validation, build, and review state. Campaign freezes recipient and attachment evidence for the selected version; later source changes do not silently alter that build. When the Templates module is installed, its single Templates navigation entry owns the reusable library while campaign-specific composition remains in the campaign workspace.",
|
||||
layer="configured",
|
||||
documentation_types=("user",),
|
||||
audience=("campaign_manager", "campaign_author"),
|
||||
@@ -900,7 +900,7 @@ manifest = ModuleManifest(
|
||||
],
|
||||
"steps": [
|
||||
"Set campaign-wide fields and purpose, then define the recipient fields and templates.",
|
||||
"Import or select recipients and inspect provenance, exclusions, and review-required rows.",
|
||||
"Import or select recipients, optionally confirm a counted activate-all or deactivate-all draft action, and inspect provenance, exclusions, and review-required rows.",
|
||||
"Select managed attachment versions and an authorized Mail profile when those capabilities are used.",
|
||||
"Validate the relevant sections and resolve every blocker without hiding warnings.",
|
||||
"Build the selected version and inspect representative and exceptional rendered messages.",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { CircleCheck, CircleX } from "lucide-react";
|
||||
import type { ApiSettings } from "../../types";
|
||||
import {
|
||||
getCampaignPostboxCatalog,
|
||||
@@ -13,6 +14,7 @@ import {
|
||||
"../../api/campaigns";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
import { Card } from "@govoplan/core-webui";
|
||||
import { ConfirmDialog } from "@govoplan/core-webui";
|
||||
import { FormField } from "@govoplan/core-webui";
|
||||
import { usePlatformModuleInstalled } from "@govoplan/core-webui";
|
||||
import CampaignDraftPageScaffold from "./components/CampaignDraftPageScaffold";
|
||||
@@ -39,7 +41,7 @@ import {
|
||||
createAddressSourceImportProvenance
|
||||
} from "./utils/addressSourceImport";
|
||||
import { addressesFromValue, type MailboxAddress } from "@govoplan/core-webui";
|
||||
import { insertAfter, moveArrayItem, useGuardedNavigate, usePlatformLanguage } from "@govoplan/core-webui";
|
||||
import { i18nMessage, insertAfter, moveArrayItem, useGuardedNavigate, usePlatformLanguage } from "@govoplan/core-webui";
|
||||
import AddressSourceImportDialog from "./recipients/AddressSourceImportDialog";
|
||||
import DistributionListImportDialog from "./recipients/DistributionListImportDialog";
|
||||
import {
|
||||
@@ -69,6 +71,12 @@ import {
|
||||
} from "./recipients/RecipientAddressEditor";
|
||||
import { RecipientImportDialog } from "./recipients/RecipientImportDialog";
|
||||
import { recipientProfileColumns } from "./recipients/recipientProfileColumns";
|
||||
|
||||
type RecipientBulkActivation = {
|
||||
active: boolean;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export default function RecipientDataPage({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) {
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const filesModuleInstalled = usePlatformModuleInstalled("files");
|
||||
@@ -91,6 +99,7 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
|
||||
const [recipientProfilesQuery, setRecipientProfilesQuery] = useState<DataGridQueryState>({ sort: null, filters: {} });
|
||||
const [recipientAddressEditorIndex, setRecipientAddressEditorIndex] = useState<number | null>(null);
|
||||
const [postboxTargetEditorIndex, setPostboxTargetEditorIndex] = useState<number | null>(null);
|
||||
const [bulkActivation, setBulkActivation] = useState<RecipientBulkActivation | null>(null);
|
||||
const [postboxCatalog, setPostboxCatalog] = useState<CampaignPostboxCatalog>({
|
||||
available: false,
|
||||
postboxes: [],
|
||||
@@ -346,6 +355,19 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
|
||||
});
|
||||
}
|
||||
|
||||
function requestBulkActivation(active: boolean) {
|
||||
if (locked) return;
|
||||
const count = inlineEntries.filter((entry) => (entry.active !== false) !== active).length;
|
||||
if (count === 0) return;
|
||||
setBulkActivation({ active, count });
|
||||
}
|
||||
|
||||
function confirmBulkActivation() {
|
||||
if (!bulkActivation || locked) return;
|
||||
replaceInlineEntries(inlineEntries.map((entry) => ({ ...entry, active: bulkActivation.active })));
|
||||
setBulkActivation(null);
|
||||
}
|
||||
|
||||
function applyRecipientImport(preview: RecipientImportPreview, mode: RecipientImportMode, provenance?: RecipientImportProvenance | null) {
|
||||
if (locked || !draft) return;
|
||||
setDraft(materializeRecipientImportWithAttachmentDefaults(draft, preview, { mode, provenance }));
|
||||
@@ -492,6 +514,22 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
|
||||
</Button>
|
||||
}
|
||||
<Button disabled={locked} onClick={() => setImportOpen(true)}>i18n:govoplan-campaign.import.d6fbc9d2</Button>
|
||||
<Button
|
||||
disabled={locked || inlineEntries.every((entry) => entry.active !== false)}
|
||||
onClick={() => requestBulkActivation(true)}>
|
||||
<CircleCheck size={16} aria-hidden="true" />
|
||||
{i18nMessage("i18n:govoplan-campaign.activate_all_value0.7e911e3e", {
|
||||
value0: inlineEntries.filter((entry) => entry.active === false).length
|
||||
})}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={locked || inlineEntries.every((entry) => entry.active === false)}
|
||||
onClick={() => requestBulkActivation(false)}>
|
||||
<CircleX size={16} aria-hidden="true" />
|
||||
{i18nMessage("i18n:govoplan-campaign.deactivate_all_value0.87e5d46f", {
|
||||
value0: inlineEntries.filter((entry) => entry.active !== false).length
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
{inlineEntries.length === 0 && Boolean(source.type) &&
|
||||
@@ -626,6 +664,23 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
|
||||
onClose={() => setRecipientAddressEditorIndex(null)} />
|
||||
|
||||
}
|
||||
<ConfirmDialog
|
||||
open={Boolean(bulkActivation)}
|
||||
title={bulkActivation?.active
|
||||
? "i18n:govoplan-campaign.activate_all_recipients.6be687f0"
|
||||
: "i18n:govoplan-campaign.deactivate_all_recipients.5939b005"}
|
||||
message={bulkActivation ? i18nMessage(
|
||||
bulkActivation.active
|
||||
? "i18n:govoplan-campaign.activate_value0_currently_inactive_recipients_the_change_r.53787f50"
|
||||
: "i18n:govoplan-campaign.deactivate_value0_currently_active_recipients_the_change_r.3bee469f",
|
||||
{ value0: bulkActivation.count }
|
||||
) : ""}
|
||||
confirmLabel={bulkActivation?.active
|
||||
? "i18n:govoplan-campaign.activate_recipients.bc3a7878"
|
||||
: "i18n:govoplan-campaign.deactivate_recipients.88d80611"}
|
||||
tone={bulkActivation?.active ? "default" : "danger"}
|
||||
onConfirm={confirmBulkActivation}
|
||||
onCancel={() => setBulkActivation(null)} />
|
||||
{postboxTargetEditorIndex !== null && inlineEntries[postboxTargetEditorIndex] &&
|
||||
<PostboxTargetsDialog
|
||||
open
|
||||
|
||||
@@ -2,6 +2,14 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = {
|
||||
"en": {
|
||||
"i18n:govoplan-campaign.activate_all_value0.7e911e3e": "Activate all ({value0})",
|
||||
"i18n:govoplan-campaign.deactivate_all_value0.87e5d46f": "Deactivate all ({value0})",
|
||||
"i18n:govoplan-campaign.activate_all_recipients.6be687f0": "Activate all recipients",
|
||||
"i18n:govoplan-campaign.deactivate_all_recipients.5939b005": "Deactivate all recipients",
|
||||
"i18n:govoplan-campaign.activate_value0_currently_inactive_recipients_the_change_r.53787f50": "Activate {value0} currently inactive recipients? The change remains a draft until you save.",
|
||||
"i18n:govoplan-campaign.deactivate_value0_currently_active_recipients_the_change_r.3bee469f": "Deactivate {value0} currently active recipients? The change remains a draft until you save.",
|
||||
"i18n:govoplan-campaign.activate_recipients.bc3a7878": "Activate recipients",
|
||||
"i18n:govoplan-campaign.deactivate_recipients.88d80611": "Deactivate recipients",
|
||||
"i18n:govoplan-campaign.guided_workflows": "Guided workflows",
|
||||
"i18n:govoplan-campaign.no_guided_workflows": "No guided workflows are available.",
|
||||
"i18n:govoplan-campaign.required_action.f2429497": "Required action",
|
||||
@@ -1335,6 +1343,14 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-campaign.zipcrypto.03bf7fb4": "ZipCrypto"
|
||||
},
|
||||
"de": {
|
||||
"i18n:govoplan-campaign.activate_all_value0.7e911e3e": "Alle aktivieren ({value0})",
|
||||
"i18n:govoplan-campaign.deactivate_all_value0.87e5d46f": "Alle deaktivieren ({value0})",
|
||||
"i18n:govoplan-campaign.activate_all_recipients.6be687f0": "Alle Empfänger aktivieren",
|
||||
"i18n:govoplan-campaign.deactivate_all_recipients.5939b005": "Alle Empfänger deaktivieren",
|
||||
"i18n:govoplan-campaign.activate_value0_currently_inactive_recipients_the_change_r.53787f50": "{value0} derzeit inaktive Empfänger aktivieren? Die Änderung bleibt ein Entwurf, bis Sie speichern.",
|
||||
"i18n:govoplan-campaign.deactivate_value0_currently_active_recipients_the_change_r.3bee469f": "{value0} derzeit aktive Empfänger deaktivieren? Die Änderung bleibt ein Entwurf, bis Sie speichern.",
|
||||
"i18n:govoplan-campaign.activate_recipients.bc3a7878": "Empfänger aktivieren",
|
||||
"i18n:govoplan-campaign.deactivate_recipients.88d80611": "Empfänger deaktivieren",
|
||||
"i18n:govoplan-campaign.guided_workflows": "Geführte Abläufe",
|
||||
"i18n:govoplan-campaign.no_guided_workflows": "Es sind keine geführten Abläufe verfügbar.",
|
||||
"i18n:govoplan-campaign.required_action.f2429497": "Erforderliche Aktion",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { readFileSync } from "node:fs";
|
||||
|
||||
const workspace = readFileSync("src/features/campaigns/CampaignWorkspace.tsx", "utf8");
|
||||
const overview = readFileSync("src/features/campaigns/CampaignOverviewPage.tsx", "utf8");
|
||||
const recipients = readFileSync("src/features/campaigns/RecipientDataPage.tsx", "utf8");
|
||||
const api = readFileSync("src/api/campaigns.ts", "utf8");
|
||||
|
||||
assert.match(workspace, /<CampaignOverviewPage settings=\{settings\} auth=\{auth\}/);
|
||||
@@ -17,5 +18,10 @@ assert.match(overview, /await archiveCampaignVersion\(settings, campaign\.id, pe
|
||||
assert.match(api, /\/api\/v1\/campaigns\/\$\{campaignId\}\/archive/);
|
||||
assert.match(api, /\/api\/v1\/campaigns\/\$\{campaignId\}\/copies/);
|
||||
assert.match(api, /\/api\/v1\/campaigns\/\$\{campaignId\}\/lifecycle-policy/);
|
||||
assert.match(recipients, /requestBulkActivation\(true\)/);
|
||||
assert.match(recipients, /requestBulkActivation\(false\)/);
|
||||
assert.match(recipients, /const count = inlineEntries\.filter/);
|
||||
assert.match(recipients, /<ConfirmDialog[\s\S]*bulkActivation/);
|
||||
assert.match(recipients, /replaceInlineEntries\(inlineEntries\.map/);
|
||||
|
||||
console.log("Campaign lifecycle UI structural contract passed.");
|
||||
|
||||
Reference in New Issue
Block a user