Integrate campaign delivery with hierarchical mail profiles
This commit is contained in:
@@ -232,27 +232,23 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
|
||||
replaceInlineEntries(nextEntries);
|
||||
}
|
||||
|
||||
function updateEntryAddressList(index: number, key: EntryAddressColumn["key"], addresses: MailboxAddress[]) {
|
||||
updateEntry(index, (entry) => entryWithAddressList(entry, key, addresses));
|
||||
}
|
||||
|
||||
function updateEntryAddressGroups(index: number, groups: Array<{key: AddressFieldKey;addresses: MailboxAddress[];}>) {
|
||||
function saveEntryAddresses(
|
||||
index: number,
|
||||
values: HeaderAddressValues,
|
||||
merges: EntryAddressMergeValues
|
||||
) {
|
||||
updateEntry(index, (entry) => {
|
||||
let nextEntry = entry;
|
||||
for (const group of groups) {
|
||||
const currentAddresses = getEntryAddresses(nextEntry, group.key);
|
||||
nextEntry = entryWithAddressList(nextEntry, group.key, dedupeAddresses([...currentAddresses, ...group.addresses]));
|
||||
for (const column of recipientAddressOverlayColumns) {
|
||||
if (!(column.key in values)) continue;
|
||||
nextEntry = entryWithAddressList(nextEntry, column.key, values[column.key] ?? []);
|
||||
if (!column.mergeKey || !(column.mergeKey in merges)) continue;
|
||||
nextEntry = { ...nextEntry, [column.mergeKey]: Boolean(merges[column.mergeKey]) };
|
||||
delete nextEntry[column.mergeKey.replace("merge_", "combine_")];
|
||||
}
|
||||
return nextEntry;
|
||||
});
|
||||
}
|
||||
|
||||
function updateEntryMerge(index: number, mergeKey: NonNullable<EntryAddressColumn["mergeKey"]>, checked: boolean) {
|
||||
updateEntry(index, (entry) => {
|
||||
const next = { ...entry, [mergeKey]: checked };
|
||||
delete next[mergeKey.replace("merge_", "combine_")];
|
||||
return next;
|
||||
});
|
||||
setRecipientAddressEditorIndex(null);
|
||||
}
|
||||
|
||||
function updateEntryField(index: number, field: string, value: unknown) {
|
||||
@@ -305,8 +301,13 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
|
||||
setAddressSourceImportOpen(false);
|
||||
}
|
||||
|
||||
function updateHeaderAddressList(key: AddressFieldKey, addresses: MailboxAddress[]) {
|
||||
patch(["recipients", key], key === "from" ? addresses.slice(0, 1) : addresses);
|
||||
function saveHeaderAddresses(values: HeaderAddressValues) {
|
||||
const nextRecipients = { ...recipientsSection };
|
||||
for (const [key, addresses] of Object.entries(values) as Array<[AddressFieldKey, MailboxAddress[]]>) {
|
||||
nextRecipients[key] = key === "from" ? addresses.slice(0, 1) : addresses;
|
||||
}
|
||||
patch(["recipients"], nextRecipients);
|
||||
setHeaderAddressEditor(null);
|
||||
}
|
||||
|
||||
async function copyHeaderAddresses(columns: EntryAddressColumn[]) {
|
||||
@@ -508,9 +509,7 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
|
||||
locked={locked}
|
||||
recipientsSection={recipientsSection}
|
||||
entryDefaults={entryDefaults}
|
||||
updateEntryAddressList={updateEntryAddressList}
|
||||
updateEntryAddressGroups={updateEntryAddressGroups}
|
||||
updateEntryMerge={updateEntryMerge}
|
||||
onSave={(values, merges) => saveEntryAddresses(recipientAddressEditorIndex, values, merges)}
|
||||
onClose={() => setRecipientAddressEditorIndex(null)} />
|
||||
|
||||
}
|
||||
@@ -520,7 +519,7 @@ export default function RecipientDataPage({ settings, campaignId }: {settings: A
|
||||
columns={headerAddressEditor.columns}
|
||||
values={headerAddressValues(headerAddressEditor.columns, recipientsSection)}
|
||||
locked={locked}
|
||||
onAddressesChange={updateHeaderAddressList}
|
||||
onSave={saveHeaderAddresses}
|
||||
onClose={() => setHeaderAddressEditor(null)} />
|
||||
|
||||
}
|
||||
@@ -534,13 +533,12 @@ type RecipientAddressEditorDialogProps = {
|
||||
locked: boolean;
|
||||
recipientsSection: Record<string, unknown>;
|
||||
entryDefaults: Record<string, unknown>;
|
||||
updateEntryAddressList: (index: number, key: EntryAddressColumn["key"], addresses: MailboxAddress[]) => void;
|
||||
updateEntryAddressGroups: (index: number, groups: Array<{key: AddressFieldKey;addresses: MailboxAddress[];}>) => void;
|
||||
updateEntryMerge: (index: number, mergeKey: NonNullable<EntryAddressColumn["mergeKey"]>, checked: boolean) => void;
|
||||
onSave: (values: HeaderAddressValues, merges: EntryAddressMergeValues) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type HeaderAddressValues = Partial<Record<AddressFieldKey, MailboxAddress[]>>;
|
||||
type EntryAddressMergeValues = Partial<Record<NonNullable<EntryAddressColumn["mergeKey"]>, boolean>>;
|
||||
|
||||
type AddressHeaderControlProps = {
|
||||
columns: EntryAddressColumn[];
|
||||
@@ -596,26 +594,33 @@ type HeaderAddressEditorDialogProps = {
|
||||
columns: EntryAddressColumn[];
|
||||
values: HeaderAddressValues;
|
||||
locked: boolean;
|
||||
onAddressesChange: (key: AddressFieldKey, addresses: MailboxAddress[]) => void;
|
||||
onSave: (values: HeaderAddressValues) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
function HeaderAddressEditorDialog({ title, columns, values, locked, onAddressesChange, onClose }: HeaderAddressEditorDialogProps) {
|
||||
function HeaderAddressEditorDialog({ title, columns, values, locked, onSave, onClose }: HeaderAddressEditorDialogProps) {
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const columnKeys = new Set(columns.map((column) => column.key));
|
||||
const [draftValues, setDraftValues] = useState<HeaderAddressValues>(() => cloneAddressValues(columns, values));
|
||||
const [validationError, setValidationError] = useState("");
|
||||
|
||||
function applyPastedAddresses(targetKey: AddressFieldKey, text: string): boolean {
|
||||
const groups = parsePastedAddressGroups(targetKey, text).filter((group) => columnKeys.has(group.key));
|
||||
if (groups.length === 0) return false;
|
||||
const nextValues: HeaderAddressValues = { ...values };
|
||||
for (const group of groups) {
|
||||
nextValues[group.key] = dedupeAddresses([...(nextValues[group.key] ?? []), ...group.addresses]);
|
||||
}
|
||||
for (const group of groups) {
|
||||
onAddressesChange(group.key, nextValues[group.key] ?? []);
|
||||
}
|
||||
setDraftValues((current) => mergeAddressGroups(current, groups));
|
||||
setValidationError("");
|
||||
return true;
|
||||
}
|
||||
|
||||
function save() {
|
||||
const prepared = prepareAddressValues(columns, draftValues, translateText);
|
||||
if (prepared.error) {
|
||||
setValidationError(prepared.error);
|
||||
return;
|
||||
}
|
||||
onSave(prepared.values);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
@@ -623,17 +628,24 @@ function HeaderAddressEditorDialog({ title, columns, values, locked, onAddresses
|
||||
className="recipient-address-editor-modal"
|
||||
bodyClassName="recipient-address-editor-body"
|
||||
onClose={onClose}
|
||||
footer={<Button onClick={onClose}>i18n:govoplan-campaign.close.bbfa773e</Button>}>
|
||||
footer={<>
|
||||
<Button onClick={onClose}>i18n:govoplan-campaign.cancel.77dfd213</Button>
|
||||
<Button variant="primary" disabled={locked} onClick={save}>i18n:govoplan-campaign.save.efc007a3</Button>
|
||||
</>}>
|
||||
|
||||
<div className="recipient-address-editor">
|
||||
{validationError && <DismissibleAlert tone="danger" dismissible={false}>{validationError}</DismissibleAlert>}
|
||||
{columns.map((column) =>
|
||||
<RecipientAddressCategoryEditor
|
||||
key={column.key}
|
||||
column={column}
|
||||
addresses={values[column.key] ?? []}
|
||||
addresses={draftValues[column.key] ?? []}
|
||||
merge={false}
|
||||
locked={locked}
|
||||
onAddressesChange={(addresses) => onAddressesChange(column.key, addresses)}
|
||||
onAddressesChange={(addresses) => {
|
||||
setDraftValues((current) => ({ ...current, [column.key]: addresses }));
|
||||
setValidationError("");
|
||||
}}
|
||||
onPasteAddresses={applyPastedAddresses} />
|
||||
|
||||
)}
|
||||
@@ -647,21 +659,41 @@ function RecipientAddressEditorDialog({
|
||||
locked,
|
||||
recipientsSection,
|
||||
entryDefaults,
|
||||
updateEntryAddressList,
|
||||
updateEntryAddressGroups,
|
||||
updateEntryMerge,
|
||||
onSave,
|
||||
onClose
|
||||
}: RecipientAddressEditorDialogProps) {
|
||||
const { translateText } = usePlatformLanguage();
|
||||
const availableColumns = recipientAddressOverlayColumns.filter((column) => entryAddressEnabled(recipientsSection, column.key));
|
||||
const availableKeys = new Set(availableColumns.map((column) => column.key));
|
||||
const [draftValues, setDraftValues] = useState<HeaderAddressValues>(() =>
|
||||
Object.fromEntries(availableColumns.map((column) => [column.key, getEntryAddresses(entry, column.key).map(cloneMailboxAddress)]))
|
||||
);
|
||||
const [draftMerges, setDraftMerges] = useState<EntryAddressMergeValues>(() =>
|
||||
Object.fromEntries(
|
||||
availableColumns
|
||||
.filter((column) => column.mergeKey)
|
||||
.map((column) => [column.mergeKey!, getEntryMerge(entry, entryDefaults, column.mergeKey!)])
|
||||
)
|
||||
);
|
||||
const [validationError, setValidationError] = useState("");
|
||||
|
||||
function applyPastedAddresses(targetKey: AddressFieldKey, text: string): boolean {
|
||||
const groups = parsePastedAddressGroups(targetKey, text).filter((group) => availableKeys.has(group.key));
|
||||
if (groups.length === 0) return false;
|
||||
updateEntryAddressGroups(index, groups);
|
||||
setDraftValues((current) => mergeAddressGroups(current, groups));
|
||||
setValidationError("");
|
||||
return true;
|
||||
}
|
||||
|
||||
function save() {
|
||||
const prepared = prepareAddressValues(availableColumns, draftValues, translateText);
|
||||
if (prepared.error) {
|
||||
setValidationError(prepared.error);
|
||||
return;
|
||||
}
|
||||
onSave(prepared.values, draftMerges);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
@@ -669,9 +701,13 @@ function RecipientAddressEditorDialog({
|
||||
className="recipient-address-editor-modal"
|
||||
bodyClassName="recipient-address-editor-body"
|
||||
onClose={onClose}
|
||||
footer={<Button onClick={onClose}>i18n:govoplan-campaign.close.bbfa773e</Button>}>
|
||||
footer={<>
|
||||
<Button onClick={onClose}>i18n:govoplan-campaign.cancel.77dfd213</Button>
|
||||
<Button variant="primary" disabled={locked} onClick={save}>i18n:govoplan-campaign.save.efc007a3</Button>
|
||||
</>}>
|
||||
|
||||
<div className="recipient-address-editor">
|
||||
{validationError && <DismissibleAlert tone="danger" dismissible={false}>{validationError}</DismissibleAlert>}
|
||||
{availableColumns.length === 0 &&
|
||||
<p className="muted">No individual recipient address fields are enabled.</p>
|
||||
}
|
||||
@@ -679,11 +715,16 @@ function RecipientAddressEditorDialog({
|
||||
<RecipientAddressCategoryEditor
|
||||
key={column.key}
|
||||
column={column}
|
||||
addresses={getEntryAddresses(entry, column.key)}
|
||||
merge={column.mergeKey ? getEntryMerge(entry, entryDefaults, column.mergeKey) : false}
|
||||
addresses={draftValues[column.key] ?? []}
|
||||
merge={column.mergeKey ? Boolean(draftMerges[column.mergeKey]) : false}
|
||||
locked={locked}
|
||||
onAddressesChange={(addresses) => updateEntryAddressList(index, column.key, addresses)}
|
||||
onMergeChange={column.mergeKey ? (merge) => updateEntryMerge(index, column.mergeKey!, merge) : undefined}
|
||||
onAddressesChange={(addresses) => {
|
||||
setDraftValues((current) => ({ ...current, [column.key]: addresses }));
|
||||
setValidationError("");
|
||||
}}
|
||||
onMergeChange={column.mergeKey ? (merge) => {
|
||||
setDraftMerges((current) => ({ ...current, [column.mergeKey!]: merge }));
|
||||
} : undefined}
|
||||
onPasteAddresses={applyPastedAddresses} />
|
||||
|
||||
)}
|
||||
@@ -716,7 +757,7 @@ function RecipientAddressCategoryEditor({ column, addresses, merge, locked, onAd
|
||||
|
||||
function commitAddresses(nextAddresses: MailboxAddress[]) {
|
||||
setDraftAddresses(nextAddresses);
|
||||
onAddressesChange(nextAddresses.filter((address) => String(address.email ?? "").trim() || String(address.name ?? "").trim()));
|
||||
onAddressesChange(nextAddresses);
|
||||
}
|
||||
|
||||
function patchAddress(addressIndex: number, patch: Partial<MailboxAddress>) {
|
||||
@@ -731,7 +772,7 @@ function RecipientAddressCategoryEditor({ column, addresses, merge, locked, onAd
|
||||
{ name: "", email: "" },
|
||||
...draftAddresses.slice(insertIndex)];
|
||||
|
||||
setDraftAddresses(nextAddresses);
|
||||
commitAddresses(nextAddresses);
|
||||
}
|
||||
|
||||
function removeAddress(addressIndex: number) {
|
||||
@@ -2147,9 +2188,59 @@ function getAddressColumn(key: AddressFieldKey): EntryAddressColumn {
|
||||
return column;
|
||||
}
|
||||
|
||||
function cloneMailboxAddress(address: MailboxAddress): MailboxAddress {
|
||||
return { name: address.name ?? "", email: address.email ?? "" };
|
||||
}
|
||||
|
||||
function cloneAddressValues(columns: EntryAddressColumn[], values: HeaderAddressValues): HeaderAddressValues {
|
||||
return Object.fromEntries(
|
||||
columns.map((column) => [column.key, (values[column.key] ?? []).map(cloneMailboxAddress)])
|
||||
) as HeaderAddressValues;
|
||||
}
|
||||
|
||||
function mergeAddressGroups(
|
||||
values: HeaderAddressValues,
|
||||
groups: Array<{key: AddressFieldKey;addresses: MailboxAddress[];}>
|
||||
): HeaderAddressValues {
|
||||
const nextValues = cloneAddressValues(recipientAddressOverlayColumns, values);
|
||||
for (const group of groups) {
|
||||
nextValues[group.key] = dedupeAddresses([
|
||||
...(nextValues[group.key] ?? []),
|
||||
...group.addresses.map(cloneMailboxAddress)
|
||||
]);
|
||||
}
|
||||
return nextValues;
|
||||
}
|
||||
|
||||
function prepareAddressValues(
|
||||
columns: EntryAddressColumn[],
|
||||
values: HeaderAddressValues,
|
||||
translateText: (value: string) => string
|
||||
): {values: HeaderAddressValues;error: string;} {
|
||||
const prepared: HeaderAddressValues = {};
|
||||
for (const column of columns) {
|
||||
const addresses: MailboxAddress[] = [];
|
||||
for (const address of values[column.key] ?? []) {
|
||||
const name = String(address.name ?? "").trim();
|
||||
const email = String(address.email ?? "").trim();
|
||||
if (!name && !email) continue;
|
||||
if (!email) {
|
||||
return {
|
||||
values: prepared,
|
||||
error: `${translateText(column.label)}: enter an email address or remove the incomplete row.`
|
||||
};
|
||||
}
|
||||
addresses.push({ name, email });
|
||||
}
|
||||
prepared[column.key] = column.allowMultiple ? dedupeAddresses(addresses) : addresses.slice(0, 1);
|
||||
}
|
||||
return { values: prepared, error: "" };
|
||||
}
|
||||
|
||||
function normalizeEditorAddressRows(column: EntryAddressColumn, addresses: MailboxAddress[]): MailboxAddress[] {
|
||||
if (column.allowMultiple) return addresses;
|
||||
return addresses.length > 0 ? addresses.slice(0, 1) : [{ name: "", email: "" }];
|
||||
const rows = addresses.map(cloneMailboxAddress);
|
||||
if (column.allowMultiple) return rows;
|
||||
return rows.length > 0 ? rows.slice(0, 1) : [{ name: "", email: "" }];
|
||||
}
|
||||
|
||||
function formatMailboxAddress(address: MailboxAddress): string {
|
||||
|
||||
Reference in New Issue
Block a user