feat: complete campaign wizards and retention reporting
This commit is contained in:
+2
-1
@@ -31,7 +31,8 @@
|
||||
"test:report-grid": "rm -rf .report-grid-test-build && mkdir -p .report-grid-test-build && printf '{\"type\":\"commonjs\"}\\n' > .report-grid-test-build/package.json && tsc -p tsconfig.report-grid-tests.json && node .report-grid-test-build/tests/report-grid-query.test.js",
|
||||
"test:review-preview-ui": "rm -rf .review-preview-test-build && mkdir -p .review-preview-test-build && printf '{\"type\":\"commonjs\"}\\n' > .review-preview-test-build/package.json && tsc -p tsconfig.review-preview-tests.json && node .review-preview-test-build/tests/review-preview-ui.test.js && node tests/delivery-mode-ui-structure.test.mjs",
|
||||
"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: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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2"
|
||||
|
||||
@@ -295,6 +295,43 @@ export type CampaignPartialValidationResponse = {
|
||||
issues: Record<string, unknown>[];
|
||||
};
|
||||
|
||||
export type CampaignRetentionReport = {
|
||||
policy_status: "configured" | "defaults" | "unavailable" | string;
|
||||
policy_reason?: string | null;
|
||||
effective_policy: {
|
||||
store_raw_campaign_json?: boolean;
|
||||
raw_campaign_json_retention_days?: number | null;
|
||||
generated_eml_retention_days?: number | null;
|
||||
stored_report_detail_retention_days?: number | null;
|
||||
mock_mailbox_retention_days?: number | null;
|
||||
audit_detail_retention_days?: number | null;
|
||||
audit_detail_level?: string;
|
||||
};
|
||||
sources: Array<{
|
||||
scope_type?: string | null;
|
||||
path?: string | null;
|
||||
label?: string | null;
|
||||
applied_fields?: string[];
|
||||
}>;
|
||||
evidence: Record<string, {
|
||||
state?: string;
|
||||
redacted_at?: string | null;
|
||||
retained_count?: number;
|
||||
expired_count?: number;
|
||||
not_generated_count?: number;
|
||||
currently_readable_count?: number;
|
||||
withdrawn_count?: number;
|
||||
redacted_summary_count?: number;
|
||||
summary_count?: number;
|
||||
}>;
|
||||
privacy_impact: {
|
||||
state: string;
|
||||
summary: string;
|
||||
retained_categories: string[];
|
||||
minimized_categories: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type CampaignSummary = {
|
||||
generated_at?: string;
|
||||
selected_version_id?: string | null;
|
||||
@@ -336,6 +373,7 @@ export type CampaignSummary = {
|
||||
attachments?: Record<string, unknown>;
|
||||
attempts?: Record<string, unknown>;
|
||||
postbox_receipts?: Record<string, unknown>;
|
||||
retention?: CampaignRetentionReport;
|
||||
delivery?: Record<string, unknown>;
|
||||
recent_failures?: Record<string, unknown>[];
|
||||
};
|
||||
|
||||
@@ -129,7 +129,7 @@ export default function CampaignListPage({ settings }: {settings: ApiSettings;})
|
||||
{
|
||||
id: "actions",
|
||||
header: "i18n:govoplan-campaign.actions.c3cd636a",
|
||||
width: 70,
|
||||
width: 120,
|
||||
sticky: "end",
|
||||
align: "right",
|
||||
render: (campaign) => <TableActionGroup actions={[{
|
||||
|
||||
@@ -172,7 +172,7 @@ export default function CampaignOverviewPage({ settings, campaignId }: {settings
|
||||
<Card
|
||||
title="i18n:govoplan-campaign.campaign_identity.a00ca574"
|
||||
collapsible
|
||||
actions={<Link className="btn btn-secondary" to="wizard/create">i18n:govoplan-campaign.edit_with_wizard.672a7d1a</Link>}>
|
||||
actions={<Link className="btn btn-secondary" to="wizard">i18n:govoplan-campaign.edit_with_wizard.672a7d1a</Link>}>
|
||||
<div className="form-grid campaign-identity-grid">
|
||||
<FormField label="i18n:govoplan-campaign.campaign_id.4c4ed79e">
|
||||
<input value={identity.external_id} onChange={(event) => patchIdentity("external_id", event.target.value)} />
|
||||
@@ -308,8 +308,9 @@ function versionColumns(setPendingLockAction: (action: PendingLockAction) => voi
|
||||
{
|
||||
id: "actions",
|
||||
header: "i18n:govoplan-campaign.actions.c3cd636a",
|
||||
width: 150,
|
||||
width: 180,
|
||||
sticky: "end",
|
||||
align: "right",
|
||||
render: (version) => {
|
||||
const isCurrent = version.id === currentVersionId;
|
||||
const temporarilyLocked = isCurrent && isTemporaryUserLockedVersion(version);
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
sendCampaignJob,
|
||||
sendUnattemptedCampaignJobs,
|
||||
type CampaignJobDetailResponse,
|
||||
type CampaignJobsResponse } from
|
||||
type CampaignJobsResponse,
|
||||
type CampaignRetentionReport } from
|
||||
"../../api/campaigns";
|
||||
import { Card } from "@govoplan/core-webui";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
@@ -103,6 +104,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
const cards = data.summary?.cards;
|
||||
const delivery = asRecord(data.summary?.delivery);
|
||||
const postboxReceipts = asRecord(data.summary?.postbox_receipts);
|
||||
const retention = data.summary?.retention;
|
||||
const rateLimit = asRecord(delivery.rate_limit);
|
||||
const imapPolicy = asRecord(delivery.imap_append_sent);
|
||||
|
||||
@@ -500,6 +502,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
<div><dt>Withdrawn / expired copies</dt><dd>{String(Number(postboxReceipts.withdrawn_message_count ?? 0) + Number(postboxReceipts.expired_message_count ?? 0))}</dd></div>
|
||||
</dl>
|
||||
</Card>
|
||||
<RetentionPrivacyCard retention={retention} />
|
||||
<Card title="i18n:govoplan-campaign.explicit_delivery_actions.b35e72a4">
|
||||
<p className="muted">i18n:govoplan-campaign.these_actions_never_include_smtp_accepted_or_unr.449d0a80</p>
|
||||
<div className="button-row compact-actions">
|
||||
@@ -623,6 +626,102 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
|
||||
}
|
||||
|
||||
function RetentionPrivacyCard({ retention }: { retention?: CampaignRetentionReport }) {
|
||||
if (!retention) {
|
||||
return (
|
||||
<Card title="i18n:govoplan-campaign.retention_and_privacy.24418676">
|
||||
<p className="muted">i18n:govoplan-campaign.retention_information_is_unavailable.f68cc4d1</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
const policy = retention.effective_policy;
|
||||
const evidenceRows = [
|
||||
["i18n:govoplan-campaign.raw_campaign_json.53d8522d", retention.evidence.raw_campaign_json],
|
||||
["i18n:govoplan-campaign.stored_report_detail.13a437d7", retention.evidence.stored_report_detail],
|
||||
["i18n:govoplan-campaign.generated_message_files.2d86ef64", retention.evidence.generated_eml],
|
||||
["i18n:govoplan-campaign.postbox_copies.080b404f", retention.evidence.postbox_copies]
|
||||
] as const;
|
||||
return (
|
||||
<Card title="i18n:govoplan-campaign.retention_and_privacy.24418676">
|
||||
<p>{retention.privacy_impact.summary}</p>
|
||||
{retention.policy_reason && <p className="muted">{retention.policy_reason}</p>}
|
||||
<dl className="detail-list">
|
||||
<div>
|
||||
<dt>i18n:govoplan-campaign.policy_state.7e955a0d</dt>
|
||||
<dd>
|
||||
<StatusBadge
|
||||
status={retention.policy_status === "configured" ? "success" : retention.policy_status === "defaults" ? "warning" : "error"}
|
||||
label={humanize(retention.policy_status)}
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>i18n:govoplan-campaign.policy_sources.a592802f</dt>
|
||||
<dd>{retention.sources.map((source) => source.label || source.path).filter(Boolean).join(" → ") || "—"}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>i18n:govoplan-campaign.raw_json_retention.19ea35f7</dt>
|
||||
<dd>{policy.store_raw_campaign_json === false ? "i18n:govoplan-campaign.do_not_retain.9fd25c4d" : retentionDuration(policy.raw_campaign_json_retention_days)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>i18n:govoplan-campaign.generated_message_retention.ce6366ca</dt>
|
||||
<dd>{retentionDuration(policy.generated_eml_retention_days)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>i18n:govoplan-campaign.report_detail_retention.68e30587</dt>
|
||||
<dd>{retentionDuration(policy.stored_report_detail_retention_days)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>i18n:govoplan-campaign.audit_detail.2a85f905</dt>
|
||||
<dd>{humanize(policy.audit_detail_level ?? "full")} · {retentionDuration(policy.audit_detail_retention_days)}</dd>
|
||||
</div>
|
||||
{evidenceRows.map(([label, evidence]) =>
|
||||
<div key={label}>
|
||||
<dt>{label}</dt>
|
||||
<dd>
|
||||
<StatusBadge
|
||||
status={retentionEvidenceTone(evidence?.state)}
|
||||
label={humanize(evidence?.state ?? "unavailable")}
|
||||
/>
|
||||
<span className="muted"> · {retentionEvidenceDetail(evidence)}</span>
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function retentionDuration(days?: number | null): string {
|
||||
if (days === null || days === undefined) return "i18n:govoplan-campaign.no_automatic_expiry.52cb62f8";
|
||||
if (days === 0) return "i18n:govoplan-campaign.remove_when_eligible.385ff0eb";
|
||||
return i18nMessage("i18n:govoplan-campaign.value_days.2bf9b447", { value0: String(days) });
|
||||
}
|
||||
|
||||
function retentionEvidenceTone(state?: string): string {
|
||||
if (state === "retained") return "success";
|
||||
if (state === "redacted" || state === "expired") return "inactive";
|
||||
if (state === "partially_redacted" || state === "partially_expired") return "warning";
|
||||
if (state === "unavailable") return "error";
|
||||
return "info";
|
||||
}
|
||||
|
||||
function retentionEvidenceDetail(
|
||||
evidence?: CampaignRetentionReport["evidence"][string]
|
||||
): string {
|
||||
if (!evidence) return "—";
|
||||
const counts = [
|
||||
evidence.retained_count !== undefined ? `${evidence.retained_count} retained` : "",
|
||||
evidence.expired_count !== undefined ? `${evidence.expired_count} expired` : "",
|
||||
evidence.redacted_summary_count !== undefined
|
||||
? `${evidence.redacted_summary_count}/${evidence.summary_count ?? 0} redacted`
|
||||
: "",
|
||||
evidence.currently_readable_count !== undefined ? `${evidence.currently_readable_count} readable` : "",
|
||||
evidence.withdrawn_count !== undefined ? `${evidence.withdrawn_count} withdrawn` : ""
|
||||
].filter(Boolean);
|
||||
return counts.join(", ") || (evidence.redacted_at ? formatDateTime(evidence.redacted_at) : "—");
|
||||
}
|
||||
|
||||
function PostboxTargetEvidenceSection({ targets }: { targets: unknown[] }) {
|
||||
const rows = targets.map(asRecord);
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
@@ -18,6 +18,7 @@ const ReviewSendPage = lazy(() => import("./ReviewSendPage"));
|
||||
const CreateWizard = lazy(() => import("./wizard/CreateWizard"));
|
||||
const ReviewWizard = lazy(() => import("./wizard/ReviewWizard"));
|
||||
const SendWizard = lazy(() => import("./wizard/SendWizard"));
|
||||
const WizardDirectoryPage = lazy(() => import("./wizard/WizardDirectoryPage"));
|
||||
const CampaignJsonView = lazy(() => import("./CampaignJsonView"));
|
||||
const CampaignReportPage = lazy(() => import("./CampaignReportPage"));
|
||||
const CampaignAuditPage = lazy(() => import("./CampaignAuditPage"));
|
||||
@@ -109,9 +110,10 @@ function CampaignWorkspaceInner({ settings, auth }: { settings: ApiSettings; aut
|
||||
<Route path="reports" element={<Navigate to="../report" replace />} />
|
||||
<Route path="audit" element={<CampaignAuditPage settings={settings} campaignId={campaignId || ""} />} />
|
||||
<Route path="json" element={<CampaignJsonView settings={settings} campaignId={campaignId || ""} />} />
|
||||
<Route path="wizard" element={<WizardDirectoryPage settings={settings} auth={auth} campaignId={campaignId || ""} />} />
|
||||
<Route path="wizard/create" element={<CreateWizard settings={settings} campaignId={campaignId || ""} />} />
|
||||
<Route path="wizard/review" element={<ReviewWizard />} />
|
||||
<Route path="wizard/send" element={<SendWizard />} />
|
||||
<Route path="wizard/review" element={<ReviewWizard settings={settings} auth={auth} campaignId={campaignId || ""} />} />
|
||||
<Route path="wizard/send" element={<SendWizard settings={settings} auth={auth} campaignId={campaignId || ""} />} />
|
||||
<Route path="create" element={<Navigate to="../wizard/create" replace />} />
|
||||
<Route path="campaign" element={<Navigate to="../data" replace />} />
|
||||
<Route path="mail" element={<Navigate to="../mail-settings" replace />} />
|
||||
|
||||
@@ -111,7 +111,17 @@ type WorkflowBusy = "validate" | "build" | "inspect" | "mock" | "mailbox" | "sen
|
||||
|
||||
const MESSAGE_REVIEW_ISSUE_STATUSES = ["warning", "needs_review", "blocked", "excluded"];
|
||||
|
||||
export default function ReviewSendPage({ settings, auth, campaignId }: {settings: ApiSettings;auth: AuthInfo;campaignId: string;}) {
|
||||
export default function ReviewSendPage({
|
||||
settings,
|
||||
auth,
|
||||
campaignId,
|
||||
initialStageId
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
auth: AuthInfo;
|
||||
campaignId: string;
|
||||
initialStageId?: string;
|
||||
}) {
|
||||
const navigate = useGuardedNavigate();
|
||||
const devMailboxCapability = usePlatformUiCapability<MailDevMailboxUiCapability>("mail.devMailbox");
|
||||
const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks();
|
||||
@@ -188,6 +198,7 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
const [selectedDeliveryJobDetail, setSelectedDeliveryJobDetail] = useState<Record<string, unknown> | null>(null);
|
||||
const persistedReview = storedMessageReviewState(version);
|
||||
const persistedReviewKey = `${persistedReview.buildToken}|${persistedReview.inspectionComplete ? "1" : "0"}|${persistedReview.reviewedMessageKeys.join(",")}|${JSON.stringify(persistedReview.issueDecisions)}`;
|
||||
const initialStageScrollKey = useRef("");
|
||||
|
||||
useEffect(() => {
|
||||
setBuiltReviewRows([]);
|
||||
@@ -218,6 +229,20 @@ export default function ReviewSendPage({ settings, auth, campaignId }: {settings
|
||||
resetDeltaWatermark();
|
||||
}, [version?.id, resetDeltaWatermark]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialStageId || loading || !version?.id) return;
|
||||
const key = `${version.id}:${initialStageId}`;
|
||||
if (initialStageScrollKey.current === key) return;
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
const target = document.getElementById(initialStageId);
|
||||
if (!target) return;
|
||||
initialStageScrollKey.current = key;
|
||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
target.focus({ preventScroll: true });
|
||||
});
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}, [initialStageId, loading, version?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
setMessageReviewComplete(persistedReview.inspectionComplete);
|
||||
setReviewedMessageKeys(new Set(persistedReview.reviewedMessageKeys));
|
||||
|
||||
@@ -120,7 +120,7 @@ export function WorkflowStage({
|
||||
} as CSSProperties;
|
||||
|
||||
return (
|
||||
<section id={stage.id} className="review-flow-stage" data-state={stage.state} style={style}>
|
||||
<section id={stage.id} className="review-flow-stage" data-state={stage.state} style={style} tabIndex={-1}>
|
||||
<div className="review-flow-stage-marker" aria-hidden="true">
|
||||
<div className="review-flow-stage-node">
|
||||
<Icon size={20} strokeWidth={1.8} />
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { Card } from "@govoplan/core-webui";
|
||||
import { MetricCard } from "@govoplan/core-webui";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
export default function ReviewWizard() {
|
||||
return (
|
||||
<div className="content-pad workspace-data-page">
|
||||
<div className="page-heading workspace-heading">
|
||||
<h1>i18n:govoplan-campaign.review_wizard.3d8bf0aa</h1>
|
||||
</div>
|
||||
<div className="metric-grid">
|
||||
<MetricCard label="i18n:govoplan-campaign.needs_review.33a506cf" value="—" tone="warning" />
|
||||
<MetricCard label="i18n:govoplan-campaign.missing_attachments.729ad125" value="—" tone="warning" />
|
||||
<MetricCard label="i18n:govoplan-campaign.ambiguous_matches.dc658a9c" value="—" tone="info" />
|
||||
<MetricCard label="i18n:govoplan-campaign.blocked.99613c74" value="—" tone="danger" />
|
||||
</div>
|
||||
<Card title="i18n:govoplan-campaign.resolution_workflow.708d6c0b">
|
||||
<p className="muted">i18n:govoplan-campaign.this_wizard_will_guide_users_through_issues_one_.5cb212c3</p>
|
||||
<Button variant="primary">i18n:govoplan-campaign.start_review.d0bc5cdf</Button>
|
||||
</Card>
|
||||
</div>);
|
||||
import type { ApiSettings, AuthInfo } from "@govoplan/core-webui";
|
||||
import ReviewSendPage from "../ReviewSendPage";
|
||||
|
||||
export default function ReviewWizard({
|
||||
settings,
|
||||
auth,
|
||||
campaignId
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
auth: AuthInfo;
|
||||
campaignId: string;
|
||||
}) {
|
||||
return (
|
||||
<ReviewSendPage
|
||||
settings={settings}
|
||||
auth={auth}
|
||||
campaignId={campaignId}
|
||||
initialStageId="workflow-build-review"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { Card } from "@govoplan/core-webui";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
export default function SendWizard() {
|
||||
return (
|
||||
<div className="content-pad workspace-data-page">
|
||||
<div className="page-heading workspace-heading">
|
||||
<h1>i18n:govoplan-campaign.send_wizard.3c137422</h1>
|
||||
</div>
|
||||
<div className="dashboard-grid">
|
||||
<Card title="i18n:govoplan-campaign.test_send.03dfff38">
|
||||
<p className="muted">i18n:govoplan-campaign.send_one_generated_message_to_a_test_address.bc0a4e47</p>
|
||||
<Button>i18n:govoplan-campaign.open_test_send_dialog.661db713</Button>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-campaign.queue_estimate.5480288a">
|
||||
<p className="muted">i18n:govoplan-campaign.estimated_duration_will_be_based_on_ready_jobs_a.15b63283</p>
|
||||
<Button variant="primary">i18n:govoplan-campaign.queue_dry_run.800e1606</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</div>);
|
||||
import type { ApiSettings, AuthInfo } from "@govoplan/core-webui";
|
||||
import ReviewSendPage from "../ReviewSendPage";
|
||||
|
||||
export default function SendWizard({
|
||||
settings,
|
||||
auth,
|
||||
campaignId
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
auth: AuthInfo;
|
||||
campaignId: string;
|
||||
}) {
|
||||
return (
|
||||
<ReviewSendPage
|
||||
settings={settings}
|
||||
auth={auth}
|
||||
campaignId={campaignId}
|
||||
initialStageId="workflow-send"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { Link } from "react-router";
|
||||
import {
|
||||
PageTitle,
|
||||
WizardDirectory,
|
||||
usePlatformUiCapabilities,
|
||||
wizardEntriesForContext,
|
||||
type ApiSettings,
|
||||
type AuthInfo,
|
||||
type WizardDirectoriesUiCapability
|
||||
} from "@govoplan/core-webui";
|
||||
|
||||
import { campaignWizardDirectories } from "./directory";
|
||||
|
||||
export default function WizardDirectoryPage({
|
||||
settings,
|
||||
auth,
|
||||
campaignId
|
||||
}: {
|
||||
settings: ApiSettings;
|
||||
auth: AuthInfo;
|
||||
campaignId: string;
|
||||
}) {
|
||||
const contributed = usePlatformUiCapabilities<WizardDirectoriesUiCapability>(
|
||||
"wizard.directories"
|
||||
);
|
||||
const capabilities = contributed.some((capability) =>
|
||||
capability.directories.some(
|
||||
(directory) => directory.id === "campaigns.resource"
|
||||
)
|
||||
)
|
||||
? contributed
|
||||
: [...contributed, campaignWizardDirectories];
|
||||
const entries = wizardEntriesForContext(capabilities, {
|
||||
settings,
|
||||
auth,
|
||||
contextId: "campaign.resource",
|
||||
resourceId: campaignId
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="content-pad workspace-data-page">
|
||||
<div className="page-heading split workspace-heading">
|
||||
<PageTitle>i18n:govoplan-campaign.guided_workflows</PageTitle>
|
||||
<Link className="btn btn-secondary" to="../">
|
||||
i18n:govoplan-campaign.back_to_overview.ec986cba
|
||||
</Link>
|
||||
</div>
|
||||
<WizardDirectory
|
||||
entries={entries}
|
||||
auth={auth}
|
||||
emptyText="i18n:govoplan-campaign.no_guided_workflows"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type {
|
||||
WizardDirectoriesUiCapability,
|
||||
WizardDirectoryEntry
|
||||
} from "@govoplan/core-webui";
|
||||
|
||||
const campaignRead = ["campaigns:campaign:read"];
|
||||
|
||||
export function campaignWizardEntries(
|
||||
campaignId: string
|
||||
): WizardDirectoryEntry[] {
|
||||
const basePath = `/campaigns/${encodeURIComponent(campaignId)}/wizard`;
|
||||
return [
|
||||
{
|
||||
id: "configure",
|
||||
moduleId: "campaigns",
|
||||
label: "i18n:govoplan-campaign.create_campaign.59812bbc",
|
||||
description: "i18n:govoplan-campaign.name_and_scenario.f2bc5241",
|
||||
to: `${basePath}/create`,
|
||||
actionLabel: "i18n:govoplan-campaign.open.cf9b7706",
|
||||
anyOf: campaignRead,
|
||||
order: 10
|
||||
},
|
||||
{
|
||||
id: "review",
|
||||
moduleId: "campaigns",
|
||||
label: "i18n:govoplan-campaign.review_wizard.3d8bf0aa",
|
||||
description: "i18n:govoplan-campaign.lock_and_validate_the_campaign_then_inspect_bloc.e22b9266",
|
||||
to: `${basePath}/review`,
|
||||
actionLabel: "i18n:govoplan-campaign.open.cf9b7706",
|
||||
anyOf: campaignRead,
|
||||
order: 20
|
||||
},
|
||||
{
|
||||
id: "send",
|
||||
moduleId: "campaigns",
|
||||
label: "i18n:govoplan-campaign.send_wizard.3c137422",
|
||||
description: "i18n:govoplan-campaign.review_the_final_execution_summary_optionally_ru.6b32b00a",
|
||||
to: `${basePath}/send`,
|
||||
actionLabel: "i18n:govoplan-campaign.open.cf9b7706",
|
||||
anyOf: campaignRead,
|
||||
order: 30
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export const campaignWizardDirectories: WizardDirectoriesUiCapability = {
|
||||
directories: [
|
||||
{
|
||||
id: "campaigns.resource",
|
||||
moduleId: "campaigns",
|
||||
contextId: "campaign.resource",
|
||||
order: 20,
|
||||
entries: ({ resourceId }) =>
|
||||
resourceId ? campaignWizardEntries(resourceId) : []
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -2,6 +2,8 @@ import type { PlatformTranslations } from "@govoplan/core-webui";
|
||||
|
||||
export const generatedTranslations: PlatformTranslations = {
|
||||
"en": {
|
||||
"i18n:govoplan-campaign.guided_workflows": "Guided workflows",
|
||||
"i18n:govoplan-campaign.no_guided_workflows": "No guided workflows are available.",
|
||||
"i18n:govoplan-campaign.1_campaign.ccd70074": "1 campaign",
|
||||
"i18n:govoplan-campaign.2_campaigns.35b84804": "2 campaigns",
|
||||
"i18n:govoplan-campaign.appended.979f5a82": "Appended",
|
||||
@@ -317,6 +319,22 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-campaign.campaign_policies.0b5de1f5": "Campaign policies",
|
||||
"i18n:govoplan-campaign.campaign_queues.657785f4": "Campaign queues",
|
||||
"i18n:govoplan-campaign.campaign_retention_policy.fcc9897c": "Campaign retention policy",
|
||||
"i18n:govoplan-campaign.retention_and_privacy.24418676": "Retention and privacy",
|
||||
"i18n:govoplan-campaign.retention_information_is_unavailable.f68cc4d1": "Retention information is unavailable.",
|
||||
"i18n:govoplan-campaign.policy_state.7e955a0d": "Policy state",
|
||||
"i18n:govoplan-campaign.policy_sources.a592802f": "Policy sources",
|
||||
"i18n:govoplan-campaign.raw_json_retention.19ea35f7": "Raw JSON retention",
|
||||
"i18n:govoplan-campaign.generated_message_retention.ce6366ca": "Generated message retention",
|
||||
"i18n:govoplan-campaign.report_detail_retention.68e30587": "Report detail retention",
|
||||
"i18n:govoplan-campaign.audit_detail.2a85f905": "Audit detail",
|
||||
"i18n:govoplan-campaign.raw_campaign_json.53d8522d": "Raw campaign JSON",
|
||||
"i18n:govoplan-campaign.stored_report_detail.13a437d7": "Stored report detail",
|
||||
"i18n:govoplan-campaign.generated_message_files.2d86ef64": "Generated message files",
|
||||
"i18n:govoplan-campaign.postbox_copies.080b404f": "Postbox copies",
|
||||
"i18n:govoplan-campaign.do_not_retain.9fd25c4d": "Do not retain",
|
||||
"i18n:govoplan-campaign.no_automatic_expiry.52cb62f8": "No automatic expiry",
|
||||
"i18n:govoplan-campaign.remove_when_eligible.385ff0eb": "Remove when eligible",
|
||||
"i18n:govoplan-campaign.value_days.2bf9b447": "{value0} days",
|
||||
"i18n:govoplan-campaign.campaign_review_and_sending_workflow.e784ea7b": "Campaign review and sending workflow",
|
||||
"i18n:govoplan-campaign.campaign_root.7eccd949": "Campaign root",
|
||||
"i18n:govoplan-campaign.campaign_sender.b12e8ae2": "Campaign sender",
|
||||
@@ -1284,6 +1302,8 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-campaign.zipcrypto.03bf7fb4": "ZipCrypto"
|
||||
},
|
||||
"de": {
|
||||
"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.1_campaign.ccd70074": "1 campaign",
|
||||
"i18n:govoplan-campaign.2_campaigns.35b84804": "2 campaigns",
|
||||
"i18n:govoplan-campaign.appended.979f5a82": "Abgelegt",
|
||||
@@ -1599,6 +1619,22 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-campaign.campaign_policies.0b5de1f5": "Campaign policies",
|
||||
"i18n:govoplan-campaign.campaign_queues.657785f4": "Campaign queues",
|
||||
"i18n:govoplan-campaign.campaign_retention_policy.fcc9897c": "Campaign retention policy",
|
||||
"i18n:govoplan-campaign.retention_and_privacy.24418676": "Aufbewahrung und Datenschutz",
|
||||
"i18n:govoplan-campaign.retention_information_is_unavailable.f68cc4d1": "Aufbewahrungsinformationen sind nicht verfügbar.",
|
||||
"i18n:govoplan-campaign.policy_state.7e955a0d": "Richtlinienstatus",
|
||||
"i18n:govoplan-campaign.policy_sources.a592802f": "Richtlinienquellen",
|
||||
"i18n:govoplan-campaign.raw_json_retention.19ea35f7": "Aufbewahrung der Rohdaten",
|
||||
"i18n:govoplan-campaign.generated_message_retention.ce6366ca": "Aufbewahrung erzeugter Nachrichten",
|
||||
"i18n:govoplan-campaign.report_detail_retention.68e30587": "Aufbewahrung der Berichtsdetails",
|
||||
"i18n:govoplan-campaign.audit_detail.2a85f905": "Auditdetails",
|
||||
"i18n:govoplan-campaign.raw_campaign_json.53d8522d": "Kampagnen-Rohdaten",
|
||||
"i18n:govoplan-campaign.stored_report_detail.13a437d7": "Gespeicherte Berichtsdetails",
|
||||
"i18n:govoplan-campaign.generated_message_files.2d86ef64": "Erzeugte Nachrichtendateien",
|
||||
"i18n:govoplan-campaign.postbox_copies.080b404f": "Postfachkopien",
|
||||
"i18n:govoplan-campaign.do_not_retain.9fd25c4d": "Nicht aufbewahren",
|
||||
"i18n:govoplan-campaign.no_automatic_expiry.52cb62f8": "Kein automatischer Ablauf",
|
||||
"i18n:govoplan-campaign.remove_when_eligible.385ff0eb": "Nach Abschluss entfernen",
|
||||
"i18n:govoplan-campaign.value_days.2bf9b447": "{value0} Tage",
|
||||
"i18n:govoplan-campaign.campaign_review_and_sending_workflow.e784ea7b": "Campaign review and sending workflow",
|
||||
"i18n:govoplan-campaign.campaign_root.7eccd949": "Campaign root",
|
||||
"i18n:govoplan-campaign.campaign_sender.b12e8ae2": "Campaign sender",
|
||||
|
||||
+3
-1
@@ -12,6 +12,7 @@ import { getCampaign } from "./api/campaigns";
|
||||
import CampaignActivityWidget from "./features/campaigns/CampaignActivityWidget";
|
||||
import { OPERATOR_QUEUE_ROUTE_SCOPES } from "./features/operator/operatorQueueAccess";
|
||||
import { generatedTranslations } from "./i18n/generatedTranslations";
|
||||
import { campaignWizardDirectories } from "./features/campaigns/wizard/directory";
|
||||
import "./styles/campaign-workspace.css";
|
||||
|
||||
const CampaignModulePage = lazy(() => import("./features/campaigns/CampaignModulePage"));
|
||||
@@ -103,7 +104,8 @@ export const campaignModule: PlatformWebModule = {
|
||||
{ path: "/campaigns/:campaignId/*", anyOf: campaignRead, order: 22, render: ({ settings, auth }) => createElement(CampaignResourceRoute, { settings, auth }) },
|
||||
{ path: "/templates", order: 90, render: () => createElement(TemplatesPage) }],
|
||||
uiCapabilities: {
|
||||
"dashboard.widgets": campaignDashboardWidgets
|
||||
"dashboard.widgets": campaignDashboardWidgets,
|
||||
"wizard.directories": campaignWizardDirectories
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
function read(path) {
|
||||
return readFileSync(new URL(path, import.meta.url), "utf8");
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
const workspace = read("../src/features/campaigns/CampaignWorkspace.tsx");
|
||||
const reviewWizard = read("../src/features/campaigns/wizard/ReviewWizard.tsx");
|
||||
const sendWizard = read("../src/features/campaigns/wizard/SendWizard.tsx");
|
||||
const directoryPage = read(
|
||||
"../src/features/campaigns/wizard/WizardDirectoryPage.tsx"
|
||||
);
|
||||
const directory = read("../src/features/campaigns/wizard/directory.ts");
|
||||
const moduleSource = read("../src/module.ts");
|
||||
|
||||
assert(
|
||||
workspace.includes('path="wizard"')
|
||||
&& workspace.includes("<WizardDirectoryPage"),
|
||||
"campaign resources expose the wizard directory route"
|
||||
);
|
||||
assert(
|
||||
reviewWizard.includes("<ReviewSendPage")
|
||||
&& reviewWizard.includes('initialStageId="workflow-build-review"'),
|
||||
"the review wizard focuses the canonical build and review stage"
|
||||
);
|
||||
assert(
|
||||
sendWizard.includes("<ReviewSendPage")
|
||||
&& sendWizard.includes('initialStageId="workflow-send"'),
|
||||
"the send wizard focuses the canonical send stage"
|
||||
);
|
||||
assert(
|
||||
!reviewWizard.includes("<MetricCard")
|
||||
&& !sendWizard.includes("queue_dry_run"),
|
||||
"placeholder review and send surfaces were removed"
|
||||
);
|
||||
assert(
|
||||
directoryPage.includes("wizardEntriesForContext")
|
||||
&& directoryPage.includes("<WizardDirectory"),
|
||||
"the page uses the reusable wizard directory contract and renderer"
|
||||
);
|
||||
assert(
|
||||
directory.includes('contextId: "campaign.resource"')
|
||||
&& directory.includes('id: "configure"')
|
||||
&& directory.includes('id: "review"')
|
||||
&& directory.includes('id: "send"'),
|
||||
"Campaign contributes configure, review, and send entries"
|
||||
);
|
||||
assert(
|
||||
moduleSource.includes('"wizard.directories": campaignWizardDirectories'),
|
||||
"Campaign publishes its wizard directory capability"
|
||||
);
|
||||
Reference in New Issue
Block a user