feat(webui): add campaign metric drill-downs
This commit is contained in:
@@ -246,6 +246,21 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
||||
patchZipCollection({ ...zipConfig, archives: moveArrayItem(zipConfig.archives, index, targetIndex) });
|
||||
}
|
||||
|
||||
function revealAttachmentSection(id: string) {
|
||||
const section = document.getElementById(id);
|
||||
if (!section) return;
|
||||
section.querySelector<HTMLButtonElement>('.card-collapse-toggle[aria-expanded="false"]')?.click();
|
||||
window.requestAnimationFrame(() => {
|
||||
section.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
section.focus({ preventScroll: true });
|
||||
});
|
||||
}
|
||||
|
||||
function openRecipients() {
|
||||
const query = version?.id ? `?version=${encodeURIComponent(version.id)}` : "";
|
||||
navigate(`/campaigns/${campaignId}/recipients${query}`);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<PageLayout
|
||||
@@ -257,8 +272,7 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
||||
error={error}
|
||||
actions={<PageActionBar
|
||||
variant="editor"
|
||||
dirty={dirty}
|
||||
saving={loading}
|
||||
state={loading ? "saving" : dirty ? "dirty" : "clean"}
|
||||
contextActions={filesModuleInstalled ? <Button onClick={() => navigate("/files")}>i18n:govoplan-campaign.manage_files.90a419f7</Button> : 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 }}
|
||||
@@ -272,13 +286,13 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
||||
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
|
||||
<>
|
||||
<MetricGrid>
|
||||
<MetricCard label="i18n:govoplan-campaign.base_paths.edf35a97" value={basePaths.length} tone="neutral" />
|
||||
<MetricCard label="i18n:govoplan-campaign.global_attachments.438263a1" value={`${globalSummary.direct} / ${globalSummary.rules}`} tone="info" detail="direct / rules" />
|
||||
<MetricCard label="i18n:govoplan-campaign.per_recipient_patterns.53da30da" value={individualRulesCount} tone="neutral" />
|
||||
<MetricCard label="i18n:govoplan-campaign.base_paths.edf35a97" value={basePaths.length} tone="neutral" drilldown={{ label: i18nMessage("i18n:govoplan-campaign.open_value.a34416a9", { value0: "Attachment sources" }), onActivate: () => revealAttachmentSection("campaign-attachment-sources") }} />
|
||||
<MetricCard label="i18n:govoplan-campaign.global_attachments.438263a1" value={`${globalSummary.direct} / ${globalSummary.rules}`} tone="info" detail="direct / rules" drilldown={{ label: "i18n:govoplan-campaign.review_attachment_rules.552c2116", onActivate: () => revealAttachmentSection("campaign-global-attachments") }} />
|
||||
<MetricCard label="i18n:govoplan-campaign.per_recipient_patterns.53da30da" value={individualRulesCount} tone="neutral" drilldown={{ label: i18nMessage("i18n:govoplan-campaign.open_value.a34416a9", { value0: "Recipients" }), onActivate: openRecipients }} />
|
||||
<MetricCard label="i18n:govoplan-campaign.upload_support.1c54931c" value={managedFilesAvailable ? "Files" : "Manual"} tone={managedFilesAvailable ? "good" : filesModuleInstalled ? "warning" : "neutral"} />
|
||||
</MetricGrid>
|
||||
|
||||
<Card title="i18n:govoplan-campaign.attachment_sources.8ef0a6ce">
|
||||
<Card id="campaign-attachment-sources" tabIndex={-1} title="i18n:govoplan-campaign.attachment_sources.8ef0a6ce">
|
||||
<div className="admin-table-surface attachment-sources-table-surface">
|
||||
<DataGrid
|
||||
id={`campaign-${campaignId}-attachment-sources`}
|
||||
@@ -373,7 +387,7 @@ export default function AttachmentsDataPage({ settings, campaignId }: {settings:
|
||||
}
|
||||
</Card>
|
||||
|
||||
<Card title="i18n:govoplan-campaign.global_attachments.492bd841" collapsible>
|
||||
<Card id="campaign-global-attachments" tabIndex={-1} title="i18n:govoplan-campaign.global_attachments.492bd841" collapsible>
|
||||
<AttachmentRulesDataGrid
|
||||
id={`campaign-${campaignId}-global-attachments`}
|
||||
rules={globalRules}
|
||||
|
||||
@@ -21,7 +21,7 @@ export default function CampaignAuditPage({ settings, campaignId }: {settings: A
|
||||
actions={<PageActionBar
|
||||
variant="detail"
|
||||
refreshable
|
||||
reloadAction={<Button onClick={() => void reload({ force: true })} disabled={loading}>i18n:govoplan-campaign.reload.cce71553</Button>}
|
||||
reloadAction={{ onReload: () => void reload({ force: true }), loading }}
|
||||
/>}
|
||||
>
|
||||
<Card title="i18n:govoplan-campaign.recent_audit_events.7ec32b1d">
|
||||
|
||||
@@ -159,8 +159,7 @@ export default function CampaignFieldsPage({ settings, campaignId }: {settings:
|
||||
error={error}
|
||||
actions={<PageActionBar
|
||||
variant="editor"
|
||||
dirty={dirty}
|
||||
saving={loading}
|
||||
state={loading ? "saving" : dirty ? "dirty" : "clean"}
|
||||
discardAction={{ label: "i18n:govoplan-campaign.discard.36fff63c", onClick: () => 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 }}
|
||||
/>}
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function CampaignJsonView({ settings, campaignId }: {settings: Ap
|
||||
actions={<PageActionBar
|
||||
variant="detail"
|
||||
refreshable
|
||||
reloadAction={<Button onClick={() => void reload({ force: true })} disabled={loading}>i18n:govoplan-campaign.reload.cce71553</Button>}
|
||||
reloadAction={{ onReload: () => void reload({ force: true }), loading }}
|
||||
primaryActions={<Button onClick={() => downloadJson(filename, campaignJson)} disabled={!version}>i18n:govoplan-campaign.download_json.d296a30a</Button>}
|
||||
/>}
|
||||
notices={
|
||||
|
||||
@@ -153,7 +153,7 @@ export default function CampaignListPage({ settings }: {settings: ApiSettings;})
|
||||
actions={<PageActionBar
|
||||
variant="collection"
|
||||
refreshable
|
||||
reloadAction={<Button onClick={() => void load(null)} disabled={loading}>i18n:govoplan-campaign.reload.cce71553</Button>}
|
||||
reloadAction={{ onReload: () => void load(null), loading }}
|
||||
createAction={<Button variant="primary" onClick={create} disabled={creating}>
|
||||
{creating ? "i18n:govoplan-campaign.creating.94d7d8ee" : "i18n:govoplan-campaign.new_campaign.aaf9a8a4"}
|
||||
</Button>}
|
||||
|
||||
@@ -114,6 +114,19 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
|
||||
const canCopy = Boolean(data.currentVersion) && hasScope(auth, "campaigns:campaign:copy");
|
||||
const canSchedule = Boolean(data.currentVersion) && hasScope(auth, "campaigns:campaign:schedule") && hasScope(auth, "campaigns:campaign:copy");
|
||||
|
||||
function openSection(section: string, fragment = "") {
|
||||
const params = new URLSearchParams();
|
||||
if (data.currentVersion?.id) params.set("version", data.currentVersion.id);
|
||||
const query = params.toString();
|
||||
navigate(`/campaigns/${campaignId}/${section}${query ? `?${query}` : ""}${fragment}`);
|
||||
}
|
||||
|
||||
function openQueue() {
|
||||
const params = new URLSearchParams({ campaign: campaignId });
|
||||
if (data.currentVersion?.id) params.set("version", data.currentVersion.id);
|
||||
navigate(`/campaigns/queue?${params.toString()}`);
|
||||
}
|
||||
|
||||
useUnsavedDraftGuard({
|
||||
dirty: identityDirty,
|
||||
onSave: saveIdentity,
|
||||
@@ -343,8 +356,7 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
|
||||
success={message}
|
||||
actions={<PageActionBar
|
||||
variant="editor"
|
||||
dirty={identityDirty}
|
||||
saving={savingIdentity}
|
||||
state={savingIdentity ? "saving" : identityDirty ? "dirty" : "clean"}
|
||||
primaryActions={<>
|
||||
{canCopy && data.currentVersion && <Button
|
||||
onClick={() => void prepareLifecycleAction("copy_campaign", data.currentVersion ?? undefined)}
|
||||
@@ -386,10 +398,10 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
|
||||
|
||||
<LoadingFrame loading={loading} label="i18n:govoplan-campaign.loading_campaign_overview.ffa1adf0">
|
||||
<MetricGrid>
|
||||
<MetricCard label="i18n:govoplan-campaign.queueable.ea776f8d" value={data.summary?.cards?.queueable ?? "—"} tone="good" detail="i18n:govoplan-campaign.ready_or_warning.4dcce676" />
|
||||
<MetricCard label="i18n:govoplan-campaign.needs_attention.a126722e" value={data.summary?.cards?.needs_attention ?? "—"} tone="warning" detail="i18n:govoplan-campaign.review_first.741ac781" />
|
||||
<MetricCard label="i18n:govoplan-campaign.sent.35f49dcf" value={data.summary?.cards?.sent ?? "—"} tone="info" detail="i18n:govoplan-campaign.smtp_success.3591a856" />
|
||||
<MetricCard label="i18n:govoplan-campaign.failed.09fef5d8" value={data.summary?.cards?.failed ?? "—"} tone="danger" detail="i18n:govoplan-campaign.smtp_failures.00b33b85" />
|
||||
<MetricCard label="i18n:govoplan-campaign.queueable.ea776f8d" value={data.summary?.cards?.queueable ?? "—"} tone="good" detail="i18n:govoplan-campaign.ready_or_warning.4dcce676" drilldown={{ label: "i18n:govoplan-campaign.review.e29a79fe", onActivate: () => openSection("review", "#workflow-send") }} />
|
||||
<MetricCard label="i18n:govoplan-campaign.needs_attention.a126722e" value={data.summary?.cards?.needs_attention ?? "—"} tone="warning" detail="i18n:govoplan-campaign.review_first.741ac781" drilldown={{ label: "i18n:govoplan-campaign.review.e29a79fe", onActivate: () => openSection("review", "#workflow-build-review") }} />
|
||||
<MetricCard label="i18n:govoplan-campaign.sent.35f49dcf" value={data.summary?.cards?.sent ?? "—"} tone="info" detail="i18n:govoplan-campaign.smtp_success.3591a856" drilldown={{ label: "i18n:govoplan-campaign.open_report.44f83158", onActivate: () => openSection("report") }} />
|
||||
<MetricCard label="i18n:govoplan-campaign.failed.09fef5d8" value={data.summary?.cards?.failed ?? "—"} tone="danger" detail="i18n:govoplan-campaign.smtp_failures.00b33b85" drilldown={{ label: "i18n:govoplan-campaign.open_report.44f83158", onActivate: () => openSection("report") }} />
|
||||
</MetricGrid>
|
||||
|
||||
<Card
|
||||
@@ -460,15 +472,15 @@ export default function CampaignOverviewPage({ settings, auth, campaignId }: {se
|
||||
</div>}>
|
||||
<MetricGrid spacing="inset">
|
||||
<MetricCard label="i18n:govoplan-campaign.version.2da600bf" value={data.currentVersion?.version_number ? `#${data.currentVersion.version_number}` : "—"} tone="neutral" />
|
||||
<MetricCard label="i18n:govoplan-campaign.fields.e8b68527" value={versionMetrics.fieldCount} tone="info" />
|
||||
<MetricCard label="i18n:govoplan-campaign.recipients.78cbf8eb" value={versionMetrics.recipientCount} tone="neutral" detail="i18n:govoplan-campaign.active_inline_recipients.8ba58f6e" />
|
||||
<MetricCard label="i18n:govoplan-campaign.template_health.22e14b59" value={versionMetrics.templateHealthValue} tone={versionMetrics.templateHealthTone} detail={versionMetrics.templateHealthDetail} />
|
||||
<MetricCard label="i18n:govoplan-campaign.fields.e8b68527" value={versionMetrics.fieldCount} tone="info" drilldown={{ label: i18nMessage("i18n:govoplan-campaign.open_value.a34416a9", { value0: "Fields" }), onActivate: () => openSection("fields") }} />
|
||||
<MetricCard label="i18n:govoplan-campaign.recipients.78cbf8eb" value={versionMetrics.recipientCount} tone="neutral" detail="i18n:govoplan-campaign.active_inline_recipients.8ba58f6e" drilldown={{ label: i18nMessage("i18n:govoplan-campaign.open_value.a34416a9", { value0: "Recipients" }), onActivate: () => openSection("recipients") }} />
|
||||
<MetricCard label="i18n:govoplan-campaign.template_health.22e14b59" value={versionMetrics.templateHealthValue} tone={versionMetrics.templateHealthTone} detail={versionMetrics.templateHealthDetail} drilldown={{ label: "i18n:govoplan-campaign.open_template_editor.1739b545", onActivate: () => openSection("template") }} />
|
||||
</MetricGrid>
|
||||
<MetricGrid spacing="inset">
|
||||
<MetricCard label="i18n:govoplan-campaign.validation_errors.e54ca4fe" value={summaryValue(data.currentVersion?.validation_summary, ["error_count", "errors", "blocked"])} />
|
||||
<MetricCard label="i18n:govoplan-campaign.warnings.1430f976" value={summaryValue(data.currentVersion?.validation_summary, ["warning_count", "warnings"])} />
|
||||
<MetricCard label="i18n:govoplan-campaign.built_messages.1fb804f2" value={summaryValue(data.currentVersion?.build_summary, ["built_count", "built", "messages_built"])} />
|
||||
<MetricCard label="i18n:govoplan-campaign.jobs_total.98da65bc" value={data.summary?.cards?.jobs_total ?? "—"} />
|
||||
<MetricCard label="i18n:govoplan-campaign.validation_errors.e54ca4fe" value={summaryValue(data.currentVersion?.validation_summary, ["error_count", "errors", "blocked"])} drilldown={{ label: "i18n:govoplan-campaign.validation_details.aa503267", onActivate: () => openSection("review", "#workflow-validate-review") }} />
|
||||
<MetricCard label="i18n:govoplan-campaign.warnings.1430f976" value={summaryValue(data.currentVersion?.validation_summary, ["warning_count", "warnings"])} drilldown={{ label: "i18n:govoplan-campaign.validation_details.aa503267", onActivate: () => openSection("review", "#workflow-validate-review") }} />
|
||||
<MetricCard label="i18n:govoplan-campaign.built_messages.1fb804f2" value={summaryValue(data.currentVersion?.build_summary, ["built_count", "built", "messages_built"])} drilldown={{ label: "i18n:govoplan-campaign.show_all_messages.1c2107a1", onActivate: () => openSection("review", "#workflow-build-review") }} />
|
||||
<MetricCard label="i18n:govoplan-campaign.jobs_total.98da65bc" value={data.summary?.cards?.jobs_total ?? "—"} drilldown={{ label: i18nMessage("i18n:govoplan-campaign.open_queue_for_value.804fbed9", { value0: campaign?.name ?? campaignId }), onActivate: openQueue }} />
|
||||
</MetricGrid>
|
||||
<div className="admin-table-surface version-history-table-surface">
|
||||
<DataGrid
|
||||
|
||||
@@ -457,7 +457,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
||||
actions={<PageActionBar
|
||||
variant="detail"
|
||||
refreshable
|
||||
reloadAction={<Button onClick={() => void reloadAll()} disabled={loading || jobsLoading}>i18n:govoplan-campaign.reload.cce71553</Button>}
|
||||
reloadAction={{ onReload: () => void reloadAll(), loading: loading || jobsLoading }}
|
||||
primaryActions={<>
|
||||
<Button onClick={() => void exportCsv()} disabled={busyAction === "csv"}>i18n:govoplan-campaign.download_csv.eaa216ad</Button>
|
||||
<Button onClick={() => setEmailOpen(true)}>i18n:govoplan-campaign.email_report.ee3e7091</Button>
|
||||
|
||||
@@ -187,8 +187,7 @@ export default function GlobalSettingsPage({ settings, auth, campaignId, view =
|
||||
error={error}
|
||||
actions={<PageActionBar
|
||||
variant="editor"
|
||||
dirty={dirty}
|
||||
saving={loading}
|
||||
state={loading ? "saving" : dirty ? "dirty" : "clean"}
|
||||
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 }}
|
||||
/>}
|
||||
|
||||
@@ -339,13 +339,12 @@ export default function MailSettingsPage({ settings, campaignId, view = "setting
|
||||
<PageActionBar
|
||||
variant="detail"
|
||||
refreshable
|
||||
reloadAction={<Button onClick={() => void reload({ force: true })} disabled={loading}>i18n:govoplan-campaign.reload.cce71553</Button>}
|
||||
reloadAction={{ onReload: () => void reload({ force: true }), loading }}
|
||||
/>
|
||||
) : (
|
||||
<PageActionBar
|
||||
variant="editor"
|
||||
dirty={dirty}
|
||||
saving={loading}
|
||||
state={loading ? "saving" : dirty ? "dirty" : "clean"}
|
||||
discardAction={{ label: "i18n:govoplan-campaign.discard.36fff63c", onClick: () => 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 }}
|
||||
/>
|
||||
|
||||
@@ -1287,7 +1287,27 @@ export default function ReviewSendPage({
|
||||
}
|
||||
|
||||
function scrollToStage(id: string) {
|
||||
document.getElementById(id)?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
revealReviewElement(id);
|
||||
}
|
||||
|
||||
function revealReviewElement(id: string) {
|
||||
const target = document.getElementById(id);
|
||||
if (!target) return;
|
||||
const stage = target.classList.contains("review-flow-stage")
|
||||
? target
|
||||
: target.closest<HTMLElement>(".review-flow-stage");
|
||||
stage?.querySelector<HTMLButtonElement>('.card-collapse-toggle[aria-expanded="false"]')?.click();
|
||||
window.requestAnimationFrame(() => {
|
||||
target.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
target.focus({ preventScroll: true });
|
||||
});
|
||||
}
|
||||
|
||||
function showBuiltDetails(filters: Record<string, string> = {}, includeAll = false) {
|
||||
setShowAllReviewJobs(includeAll);
|
||||
setReviewQuery({ sort: null, filters });
|
||||
setReviewPage(1);
|
||||
window.requestAnimationFrame(() => revealReviewElement("campaign-built-message-details"));
|
||||
}
|
||||
|
||||
const matchedAttachments = numberFrom(attachmentSummary, ["total_matched_files"]);
|
||||
@@ -1375,7 +1395,7 @@ export default function ReviewSendPage({
|
||||
actions={<PageActionBar
|
||||
variant="workspace"
|
||||
refreshable
|
||||
reloadAction={<Button onClick={() => void reload({ force: true })} disabled={loading || Boolean(busy)}>i18n:govoplan-campaign.reload.cce71553</Button>}
|
||||
reloadAction={{ onReload: () => void reload({ force: true }), loading: loading || Boolean(busy) }}
|
||||
primaryActions={<Button onClick={() => void refreshQueueStatus(false)} disabled={!version || queueStatusLoading || Boolean(busy)}>
|
||||
{queueStatusLoading ? "i18n:govoplan-campaign.refreshing_status.d8965739" : "i18n:govoplan-campaign.refresh_status.ade15a52"}
|
||||
</Button>}
|
||||
@@ -1400,9 +1420,9 @@ export default function ReviewSendPage({
|
||||
<WorkflowStage stage={stages[0]} nextState={stages[1].state} nextConnectorState={stageConnectorState(stages[1])}>
|
||||
<MetricGrid columns={4} density="compact" spacing="block" minimum="compact">
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.status.bae7d5be" value={validationPresent ? validationOk ? "i18n:govoplan-campaign.passed.271d60f4" : "i18n:govoplan-campaign.needs_attention.a126722e" : "i18n:govoplan-campaign.not_run.9e019cd5"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.blocking.d785c0d4" value={validationPresent ? validationErrors : "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.warnings.1430f976" value={validationPresent ? validationWarnings : "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.jobs_needing_attention.95613b02" value={cards?.needs_attention ?? "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.blocking.d785c0d4" value={validationPresent ? validationErrors : "—"} drilldown={validationErrors > 0 && visibleValidationIssues.length > 0 ? { label: "i18n:govoplan-campaign.validation_details.aa503267", onActivate: () => revealReviewElement("campaign-validation-details") } : undefined} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.warnings.1430f976" value={validationPresent ? validationWarnings : "—"} drilldown={validationWarnings > 0 && visibleValidationIssues.length > 0 ? { label: "i18n:govoplan-campaign.validation_details.aa503267", onActivate: () => revealReviewElement("campaign-validation-details") } : undefined} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.jobs_needing_attention.95613b02" value={cards?.needs_attention ?? "—"} drilldown={hasBuild && Number(cards?.needs_attention ?? 0) > 0 ? { label: "i18n:govoplan-campaign.review_candidates.438b8b57", onActivate: () => showBuiltDetails() } : undefined} />
|
||||
</MetricGrid>
|
||||
<ValidationWorkflowGuidance
|
||||
errors={validationErrors}
|
||||
@@ -1412,17 +1432,19 @@ export default function ReviewSendPage({
|
||||
{validationPresent && validationErrors === 0 &&
|
||||
<p className="review-flow-inline-note is-complete"><Check size={17} aria-hidden="true" /> i18n:govoplan-campaign.no_blocking_validation_exceptions_remain.73186d0d</p>
|
||||
}
|
||||
<AttachmentLinkingPreview
|
||||
preview={attachmentPreview}
|
||||
loading={attachmentPreviewLoading}
|
||||
error={attachmentPreviewError}
|
||||
linking={attachmentLinking}
|
||||
disabled={!version || readOnlyVersion || readyForDelivery || Boolean(busy)}
|
||||
onRefresh={() => void reloadAttachmentPreview(false)}
|
||||
onLink={() => void linkMatchedAttachmentFiles()} />
|
||||
<div id="campaign-attachment-preview" tabIndex={-1}>
|
||||
<AttachmentLinkingPreview
|
||||
preview={attachmentPreview}
|
||||
loading={attachmentPreviewLoading}
|
||||
error={attachmentPreviewError}
|
||||
linking={attachmentLinking}
|
||||
disabled={!version || readOnlyVersion || readyForDelivery || Boolean(busy)}
|
||||
onRefresh={() => void reloadAttachmentPreview(false)}
|
||||
onLink={() => void linkMatchedAttachmentFiles()} />
|
||||
</div>
|
||||
|
||||
{visibleValidationIssues.length > 0 &&
|
||||
<div className="review-flow-data-section">
|
||||
<div id="campaign-validation-details" className="review-flow-data-section" tabIndex={-1}>
|
||||
<h3>i18n:govoplan-campaign.validation_details.aa503267</h3>
|
||||
<DescriptionList variant="inline">
|
||||
{visibleValidationIssues.map((issue, index) =>
|
||||
@@ -1457,13 +1479,13 @@ export default function ReviewSendPage({
|
||||
|
||||
<WorkflowStage stage={stages[1]} nextState={stages[2].state} nextConnectorState={stageConnectorState(stages[2])}>
|
||||
<MetricGrid columns={4} density="compact" spacing="block" minimum="compact">
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.built.a6ad3f82" value={hasBuild ? builtCount : "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.blocked.99613c74" value={hasBuild ? buildBlocked : "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.need_review.201a4493" value={hasBuild ? buildNeedsReview : "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.reviewed.31ef8593" value={hasBuild ? buildReviewProgress.reviewed : "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.remaining.cc632b5e" value={hasBuild ? buildReviewProgress.remaining : "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.review_candidates.438b8b57" value={reviewJobs.total || "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.attachment_issues.69748336" value={missingAttachments + ambiguousAttachments} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.built.a6ad3f82" value={hasBuild ? builtCount : "—"} drilldown={hasBuild && builtCount > 0 ? { label: "i18n:govoplan-campaign.show_all_messages.1c2107a1", onActivate: () => showBuiltDetails({}, true) } : undefined} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.blocked.99613c74" value={hasBuild ? buildBlocked : "—"} drilldown={buildBlocked > 0 ? { label: "i18n:govoplan-campaign.review.e29a79fe", onActivate: () => showBuiltDetails({ validation: 'list:["blocked"]' }) } : undefined} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.need_review.201a4493" value={hasBuild ? buildNeedsReview : "—"} drilldown={buildNeedsReview > 0 ? { label: "i18n:govoplan-campaign.review_candidates.438b8b57", onActivate: () => showBuiltDetails({ validation: 'list:["warning","needs_review"]' }) } : undefined} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.reviewed.31ef8593" value={hasBuild ? buildReviewProgress.reviewed : "—"} drilldown={buildReviewProgress.reviewed > 0 ? { label: "i18n:govoplan-campaign.reviewed.31ef8593", onActivate: () => showBuiltDetails({ reviewed: 'list:["yes"]' }, true) } : undefined} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.remaining.cc632b5e" value={hasBuild ? buildReviewProgress.remaining : "—"} drilldown={buildReviewProgress.remaining > 0 ? { label: "i18n:govoplan-campaign.review.e29a79fe", onActivate: () => showBuiltDetails({ reviewed: 'list:["no"]' }) } : undefined} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.review_candidates.438b8b57" value={reviewJobs.total || "—"} drilldown={reviewJobs.total > 0 ? { label: "i18n:govoplan-campaign.review_candidates.438b8b57", onActivate: () => showBuiltDetails() } : undefined} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.attachment_issues.69748336" value={missingAttachments + ambiguousAttachments} drilldown={missingAttachments + ambiguousAttachments > 0 ? { label: "i18n:govoplan-campaign.review_attachment_rules.552c2116", onActivate: () => revealReviewElement("campaign-attachment-preview") } : undefined} />
|
||||
</MetricGrid>
|
||||
{hasBuild && <BuiltMessageReviewProgress progress={buildReviewProgress} />}
|
||||
{hasBuild && <BuiltMessageWorkflowGuidance progress={buildReviewProgress} buildWarnings={buildWarnings} />}
|
||||
@@ -1518,7 +1540,7 @@ export default function ReviewSendPage({
|
||||
<p className="review-flow-inline-note is-complete"><Check size={17} aria-hidden="true" /> i18n:govoplan-campaign.all_built_messages_are_ready_no_manual_review_ac.c5549791</p>
|
||||
}
|
||||
{hasBuild &&
|
||||
<div className="review-flow-data-section">
|
||||
<div id="campaign-built-message-details" className="review-flow-data-section" tabIndex={-1}>
|
||||
<div className="page-heading split">
|
||||
<div className="button-row compact-actions">
|
||||
<Button onClick={() => {setShowAllReviewJobs((value) => !value);setReviewPage(1);setJobsLoadedKey("");}} disabled={busy === "inspect"}>
|
||||
@@ -1617,10 +1639,10 @@ export default function ReviewSendPage({
|
||||
|
||||
<WorkflowStage stage={stages[3]} nextState={stages[4].state} nextConnectorState={stageConnectorState(stages[4])}>
|
||||
<MetricGrid columns={4} density="compact" spacing="block" minimum="compact">
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.recipients.78cbf8eb" value={jobsTotal || "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.messages_to_send.f7763bf9" value={builtCount || jobsTotal || "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.matched_attachments.ead1eeb1" value={matchedAttachments || "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.missing_ambiguous.fffdd3f5" value={`${missingAttachments} / ${ambiguousAttachments}`} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.recipients.78cbf8eb" value={jobsTotal || "—"} drilldown={hasBuild && jobsTotal > 0 ? { label: "i18n:govoplan-campaign.show_all_messages.1c2107a1", onActivate: () => showBuiltDetails({}, true) } : undefined} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.messages_to_send.f7763bf9" value={builtCount || jobsTotal || "—"} drilldown={hasBuild && builtCount > 0 ? { label: "i18n:govoplan-campaign.show_all_messages.1c2107a1", onActivate: () => showBuiltDetails({}, true) } : undefined} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.matched_attachments.ead1eeb1" value={matchedAttachments || "—"} drilldown={matchedAttachments > 0 ? { label: "i18n:govoplan-campaign.review_attachment_rules.552c2116", onActivate: () => revealReviewElement("campaign-attachment-preview") } : undefined} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.missing_ambiguous.fffdd3f5" value={`${missingAttachments} / ${ambiguousAttachments}`} drilldown={missingAttachments + ambiguousAttachments > 0 ? { label: "i18n:govoplan-campaign.review_attachment_rules.552c2116", onActivate: () => revealReviewElement("campaign-attachment-preview") } : undefined} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.rate_limit.d08e55f5" value={messagesPerMinute > 0 ? i18nMessage("i18n:govoplan-campaign.value_min.c9d89eae", { value0: messagesPerMinute }) : "i18n:govoplan-campaign.not_set.93039e60"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.minimum_duration.91a71a6d" value={estimatedMinutes ? i18nMessage("i18n:govoplan-campaign.about_value_min.7c2e77fc", { value0: estimatedMinutes }) : "—"} />
|
||||
<MetricCard density="compact" surface="subtle" label="i18n:govoplan-campaign.imap_append.8c0d9e96" value={Boolean(imapAppend.enabled) ? "i18n:govoplan-campaign.enabled.df174a3f" : "i18n:govoplan-campaign.disabled.f4f4473d"} />
|
||||
|
||||
@@ -421,8 +421,7 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
|
||||
success={contentLibraryNotice}
|
||||
actions={<PageActionBar
|
||||
variant="editor"
|
||||
dirty={dirty}
|
||||
saving={loading}
|
||||
state={loading ? "saving" : dirty ? "dirty" : "clean"}
|
||||
contextActions={<Button onClick={() => window.location.assign("/templates")}>i18n:govoplan-campaign.manage_templates.23688071</Button>}
|
||||
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 }}
|
||||
|
||||
@@ -24,6 +24,7 @@ type CampaignDraftPageScaffoldProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
// semantic-editor-guard: delegated to each caller's useCampaignDraftEditor lifecycle.
|
||||
export default function CampaignDraftPageScaffold({
|
||||
settings,
|
||||
campaignId,
|
||||
@@ -53,8 +54,7 @@ export default function CampaignDraftPageScaffold({
|
||||
error={error || ""}
|
||||
actions={<PageActionBar
|
||||
variant="editor"
|
||||
dirty={dirty}
|
||||
saving={loading}
|
||||
state={loading ? "saving" : dirty ? "dirty" : "clean"}
|
||||
discardAction={{ label: "i18n:govoplan-campaign.discard.36fff63c", onClick: onReload }}
|
||||
saveAction={{ label: "i18n:govoplan-campaign.save.efc007a3", onClick: onSave, disabled: (locked || !draft) && dirty, disabledReason: locked && dirty ? "This campaign version is locked." : !draft && dirty ? "The campaign draft is not available." : undefined }}
|
||||
/>}
|
||||
|
||||
@@ -771,7 +771,7 @@ export default function OperatorQueuePage({ settings, auth }: {settings: ApiSett
|
||||
actions={<PageActionBar
|
||||
variant="collection"
|
||||
refreshable
|
||||
reloadAction={<Button onClick={() => void refreshAll()} disabled={loading || jobsLoading}>i18n:govoplan-campaign.refresh.56e3badc</Button>}
|
||||
reloadAction={{ onReload: () => void refreshAll(), loading: loading || jobsLoading }}
|
||||
/>}
|
||||
notices={jobsError ? <DismissibleAlert tone="danger" resetKey={jobsError} floating>{jobsError}</DismissibleAlert> : undefined}
|
||||
>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { DescriptionList } from "@govoplan/core-webui";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Eye, RefreshCw } from "lucide-react";
|
||||
import { Eye } from "lucide-react";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { ContentGrid,
|
||||
Button,
|
||||
Card,
|
||||
DataGrid,
|
||||
LoadingFrame,
|
||||
@@ -177,12 +176,10 @@ export default function AggregateReportsPage({ settings }: {settings: ApiSetting
|
||||
actions={<PageActionBar
|
||||
variant="overview"
|
||||
refreshable
|
||||
reloadAction={<Button
|
||||
onClick={() => void Promise.all([loadCampaigns(), selectedFromUrl ? loadReport(selectedFromUrl) : Promise.resolve()])}
|
||||
disabled={listLoading || reportLoading}
|
||||
>
|
||||
<RefreshCw size={16} aria-hidden="true" /> i18n:govoplan-campaign.refresh.56e3badc
|
||||
</Button>}
|
||||
reloadAction={{
|
||||
onReload: () => void Promise.all([loadCampaigns(), selectedFromUrl ? loadReport(selectedFromUrl) : Promise.resolve()]),
|
||||
loading: listLoading || reportLoading
|
||||
}}
|
||||
/>}
|
||||
>
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ const reviewPresentation = readFileSync(
|
||||
"utf8"
|
||||
);
|
||||
const operatorPage = readFileSync("src/features/operator/OperatorQueuePage.tsx", "utf8");
|
||||
const overviewPage = readFileSync("src/features/campaigns/CampaignOverviewPage.tsx", "utf8");
|
||||
const attachmentsPage = readFileSync("src/features/campaigns/AttachmentsDataPage.tsx", "utf8");
|
||||
const deliveryMode = readFileSync("src/features/campaigns/utils/deliveryMode.ts", "utf8");
|
||||
const translationCatalog = readFileSync("src/i18n/generatedTranslations.ts", "utf8");
|
||||
|
||||
@@ -49,6 +51,13 @@ const outcomeCardsStart = page.indexOf("Aggregate outcome cards are deliberately
|
||||
const outcomeCardsEnd = page.indexOf('<Card title="i18n:govoplan-campaign.population_and_exclusions', outcomeCardsStart);
|
||||
assert(outcomeCardsStart >= 0 && outcomeCardsEnd > outcomeCardsStart, "the aggregate privacy boundary is documented beside the outcome cards");
|
||||
assert(!page.slice(outcomeCardsStart, outcomeCardsEnd).includes("onClick"), "aggregate outcome cards cannot create unsuppressed filtered subgroups");
|
||||
assert(!page.slice(outcomeCardsStart, outcomeCardsEnd).includes("drilldown="), "privacy-suppressed aggregates remain informational metrics");
|
||||
assert(overviewPage.includes("drilldown={{"), "campaign overview metrics expose explicit destinations");
|
||||
assert(overviewPage.includes("openSection(\"recipients\")"), "the recipient count opens its owning collection");
|
||||
assert(attachmentsPage.includes('revealAttachmentSection("campaign-global-attachments")'), "attachment counts reveal their underlying editor section");
|
||||
assert(attachmentsPage.includes("openRecipients"), "per-recipient attachment counts open recipient details");
|
||||
assert(reviewPage.includes("showBuiltDetails({ validation:"), "review counts apply a relevant table filter");
|
||||
assert(reviewPage.includes('revealReviewElement("campaign-attachment-preview")'), "attachment-result counts reveal the evidence preview");
|
||||
assert(!page.includes("{report.population.denominator_definition}"), "the known denominator contract uses a localized UI explanation");
|
||||
assert(!page.includes("{report.privacy.rule}"), "the known privacy contract uses a localized UI explanation");
|
||||
assert(page.includes("all_persisted_recipient_delivery_jobs_for_the_se.208e90cc"), "the denominator explanation is bilingual");
|
||||
|
||||
@@ -17,6 +17,7 @@ assert.match(guidance, /documentation=\{\{ topicId: documentationTopicId \}\}/);
|
||||
assert.match(guidance, /campaigns\.workflow\.prepare-validate-and-build/);
|
||||
assert.match(guidance, /campaigns\.workflow\.complete-review/);
|
||||
assert.match(page, /calculateBuildReviewProgress/);
|
||||
assert.match(page, /<WorkflowFact label="i18n:govoplan-campaign\.remaining\.cc632b5e"/);
|
||||
assert.match(page, /<MetricCard[^>]+label="i18n:govoplan-campaign\.remaining\.cc632b5e"/);
|
||||
assert.match(page, /showBuiltDetails\(\{ reviewed: 'list:\["no"\]' \}\)/);
|
||||
assert.doesNotMatch(page, /blocked_or_failed_message_s_must_be_resolved_bef/);
|
||||
assert.doesNotMatch(page, /resolve_the_blocking_entries_then_validate_again/);
|
||||
|
||||
Reference in New Issue
Block a user