refactor(campaign-ui): consolidate recipient editing
This commit is contained in:
@@ -7,7 +7,6 @@ import CampaignOverviewPage from "./CampaignOverviewPage";
|
||||
import CampaignFieldsPage from "./CampaignFieldsPage";
|
||||
import GlobalSettingsPage from "./GlobalSettingsPage";
|
||||
import RecipientDataPage from "./RecipientDataPage";
|
||||
import RecipientDetailsPage from "./RecipientDetailsPage";
|
||||
import TemplateDataPage from "./TemplateDataPage";
|
||||
import AttachmentsDataPage from "./AttachmentsDataPage";
|
||||
import MailSettingsPage from "./MailSettingsPage";
|
||||
@@ -26,7 +25,7 @@ const sectionPaths: Record<CampaignWorkspaceSection, string> = {
|
||||
policies: "policies",
|
||||
fields: "fields",
|
||||
recipients: "recipients",
|
||||
"recipient-data": "recipient-data",
|
||||
"recipient-data": "recipients",
|
||||
template: "template",
|
||||
files: "files",
|
||||
"mail-settings": "mail-settings",
|
||||
@@ -85,7 +84,7 @@ function CampaignWorkspaceInner({ settings, auth }: { settings: ApiSettings; aut
|
||||
<Route path="data" element={<Navigate to="../recipients" replace />} />
|
||||
<Route path="fields" element={<CampaignFieldsPage settings={settings} campaignId={campaignId || ""} />} />
|
||||
<Route path="recipients" element={<RecipientDataPage settings={settings} campaignId={campaignId || ""} />} />
|
||||
<Route path="recipient-data" element={<RecipientDetailsPage settings={settings} campaignId={campaignId || ""} />} />
|
||||
<Route path="recipient-data" element={<Navigate to="../recipients" replace />} />
|
||||
<Route path="template" element={<TemplateDataPage settings={settings} campaignId={campaignId || ""} />} />
|
||||
<Route path="files" element={<AttachmentsDataPage settings={settings} campaignId={campaignId || ""} />} />
|
||||
<Route path="attachments" element={<Navigate to="../files" replace />} />
|
||||
@@ -127,7 +126,7 @@ function sectionFromPath(pathname: string): CampaignWorkspaceSection {
|
||||
if (section === "policies" || section === "policy") return "policies";
|
||||
if (section === "fields") return "fields";
|
||||
if (section === "recipients") return "recipients";
|
||||
if (section === "recipient-data" || section === "recipient-details") return "recipient-data";
|
||||
if (section === "recipient-data" || section === "recipient-details") return "recipients";
|
||||
if (section === "template") return "template";
|
||||
if (section === "files" || section === "attachments") return "files";
|
||||
if (section === "mail-settings" || section === "server-settings" || section === "mail") return "mail-settings";
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,240 +0,0 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { usePlatformModuleInstalled } from "@govoplan/core-webui";
|
||||
import type { ApiSettings } from "../../types";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
import { Card } from "@govoplan/core-webui";
|
||||
import CampaignDraftPageScaffold from "./components/CampaignDraftPageScaffold";
|
||||
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
|
||||
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
|
||||
import { asArray, asRecord, isAuditLockedVersion } from "./utils/campaignView";
|
||||
import FieldValueInput from "./components/FieldValueInput";
|
||||
import AttachmentRulesOverlay from "./components/AttachmentRulesOverlay";
|
||||
import { getDraftFields } from "./utils/fieldDefinitions";
|
||||
import { getIndividualAttachmentBasePaths, normalizeAttachmentBasePaths, normalizeAttachmentRules, normalizeAttachmentZipCollection, type AttachmentRule, type AttachmentZipCollection } from "./utils/attachments";
|
||||
import { materializeRecipientImportWithAttachmentDefaults, type RecipientImportMode, type RecipientImportPreview } from "./utils/bulkImport";
|
||||
import { buildTemplatePreviewContext } from "./utils/templatePlaceholders";
|
||||
import { RecipientImportDialog } from "./RecipientDataPage";
|
||||
import { addressesFromValue } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert, i18nMessage } from "@govoplan/core-webui";
|
||||
import DataGrid, { type DataGridColumn } from "../../components/table/DataGrid";
|
||||
|
||||
|
||||
export default function RecipientDetailsPage({ settings, campaignId }: {settings: ApiSettings;campaignId: string;}) {
|
||||
const filesModuleInstalled = usePlatformModuleInstalled("files");
|
||||
const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId);
|
||||
const [importOpen, setImportOpen] = useState(false);
|
||||
const [recipientDataPage, setRecipientDataPage] = useState(1);
|
||||
const [recipientDataPageSize, setRecipientDataPageSize] = useState(10);
|
||||
|
||||
const version = data.currentVersion;
|
||||
const locked = isAuditLockedVersion(version, data.campaign?.current_version_id);
|
||||
const { draft, setDraft, displayDraft, dirty, saveState, localError, patch, markDirty, saveDraft } = useCampaignDraftEditor({
|
||||
settings,
|
||||
campaignId,
|
||||
version,
|
||||
locked,
|
||||
reload,
|
||||
setError,
|
||||
currentStep: "recipient-data",
|
||||
unsavedTitle: "i18n:govoplan-campaign.unsaved_recipient_data_changes.c810507e",
|
||||
unsavedMessage: "i18n:govoplan-campaign.recipient_field_values_or_attachments_have_unsav.49ab1870"
|
||||
});
|
||||
const entries = asRecord(displayDraft.entries);
|
||||
const inlineEntries = asArray(entries.inline).map(asRecord);
|
||||
const source = asRecord(entries.source);
|
||||
const fieldDefinitions = useMemo(() => getDraftFields(displayDraft), [displayDraft]);
|
||||
const attachmentSection = asRecord(displayDraft.attachments);
|
||||
const attachmentBasePaths = useMemo(() => normalizeAttachmentBasePaths(attachmentSection.base_paths, attachmentSection), [attachmentSection]);
|
||||
const individualAttachmentBasePaths = useMemo(() => getIndividualAttachmentBasePaths(attachmentBasePaths), [attachmentBasePaths]);
|
||||
const importBasePaths = useMemo(() => normalizeAttachmentBasePaths(attachmentSection.base_paths, attachmentSection, true), [attachmentSection]);
|
||||
const defaultImportBasePath = useMemo(() => importBasePaths.find((basePath) => basePath.allow_individual) ?? importBasePaths[0] ?? null, [importBasePaths]);
|
||||
const zipConfig = useMemo(() => normalizeAttachmentZipCollection(attachmentSection.zip), [attachmentSection.zip]);
|
||||
|
||||
|
||||
function replaceInlineEntries(nextEntries: Record<string, unknown>[]) {
|
||||
patch(["entries", "inline"], nextEntries);
|
||||
}
|
||||
|
||||
function updateEntry(index: number, updater: (entry: Record<string, unknown>) => Record<string, unknown>) {
|
||||
const nextEntries = inlineEntries.map((entry, currentIndex) => currentIndex === index ? updater(entry) : entry);
|
||||
replaceInlineEntries(nextEntries);
|
||||
}
|
||||
|
||||
function updateEntryField(index: number, field: string, value: unknown) {
|
||||
updateEntry(index, (entry) => ({
|
||||
...entry,
|
||||
fields: {
|
||||
...asRecord(entry.fields),
|
||||
[field]: value
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
function updateEntryAttachments(index: number, attachments: AttachmentRule[]) {
|
||||
updateEntry(index, (entry) => ({ ...entry, attachments }));
|
||||
}
|
||||
|
||||
function applyRecipientImport(preview: RecipientImportPreview, mode: RecipientImportMode) {
|
||||
if (locked || !draft) return;
|
||||
setDraft(materializeRecipientImportWithAttachmentDefaults(draft, preview, { mode }));
|
||||
markDirty();
|
||||
setImportOpen(false);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<CampaignDraftPageScaffold
|
||||
settings={settings}
|
||||
campaignId={campaignId}
|
||||
title="i18n:govoplan-campaign.recipient_data.c2baaf10"
|
||||
loading={loading}
|
||||
version={version}
|
||||
versions={data.versions}
|
||||
saveState={saveState}
|
||||
onReload={reload}
|
||||
onSave={() => saveDraft("manual")}
|
||||
dirty={dirty}
|
||||
locked={locked}
|
||||
draft={draft}
|
||||
error={error}
|
||||
localError={localError}
|
||||
currentVersionId={data.campaign?.current_version_id}>
|
||||
|
||||
<Card title="i18n:govoplan-campaign.recipient_field_values_and_attachments.96e670ca" actions={<Button disabled={locked} onClick={() => setImportOpen(true)}>i18n:govoplan-campaign.import.d6fbc9d2</Button>}>
|
||||
{inlineEntries.length === 0 && !source.type && <p className="muted">i18n:govoplan-campaign.no_recipient_profiles_are_stored_in_the_current_.e9f9a224</p>}
|
||||
{inlineEntries.length === 0 && Boolean(source.type) &&
|
||||
<DismissibleAlert tone="info">i18n:govoplan-campaign.this_campaign_references_an_external_recipient_s.0d1ae252</DismissibleAlert>
|
||||
}
|
||||
{inlineEntries.length > 0 &&
|
||||
<div className="admin-table-surface recipient-data-table-surface">
|
||||
<DataGrid
|
||||
id={`campaign-${campaignId}-recipient-data`}
|
||||
rows={inlineEntries}
|
||||
columns={recipientDataColumns({ settings, campaignId, draft: displayDraft, locked, filesModuleInstalled, fieldDefinitions, individualAttachmentBasePaths, zipConfig, updateEntryAttachments, updateEntryField })}
|
||||
getRowKey={(entry, index) => String(entry.id || index)}
|
||||
emptyText="i18n:govoplan-campaign.no_recipient_data_found.a12be7d0"
|
||||
className="recipient-table-wrap recipient-data-table-wrap recipient-data-table"
|
||||
pagination={{
|
||||
page: recipientDataPage,
|
||||
pageSize: recipientDataPageSize,
|
||||
pageSizeOptions: [10, 25, 50, 100, 250],
|
||||
onPageChange: setRecipientDataPage,
|
||||
onPageSizeChange: (pageSize) => {
|
||||
setRecipientDataPageSize(pageSize);
|
||||
setRecipientDataPage(1);
|
||||
}
|
||||
}} />
|
||||
|
||||
</div>
|
||||
}
|
||||
</Card>
|
||||
</CampaignDraftPageScaffold>
|
||||
|
||||
{importOpen &&
|
||||
<RecipientImportDialog
|
||||
settings={settings}
|
||||
campaignId={campaignId}
|
||||
existingEntries={inlineEntries}
|
||||
existingFields={fieldDefinitions}
|
||||
defaultAttachmentBasePath={defaultImportBasePath}
|
||||
onCancel={() => setImportOpen(false)}
|
||||
onImport={applyRecipientImport} />
|
||||
|
||||
}
|
||||
</>);
|
||||
|
||||
}
|
||||
|
||||
type RecipientDataColumnContext = {
|
||||
settings: ApiSettings;
|
||||
campaignId: string;
|
||||
draft: Record<string, unknown>;
|
||||
locked: boolean;
|
||||
filesModuleInstalled: boolean;
|
||||
fieldDefinitions: ReturnType<typeof getDraftFields>;
|
||||
individualAttachmentBasePaths: ReturnType<typeof getIndividualAttachmentBasePaths>;
|
||||
zipConfig: AttachmentZipCollection;
|
||||
updateEntryAttachments: (index: number, attachments: AttachmentRule[]) => void;
|
||||
updateEntryField: (index: number, field: string, value: unknown) => void;
|
||||
};
|
||||
|
||||
function recipientDataColumns({ settings, campaignId, draft, locked, filesModuleInstalled, fieldDefinitions, individualAttachmentBasePaths, zipConfig, updateEntryAttachments, updateEntryField }: RecipientDataColumnContext): DataGridColumn<Record<string, unknown>>[] {
|
||||
return [
|
||||
{ id: "number", header: "#", width: 70, sortable: true, filterType: "integer", sticky: "start", render: (_entry, index) => <span className="mono-small recipient-index-cell">{index + 1}</span>, value: (_entry, index) => index + 1 },
|
||||
{
|
||||
id: "recipient",
|
||||
header: "i18n:govoplan-campaign.recipient.90343260",
|
||||
width: 260,
|
||||
resizable: true,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
sticky: "start",
|
||||
render: (entry) =>
|
||||
<Link className="recipient-data-identity" to="../recipients" title="i18n:govoplan-campaign.open_recipient_address_profile.f7daa676">
|
||||
<span className="recipient-data-address">{firstRecipientEmail(entry) || "i18n:govoplan-campaign.no_to_address.683350f9"}</span>
|
||||
{extraRecipientCount(entry) > 0 && <span className="recipient-extra-bubble">+{extraRecipientCount(entry)}</span>}
|
||||
</Link>,
|
||||
|
||||
value: firstRecipientEmail
|
||||
},
|
||||
{
|
||||
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.map((field): DataGridColumn<Record<string, unknown>> => ({
|
||||
id: `field-${field.name}`,
|
||||
header: field.label || field.name,
|
||||
width: 190,
|
||||
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 || field.can_override === false}
|
||||
placeholder={field.can_override === false ? "i18n:govoplan-campaign.uses_global_value.98eae5e0" : undefined}
|
||||
onChange={(value) => updateEntryField(index, field.name, value)} />);
|
||||
|
||||
|
||||
},
|
||||
value: (entry) => String(asRecord(entry.fields)[field.name] ?? "")
|
||||
}))];
|
||||
|
||||
}
|
||||
|
||||
function firstRecipientEmail(entry: Record<string, unknown>): string {
|
||||
return (addressesFromValue(entry.to)[0] ?? addressesFromValue(entry.recipient)[0] ?? addressesFromValue(entry)[0])?.email ?? "";
|
||||
}
|
||||
|
||||
function extraRecipientCount(entry: Record<string, unknown>): number {
|
||||
const count = addressesFromValue(entry.to).length;
|
||||
return Math.max(0, count - 1);
|
||||
}
|
||||
@@ -11,7 +11,6 @@ const campaignSubnav: ModuleSubnavGroup<CampaignWorkspaceSection>[] = [
|
||||
{ id: "fields", label: "i18n:govoplan-campaign.fields.e8b68527" },
|
||||
{ id: "files", label: "i18n:govoplan-campaign.attachments.6771ade6" },
|
||||
{ id: "recipients", label: "i18n:govoplan-campaign.sender_recipients.922c6d24" },
|
||||
{ id: "recipient-data", label: "i18n:govoplan-campaign.recipient_data.c2baaf10" },
|
||||
{ id: "template", label: "i18n:govoplan-campaign.template.3ec1ae06" }]
|
||||
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user