DataGrid - initial commit
This commit is contained in:
@@ -9,6 +9,8 @@ import LockedVersionNotice from "./components/LockedVersionNotice";
|
||||
import VersionLine from "./components/VersionLine";
|
||||
import ToggleSwitch from "../../components/ToggleSwitch";
|
||||
import EmailAddressInput from "../../components/email/EmailAddressInput";
|
||||
import DismissibleAlert from "../../components/DismissibleAlert";
|
||||
import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid";
|
||||
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
|
||||
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
|
||||
import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView";
|
||||
@@ -134,8 +136,8 @@ export default function RecipientDataPage({ settings, campaignId }: { settings:
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && <div className="alert danger">{error}</div>}
|
||||
{localError && <div className="alert danger">{localError}</div>}
|
||||
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
||||
{localError && <DismissibleAlert tone="danger" resetKey={localError}>{localError}</DismissibleAlert>}
|
||||
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} reload={reload} message="Create an editable copy before changing sender or recipient profiles." />}
|
||||
|
||||
<LoadingFrame loading={loading || !draft} label="Loading campaign draft…">
|
||||
@@ -224,51 +226,17 @@ export default function RecipientDataPage({ settings, campaignId }: { settings:
|
||||
>
|
||||
{inlineEntries.length === 0 && !source.type && <p className="muted">No recipient profiles are stored in the current version yet.</p>}
|
||||
{inlineEntries.length === 0 && Boolean(source.type) && (
|
||||
<div className="alert info">This campaign references an external recipient source. A parsed preview table will be added when file/source preview support is implemented.</div>
|
||||
<DismissibleAlert tone="info">This campaign references an external recipient source. A parsed preview table will be added when file/source preview support is implemented.</DismissibleAlert>
|
||||
)}
|
||||
{inlineEntries.length > 0 && (
|
||||
<div className="app-table-wrap recipient-table-wrap">
|
||||
<table className="app-table recipient-table recipient-address-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
{entryAddressColumns.map((column) => <th key={column.key}>{column.label}</th>)}
|
||||
<th>Active</th>
|
||||
<th aria-label="Actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{inlineEntries.slice(0, 100).map((entry, index) => (
|
||||
<tr key={String(entry.id || index)}>
|
||||
<td className="mono-small">{index + 1}</td>
|
||||
{entryAddressColumns.map((column) => (
|
||||
<td key={column.key}>
|
||||
<EmailAddressInput
|
||||
value={getEntryAddresses(entry, column.key)}
|
||||
suggestions={addressSuggestions}
|
||||
allowMultiple={column.allowMultiple}
|
||||
compact
|
||||
disabled={locked}
|
||||
addLabel={column.addLabel}
|
||||
emptyText={column.emptyText}
|
||||
onChange={(addresses) => updateEntryAddressList(index, column.key, addresses)}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
<td>
|
||||
<ToggleSwitch
|
||||
label="Active"
|
||||
checked={entry.active !== false}
|
||||
disabled={locked}
|
||||
onChange={(checked) => updateEntry(index, (current) => ({ ...current, active: checked }))}
|
||||
/>
|
||||
</td>
|
||||
<td className="table-action-cell"><Button variant="danger" onClick={() => removeEntry(index)} disabled={locked}>Remove</Button></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<DataGrid
|
||||
id={`campaign-${campaignId}-recipient-profiles`}
|
||||
rows={inlineEntries.slice(0, 100)}
|
||||
columns={recipientProfileColumns({ locked, entryAddressColumns, addressSuggestions, updateEntryAddressList, updateEntry, removeEntry })}
|
||||
getRowKey={(entry, index) => String(entry.id || index)}
|
||||
emptyText="No recipient profiles are stored in the current version yet."
|
||||
className="recipient-table-wrap recipient-address-table"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -281,6 +249,43 @@ export default function RecipientDataPage({ settings, campaignId }: { settings:
|
||||
);
|
||||
}
|
||||
|
||||
type RecipientProfileColumnContext = {
|
||||
locked: boolean;
|
||||
entryAddressColumns: EntryAddressColumn[];
|
||||
addressSuggestions: MailboxAddress[];
|
||||
updateEntryAddressList: (index: number, key: EntryAddressColumn["key"], addresses: MailboxAddress[]) => void;
|
||||
updateEntry: (index: number, updater: (entry: Record<string, unknown>) => Record<string, unknown>) => void;
|
||||
removeEntry: (index: number) => void;
|
||||
};
|
||||
|
||||
function recipientProfileColumns({ locked, entryAddressColumns, addressSuggestions, updateEntryAddressList, updateEntry, removeEntry }: RecipientProfileColumnContext): DataGridColumn<Record<string, unknown>>[] {
|
||||
return [
|
||||
{ id: "number", header: "#", width: 70, sortable: true, sticky: "start", render: (_entry, index) => <span className="mono-small">{index + 1}</span>, value: (_entry, index) => index + 1 },
|
||||
...entryAddressColumns.map((column): DataGridColumn<Record<string, unknown>> => ({
|
||||
id: column.key,
|
||||
header: column.label,
|
||||
width: column.key === "to" ? "minmax(260px, 1.2fr)" : 250,
|
||||
resizable: true,
|
||||
filterable: true,
|
||||
render: (entry, index) => (
|
||||
<EmailAddressInput
|
||||
value={getEntryAddresses(entry, column.key)}
|
||||
suggestions={addressSuggestions}
|
||||
allowMultiple={column.allowMultiple}
|
||||
compact
|
||||
disabled={locked}
|
||||
addLabel={column.addLabel}
|
||||
emptyText={column.emptyText}
|
||||
onChange={(addresses) => updateEntryAddressList(index, column.key, addresses)}
|
||||
/>
|
||||
),
|
||||
value: (entry) => getEntryAddresses(entry, column.key).map((address) => `${address.name ?? ""} ${address.email ?? ""}`).join(", ")
|
||||
})),
|
||||
{ id: "active", header: "Active", width: 130, sortable: true, filterable: true, render: (entry, index) => <ToggleSwitch label="Active" checked={entry.active !== false} disabled={locked} onChange={(checked) => updateEntry(index, (current) => ({ ...current, active: checked }))} />, value: (entry) => entry.active !== false ? "active" : "inactive" },
|
||||
{ id: "actions", header: "Actions", width: 120, sticky: "end", render: (_entry, index) => <Button variant="danger" onClick={() => removeEntry(index)} disabled={locked}>Remove</Button> }
|
||||
];
|
||||
}
|
||||
|
||||
function getEntryAddresses(entry: Record<string, unknown>, key: EntryAddressColumn["key"]): MailboxAddress[] {
|
||||
if (key === "to") {
|
||||
const explicit = addressesFromValue(entry.to);
|
||||
|
||||
Reference in New Issue
Block a user