Complete recipient search and delivery evidence
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
"test:policy-ui": "rm -rf .policy-test-build && mkdir -p .policy-test-build && printf '{\"type\":\"commonjs\"}\\n' > .policy-test-build/package.json && tsc -p tsconfig.policy-tests.json && node .policy-test-build/tests/policy-ui.test.js",
|
"test:policy-ui": "rm -rf .policy-test-build && mkdir -p .policy-test-build && printf '{\"type\":\"commonjs\"}\\n' > .policy-test-build/package.json && tsc -p tsconfig.policy-tests.json && node .policy-test-build/tests/policy-ui.test.js",
|
||||||
"test:template-preview": "rm -rf .template-preview-test-build && mkdir -p .template-preview-test-build && printf '{\"type\":\"commonjs\"}\\n' > .template-preview-test-build/package.json && tsc -p tsconfig.template-preview-tests.json && node .template-preview-test-build/tests/template-preview-draft.test.js",
|
"test:template-preview": "rm -rf .template-preview-test-build && mkdir -p .template-preview-test-build && printf '{\"type\":\"commonjs\"}\\n' > .template-preview-test-build/package.json && tsc -p tsconfig.template-preview-tests.json && node .template-preview-test-build/tests/template-preview-draft.test.js",
|
||||||
"test:import-utils": "rm -rf .import-test-build && mkdir -p .import-test-build && printf '{\"type\":\"commonjs\"}\\n' > .import-test-build/package.json && tsc -p tsconfig.import-tests.json && node .import-test-build/tests/import-utils.test.js",
|
"test:import-utils": "rm -rf .import-test-build && mkdir -p .import-test-build && printf '{\"type\":\"commonjs\"}\\n' > .import-test-build/package.json && tsc -p tsconfig.import-tests.json && node .import-test-build/tests/import-utils.test.js",
|
||||||
|
"test:recipient-search": "node tests/recipient-search-ui-structure.test.mjs",
|
||||||
"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: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: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:operator-queue": "node --experimental-strip-types --test tests/operator-queue-model.test.ts && node tests/operator-queue-ui-structure.test.mjs",
|
||||||
|
|||||||
@@ -561,7 +561,9 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
|||||||
<div><dt>Postbox targets</dt><dd>{String(detail.job.postbox_target_count ?? 0)}</dd></div>
|
<div><dt>Postbox targets</dt><dd>{String(detail.job.postbox_target_count ?? 0)}</dd></div>
|
||||||
<div><dt>i18n:govoplan-campaign.imap_state.03b83be0</dt><dd><StatusBadge status={String(detail.job.imap_status ?? "unknown")} label={deliveryStatusLabel(String(detail.job.imap_status ?? "unknown"))} /></dd></div>
|
<div><dt>i18n:govoplan-campaign.imap_state.03b83be0</dt><dd><StatusBadge status={String(detail.job.imap_status ?? "unknown")} label={deliveryStatusLabel(String(detail.job.imap_status ?? "unknown"))} /></dd></div>
|
||||||
<div><dt>i18n:govoplan-campaign.attachments.6771ade6</dt><dd>{String(detail.job.matched_file_count ?? detail.job.attachment_count ?? 0)}</dd></div>
|
<div><dt>i18n:govoplan-campaign.attachments.6771ade6</dt><dd>{String(detail.job.matched_file_count ?? detail.job.attachment_count ?? 0)}</dd></div>
|
||||||
|
<div><dt>Message SHA-256</dt><dd><code>{String(detail.job.eml_sha256 ?? "—")}</code></dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
|
<AttachmentEvidenceSection attachments={Array.isArray(detail.job.attachments) ? detail.job.attachments : []} />
|
||||||
<AttemptHistoryTable kind="smtp" rows={detail.attempts.smtp ?? []} />
|
<AttemptHistoryTable kind="smtp" rows={detail.attempts.smtp ?? []} />
|
||||||
<AttemptHistoryTable kind="postbox" rows={detail.attempts.postbox ?? []} />
|
<AttemptHistoryTable kind="postbox" rows={detail.attempts.postbox ?? []} />
|
||||||
<AttemptHistoryTable kind="imap" rows={detail.attempts.imap ?? []} />
|
<AttemptHistoryTable kind="imap" rows={detail.attempts.imap ?? []} />
|
||||||
@@ -585,6 +587,114 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AttachmentEvidenceRow = {
|
||||||
|
id: string;
|
||||||
|
rule: string;
|
||||||
|
status: string;
|
||||||
|
delivery: string;
|
||||||
|
file: string;
|
||||||
|
assetId: string;
|
||||||
|
versionId: string;
|
||||||
|
sourceRevision: string;
|
||||||
|
checksum: string;
|
||||||
|
sizeBytes: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
function AttachmentEvidenceSection({ attachments }: {attachments: unknown[];}) {
|
||||||
|
const rows = attachmentEvidenceRows(attachments);
|
||||||
|
const columns: DataGridColumn<AttachmentEvidenceRow>[] = [
|
||||||
|
{ id: "rule", header: "Rule", width: 180, resizable: true, sortable: true, filterable: true, value: (row) => row.rule },
|
||||||
|
{ id: "status", header: "Status", width: 130, sortable: true, filterable: true, render: (row) => <StatusBadge status={row.status} />, value: (row) => row.status },
|
||||||
|
{ id: "delivery", header: "Attachment output", width: 220, resizable: true, filterable: true, value: (row) => row.delivery },
|
||||||
|
{ id: "file", header: "Frozen file", width: "minmax(240px, 1fr)", minWidth: 220, resizable: true, filterable: true, value: (row) => row.file },
|
||||||
|
{
|
||||||
|
id: "version",
|
||||||
|
header: "Managed version",
|
||||||
|
width: 230,
|
||||||
|
resizable: true,
|
||||||
|
filterable: true,
|
||||||
|
value: (row) => `${row.versionId} ${row.assetId} ${row.sourceRevision}`,
|
||||||
|
render: (row) =>
|
||||||
|
<span className="campaign-evidence-identifiers">
|
||||||
|
<code title={row.versionId}>{row.versionId || "Legacy source"}</code>
|
||||||
|
{row.assetId && <small title={row.assetId}>Asset {shortEvidenceId(row.assetId)}</small>}
|
||||||
|
{row.sourceRevision && <small title={row.sourceRevision}>Source {shortEvidenceId(row.sourceRevision)}</small>}
|
||||||
|
</span>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "checksum",
|
||||||
|
header: "SHA-256",
|
||||||
|
width: 170,
|
||||||
|
filterable: true,
|
||||||
|
value: (row) => row.checksum,
|
||||||
|
render: (row) => <code title={row.checksum}>{row.checksum ? shortEvidenceId(row.checksum) : "—"}</code>
|
||||||
|
},
|
||||||
|
{ id: "size", header: "Size", width: 110, align: "right", sortable: true, value: (row) => row.sizeBytes ?? -1, render: (row) => row.sizeBytes === null ? "—" : `${row.sizeBytes.toLocaleString()} B` }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="attempt-history-section campaign-attachment-evidence">
|
||||||
|
<h3>Frozen attachment evidence</h3>
|
||||||
|
{rows.length === 0 ?
|
||||||
|
<p className="muted small-note">No attachment rule or frozen file is recorded for this delivery job.</p> :
|
||||||
|
<>
|
||||||
|
<p className="muted small-note">Exact managed versions and checksums shown here are the immutable files used when this message was built.</p>
|
||||||
|
<DataGrid
|
||||||
|
id="campaign-job-attachment-evidence"
|
||||||
|
rows={rows}
|
||||||
|
columns={columns}
|
||||||
|
getRowKey={(row) => row.id}
|
||||||
|
resizeBehavior="free" />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachmentEvidenceRows(attachments: unknown[]): AttachmentEvidenceRow[] {
|
||||||
|
const rows: AttachmentEvidenceRow[] = [];
|
||||||
|
attachments.forEach((value, ruleIndex) => {
|
||||||
|
const attachment = asRecord(value);
|
||||||
|
const rule = String(attachment.label ?? attachment.attachment_id ?? `Rule ${ruleIndex + 1}`);
|
||||||
|
const status = String(attachment.status ?? "unknown");
|
||||||
|
const zipEnabled = attachment.zip_enabled === true;
|
||||||
|
const delivery = zipEnabled
|
||||||
|
? `ZIP ${String(attachment.zip_filename ?? attachment.zip_archive_id ?? "")}`.trim() + ` (${String(attachment.zip_mode ?? "inherit")})`
|
||||||
|
: "Direct attachment";
|
||||||
|
const managedMatches = Array.isArray(attachment.managed_matches) ? attachment.managed_matches.map(asRecord) : [];
|
||||||
|
const legacyMatches = Array.isArray(attachment.matches) ? attachment.matches : [];
|
||||||
|
const matches: Array<Record<string, unknown> | string | null> = managedMatches.length > 0
|
||||||
|
? managedMatches
|
||||||
|
: legacyMatches.length > 0
|
||||||
|
? legacyMatches.map((match) => typeof match === "string" ? match : asRecord(match))
|
||||||
|
: [null];
|
||||||
|
|
||||||
|
matches.forEach((match, matchIndex) => {
|
||||||
|
const managed = typeof match === "string" || match === null ? {} : match;
|
||||||
|
const filename = typeof match === "string"
|
||||||
|
? match
|
||||||
|
: String(managed.display_path ?? managed.relative_path ?? managed.filename ?? "No matched file");
|
||||||
|
rows.push({
|
||||||
|
id: `${ruleIndex}:${matchIndex}:${String(managed.version_id ?? filename)}`,
|
||||||
|
rule,
|
||||||
|
status,
|
||||||
|
delivery,
|
||||||
|
file: filename,
|
||||||
|
assetId: String(managed.asset_id ?? ""),
|
||||||
|
versionId: String(managed.version_id ?? ""),
|
||||||
|
sourceRevision: String(managed.source_revision ?? ""),
|
||||||
|
checksum: String(managed.checksum_sha256 ?? ""),
|
||||||
|
sizeBytes: Number.isFinite(Number(managed.size_bytes)) ? Number(managed.size_bytes) : null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortEvidenceId(value: string): string {
|
||||||
|
return value.length > 16 ? `${value.slice(0, 16)}...` : value;
|
||||||
|
}
|
||||||
|
|
||||||
function AttemptHistoryTable({ kind, rows }: {kind: "smtp" | "imap" | "postbox";rows: Record<string, unknown>[];}) {
|
function AttemptHistoryTable({ kind, rows }: {kind: "smtp" | "imap" | "postbox";rows: Record<string, unknown>[];}) {
|
||||||
const title = kind === "smtp"
|
const title = kind === "smtp"
|
||||||
? "i18n:govoplan-campaign.smtp_attempts.eb0a9ca6"
|
? "i18n:govoplan-campaign.smtp_attempts.eb0a9ca6"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { DismissibleAlert } from "@govoplan/core-webui";
|
|||||||
import { Dialog } from "@govoplan/core-webui";
|
import { Dialog } from "@govoplan/core-webui";
|
||||||
import { SegmentedControl } from "@govoplan/core-webui";
|
import { SegmentedControl } from "@govoplan/core-webui";
|
||||||
import { TableActionGroup } from "@govoplan/core-webui";
|
import { TableActionGroup } from "@govoplan/core-webui";
|
||||||
import { DataGrid, DataGridEmptyAction, DataGridRowActions, type DataGridColumn } from "@govoplan/core-webui";
|
import { DataGrid, DataGridEmptyAction, DataGridRowActions, type DataGridColumn, type DataGridQueryState } from "@govoplan/core-webui";
|
||||||
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
|
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
|
||||||
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
|
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
|
||||||
import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView";
|
import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView";
|
||||||
@@ -124,6 +124,7 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
|
|||||||
const [addressSources, setAddressSources] = useState<CampaignRecipientAddressSource[]>([]);
|
const [addressSources, setAddressSources] = useState<CampaignRecipientAddressSource[]>([]);
|
||||||
const [recipientProfilesPage, setRecipientProfilesPage] = useState(1);
|
const [recipientProfilesPage, setRecipientProfilesPage] = useState(1);
|
||||||
const [recipientProfilesPageSize, setRecipientProfilesPageSize] = useState(10);
|
const [recipientProfilesPageSize, setRecipientProfilesPageSize] = useState(10);
|
||||||
|
const [recipientProfilesQuery, setRecipientProfilesQuery] = useState<DataGridQueryState>({ sort: null, filters: {} });
|
||||||
const [recipientAddressEditorIndex, setRecipientAddressEditorIndex] = useState<number | null>(null);
|
const [recipientAddressEditorIndex, setRecipientAddressEditorIndex] = useState<number | null>(null);
|
||||||
const [postboxTargetEditorIndex, setPostboxTargetEditorIndex] = useState<number | null>(null);
|
const [postboxTargetEditorIndex, setPostboxTargetEditorIndex] = useState<number | null>(null);
|
||||||
const [postboxCatalog, setPostboxCatalog] = useState<CampaignPostboxCatalog>({
|
const [postboxCatalog, setPostboxCatalog] = useState<CampaignPostboxCatalog>({
|
||||||
@@ -526,6 +527,7 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
|
|||||||
fieldDefinitions,
|
fieldDefinitions,
|
||||||
individualAttachmentBasePaths,
|
individualAttachmentBasePaths,
|
||||||
zipConfig,
|
zipConfig,
|
||||||
|
addressFilter: recipientProfilesQuery.filters.recipients ?? "",
|
||||||
translateText,
|
translateText,
|
||||||
openAddressEditor: setRecipientAddressEditorIndex,
|
openAddressEditor: setRecipientAddressEditorIndex,
|
||||||
openPostboxTargetEditor: setPostboxTargetEditorIndex,
|
openPostboxTargetEditor: setPostboxTargetEditorIndex,
|
||||||
@@ -549,7 +551,8 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
|
|||||||
setRecipientProfilesPageSize(pageSize);
|
setRecipientProfilesPageSize(pageSize);
|
||||||
setRecipientProfilesPage(1);
|
setRecipientProfilesPage(1);
|
||||||
}
|
}
|
||||||
}} />
|
}}
|
||||||
|
onQueryChange={setRecipientProfilesQuery} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -1735,6 +1738,7 @@ type RecipientProfileColumnContext = {
|
|||||||
fieldDefinitions: ReturnType<typeof getDraftFields>;
|
fieldDefinitions: ReturnType<typeof getDraftFields>;
|
||||||
individualAttachmentBasePaths: ReturnType<typeof getIndividualAttachmentBasePaths>;
|
individualAttachmentBasePaths: ReturnType<typeof getIndividualAttachmentBasePaths>;
|
||||||
zipConfig: AttachmentZipCollection;
|
zipConfig: AttachmentZipCollection;
|
||||||
|
addressFilter: string;
|
||||||
translateText: (value: string) => string;
|
translateText: (value: string) => string;
|
||||||
openAddressEditor: (index: number) => void;
|
openAddressEditor: (index: number) => void;
|
||||||
openPostboxTargetEditor: (index: number) => void;
|
openPostboxTargetEditor: (index: number) => void;
|
||||||
@@ -1746,7 +1750,7 @@ type RecipientProfileColumnContext = {
|
|||||||
removeEntry: (index: number) => void;
|
removeEntry: (index: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function recipientProfileColumns({ settings, campaignId, draft, locked, filesModuleInstalled, postboxModuleInstalled, postboxCatalog, entries, fieldDefinitions, individualAttachmentBasePaths, zipConfig, translateText, openAddressEditor, openPostboxTargetEditor, updateEntry, updateEntryAttachments, updateEntryField, addRecipient, moveEntry, removeEntry }: RecipientProfileColumnContext): DataGridColumn<Record<string, unknown>>[] {
|
function recipientProfileColumns({ settings, campaignId, draft, locked, filesModuleInstalled, postboxModuleInstalled, postboxCatalog, entries, fieldDefinitions, individualAttachmentBasePaths, zipConfig, addressFilter, translateText, openAddressEditor, openPostboxTargetEditor, updateEntry, updateEntryAttachments, updateEntryField, addRecipient, moveEntry, removeEntry }: RecipientProfileColumnContext): DataGridColumn<Record<string, unknown>>[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: "number",
|
id: "number",
|
||||||
@@ -1770,6 +1774,7 @@ function recipientProfileColumns({ settings, campaignId, draft, locked, filesMod
|
|||||||
filterable: true,
|
filterable: true,
|
||||||
render: (entry, index) => {
|
render: (entry, index) => {
|
||||||
const summary = recipientAddressSummary(entry, translateText);
|
const summary = recipientAddressSummary(entry, translateText);
|
||||||
|
const hiddenMatch = hiddenRecipientAddressMatch(entry, addressFilter);
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
<span className="recipient-address-editor-main">
|
<span className="recipient-address-editor-main">
|
||||||
@@ -1777,6 +1782,11 @@ function recipientProfileColumns({ settings, campaignId, draft, locked, filesMod
|
|||||||
{summary.badges.map((badge) =>
|
{summary.badges.map((badge) =>
|
||||||
<span className="recipient-extra-bubble" key={badge}>{badge}</span>
|
<span className="recipient-extra-bubble" key={badge}>{badge}</span>
|
||||||
)}
|
)}
|
||||||
|
{hiddenMatch &&
|
||||||
|
<span className="recipient-hidden-address-match">
|
||||||
|
Matched in {hiddenMatch.label}: {hiddenMatch.address}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
{!locked && <Pencil aria-hidden="true" />}
|
{!locked && <Pencil aria-hidden="true" />}
|
||||||
</>
|
</>
|
||||||
@@ -1936,6 +1946,32 @@ function recipientAddressFilterValue(entry: Record<string, unknown>): string {
|
|||||||
join(", ");
|
join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hiddenRecipientAddressMatch(
|
||||||
|
entry: Record<string, unknown>,
|
||||||
|
filterValue: string
|
||||||
|
): {label: string;address: string;} | null {
|
||||||
|
const query = filterValue.trim().toLowerCase();
|
||||||
|
if (!query) return null;
|
||||||
|
|
||||||
|
const primary = getEntryAddresses(entry, "to")[0];
|
||||||
|
if (primary && formatMailboxAddress(primary).toLowerCase().includes(query)) return null;
|
||||||
|
|
||||||
|
const labels: Record<AddressFieldKey, string> = {
|
||||||
|
from: "From",
|
||||||
|
reply_to: "Reply-To",
|
||||||
|
to: "To",
|
||||||
|
cc: "CC",
|
||||||
|
bcc: "BCC"
|
||||||
|
};
|
||||||
|
for (const column of recipientAddressOverlayColumns) {
|
||||||
|
const addresses = getEntryAddresses(entry, column.key);
|
||||||
|
const hiddenAddresses = column.key === "to" ? addresses.slice(1) : addresses;
|
||||||
|
const match = hiddenAddresses.find((address) => formatMailboxAddress(address).toLowerCase().includes(query));
|
||||||
|
if (match) return { label: labels[column.key], address: formatMailboxAddress(match) };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function addressCountBadge(count: number, label: string): string | null {
|
function addressCountBadge(count: number, label: string): string | null {
|
||||||
if (count <= 0) return null;
|
if (count <= 0) return null;
|
||||||
return `+${count} ${label}`;
|
return `+${count} ${label}`;
|
||||||
|
|||||||
@@ -930,9 +930,20 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recipient-hidden-address-match {
|
||||||
|
flex-basis: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.recipient-data-address.is-empty {
|
.recipient-data-address.is-empty {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -2380,6 +2391,28 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.campaign-attachment-evidence .data-grid-shell {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.campaign-evidence-identifiers {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.campaign-evidence-identifiers code,
|
||||||
|
.campaign-evidence-identifiers small {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.campaign-evidence-identifiers small {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.attempt-history-wrap {
|
.attempt-history-wrap {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border: var(--border-line);
|
border: var(--border-line);
|
||||||
|
|||||||
28
webui/tests/recipient-search-ui-structure.test.mjs
Normal file
28
webui/tests/recipient-search-ui-structure.test.mjs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const source = readFileSync(
|
||||||
|
new URL("../src/features/campaigns/RecipientDataPage.tsx", import.meta.url),
|
||||||
|
"utf8"
|
||||||
|
).replace(/\s+/g, " ");
|
||||||
|
|
||||||
|
function assertIncludes(fragment, message) {
|
||||||
|
if (!source.includes(fragment.replace(/\s+/g, " "))) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of ["from", "reply_to", "to", "cc", "bcc"]) {
|
||||||
|
assertIncludes(`key: "${field}"`, `recipient search must retain the ${field} address category`);
|
||||||
|
}
|
||||||
|
assertIncludes(
|
||||||
|
"recipientAddressOverlayColumns. flatMap((column) => getEntryAddresses(entry, column.key))",
|
||||||
|
"recipient filtering must search every configured address category"
|
||||||
|
);
|
||||||
|
assertIncludes(
|
||||||
|
"onQueryChange={setRecipientProfilesQuery}",
|
||||||
|
"the recipient page must observe the shared DataGrid filter"
|
||||||
|
);
|
||||||
|
assertIncludes(
|
||||||
|
"Matched in {hiddenMatch.label}: {hiddenMatch.address}",
|
||||||
|
"a row must explain when its match came from a hidden address"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Campaign recipient search covers and explains hidden address matches.");
|
||||||
@@ -86,3 +86,6 @@ assert(reportSource.includes('return status === "skipped" ? "i18n:govoplan-campa
|
|||||||
assert(reportSource.includes("cards?.skipped ?? jobs.counts.send?.skipped"), "SMTP skipped has a separate report count");
|
assert(reportSource.includes("cards?.skipped ?? jobs.counts.send?.skipped"), "SMTP skipped has a separate report count");
|
||||||
assert(reportSource.includes("cards?.imap_skipped ?? jobs.counts.imap?.skipped"), "IMAP skipped has a separate report count");
|
assert(reportSource.includes("cards?.imap_skipped ?? jobs.counts.imap?.skipped"), "IMAP skipped has a separate report count");
|
||||||
assert(!reportSource.includes("setPage((value) => Math.max(1, value - 1))"), "the one-off report pager is removed in favor of the central DataGrid pager");
|
assert(!reportSource.includes("setPage((value) => Math.max(1, value - 1))"), "the one-off report pager is removed in favor of the central DataGrid pager");
|
||||||
|
assert(reportSource.includes("<AttachmentEvidenceSection"), "job detail exposes frozen attachment evidence");
|
||||||
|
assert(reportSource.includes("managed.version_id") && reportSource.includes("managed.checksum_sha256"), "attachment evidence identifies the exact managed file version and checksum");
|
||||||
|
assert(reportSource.includes("detail.job.eml_sha256"), "job detail exposes the immutable message digest");
|
||||||
|
|||||||
Reference in New Issue
Block a user