DataGrid list type; Review/Send refinment; User lock; Table actions

This commit is contained in:
2026-06-13 19:28:48 +02:00
parent 5937dfe97e
commit c72df498e7
15 changed files with 1318 additions and 218 deletions

View File

@@ -10,7 +10,7 @@ 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 DataGrid, { DataGridRowActions, type DataGridColumn } from "../../components/table/DataGrid";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView";
@@ -20,6 +20,7 @@ import {
collectCampaignAddressSuggestions,
type MailboxAddress
} from "../../utils/emailAddresses";
import { insertAfter, moveArrayItem } from "../../utils/arrayOrder";
const recipientHeaderRows = [
{ key: "to", label: "To", toggleKey: "allow_individual_to", toggleLabel: "Allow individual To", addLabel: "Add recipient", emptyText: "No global recipients configured." },
@@ -79,7 +80,7 @@ export default function RecipientDataPage({ settings, campaignId }: { settings:
patch(["entries", "inline"], nextEntries);
}
function addRecipient() {
function addRecipient(afterIndex = inlineEntries.length - 1) {
const nextIndex = inlineEntries.length + 1;
const newEntry = {
id: uniqueEntryId(inlineEntries, `recipient-${nextIndex}`),
@@ -92,7 +93,7 @@ export default function RecipientDataPage({ settings, campaignId }: { settings:
from: {},
reply_to: []
};
replaceInlineEntries([...inlineEntries, newEntry]);
replaceInlineEntries(insertAfter(inlineEntries, afterIndex, newEntry));
}
function updateEntry(index: number, updater: (entry: Record<string, unknown>) => Record<string, unknown>) {
@@ -122,6 +123,11 @@ export default function RecipientDataPage({ settings, campaignId }: { settings:
replaceInlineEntries(inlineEntries.filter((_, currentIndex) => currentIndex !== index));
}
function moveEntry(index: number, targetIndex: number) {
if (locked || index === targetIndex) return;
replaceInlineEntries(moveArrayItem(inlineEntries, index, targetIndex));
}
return (
<div className="content-pad workspace-data-page">
@@ -222,19 +228,18 @@ export default function RecipientDataPage({ settings, campaignId }: { settings:
<Card
title="Recipient profiles"
actions={[<Button key="import" disabled>Import</Button>, <Button key="add" variant="primary" onClick={addRecipient} disabled={locked}>Add recipient</Button>]}
actions={<Button disabled>Import</Button>}
>
{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) && (
<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 && (
{!source.type && (
<DataGrid
id={`campaign-${campaignId}-recipient-profiles`}
rows={inlineEntries.slice(0, 100)}
columns={recipientProfileColumns({ locked, entryAddressColumns, addressSuggestions, updateEntryAddressList, updateEntry, removeEntry })}
columns={recipientProfileColumns({ locked, entries: inlineEntries, entryAddressColumns, addressSuggestions, updateEntryAddressList, updateEntry, addRecipient, moveEntry, removeEntry })}
getRowKey={(entry, index) => String(entry.id || index)}
emptyText="No recipient profiles are stored in the current version yet."
emptyText={<div className="data-grid-empty-action"><span>No recipient profiles are stored in the current version yet.</span><Button variant="primary" onClick={() => addRecipient(-1)} disabled={locked}>Add recipient</Button></div>}
className="recipient-table-wrap recipient-address-table"
/>
)}
@@ -251,14 +256,17 @@ export default function RecipientDataPage({ settings, campaignId }: { settings:
type RecipientProfileColumnContext = {
locked: boolean;
entries: Record<string, unknown>[];
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;
addRecipient: (afterIndex?: number) => void;
moveEntry: (index: number, targetIndex: number) => void;
removeEntry: (index: number) => void;
};
function recipientProfileColumns({ locked, entryAddressColumns, addressSuggestions, updateEntryAddressList, updateEntry, removeEntry }: RecipientProfileColumnContext): DataGridColumn<Record<string, unknown>>[] {
function recipientProfileColumns({ locked, entries, entryAddressColumns, addressSuggestions, updateEntryAddressList, updateEntry, addRecipient, moveEntry, 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>> => ({
@@ -282,7 +290,25 @@ function recipientProfileColumns({ locked, entryAddressColumns, addressSuggestio
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> }
{
id: "actions",
header: "Actions",
width: 150,
sticky: "end",
render: (_entry, index) => (
<DataGridRowActions
disabled={locked}
onAddBelow={() => addRecipient(index)}
onRemove={() => removeEntry(index)}
onMoveUp={index > 0 ? () => moveEntry(index, index - 1) : undefined}
onMoveDown={index < entries.length - 1 ? () => moveEntry(index, index + 1) : undefined}
addLabel="Add recipient below"
removeLabel="Remove recipient"
moveUpLabel="Move recipient up"
moveDownLabel="Move recipient down"
/>
)
}
];
}