chore: sync GovOPlaN module split state

This commit is contained in:
2026-07-10 12:51:18 +02:00
parent 57f6066bf7
commit 52e6ed49c5
66 changed files with 8868 additions and 4072 deletions

View File

@@ -19,11 +19,11 @@ import { importedRowsNeedAttachmentSource, materializeRecipientImport, type Reci
import { buildTemplatePreviewContext } from "./utils/templatePlaceholders";
import { RecipientImportDialog } from "./RecipientDataPage";
import { addressesFromValue } from "@govoplan/core-webui";
import { DismissibleAlert } 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 }) {
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);
@@ -40,8 +40,8 @@ export default function RecipientDetailsPage({ settings, campaignId }: { setting
reload,
setError,
currentStep: "recipient-data",
unsavedTitle: "Unsaved recipient data changes",
unsavedMessage: "Recipient field values or attachments have unsaved changes. Save them before leaving, or discard them and continue."
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);
@@ -95,16 +95,16 @@ export default function RecipientDetailsPage({ settings, campaignId }: { setting
}
const importedDraft = materializeRecipientImport(draft, preview, { mode, attachmentBasePath });
const nextDraft = needsAttachmentSource
? {
...importedDraft,
attachments: {
...asRecord(importedDraft.attachments),
base_paths: nextBasePaths,
base_path: nextBasePaths[0]?.path || "."
}
const nextDraft = needsAttachmentSource ?
{
...importedDraft,
attachments: {
...asRecord(importedDraft.attachments),
base_paths: nextBasePaths,
base_path: nextBasePaths[0]?.path || "."
}
: importedDraft;
} :
importedDraft;
setDraft(nextDraft);
markDirty();
setImportOpen(false);
@@ -115,65 +115,65 @@ export default function RecipientDetailsPage({ settings, campaignId }: { setting
<div className="content-pad workspace-data-page">
<div className="page-heading split workspace-heading">
<div>
<PageTitle loading={loading}>Recipient data</PageTitle>
<PageTitle loading={loading}>i18n:govoplan-campaign.recipient_data.c2baaf10</PageTitle>
<VersionLine version={version} versions={data.versions} status={saveState} />
</div>
<div className="button-row compact-actions">
<Button onClick={reload} disabled={loading}>Reload</Button>
<Button variant="primary" onClick={() => saveDraft("manual")} disabled={!dirty || locked || !draft}>Save</Button>
<Button onClick={reload} disabled={loading}>i18n:govoplan-campaign.reload.cce71553</Button>
<Button variant="primary" onClick={() => saveDraft("manual")} disabled={!dirty || locked || !draft}>i18n:govoplan-campaign.save.efc007a3</Button>
</div>
</div>
{error && <DismissibleAlert tone="danger" resetKey={error} floating>{error}</DismissibleAlert>}
{localError && <DismissibleAlert tone="danger" resetKey={localError} floating>{localError}</DismissibleAlert>}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} currentVersionId={data.campaign?.current_version_id} reload={reload} message="This page is read-only for the selected version." />}
{locked && <LockedVersionNotice settings={settings} campaignId={campaignId} version={version} currentVersionId={data.campaign?.current_version_id} reload={reload} message="i18n:govoplan-campaign.this_page_is_read_only_for_the_selected_version.dacf5743" />}
<LoadingFrame loading={loading || !draft} label="Loading campaign draft">
<LoadingFrame loading={loading || !draft} label="i18n:govoplan-campaign.loading_campaign_draft.1cf47e50">
<>
<Card title="Recipient field values and attachments" actions={<Button disabled={locked} onClick={() => setImportOpen(true)}>Import</Button>}>
{inlineEntries.length === 0 && !source.type && <p className="muted">No recipient profiles are stored in the current version yet. Add recipients first, then maintain their data here.</p>}
{inlineEntries.length === 0 && Boolean(source.type) && (
<DismissibleAlert tone="info">This campaign references an external recipient source. A parsed data preview will be added when file/source preview support is implemented.</DismissibleAlert>
)}
{inlineEntries.length > 0 && (
<div className="admin-table-surface recipient-data-table-surface">
<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="No recipient data found."
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);
}
}}
/>
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>
</>
</LoadingFrame>
{importOpen && (
<RecipientImportDialog
settings={settings}
campaignId={campaignId}
existingEntries={inlineEntries}
existingFields={fieldDefinitions}
defaultAttachmentBasePath={defaultImportBasePath}
onCancel={() => setImportOpen(false)}
onImport={applyRecipientImport}
/>
)}
</div>
);
{importOpen &&
<RecipientImportDialog
settings={settings}
campaignId={campaignId}
existingEntries={inlineEntries}
existingFields={fieldDefinitions}
defaultAttachmentBasePath={defaultImportBasePath}
onCancel={() => setImportOpen(false)}
onImport={applyRecipientImport} />
}
</div>);
}
type RecipientDataColumnContext = {
@@ -191,72 +191,72 @@ type RecipientDataColumnContext = {
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: "Recipient",
width: 260,
resizable: true,
sortable: true,
filterable: true,
sticky: "start",
render: (entry) => (
<Link className="recipient-data-identity" to="../recipients" title="Open recipient address profile">
<span className="recipient-data-address">{firstRecipientEmail(entry) || "No To address"}</span>
{ 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
</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)} />);
},
{
id: "attachments",
header: "Attachments",
width: 180,
filterable: true,
render: (entry, index) => {
const attachments = normalizeAttachmentRules(entry.attachments);
return (
<AttachmentRulesOverlay
title={`Attachments for recipient ${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(", ")
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)} />);
},
...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 ? "Uses global value" : undefined}
onChange={(value) => updateEntryField(index, field.name, value)}
/>
);
},
value: (entry) => String(asRecord(entry.fields)[field.name] ?? "")
}))
];
value: (entry) => String(asRecord(entry.fields)[field.name] ?? "")
}))];
}
function firstRecipientEmail(entry: Record<string, unknown>): string {