Refactor campaign recipient and review presentation
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
import { Pencil } from "lucide-react";
|
||||
import type { ApiSettings } from "../../../types";
|
||||
import type { CampaignPostboxCatalog } from "../../../api/campaigns";
|
||||
import {
|
||||
Button,
|
||||
DataGridRowActions,
|
||||
ToggleSwitch,
|
||||
i18nMessage,
|
||||
type DataGridColumn
|
||||
} from "@govoplan/core-webui";
|
||||
import AttachmentRulesOverlay from "../components/AttachmentRulesOverlay";
|
||||
import FieldValueInput from "../components/FieldValueInput";
|
||||
import {
|
||||
normalizePostboxTargets
|
||||
} from "../components/PostboxTargetsDialog";
|
||||
import { buildTemplatePreviewContext } from "../utils/templatePlaceholders";
|
||||
import {
|
||||
getIndividualAttachmentBasePaths,
|
||||
normalizeAttachmentRules,
|
||||
type AttachmentRule,
|
||||
type AttachmentZipCollection
|
||||
} from "../utils/attachments";
|
||||
import { getDraftFields } from "../utils/fieldDefinitions";
|
||||
import { asRecord } from "../utils/campaignView";
|
||||
import {
|
||||
getEntryAddresses,
|
||||
hiddenRecipientAddressMatch,
|
||||
recipientAddressFilterValue,
|
||||
recipientAddressSummary
|
||||
} from "./RecipientAddressEditor";
|
||||
|
||||
export type RecipientProfileColumnContext = {
|
||||
settings: ApiSettings;
|
||||
campaignId: string;
|
||||
draft: Record<string, unknown>;
|
||||
locked: boolean;
|
||||
filesModuleInstalled: boolean;
|
||||
postboxModuleInstalled: boolean;
|
||||
postboxCatalog: CampaignPostboxCatalog;
|
||||
entries: Record<string, unknown>[];
|
||||
fieldDefinitions: ReturnType<typeof getDraftFields>;
|
||||
individualAttachmentBasePaths: ReturnType<typeof getIndividualAttachmentBasePaths>;
|
||||
zipConfig: AttachmentZipCollection;
|
||||
addressFilter: string;
|
||||
translateText: (value: string) => string;
|
||||
openAddressEditor: (index: number) => void;
|
||||
openPostboxTargetEditor: (index: number) => void;
|
||||
updateEntry: (index: number, updater: (entry: Record<string, unknown>) => Record<string, unknown>) => void;
|
||||
updateEntryAttachments: (index: number, attachments: AttachmentRule[]) => void;
|
||||
updateEntryField: (index: number, field: string, value: unknown) => void;
|
||||
addRecipient: (afterIndex?: number) => void;
|
||||
moveEntry: (index: number, targetIndex: number) => void;
|
||||
removeEntry: (index: number) => void;
|
||||
};
|
||||
|
||||
export function recipientProfileColumns({ settings, campaignId, draft, locked, filesModuleInstalled, postboxModuleInstalled, postboxCatalog, entries, fieldDefinitions, individualAttachmentBasePaths, zipConfig, addressFilter, translateText, openAddressEditor, openPostboxTargetEditor, updateEntry, updateEntryAttachments, updateEntryField, addRecipient, moveEntry, removeEntry }: RecipientProfileColumnContext): DataGridColumn<Record<string, unknown>>[] {
|
||||
return [
|
||||
{
|
||||
id: "number",
|
||||
header: "#",
|
||||
width: 72,
|
||||
sortable: true,
|
||||
filterType: "integer",
|
||||
sticky: "start",
|
||||
render: (_entry, index) =>
|
||||
<span className="recipient-data-number">
|
||||
{index + 1}
|
||||
</span>,
|
||||
|
||||
value: (_entry, index) => index + 1
|
||||
},
|
||||
{
|
||||
id: "recipients",
|
||||
header: "Recipient(s)",
|
||||
width: "minmax(320px, 1.4fr)",
|
||||
maxWidth: 640,
|
||||
resizable: true,
|
||||
filterable: true,
|
||||
render: (entry, index) => {
|
||||
const summary = recipientAddressSummary(entry, translateText);
|
||||
const hiddenMatch = hiddenRecipientAddressMatch(entry, addressFilter);
|
||||
const content = (
|
||||
<>
|
||||
<span className="recipient-address-editor-main">
|
||||
<span className={`recipient-data-address ${summary.empty ? "is-empty" : ""}`}>{summary.primary}</span>
|
||||
{summary.badges.map((badge) =>
|
||||
<span className="recipient-extra-bubble" key={badge}>{badge}</span>
|
||||
)}
|
||||
{hiddenMatch &&
|
||||
<span className="recipient-hidden-address-match">
|
||||
Matched in {hiddenMatch.label}: {hiddenMatch.address}
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
{!locked && <Pencil aria-hidden="true" />}
|
||||
</>
|
||||
);
|
||||
if (locked) {
|
||||
return <div className="recipient-address-editor-trigger is-readonly">{content}</div>;
|
||||
}
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="recipient-address-editor-trigger"
|
||||
onClick={() => openAddressEditor(index)}>
|
||||
{content}
|
||||
</button>);
|
||||
},
|
||||
value: recipientAddressFilterValue
|
||||
},
|
||||
{ id: "active", header: "i18n:govoplan-campaign.active.a733b809", width: 130, sortable: true, filterable: true, columnType: "from-list", list: { options: [{ value: "active", label: "i18n:govoplan-campaign.active.a733b809" }, { value: "inactive", label: "i18n:govoplan-campaign.inactive.09af574c" }] }, render: (entry, index) => <ToggleSwitch label="i18n:govoplan-campaign.active.a733b809" checked={entry.active !== false} disabled={locked} onChange={(checked) => updateEntry(index, (current) => ({ ...current, active: checked }))} />, value: (entry) => entry.active !== false ? "active" : "inactive" },
|
||||
...(postboxModuleInstalled ? [{
|
||||
id: "delivery",
|
||||
header: "Delivery",
|
||||
width: "minmax(260px, 0.9fr)",
|
||||
maxWidth: 480,
|
||||
resizable: true,
|
||||
filterable: true,
|
||||
render: (entry, index) => {
|
||||
const targets = normalizePostboxTargets(entry.postbox_targets);
|
||||
return (
|
||||
<div className="campaign-recipient-delivery-cell">
|
||||
<select
|
||||
value={typeof entry.channel_policy === "string" ? entry.channel_policy : ""}
|
||||
disabled={locked}
|
||||
aria-label={`Recipient ${index + 1} delivery policy`}
|
||||
onChange={(event) => updateEntry(index, (current) => {
|
||||
const next = { ...current };
|
||||
if (event.target.value) next.channel_policy = event.target.value;
|
||||
else delete next.channel_policy;
|
||||
return next;
|
||||
})}
|
||||
>
|
||||
<option value="">Campaign default</option>
|
||||
<option value="mail">Mail</option>
|
||||
<option value="postbox">Postbox</option>
|
||||
<option value="mail_and_postbox">Mail and Postbox</option>
|
||||
<option value="mail_then_postbox">Mail, then Postbox fallback</option>
|
||||
<option value="postbox_then_mail">Postbox, then Mail fallback</option>
|
||||
</select>
|
||||
<Button
|
||||
disabled={locked || !postboxCatalog.available}
|
||||
onClick={() => openPostboxTargetEditor(index)}
|
||||
>
|
||||
Postboxes ({targets.length})
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
value: (entry) => `${String(entry.channel_policy ?? "default")} ${normalizePostboxTargets(entry.postbox_targets).map((target) => target.label ?? target.postbox_id ?? target.template_id ?? "").join(" ")}`
|
||||
} as DataGridColumn<Record<string, unknown>>] : []),
|
||||
...(individualAttachmentBasePaths.length > 0 ? [{
|
||||
id: "attachments",
|
||||
header: "i18n:govoplan-campaign.attachments.6771ade6",
|
||||
width: 180,
|
||||
filterable: true,
|
||||
render: (entry, index) => {
|
||||
const attachments = normalizeAttachmentRules(entry.attachments);
|
||||
return (
|
||||
<AttachmentRulesOverlay
|
||||
title={i18nMessage("i18n:govoplan-campaign.attachments_for_recipient_value.9a8df82d", { value0: index + 1 })}
|
||||
rules={attachments}
|
||||
settings={settings}
|
||||
campaignId={campaignId}
|
||||
disabled={locked}
|
||||
buttonLabel={`entries: ${attachments.length}`}
|
||||
basePaths={individualAttachmentBasePaths}
|
||||
zipConfig={zipConfig}
|
||||
filesModuleInstalled={filesModuleInstalled}
|
||||
previewContext={buildTemplatePreviewContext(draft, entry)}
|
||||
onChange={(rules) => updateEntryAttachments(index, rules)} />);
|
||||
|
||||
|
||||
},
|
||||
value: (entry) => normalizeAttachmentRules(entry.attachments).map((rule) => `${rule.label ?? ""} ${rule.file_filter ?? ""}`).join(", ")
|
||||
}] : []),
|
||||
...fieldDefinitions.filter((field) => field.can_override !== false).map((field): DataGridColumn<Record<string, unknown>> => ({
|
||||
id: `field-${field.name}`,
|
||||
header: field.label || field.name,
|
||||
width: 190,
|
||||
minWidth: 160,
|
||||
maxWidth: 360,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
filterType: field.type === "integer" ? "integer" : field.type === "double" ? "number" : field.type === "date" ? "date" : "text",
|
||||
render: (entry, index) => {
|
||||
const fields = asRecord(entry.fields);
|
||||
return (
|
||||
<FieldValueInput
|
||||
className="recipient-field-input"
|
||||
fieldType={field.type}
|
||||
value={fields[field.name]}
|
||||
disabled={locked}
|
||||
onChange={(value) => updateEntryField(index, field.name, value)} />);
|
||||
|
||||
|
||||
},
|
||||
value: (entry) => String(asRecord(entry.fields)[field.name] ?? "")
|
||||
})),
|
||||
{
|
||||
id: "actions",
|
||||
header: "i18n:govoplan-campaign.actions.c3cd636a",
|
||||
width: 180,
|
||||
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="i18n:govoplan-campaign.add_recipient_below.52fb47d8"
|
||||
removeLabel="i18n:govoplan-campaign.remove_recipient.d1bc9f53"
|
||||
moveUpLabel="i18n:govoplan-campaign.move_recipient_up.90c6bccc"
|
||||
moveDownLabel="i18n:govoplan-campaign.move_recipient_down.ead5466c" />
|
||||
|
||||
|
||||
}];
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user