DataGrid - initial commit
This commit is contained in:
@@ -13,6 +13,8 @@ import Card from "../../components/Card";
|
||||
import MetricCard from "../../components/MetricCard";
|
||||
import StatusBadge from "../../components/StatusBadge";
|
||||
import ToggleSwitch from "../../components/ToggleSwitch";
|
||||
import DismissibleAlert from "../../components/DismissibleAlert";
|
||||
import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid";
|
||||
import { mockSendCampaign, sendCampaignNow } from "../../api/campaigns";
|
||||
import { getMockMailboxMessage, type MockMailboxMessage } from "../../api/mail";
|
||||
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
|
||||
@@ -146,9 +148,9 @@ export default function SendDataPage({ settings, campaignId }: { settings: ApiSe
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="alert danger">{error}</div>}
|
||||
{sendMessage && <div className="alert info">{sendMessage}</div>}
|
||||
{mockMessage && <div className="alert info">{mockMessage}</div>}
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||
{sendMessage && <DismissibleAlert tone="info" resetKey={sendMessage}>{sendMessage}</DismissibleAlert>}
|
||||
{mockMessage && <DismissibleAlert tone="info" resetKey={mockMessage}>{mockMessage}</DismissibleAlert>}
|
||||
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} reload={reload} message="Send snapshot. Copy to edit." />}
|
||||
|
||||
<LoadingFrame loading={loading || queuedOrSending} label={queuedOrSending ? "Refreshing send state…" : "Loading send data…"}>
|
||||
@@ -181,89 +183,49 @@ export default function SendDataPage({ settings, campaignId }: { settings: ApiSe
|
||||
|
||||
{mockResult && (
|
||||
<div className="mock-flow-panel">
|
||||
<div className="app-table-wrap data-table-wrap">
|
||||
<table className="app-table data-table compact-table">
|
||||
<thead><tr><th>Step</th><th>Status</th><th>Summary</th></tr></thead>
|
||||
<tbody>
|
||||
{mockSteps.map((step) => (
|
||||
<tr key={String(step.key)}>
|
||||
<td>{String(step.label ?? step.key)}</td>
|
||||
<td><StatusBadge status={String(step.status ?? "info")} /></td>
|
||||
<td>{stringifyPreview(asRecord(step.summary), 240)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataGrid
|
||||
id={`campaign-${campaignId}-mock-steps`}
|
||||
rows={mockSteps}
|
||||
columns={mockStepColumns()}
|
||||
getRowKey={(step, index) => String(step.key ?? index)}
|
||||
emptyText="No mock steps returned."
|
||||
className="data-table-wrap data-table compact-table"
|
||||
/>
|
||||
|
||||
<h3>Built messages</h3>
|
||||
<div className="app-table-wrap data-table-wrap">
|
||||
<table className="app-table data-table compact-table">
|
||||
<thead><tr><th>#</th><th>Recipient</th><th>Subject</th><th>Validation</th><th>Attachments</th><th>Issues</th></tr></thead>
|
||||
<tbody>
|
||||
{mockMessages.map((message) => {
|
||||
const to = asArray(message.to).map(asRecord);
|
||||
const issues = asArray(message.issues).map(asRecord);
|
||||
return (
|
||||
<tr key={String(message.entry_index ?? message.entry_id)}>
|
||||
<td>{String(message.entry_index ?? "—")}</td>
|
||||
<td>{to.map((item) => String(item.email ?? "")).filter(Boolean).join(", ") || "—"}</td>
|
||||
<td>{String(message.subject ?? "—")}</td>
|
||||
<td><StatusBadge status={String(message.validation_status ?? "unknown")} /></td>
|
||||
<td>{String(message.attachment_count ?? 0)}</td>
|
||||
<td>{issues.length === 0 ? "—" : issues.map((issue) => String(issue.message ?? issue.code ?? "issue")).join(" · ")}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{mockMessages.length === 0 && <tr><td colSpan={6}>No messages were built.</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataGrid
|
||||
id={`campaign-${campaignId}-mock-built-messages`}
|
||||
rows={mockMessages}
|
||||
columns={mockBuiltMessageColumns()}
|
||||
getRowKey={(message, index) => String(message.entry_index ?? message.entry_id ?? index)}
|
||||
emptyText="No messages were built."
|
||||
className="data-table-wrap data-table compact-table"
|
||||
/>
|
||||
|
||||
{mockRows.length > 0 && (
|
||||
<>
|
||||
<h3>Mock send results</h3>
|
||||
<div className="app-table-wrap data-table-wrap">
|
||||
<table className="app-table data-table compact-table">
|
||||
<thead><tr><th>#</th><th>Status</th><th>Recipient</th><th>SMTP</th><th>IMAP</th><th>Message</th></tr></thead>
|
||||
<tbody>
|
||||
{mockRows.map((row, index) => {
|
||||
const to = asArray(row.to).map(asRecord);
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td>{String(row.entry_index ?? index + 1)}</td>
|
||||
<td><StatusBadge status={String(row.status ?? "info")} /></td>
|
||||
<td>{to.map((item) => String(item.email ?? "")).filter(Boolean).join(", ") || "—"}</td>
|
||||
<td>{String(row.smtp_message_id ?? row.status ?? "—")}</td>
|
||||
<td>{String(row.imap_message_id ?? row.imap_status ?? "—")}</td>
|
||||
<td>{String(row.message ?? "—")}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataGrid
|
||||
id={`campaign-${campaignId}-mock-send-results`}
|
||||
rows={mockRows}
|
||||
columns={mockSendResultColumns()}
|
||||
getRowKey={(_row, index) => `mock-send-${index}`}
|
||||
emptyText="No mock send results returned."
|
||||
className="data-table-wrap data-table compact-table"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h3>Mock mailbox</h3>
|
||||
<div className="app-table-wrap data-table-wrap">
|
||||
<table className="app-table data-table compact-table">
|
||||
<thead><tr><th>Kind</th><th>Subject</th><th>Envelope / folder</th><th>Attachments</th><th>Actions</th></tr></thead>
|
||||
<tbody>
|
||||
{mockMailboxMessages.map((message) => (
|
||||
<tr key={String(message.id)}>
|
||||
<td><span className="status-badge neutral">{String(message.kind ?? "mock")}</span></td>
|
||||
<td>{String(message.subject ?? "—")}</td>
|
||||
<td>{String(message.envelope_from ?? message.folder ?? "—")} → {asArray(message.envelope_recipients).join(", ") || String(message.folder ?? "—")}</td>
|
||||
<td>{String(message.attachment_count ?? 0)}</td>
|
||||
<td><Button onClick={() => void openMockMessage(String(message.id))}>Open</Button></td>
|
||||
</tr>
|
||||
))}
|
||||
{mockMailboxMessages.length === 0 && <tr><td colSpan={5}>No mock messages captured in this run.</td></tr>}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataGrid
|
||||
id={`campaign-${campaignId}-mock-mailbox`}
|
||||
rows={mockMailboxMessages}
|
||||
columns={mockMailboxColumns(openMockMessage)}
|
||||
getRowKey={(message, index) => String(message.id ?? index)}
|
||||
emptyText="No mock messages captured in this run."
|
||||
className="data-table-wrap data-table compact-table"
|
||||
/>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
@@ -271,8 +233,8 @@ export default function SendDataPage({ settings, campaignId }: { settings: ApiSe
|
||||
</Card>
|
||||
|
||||
<Card title="Send campaign" actions={<span className="muted small-note">Requires a validated, locked and built version. Sending makes it final.</span>}>
|
||||
{!readyForDelivery && <div className="alert warning compact-alert">Validate and lock this version in Review before dry-run or sending.</div>}
|
||||
{readyForDelivery && !hasBuild && <div className="alert warning compact-alert">Build the queue in Review before dry-run or sending.</div>}
|
||||
{!readyForDelivery && <DismissibleAlert tone="warning" compact>Validate and lock this version in Review before dry-run or sending.</DismissibleAlert>}
|
||||
{readyForDelivery && !hasBuild && <DismissibleAlert tone="warning" compact>Build the queue in Review before dry-run or sending.</DismissibleAlert>}
|
||||
<div className="button-row compact-actions">
|
||||
<Button onClick={() => void runSendNow(true)} disabled={!version || loading || sendBusy || !readyForDelivery || !hasBuild}>Dry run</Button>
|
||||
<Button variant="primary" onClick={() => setSendConfirmOpen(true)} disabled={!version || loading || sendBusy || !readyForDelivery || !hasBuild}>
|
||||
@@ -292,22 +254,14 @@ export default function SendDataPage({ settings, campaignId }: { settings: ApiSe
|
||||
Attempted {String(sendResult.attempted_count ?? "—")}, sent {String(sendResult.sent_count ?? "—")}, failed {String(sendResult.failed_count ?? "—")}, skipped {String(sendResult.skipped_count ?? "—")}.
|
||||
</p>
|
||||
{resultRows.length > 0 && (
|
||||
<div className="app-table-wrap data-table-wrap">
|
||||
<table className="app-table data-table compact-table">
|
||||
<thead>
|
||||
<tr><th>Status</th><th>Job</th><th>Message</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{resultRows.slice(0, 10).map((row, index) => (
|
||||
<tr key={index}>
|
||||
<td><StatusBadge status={String(row.status ?? "info")} /></td>
|
||||
<td>{String(row.job_id ?? row.version_id ?? "—")}</td>
|
||||
<td>{String(row.message ?? stringifyPreview(row, 180))}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataGrid
|
||||
id={`campaign-${campaignId}-send-results`}
|
||||
rows={resultRows.slice(0, 10)}
|
||||
columns={sendResultColumns()}
|
||||
getRowKey={(_row, index) => `send-result-${index}`}
|
||||
emptyText="No send results returned."
|
||||
className="data-table-wrap data-table compact-table"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -372,6 +326,54 @@ export default function SendDataPage({ settings, campaignId }: { settings: ApiSe
|
||||
}
|
||||
|
||||
|
||||
function mockStepColumns(): DataGridColumn<Record<string, unknown>>[] {
|
||||
return [
|
||||
{ id: "step", header: "Step", width: 190, sortable: true, filterable: true, sticky: "start", value: (step) => String(step.label ?? step.key ?? "") },
|
||||
{ id: "status", header: "Status", width: 140, sortable: true, filterable: true, render: (step) => <StatusBadge status={String(step.status ?? "info")} />, value: (step) => String(step.status ?? "info") },
|
||||
{ id: "summary", header: "Summary", width: "minmax(320px, 1fr)", filterable: true, render: (step) => stringifyPreview(asRecord(step.summary), 240), value: (step) => stringifyPreview(asRecord(step.summary), 240) }
|
||||
];
|
||||
}
|
||||
|
||||
function mockBuiltMessageColumns(): DataGridColumn<Record<string, unknown>>[] {
|
||||
return [
|
||||
{ id: "number", header: "#", width: 80, sortable: true, sticky: "start", value: (message) => String(message.entry_index ?? "—") },
|
||||
{ id: "recipient", header: "Recipient", width: 240, resizable: true, sortable: true, filterable: true, value: (message) => asArray(message.to).map(asRecord).map((item) => String(item.email ?? "")).filter(Boolean).join(", ") || "—" },
|
||||
{ id: "subject", header: "Subject", width: "minmax(260px, 1fr)", resizable: true, sortable: true, filterable: true, value: (message) => String(message.subject ?? "—") },
|
||||
{ id: "validation", header: "Validation", width: 150, sortable: true, filterable: true, render: (message) => <StatusBadge status={String(message.validation_status ?? "unknown")} />, value: (message) => String(message.validation_status ?? "unknown") },
|
||||
{ id: "attachments", header: "Attachments", width: 130, sortable: true, filterable: true, value: (message) => String(message.attachment_count ?? 0) },
|
||||
{ id: "issues", header: "Issues", width: 260, resizable: true, filterable: true, value: (message) => { const issues = asArray(message.issues).map(asRecord); return issues.length === 0 ? "—" : issues.map((issue) => String(issue.message ?? issue.code ?? "issue")).join(" · "); } }
|
||||
];
|
||||
}
|
||||
|
||||
function mockSendResultColumns(): DataGridColumn<Record<string, unknown>>[] {
|
||||
return [
|
||||
{ id: "number", header: "#", width: 80, sortable: true, sticky: "start", value: (row, index) => String(row.entry_index ?? index + 1) },
|
||||
{ id: "status", header: "Status", width: 140, sortable: true, filterable: true, render: (row) => <StatusBadge status={String(row.status ?? "info")} />, value: (row) => String(row.status ?? "info") },
|
||||
{ id: "recipient", header: "Recipient", width: 240, resizable: true, sortable: true, filterable: true, value: (row) => asArray(row.to).map(asRecord).map((item) => String(item.email ?? "")).filter(Boolean).join(", ") || "—" },
|
||||
{ id: "smtp", header: "SMTP", width: 190, sortable: true, filterable: true, value: (row) => String(row.smtp_message_id ?? row.status ?? "—") },
|
||||
{ id: "imap", header: "IMAP", width: 190, sortable: true, filterable: true, value: (row) => String(row.imap_message_id ?? row.imap_status ?? "—") },
|
||||
{ id: "message", header: "Message", width: "minmax(260px, 1fr)", filterable: true, value: (row) => String(row.message ?? "—") }
|
||||
];
|
||||
}
|
||||
|
||||
function mockMailboxColumns(openMockMessage: (id: string) => Promise<void>): DataGridColumn<Record<string, unknown>>[] {
|
||||
return [
|
||||
{ id: "kind", header: "Kind", width: 130, sortable: true, filterable: true, sticky: "start", render: (message) => <span className="status-badge neutral">{String(message.kind ?? "mock")}</span>, value: (message) => String(message.kind ?? "mock") },
|
||||
{ id: "subject", header: "Subject", width: "minmax(240px, 1fr)", sortable: true, filterable: true, value: (message) => String(message.subject ?? "—") },
|
||||
{ id: "envelope", header: "Envelope / folder", width: 300, resizable: true, filterable: true, value: (message) => `${String(message.envelope_from ?? message.folder ?? "—")} → ${asArray(message.envelope_recipients).join(", ") || String(message.folder ?? "—")}` },
|
||||
{ id: "attachments", header: "Attachments", width: 130, sortable: true, filterable: true, value: (message) => String(message.attachment_count ?? 0) },
|
||||
{ id: "actions", header: "Actions", width: 110, sticky: "end", render: (message) => <Button onClick={() => void openMockMessage(String(message.id))}>Open</Button> }
|
||||
];
|
||||
}
|
||||
|
||||
function sendResultColumns(): DataGridColumn<Record<string, unknown>>[] {
|
||||
return [
|
||||
{ id: "status", header: "Status", width: 140, sortable: true, filterable: true, sticky: "start", render: (row) => <StatusBadge status={String(row.status ?? "info")} />, value: (row) => String(row.status ?? "info") },
|
||||
{ id: "job", header: "Job", width: 180, sortable: true, filterable: true, value: (row) => String(row.job_id ?? row.version_id ?? "—") },
|
||||
{ id: "message", header: "Message", width: "minmax(320px, 1fr)", filterable: true, value: (row) => String(row.message ?? stringifyPreview(row, 180)) }
|
||||
];
|
||||
}
|
||||
|
||||
function mockMessageMetaItems(message: MockMailboxMessage) {
|
||||
return [
|
||||
{ label: "From", value: message.from_header || message.envelope_from || "—" },
|
||||
|
||||
Reference in New Issue
Block a user