feat(campaign): filter reports from outcome counts
This commit is contained in:
@@ -28,6 +28,13 @@ import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
|
|||||||
import { asRecord, formatDateTime, humanize } from "./utils/campaignView";
|
import { asRecord, formatDateTime, humanize } from "./utils/campaignView";
|
||||||
import { emptyCampaignJobsResponse } from "./utils/jobDeltas";
|
import { emptyCampaignJobsResponse } from "./utils/jobDeltas";
|
||||||
import type { CampaignJobSortColumn } from "./utils/jobListQuery";
|
import type { CampaignJobSortColumn } from "./utils/jobListQuery";
|
||||||
|
import {
|
||||||
|
DEFAULT_REPORT_GRID_SORT,
|
||||||
|
activeReportGridShortcut,
|
||||||
|
reportGridQueriesEqual,
|
||||||
|
toggleReportGridShortcut,
|
||||||
|
type ReportGridShortcutId
|
||||||
|
} from "./utils/reportGridShortcuts";
|
||||||
|
|
||||||
const SEND_STATUS_OPTIONS: DataGridListOption[] = [
|
const SEND_STATUS_OPTIONS: DataGridListOption[] = [
|
||||||
"not_queued",
|
"not_queued",
|
||||||
@@ -70,7 +77,6 @@ const QUEUE_STATUS_OPTIONS: DataGridListOption[] = [
|
|||||||
"cancelled"].
|
"cancelled"].
|
||||||
map((value) => ({ value, label: humanize(value) }));
|
map((value) => ({ value, label: humanize(value) }));
|
||||||
|
|
||||||
const DEFAULT_JOB_GRID_SORT = { columnId: "number", direction: "asc" as const };
|
|
||||||
const JOB_GRID_QUERY_DELAY_MS = 300;
|
const JOB_GRID_QUERY_DELAY_MS = 300;
|
||||||
|
|
||||||
type ReconcileRequest = {jobId: string;decision: "smtp_accepted" | "not_sent";} | null;
|
type ReconcileRequest = {jobId: string;decision: "smtp_accepted" | "not_sent";} | null;
|
||||||
@@ -90,7 +96,7 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
|||||||
const [pageSize, setPageSize] = useState(50);
|
const [pageSize, setPageSize] = useState(50);
|
||||||
const [initialGridFilters] = useState<Record<string, string | string[]>>(() => initialReportGridFilters());
|
const [initialGridFilters] = useState<Record<string, string | string[]>>(() => initialReportGridFilters());
|
||||||
const initialGridQuery = useMemo<DataGridQueryState>(() => ({
|
const initialGridQuery = useMemo<DataGridQueryState>(() => ({
|
||||||
sort: DEFAULT_JOB_GRID_SORT,
|
sort: DEFAULT_REPORT_GRID_SORT,
|
||||||
filters: serializeInitialGridFilters(initialGridFilters)
|
filters: serializeInitialGridFilters(initialGridFilters)
|
||||||
}), [initialGridFilters]);
|
}), [initialGridFilters]);
|
||||||
const [jobGridQuery, setJobGridQuery] = useState<DataGridQueryState>(initialGridQuery);
|
const [jobGridQuery, setJobGridQuery] = useState<DataGridQueryState>(initialGridQuery);
|
||||||
@@ -110,16 +116,45 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handle = window.setTimeout(() => {
|
const handle = window.setTimeout(() => {
|
||||||
setAppliedQuery(query.trim());
|
setAppliedQuery(query.trim());
|
||||||
setAppliedJobGridQuery((current) => dataGridQueriesEqual(current, jobGridQuery) ? current : jobGridQuery);
|
setAppliedJobGridQuery((current) => reportGridQueriesEqual(current, jobGridQuery) ? current : jobGridQuery);
|
||||||
setPage(1);
|
setPage(1);
|
||||||
}, JOB_GRID_QUERY_DELAY_MS);
|
}, JOB_GRID_QUERY_DELAY_MS);
|
||||||
return () => window.clearTimeout(handle);
|
return () => window.clearTimeout(handle);
|
||||||
}, [query, jobGridQuery]);
|
}, [query, jobGridQuery]);
|
||||||
|
|
||||||
const handleJobGridQuery = useCallback((next: DataGridQueryState) => {
|
const handleJobGridQuery = useCallback((next: DataGridQueryState) => {
|
||||||
setJobGridQuery((current) => dataGridQueriesEqual(current, next) ? current : next);
|
setJobGridQuery((current) => reportGridQueriesEqual(current, next) ? current : next);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const activeJobGridShortcut = useMemo(
|
||||||
|
() => query.trim() ? null : activeReportGridShortcut(jobGridQuery),
|
||||||
|
[jobGridQuery, query]
|
||||||
|
);
|
||||||
|
|
||||||
|
const applyJobGridShortcut = useCallback((shortcutId: ReportGridShortcutId) => {
|
||||||
|
const next = toggleReportGridShortcut(jobGridQuery, shortcutId);
|
||||||
|
setQuery("");
|
||||||
|
setAppliedQuery("");
|
||||||
|
setJobGridQuery(next);
|
||||||
|
setAppliedJobGridQuery(next);
|
||||||
|
setPage(1);
|
||||||
|
}, [jobGridQuery]);
|
||||||
|
|
||||||
|
const deliveryOutcomeShortcuts: { label: string; value: string | number; shortcutId: ReportGridShortcutId }[] = [
|
||||||
|
{ label: "i18n:govoplan-campaign.jobs_total.98da65bc", value: cards?.jobs_total ?? "—", shortcutId: "all" },
|
||||||
|
{ label: "i18n:govoplan-campaign.smtp_accepted.e3aa7603", value: cards?.smtp_accepted ?? cards?.sent ?? 0, shortcutId: "smtp_accepted" },
|
||||||
|
{ label: "i18n:govoplan-campaign.failed.09fef5d8", value: cards?.failed ?? 0, shortcutId: "failed" },
|
||||||
|
{ label: "i18n:govoplan-campaign.outcome_unknown.6e929fca", value: cards?.outcome_unknown ?? 0, shortcutId: "outcome_unknown" },
|
||||||
|
{ label: "i18n:govoplan-campaign.not_attempted.e1be3c69", value: cards?.not_attempted ?? 0, shortcutId: "not_attempted" },
|
||||||
|
{ label: "i18n:govoplan-campaign.smtp_skipped_excluded_.df6eca19", value: cards?.skipped ?? jobs.counts.send?.skipped ?? 0, shortcutId: "smtp_skipped" },
|
||||||
|
{ label: "i18n:govoplan-campaign.cancelled.a1bf92ef", value: cards?.cancelled ?? 0, shortcutId: "cancelled" }
|
||||||
|
];
|
||||||
|
const imapOutcomeShortcuts: { label: string; value: string | number; shortcutId: ReportGridShortcutId }[] = [
|
||||||
|
{ label: "i18n:govoplan-campaign.imap_appended.56017ea3", value: cards?.imap_appended ?? 0, shortcutId: "imap_appended" },
|
||||||
|
{ label: "i18n:govoplan-campaign.imap_failed.50dbca55", value: cards?.imap_failed ?? 0, shortcutId: "imap_failed" },
|
||||||
|
{ label: "i18n:govoplan-campaign.imap_skipped.5a97b542", value: cards?.imap_skipped ?? jobs.counts.imap?.skipped ?? 0, shortcutId: "imap_skipped" }
|
||||||
|
];
|
||||||
|
|
||||||
const loadJobs = useCallback(async () => {
|
const loadJobs = useCallback(async () => {
|
||||||
if (!campaignId) return;
|
if (!campaignId) return;
|
||||||
const requestId = ++jobsRequestRef.current;
|
const requestId = ++jobsRequestRef.current;
|
||||||
@@ -383,20 +418,36 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
|||||||
<Card title="i18n:govoplan-campaign.delivery_outcome.f9d7c085">
|
<Card title="i18n:govoplan-campaign.delivery_outcome.f9d7c085">
|
||||||
<dl className="detail-list">
|
<dl className="detail-list">
|
||||||
<div><dt>i18n:govoplan-campaign.generated.8eefdd52</dt><dd>{formatDateTime(data.summary?.generated_at)}</dd></div>
|
<div><dt>i18n:govoplan-campaign.generated.8eefdd52</dt><dd>{formatDateTime(data.summary?.generated_at)}</dd></div>
|
||||||
<div><dt>i18n:govoplan-campaign.jobs_total.98da65bc</dt><dd>{cards?.jobs_total ?? "—"}</dd></div>
|
{deliveryOutcomeShortcuts.map(({ label, value, shortcutId }) => {
|
||||||
<div><dt>i18n:govoplan-campaign.smtp_accepted.e3aa7603</dt><dd>{cards?.smtp_accepted ?? cards?.sent ?? 0}</dd></div>
|
const active = activeJobGridShortcut === shortcutId;
|
||||||
<div><dt>i18n:govoplan-campaign.failed.09fef5d8</dt><dd>{cards?.failed ?? 0}</dd></div>
|
return (
|
||||||
<div><dt>i18n:govoplan-campaign.outcome_unknown.6e929fca</dt><dd>{cards?.outcome_unknown ?? 0}</dd></div>
|
<div key={shortcutId}>
|
||||||
<div><dt>i18n:govoplan-campaign.not_attempted.e1be3c69</dt><dd>{cards?.not_attempted ?? 0}</dd></div>
|
<dt>{label}</dt>
|
||||||
<div><dt>i18n:govoplan-campaign.smtp_skipped_excluded_.df6eca19</dt><dd>{cards?.skipped ?? jobs.counts.send?.skipped ?? 0}</dd></div>
|
<dd>
|
||||||
<div><dt>i18n:govoplan-campaign.cancelled.a1bf92ef</dt><dd>{cards?.cancelled ?? 0}</dd></div>
|
<Button type="button" variant={active ? "primary" : "ghost"} aria-pressed={active} onClick={() => applyJobGridShortcut(shortcutId)}>
|
||||||
|
{value}
|
||||||
|
</Button>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</dl>
|
</dl>
|
||||||
</Card>
|
</Card>
|
||||||
<Card title="i18n:govoplan-campaign.imap_and_execution_plan.4c80c058">
|
<Card title="i18n:govoplan-campaign.imap_and_execution_plan.4c80c058">
|
||||||
<dl className="detail-list">
|
<dl className="detail-list">
|
||||||
<div><dt>i18n:govoplan-campaign.imap_appended.56017ea3</dt><dd>{cards?.imap_appended ?? 0}</dd></div>
|
{imapOutcomeShortcuts.map(({ label, value, shortcutId }) => {
|
||||||
<div><dt>i18n:govoplan-campaign.imap_failed.50dbca55</dt><dd>{cards?.imap_failed ?? 0}</dd></div>
|
const active = activeJobGridShortcut === shortcutId;
|
||||||
<div><dt>i18n:govoplan-campaign.imap_skipped.5a97b542</dt><dd>{cards?.imap_skipped ?? jobs.counts.imap?.skipped ?? 0}</dd></div>
|
return (
|
||||||
|
<div key={shortcutId}>
|
||||||
|
<dt>{label}</dt>
|
||||||
|
<dd>
|
||||||
|
<Button type="button" variant={active ? "primary" : "ghost"} aria-pressed={active} onClick={() => applyJobGridShortcut(shortcutId)}>
|
||||||
|
{value}
|
||||||
|
</Button>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<div><dt>i18n:govoplan-campaign.append_policy.f195cb05</dt><dd>{imapPolicy.enabled === true ? i18nMessage("i18n:govoplan-campaign.enabled_value.e395e48f", { value0: String(imapPolicy.folder ?? "i18n:govoplan-campaign.auto.0d612c12") }) : "i18n:govoplan-campaign.disabled.f4f4473d"}</dd></div>
|
<div><dt>i18n:govoplan-campaign.append_policy.f195cb05</dt><dd>{imapPolicy.enabled === true ? i18nMessage("i18n:govoplan-campaign.enabled_value.e395e48f", { value0: String(imapPolicy.folder ?? "i18n:govoplan-campaign.auto.0d612c12") }) : "i18n:govoplan-campaign.disabled.f4f4473d"}</dd></div>
|
||||||
<div><dt>i18n:govoplan-campaign.rate_limit.d08e55f5</dt><dd>{rateLimit.messages_per_minute ? i18nMessage("i18n:govoplan-campaign.value_minute.aeb1a9ea", { value0: String(rateLimit.messages_per_minute) }) : "—"}</dd></div>
|
<div><dt>i18n:govoplan-campaign.rate_limit.d08e55f5</dt><dd>{rateLimit.messages_per_minute ? i18nMessage("i18n:govoplan-campaign.value_minute.aeb1a9ea", { value0: String(rateLimit.messages_per_minute) }) : "—"}</dd></div>
|
||||||
<div><dt>i18n:govoplan-campaign.minimum_remaining_duration.639b792c</dt><dd>{String(delivery.estimated_remaining_send_human ?? "—")}</dd></div>
|
<div><dt>i18n:govoplan-campaign.minimum_remaining_duration.639b792c</dt><dd>{String(delivery.estimated_remaining_send_human ?? "—")}</dd></div>
|
||||||
@@ -435,7 +486,8 @@ export default function CampaignReportPage({ settings, campaignId }: {settings:
|
|||||||
getRowKey={(row: Record<string, unknown>) => String(row.id ?? "")}
|
getRowKey={(row: Record<string, unknown>) => String(row.id ?? "")}
|
||||||
emptyText="i18n:govoplan-campaign.no_jobs_match_the_current_filters.b1501ff5"
|
emptyText="i18n:govoplan-campaign.no_jobs_match_the_current_filters.b1501ff5"
|
||||||
initialFilters={initialGridFilters}
|
initialFilters={initialGridFilters}
|
||||||
initialSort={DEFAULT_JOB_GRID_SORT}
|
initialSort={DEFAULT_REPORT_GRID_SORT}
|
||||||
|
query={jobGridQuery}
|
||||||
pagination={{
|
pagination={{
|
||||||
mode: "server",
|
mode: "server",
|
||||||
page,
|
page,
|
||||||
@@ -589,16 +641,6 @@ function campaignJobSortColumn(value?: string): CampaignJobSortColumn {
|
|||||||
return "number";
|
return "number";
|
||||||
}
|
}
|
||||||
|
|
||||||
function dataGridQueriesEqual(left: DataGridQueryState, right: DataGridQueryState): boolean {
|
|
||||||
if ((left.sort?.columnId ?? "") !== (right.sort?.columnId ?? "")) return false;
|
|
||||||
if ((left.sort?.direction ?? "") !== (right.sort?.direction ?? "")) return false;
|
|
||||||
const keys = new Set([...Object.keys(left.filters), ...Object.keys(right.filters)]);
|
|
||||||
for (const key of keys) {
|
|
||||||
if ((left.filters[key] ?? "") !== (right.filters[key] ?? "")) return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function retryableFailedStatus(status: string): boolean {
|
function retryableFailedStatus(status: string): boolean {
|
||||||
return status === "failed_temporary" || status === "failed_permanent";
|
return status === "failed_temporary" || status === "failed_permanent";
|
||||||
}
|
}
|
||||||
|
|||||||
85
webui/src/features/campaigns/utils/reportGridShortcuts.ts
Normal file
85
webui/src/features/campaigns/utils/reportGridShortcuts.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
export type ReportGridQueryState = {
|
||||||
|
sort: { columnId: string; direction: "asc" | "desc" } | null;
|
||||||
|
filters: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_REPORT_GRID_SORT = { columnId: "number", direction: "asc" as const };
|
||||||
|
|
||||||
|
export type ReportGridShortcutId =
|
||||||
|
| "all"
|
||||||
|
| "smtp_accepted"
|
||||||
|
| "failed"
|
||||||
|
| "outcome_unknown"
|
||||||
|
| "not_attempted"
|
||||||
|
| "smtp_skipped"
|
||||||
|
| "cancelled"
|
||||||
|
| "imap_appended"
|
||||||
|
| "imap_failed"
|
||||||
|
| "imap_skipped";
|
||||||
|
|
||||||
|
const REPORT_GRID_SHORTCUT_FILTERS: Record<ReportGridShortcutId, Record<string, string>> = {
|
||||||
|
all: {},
|
||||||
|
smtp_accepted: { send: listFilter(["smtp_accepted", "sent"]) },
|
||||||
|
failed: { send: listFilter(["failed_temporary", "failed_permanent"]) },
|
||||||
|
outcome_unknown: { send: listFilter(["outcome_unknown"]) },
|
||||||
|
not_attempted: { send: listFilter(["not_queued"]) },
|
||||||
|
smtp_skipped: { send: listFilter(["skipped"]) },
|
||||||
|
cancelled: { send: listFilter(["cancelled"]) },
|
||||||
|
imap_appended: { imap: listFilter(["appended"]) },
|
||||||
|
imap_failed: { imap: listFilter(["failed"]) },
|
||||||
|
imap_skipped: { imap: listFilter(["skipped"]) }
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the complete grid query for a count shortcut. Shortcuts are exact
|
||||||
|
* report views, so applying one intentionally clears every unrelated filter
|
||||||
|
* and restores the stable report ordering.
|
||||||
|
*/
|
||||||
|
export function reportGridQueryForShortcut(shortcutId: ReportGridShortcutId): ReportGridQueryState {
|
||||||
|
return {
|
||||||
|
sort: { ...DEFAULT_REPORT_GRID_SORT },
|
||||||
|
filters: { ...REPORT_GRID_SHORTCUT_FILTERS[shortcutId] }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Applying an already selected outcome shortcut returns to the unfiltered report. */
|
||||||
|
export function toggleReportGridShortcut(
|
||||||
|
current: ReportGridQueryState,
|
||||||
|
shortcutId: ReportGridShortcutId
|
||||||
|
): ReportGridQueryState {
|
||||||
|
const target = reportGridQueryForShortcut(shortcutId);
|
||||||
|
if (shortcutId !== "all" && reportGridFiltersEqual(current.filters, target.filters)) {
|
||||||
|
return reportGridQueryForShortcut("all");
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activeReportGridShortcut(query: ReportGridQueryState): ReportGridShortcutId | null {
|
||||||
|
const shortcutIds = Object.keys(REPORT_GRID_SHORTCUT_FILTERS) as ReportGridShortcutId[];
|
||||||
|
return shortcutIds.find((shortcutId) => reportGridFiltersEqual(
|
||||||
|
query.filters,
|
||||||
|
REPORT_GRID_SHORTCUT_FILTERS[shortcutId]
|
||||||
|
)) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reportGridQueriesEqual(left: ReportGridQueryState, right: ReportGridQueryState): boolean {
|
||||||
|
if ((left.sort?.columnId ?? "") !== (right.sort?.columnId ?? "")) return false;
|
||||||
|
if ((left.sort?.direction ?? "") !== (right.sort?.direction ?? "")) return false;
|
||||||
|
const keys = new Set([...Object.keys(left.filters), ...Object.keys(right.filters)]);
|
||||||
|
for (const key of keys) {
|
||||||
|
if ((left.filters[key] ?? "") !== (right.filters[key] ?? "")) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function listFilter(values: string[]): string {
|
||||||
|
return `list:${JSON.stringify(values)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reportGridFiltersEqual(left: Record<string, string>, right: Record<string, string>): boolean {
|
||||||
|
const keys = new Set([...Object.keys(left), ...Object.keys(right)]);
|
||||||
|
for (const key of keys) {
|
||||||
|
if ((left[key] ?? "") !== (right[key] ?? "")) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
import { campaignJobsQueryParams } from "../src/features/campaigns/utils/jobListQuery";
|
import { campaignJobsQueryParams } from "../src/features/campaigns/utils/jobListQuery";
|
||||||
|
import {
|
||||||
|
activeReportGridShortcut,
|
||||||
|
reportGridQueryForShortcut,
|
||||||
|
toggleReportGridShortcut
|
||||||
|
} from "../src/features/campaigns/utils/reportGridShortcuts";
|
||||||
|
|
||||||
declare function require(name: string): {
|
declare function require(name: string): {
|
||||||
readFileSync(path: string, encoding: string): string;
|
readFileSync(path: string, encoding: string): string;
|
||||||
@@ -34,12 +39,47 @@ assert(params.get("filter_send") === 'list:["failed_temporary","outcome_unknown"
|
|||||||
assert(params.get("filter_attempts") === "gte:2", "typed number filters retain their operator");
|
assert(params.get("filter_attempts") === "gte:2", "typed number filters retain their operator");
|
||||||
assert(!params.toString().includes("unsupported") && !params.toString().includes("must-not-leak"), "only the declared backend filter contract is serialized");
|
assert(!params.toString().includes("unsupported") && !params.toString().includes("must-not-leak"), "only the declared backend filter contract is serialized");
|
||||||
|
|
||||||
|
const shortcutFilters = {
|
||||||
|
smtp_accepted: { send: 'list:["smtp_accepted","sent"]' },
|
||||||
|
failed: { send: 'list:["failed_temporary","failed_permanent"]' },
|
||||||
|
outcome_unknown: { send: 'list:["outcome_unknown"]' },
|
||||||
|
not_attempted: { send: 'list:["not_queued"]' },
|
||||||
|
smtp_skipped: { send: 'list:["skipped"]' },
|
||||||
|
cancelled: { send: 'list:["cancelled"]' },
|
||||||
|
imap_appended: { imap: 'list:["appended"]' },
|
||||||
|
imap_failed: { imap: 'list:["failed"]' },
|
||||||
|
imap_skipped: { imap: 'list:["skipped"]' }
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
for (const [shortcutId, filters] of Object.entries(shortcutFilters)) {
|
||||||
|
const shortcut = reportGridQueryForShortcut(shortcutId as keyof typeof shortcutFilters);
|
||||||
|
assert(JSON.stringify(shortcut.filters) === JSON.stringify(filters), `${shortcutId} uses the exact backend list filter`);
|
||||||
|
assert(shortcut.sort?.columnId === "number" && shortcut.sort.direction === "asc", `${shortcutId} restores stable report ordering`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const failedShortcut = toggleReportGridShortcut({
|
||||||
|
sort: { columnId: "updated", direction: "desc" },
|
||||||
|
filters: { recipient: "old search", imap: 'list:["appended"]' }
|
||||||
|
}, "failed");
|
||||||
|
assert(JSON.stringify(failedShortcut.filters) === JSON.stringify(shortcutFilters.failed), "a shortcut clears unrelated filters");
|
||||||
|
assert(activeReportGridShortcut(failedShortcut) === "failed", "the exact shortcut query is recognized as active");
|
||||||
|
const toggledBack = toggleReportGridShortcut(failedShortcut, "failed");
|
||||||
|
assert(Object.keys(toggledBack.filters).length === 0, "selecting an active count returns to all jobs");
|
||||||
|
assert(activeReportGridShortcut(toggledBack) === "all", "the cleared query activates the all-jobs shortcut");
|
||||||
|
const sortedFailedShortcut = { ...failedShortcut, sort: { columnId: "recipient", direction: "desc" as const } };
|
||||||
|
assert(Object.keys(toggleReportGridShortcut(sortedFailedShortcut, "failed").filters).length === 0, "the active shortcut toggles to all even after the user changes sorting");
|
||||||
|
|
||||||
const reportSource = readFileSync("src/features/campaigns/CampaignReportPage.tsx", "utf8");
|
const reportSource = readFileSync("src/features/campaigns/CampaignReportPage.tsx", "utf8");
|
||||||
assert(reportSource.includes('mode: "server"'), "the report DataGrid declares server query ownership");
|
assert(reportSource.includes('mode: "server"'), "the report DataGrid declares server query ownership");
|
||||||
assert(reportSource.includes("totalRows: jobs.total"), "the shared pagination count uses the filtered backend total");
|
assert(reportSource.includes("totalRows: jobs.total"), "the shared pagination count uses the filtered backend total");
|
||||||
assert(reportSource.includes("onQueryChange={handleJobGridQuery}"), "header sort and filter changes drive the backend query");
|
assert(reportSource.includes("onQueryChange={handleJobGridQuery}"), "header sort and filter changes drive the backend query");
|
||||||
assert(reportSource.includes("initialReportGridFilters()"), "status deep links initialize the DataGrid filters");
|
assert(reportSource.includes("initialReportGridFilters()"), "status deep links initialize the DataGrid filters");
|
||||||
assert(reportSource.includes("initialReportQuery()"), "q deep links initialize the report search");
|
assert(reportSource.includes("initialReportQuery()"), "q deep links initialize the report search");
|
||||||
|
assert(reportSource.includes("query={jobGridQuery}"), "external count shortcuts synchronize the visible DataGrid query");
|
||||||
|
assert(reportSource.includes('setQuery("");') && reportSource.includes('setAppliedQuery("");'), "count shortcuts clear both visible and applied search terms");
|
||||||
|
assert(reportSource.includes("deliveryOutcomeShortcuts.map") && reportSource.includes("imapOutcomeShortcuts.map"), "top-level delivery counts use one coherent shortcut model");
|
||||||
|
assert(reportSource.includes('<Button type="button" variant={active ? "primary" : "ghost"}'), "count shortcuts render the central Button directly");
|
||||||
|
assert(reportSource.includes('aria-pressed={active}'), "count shortcuts expose their selected state accessibly");
|
||||||
assert(reportSource.includes('"skipped",\n"queued"'), "SMTP skipped is a first-class report filter option");
|
assert(reportSource.includes('"skipped",\n"queued"'), "SMTP skipped is a first-class report filter option");
|
||||||
assert(reportSource.includes("i18n:govoplan-campaign.excluded_rows_are_intentionally_omitted_from_del.421a1f00"), "the report explains excluded transport semantics through the bilingual catalog");
|
assert(reportSource.includes("i18n:govoplan-campaign.excluded_rows_are_intentionally_omitted_from_del.421a1f00"), "the report explains excluded transport semantics through the bilingual catalog");
|
||||||
assert(reportSource.includes('return status === "skipped" ? "i18n:govoplan-campaign.skipped.5a000ad7"'), "the new skipped filter and status badges use the localized label");
|
assert(reportSource.includes('return status === "skipped" ? "i18n:govoplan-campaign.skipped.5a000ad7"'), "the new skipped filter and status badges use the localized label");
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"tests/report-grid-query.test.ts",
|
"tests/report-grid-query.test.ts",
|
||||||
"src/features/campaigns/utils/jobListQuery.ts"
|
"src/features/campaigns/utils/jobListQuery.ts",
|
||||||
|
"src/features/campaigns/utils/reportGridShortcuts.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user