Implement governed hybrid campaign delivery
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { ApiSettings } from "../../types";
|
||||
import { previewCampaignAttachments, type CampaignAttachmentPreviewRule } from "../../api/campaigns";
|
||||
import {
|
||||
listCampaignPrintTemplates,
|
||||
previewCampaignAttachments,
|
||||
type CampaignAttachmentPreviewRule,
|
||||
type CampaignPrintTemplate
|
||||
} from "../../api/campaigns";
|
||||
import { Button } from "@govoplan/core-webui";
|
||||
import { Card } from "@govoplan/core-webui";
|
||||
import { FormField } from "@govoplan/core-webui";
|
||||
import { FieldLabel } from "@govoplan/core-webui";
|
||||
import { PageTitle } from "@govoplan/core-webui";
|
||||
import { LoadingFrame } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert, SegmentedControl, i18nMessage } from "@govoplan/core-webui";
|
||||
import { DismissibleAlert, SegmentedControl, ToggleSwitch, i18nMessage } from "@govoplan/core-webui";
|
||||
import { WysiwygEditor, type WysiwygEditorHandle } from "@govoplan/core-webui/wysiwyg";
|
||||
import LockedVersionNotice from "./components/LockedVersionNotice";
|
||||
import VersionLine from "./components/VersionLine";
|
||||
@@ -35,6 +40,10 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
|
||||
const [attachmentPreviewRules, setAttachmentPreviewRules] = useState<CampaignAttachmentPreviewRule[]>([]);
|
||||
const [attachmentPreviewLoading, setAttachmentPreviewLoading] = useState(false);
|
||||
const [attachmentPreviewError, setAttachmentPreviewError] = useState("");
|
||||
const [printTemplatesAvailable, setPrintTemplatesAvailable] = useState(false);
|
||||
const [printTemplates, setPrintTemplates] = useState<CampaignPrintTemplate[]>([]);
|
||||
const [printTemplatesLoading, setPrintTemplatesLoading] = useState(true);
|
||||
const [printTemplatesError, setPrintTemplatesError] = useState("");
|
||||
const subjectRef = useRef<HTMLInputElement | null>(null);
|
||||
const textRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const htmlRef = useRef<WysiwygEditorHandle | null>(null);
|
||||
@@ -55,6 +64,9 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
|
||||
onLoaded: () => setPreviewIndex(0)
|
||||
});
|
||||
const template = asRecord(displayDraft.template);
|
||||
const delivery = asRecord(displayDraft.delivery);
|
||||
const printConfig = asRecord(delivery.print);
|
||||
const selectedPrintTemplate = printTemplates.find((item) => item.id === getText(printConfig, "template_id")) ?? null;
|
||||
const templateBodyMode = normalizeTemplateBodyMode(getText(template, "body_mode", "both"));
|
||||
const visibleBodyEditor: BodyEditorMode = templateBodyMode === "html" ? "html" : templateBodyMode === "text" ? "text" : activeBodyEditor;
|
||||
const fields = useMemo(() => asArray(displayDraft.fields).map(asRecord), [displayDraft.fields]);
|
||||
@@ -145,6 +157,28 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
|
||||
};
|
||||
}, [campaignId, displayDraft, draft, previewOpen, settings.apiBaseUrl, settings.apiKey, settings.accessToken, version?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setPrintTemplatesLoading(true);
|
||||
setPrintTemplatesError("");
|
||||
void listCampaignPrintTemplates(settings, campaignId)
|
||||
.then((response) => {
|
||||
if (cancelled) return;
|
||||
setPrintTemplatesAvailable(response.available);
|
||||
setPrintTemplates(response.templates);
|
||||
})
|
||||
.catch((reason: unknown) => {
|
||||
if (cancelled) return;
|
||||
setPrintTemplatesAvailable(false);
|
||||
setPrintTemplates([]);
|
||||
setPrintTemplatesError(reason instanceof Error ? reason.message : String(reason));
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setPrintTemplatesLoading(false);
|
||||
});
|
||||
return () => { cancelled = true; };
|
||||
}, [campaignId, settings.apiBaseUrl, settings.apiKey, settings.accessToken]);
|
||||
|
||||
|
||||
function patchTemplateText(target: EditorTarget, value: string) {
|
||||
patch(["template", target], value);
|
||||
@@ -159,6 +193,22 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
|
||||
}
|
||||
}
|
||||
|
||||
function patchPrintConfig(values: Record<string, unknown>) {
|
||||
if (locked) return;
|
||||
patch(["delivery", "print"], { ...printConfig, ...values });
|
||||
}
|
||||
|
||||
function selectPrintTemplate(templateId: string) {
|
||||
const selected = printTemplates.find((item) => item.id === templateId) ?? null;
|
||||
const profile = selected?.revision?.output_profiles[0] ?? null;
|
||||
patchPrintConfig({
|
||||
template_id: selected?.id ?? null,
|
||||
template_revision: selected?.revision?.revision ?? selected?.current_revision ?? null,
|
||||
output_format: profile?.output_format ?? getText(printConfig, "output_format", "html"),
|
||||
profile_id: profile?.id ?? null
|
||||
});
|
||||
}
|
||||
|
||||
function insertPlaceholder(namespace: TemplateNamespace, name: string) {
|
||||
if (locked) return;
|
||||
const target = activeEditor === "subject" ? "subject" : visibleBodyEditor;
|
||||
@@ -328,6 +378,64 @@ export default function TemplateDataPage({ settings, campaignId }: {settings: Ap
|
||||
</Card>
|
||||
|
||||
<div className="template-side-stack">
|
||||
<Card title="Printable output">
|
||||
<LoadingFrame loading={printTemplatesLoading} label="Loading printable templates">
|
||||
<div className="form-grid">
|
||||
{printTemplatesError && <DismissibleAlert tone="danger" compact resetKey={printTemplatesError}>{printTemplatesError}</DismissibleAlert>}
|
||||
{!printTemplatesError && !printTemplatesAvailable && (
|
||||
<DismissibleAlert tone="info" compact dismissible={false}>
|
||||
Printable delivery becomes available when the Templates module is enabled. Mail-only Campaigns remain unaffected.
|
||||
</DismissibleAlert>
|
||||
)}
|
||||
{printTemplatesAvailable && (
|
||||
<>
|
||||
<FormField label="Print template" help="Used for recipients whose frozen route is postal or internal mail, including safe fallbacks.">
|
||||
<select
|
||||
value={getText(printConfig, "template_id")}
|
||||
disabled={locked}
|
||||
onChange={(event) => selectPrintTemplate(event.target.value)}
|
||||
>
|
||||
<option value="">Select a published template</option>
|
||||
{printTemplates.map((item) => (
|
||||
<option key={item.id} value={item.id} disabled={!item.published_revision_id}>
|
||||
{item.name} · {item.template_type} · r{item.revision?.revision ?? item.current_revision}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
{selectedPrintTemplate && (
|
||||
<p className="muted small-note">
|
||||
{selectedPrintTemplate.description || `${selectedPrintTemplate.template_type} template`}
|
||||
{selectedPrintTemplate.revision?.required_fields.length
|
||||
? ` · Required fields: ${selectedPrintTemplate.revision.required_fields.map((field) => field.label || field.path).join(", ")}`
|
||||
: " · No additional fields required"}
|
||||
</p>
|
||||
)}
|
||||
<FormField label="Output format">
|
||||
<SegmentedControl
|
||||
ariaLabel="Printable output format"
|
||||
value={getText(printConfig, "output_format", "html") as "html" | "text"}
|
||||
disabled={locked || !selectedPrintTemplate}
|
||||
size="content"
|
||||
width="inline"
|
||||
onChange={(outputFormat) => patchPrintConfig({ output_format: outputFormat })}
|
||||
options={[
|
||||
{ id: "html", label: "HTML" },
|
||||
{ id: "text", label: "Text" }
|
||||
]}
|
||||
/>
|
||||
</FormField>
|
||||
<ToggleSwitch
|
||||
label="Store generated output in Files"
|
||||
checked={getBool(printConfig, "persist_to_files", true)}
|
||||
disabled={locked || !selectedPrintTemplate}
|
||||
onChange={(checked) => patchPrintConfig({ persist_to_files: checked })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</LoadingFrame>
|
||||
</Card>
|
||||
<Card title="i18n:govoplan-campaign.fields.e8b68527">
|
||||
{invalidNamespacePlaceholders.length > 0 &&
|
||||
<DismissibleAlert tone="warning" resetKey={invalidNamespacePlaceholders.map((field) => field.namespace || field.raw).join(",")}>i18n:govoplan-campaign.undefined_placeholder_namespace_detected.2ef5c282 {invalidNamespacePlaceholders.map((field) => field.namespace || field.raw).join(", ")}.</DismissibleAlert>
|
||||
|
||||
Reference in New Issue
Block a user