Files
govoplan-campaign/webui/src/features/campaigns/review/reviewPresentation.tsx
T
zemion c51fc180fb
Module Package Release / publish-packages (push) Successful in 12s
Release govoplan-campaign v0.1.28: stabilize saving, review and delivery recovery
2026-09-08 01:32:26 +02:00

184 lines
14 KiB
TypeScript

import { Search } from "lucide-react";
import {
StatusBadge,
TableActionGroup,
i18nMessage,
type DataGridColumn,
type DataGridListOption
} from "@govoplan/core-webui";
import type { MockMailboxMessage } from "../../../api/mail";
import type { CampaignMessagePreviewAttachment } from "../components/MessagePreviewOverlay";
import { asArray, asRecord, stringifyPreview } from "../utils/campaignView";
import { SEND_STATUS_OPTIONS, IMAP_STATUS_OPTIONS, deliveryStatusLabel } from "../utils/deliveryStatusOptions";
import { builtMessageState } from "./builtMessageState";
import { countResolvedAttachments, formatAddressList } from "./reviewFormatters";
const MESSAGE_STATE_OPTIONS: DataGridListOption[] = [
{ value: "ready", label: "i18n:govoplan-campaign.message_state_ready" },
{ value: "needs_review", label: "i18n:govoplan-campaign.message_state_needs_review" },
{ value: "blocked", label: "i18n:govoplan-campaign.message_state_blocked_label" },
{ value: "excluded", label: "i18n:govoplan-campaign.message_state_excluded_label" }];
export function builtMessageColumns(
openMessage: (row: Record<string, unknown>) => void,
reviewedKeys: Set<string>,
inspectionComplete = false)
: DataGridColumn<Record<string, unknown>>[] {
return [
{ id: "number", header: "#", width: 70, sortable: true, filterType: "integer", sticky: "start", value: (row, index) => Number(row.entry_index ?? index + 1) },
{ id: "recipient", header: "i18n:govoplan-campaign.recipient.90343260", width: 250, resizable: true, sortable: true, filterable: true, value: (row) => formatAddressList(asRecord(row.resolved_recipients).to) || String(row.recipient_email ?? "—") },
{ id: "subject", header: "i18n:govoplan-campaign.subject.8d183dbd", width: "minmax(260px, 1fr)", maxWidth: 640, resizable: true, sortable: true, filterable: true, value: (row) => String(row.subject ?? "—") },
{
id: "messageState",
header: "i18n:govoplan-campaign.message_state_heading",
width: 145,
sortable: true,
filterable: true,
columnType: "from-list",
list: { options: MESSAGE_STATE_OPTIONS, display: "pill" },
value: (row, index) => builtMessageState(row, index, reviewedKeys, inspectionComplete).state
},
{
id: "postboxTargets",
header: "Postbox targets",
width: 145,
align: "right",
sortable: true,
filterType: "integer",
value: (row) => Number(row.postbox_target_count ?? 0),
render: (row) => {
const count = Number(row.postbox_target_count ?? 0);
return count > 0 ? (
<StatusBadge status="info" label={`${count} frozen`} />
) : (
<span className="muted"></span>
);
}
},
{ id: "attachments", header: "i18n:govoplan-campaign.attachments.6771ade6", width: 125, sortable: true, filterable: true, filterType: "integer", align: "right", value: (row) => Number(row.attachment_count ?? countResolvedAttachments(row.attachments)) },
{ id: "stateExplanation", header: "i18n:govoplan-campaign.message_state_explanation_heading", width: "minmax(220px, 1fr)", maxWidth: 480, resizable: true,
value: (row, index) => builtMessageState(row, index, reviewedKeys, inspectionComplete).explanationLabel },
{ id: "actions", header: "i18n:govoplan-campaign.actions.c3cd636a", width: 72, sticky: "end", render: (row) => <TableActionGroup actions={[{ id: "review", label: "i18n:govoplan-campaign.review.e29a79fe", icon: <Search aria-hidden="true" />, onClick: () => openMessage(row) }]} /> }];
}
export function mockSendResultColumns(): DataGridColumn<Record<string, unknown>>[] {
const options: DataGridListOption[] = [
{ value: "sent", label: "i18n:govoplan-campaign.sent.35f49dcf" },
{ value: "failed", label: "i18n:govoplan-campaign.failed.09fef5d8" },
{ value: "skipped", label: "i18n:govoplan-campaign.skipped.5a000ad7" },
{ value: "ready", label: "i18n:govoplan-campaign.ready.20c7c552" }];
return [
{ id: "number", header: "#", width: 70, sortable: true, filterType: "integer", sticky: "start", value: (row, index) => Number(row.entry_index ?? index + 1) },
{ id: "status", header: "i18n:govoplan-campaign.status.bae7d5be", width: 130, sortable: true, filterable: true, columnType: "from-list", list: { options, display: "pill" }, value: (row) => String(row.status ?? "info") },
{ id: "recipient", header: "i18n:govoplan-campaign.recipient.90343260", width: 250, resizable: true, sortable: true, filterable: true, value: (row) => formatAddressList(row.to) || asArray(row.envelope_recipients).join(", ") || "—" },
{ id: "smtp", header: "i18n:govoplan-campaign.smtp.efff9cca", width: 190, sortable: true, filterable: true, value: (row) => String(row.smtp_message_id ?? row.status ?? "—") },
{ id: "imap", header: "i18n:govoplan-campaign.imap.271f9ef2", width: 190, sortable: true, filterable: true, value: (row) => String(row.imap_message_id ?? row.imap_status ?? "—") },
{ id: "message", header: "i18n:govoplan-campaign.message.68f4145f", width: "minmax(260px, 1fr)", maxWidth: 720, resizable: true, filterable: true, value: (row) => String(row.message ?? "—") }];
}
export function sendResultColumns(): DataGridColumn<Record<string, unknown>>[] {
const statuses: DataGridListOption[] = [
{ value: "smtp_accepted", label: "i18n:govoplan-campaign.smtp_accepted.e3aa7603" },
{ value: "already_accepted", label: "i18n:govoplan-campaign.already_accepted.826e68f2" },
{ value: "outcome_unknown", label: "i18n:govoplan-campaign.outcome_unknown.6e929fca" },
{ value: "failed", label: "i18n:govoplan-campaign.failed.09fef5d8" },
{ value: "cancelled", label: "i18n:govoplan-campaign.cancelled.a1bf92ef" },
{ value: "not_claimed", label: "i18n:govoplan-campaign.not_claimed.aa712c1b" },
{ value: "dry_run", label: "i18n:govoplan-campaign.dry_run.485a3d15" }];
return [
{ id: "status", header: "i18n:govoplan-campaign.status.bae7d5be", width: 160, sortable: true, filterable: true, sticky: "start", columnType: "from-list", list: { options: statuses, display: "pill" }, render: (row) => <StatusBadge status={String(row.status ?? "info")} />, value: (row) => String(row.status ?? "info") },
{ id: "job", header: "i18n:govoplan-campaign.job.30c8cb83", width: 180, sortable: true, filterable: true, value: (row) => String(row.job_id ?? row.version_id ?? "—") },
{ id: "message", header: "i18n:govoplan-campaign.message.68f4145f", width: "minmax(320px, 1fr)", maxWidth: 720, resizable: true, filterable: true, value: (row) => String(row.message ?? stringifyPreview(row, 180)) }];
}
export function imapAppendResultColumns(): DataGridColumn<Record<string, unknown>>[] {
return [
{ id: "status", header: "i18n:govoplan-campaign.status.bae7d5be", width: 150, sticky: "start", sortable: true, filterable: true, render: (row) => <StatusBadge status={String(row.status ?? "info")} />, value: (row) => String(row.status ?? "info") },
{ id: "job", header: "i18n:govoplan-campaign.job.30c8cb83", width: 180, sortable: true, filterable: true, value: (row) => String(row.job_id ?? "-") },
{ id: "folder", header: "i18n:govoplan-campaign.folder.30baa249", width: 190, sortable: true, filterable: true, value: (row) => String(row.folder ?? "-") },
{ id: "message", header: "i18n:govoplan-campaign.message.68f4145f", width: "minmax(300px, 1fr)", maxWidth: 720, resizable: true, filterable: true, value: (row) => String(row.message ?? stringifyPreview(row, 180)) }];
}
export function imapDiagnosticColumns(openDetail: (jobId: string) => Promise<void>): DataGridColumn<Record<string, unknown>>[] {
return [
{ id: "recipient", header: "i18n:govoplan-campaign.recipient.90343260", width: 240, sticky: "start", resizable: true, sortable: true, filterable: true, value: (row) => formatAddressList(asRecord(row.resolved_recipients).to) || String(row.recipient_email ?? "-") },
{ id: "subject", header: "i18n:govoplan-campaign.subject.8d183dbd", width: "minmax(260px, 1fr)", maxWidth: 640, resizable: true, sortable: true, filterable: true, value: (row) => String(row.subject ?? "-") },
{ id: "send", header: "i18n:govoplan-campaign.smtp.efff9cca", width: 150, sortable: true, filterable: true, columnType: "from-list", list: { options: SEND_STATUS_OPTIONS, display: "pill" }, render: (row) => <StatusBadge status={String(row.send_status ?? "info")} label={deliveryStatusLabel(String(row.send_status ?? "info"))} />, value: (row) => String(row.send_status ?? "-") },
{ id: "imap", header: "i18n:govoplan-campaign.imap.271f9ef2", width: 150, sortable: true, filterable: true, columnType: "from-list", list: { options: IMAP_STATUS_OPTIONS, display: "pill" }, render: (row) => <StatusBadge status={String(row.imap_status ?? "info")} label={deliveryStatusLabel(String(row.imap_status ?? "info"))} />, value: (row) => String(row.imap_status ?? "-") },
{ id: "error", header: "i18n:govoplan-campaign.last_error.5e4df866", width: "minmax(260px, 1fr)", maxWidth: 720, resizable: true, filterable: true, value: (row) => String(row.last_error ?? "-") },
{ id: "actions", header: "i18n:govoplan-campaign.actions.c3cd636a", width: 72, sticky: "end", render: (row) => <TableActionGroup actions={[{ id: "details", label: "i18n:govoplan-campaign.details.dc3decbb", icon: <Search aria-hidden="true" />, onClick: () => void openDetail(String(row.id ?? "")) }]} /> }];
}
export function mockMailboxColumns(openMessage: (id: string) => Promise<void>): DataGridColumn<Record<string, unknown>>[] {
const options: DataGridListOption[] = [
{ value: "smtp", label: "i18n:govoplan-campaign.smtp.efff9cca" },
{ value: "imap_append", label: "i18n:govoplan-campaign.imap_append.8c0d9e96" }];
return [
{ id: "kind", header: "i18n:govoplan-campaign.kind.e00ac23f", width: 125, sortable: true, filterable: true, sticky: "start", columnType: "from-list", list: { options, display: "pill" }, value: (row) => String(row.kind ?? "mock") },
{ id: "subject", header: "i18n:govoplan-campaign.subject.8d183dbd", width: "minmax(260px, 1fr)", maxWidth: 640, resizable: true, sortable: true, filterable: true, value: (row) => String(row.subject ?? "—") },
{ id: "envelope", header: "i18n:govoplan-campaign.envelope_folder.9f30740d", width: 300, resizable: true, filterable: true, value: (row) => `${String(row.envelope_from ?? row.folder ?? "—")}${asArray(row.envelope_recipients).join(", ") || String(row.folder ?? "—")}` },
{ id: "attachments", header: "i18n:govoplan-campaign.attachments.6771ade6", width: 125, sortable: true, filterable: true, filterType: "integer", align: "right", value: (row) => Number(row.attachment_count ?? 0) },
{ id: "actions", header: "i18n:govoplan-campaign.actions.c3cd636a", width: 72, sticky: "end", render: (row) => <TableActionGroup actions={[{ id: "review", label: "i18n:govoplan-campaign.review.e29a79fe", icon: <Search aria-hidden="true" />, onClick: () => void openMessage(String(row.id ?? "")) }]} /> }];
}
export function mockMessageMetaItems(message: MockMailboxMessage) {
return [
{ label: "i18n:govoplan-campaign.from.3f66052a", value: message.from_header || message.envelope_from || "—" },
{ label: "i18n:govoplan-campaign.to.ae79ea1e", value: message.to_header || message.envelope_recipients?.join(", ") || "—" },
{ label: "i18n:govoplan-campaign.cc.1fd6a880", value: message.cc_header || null },
{ label: "i18n:govoplan-campaign.bcc.8431acad", value: message.bcc_header || null },
{ label: "i18n:govoplan-campaign.kind.e00ac23f", value: message.kind || "—" },
{ label: "i18n:govoplan-campaign.folder.30baa249", value: message.folder || "—" },
{ label: "i18n:govoplan-campaign.message_id.fa0e4fdd", value: message.message_id || "—" },
{ label: "i18n:govoplan-campaign.size.b7152342", value: `${message.size_bytes || 0} bytes` }];
}
export function mockMessageAttachments(message: MockMailboxMessage): CampaignMessagePreviewAttachment[] {
return (message.attachments ?? []).map((attachment, index) => ({
filename: attachment.filename || `Attachment ${index + 1}`,
contentType: attachment.content_type || undefined,
sizeBytes: attachment.size_bytes ?? undefined
}));
}
export function synchronousSendReason(option: Record<string, unknown>): string {
const configuredMessage = String(option.message ?? "").trim();
if (configuredMessage) return configuredMessage;
switch (String(option.reason ?? "")) {
case "recipient_limit_exceeded":return i18nMessage("i18n:govoplan-campaign.synchronous_limit_explanation", {
value0: Number(option.eligible_recipient_job_count ?? 0),
value1: Number(asRecord(option.policy).max_recipient_jobs ?? 0)
});
case "no_eligible_recipient_jobs":return "i18n:govoplan-campaign.the_built_run_has_no_eligible_message.48e5410c";
case "version_not_ready":return "i18n:govoplan-campaign.validate_lock_build_and_review_the_current_versi.d0567dcc";
case "policy_configuration_invalid":return "i18n:govoplan-campaign.the_delivery_policy_configuration_is_invalid.a804a8f8";
default:return "i18n:govoplan-campaign.delivery_options_are_still_being_evaluated.8395096e";
}
}
export function deliveryControlProgressMessage(action: "pause" | "resume" | "cancel"): string {
if (action === "pause") return "i18n:govoplan-campaign.pausing_eligible_delivery_jobs_.eb6b9d58";
if (action === "resume") return "i18n:govoplan-campaign.resuming_eligible_delivery_jobs_.dd12c5a2";
return "i18n:govoplan-campaign.cancelling_eligible_delivery_jobs_.4090fa47";
}
export function deliveryPolicySourceLabel(source: string): string {
if (source === "system" || source === "system_ceiling") return "i18n:govoplan-campaign.system_delivery_policy_source";
if (source === "tenant") return "i18n:govoplan-campaign.tenant.3ca93c78";
if (source === "deployment") return "i18n:govoplan-campaign.deployment.327a55f8";
if (source === "deployment_default") return "i18n:govoplan-campaign.deployment_default.aa39f7b4";
if (source === "deployment_ceiling") return "i18n:govoplan-campaign.deployment_ceiling.abbec75b";
return "i18n:govoplan-campaign.not_available.d1a17af1";
}