Code cleanup and deduplication

This commit is contained in:
2026-06-10 17:26:00 +02:00
parent fcc46b06fe
commit be793fb3e7
27 changed files with 474 additions and 851 deletions

View File

@@ -1,16 +1,14 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { Link } from "react-router-dom";
import type { ApiSettings } from "../../types";
import Button from "../../components/Button";
import Card from "../../components/Card";
import FormField from "../../components/FormField";
import PageTitle from "../../components/PageTitle";
import LoadingFrame from "../../components/LoadingFrame";
import { autosaveCampaignVersion } from "../../api/campaigns";
import { useCampaignWorkspaceData } from "./hooks/useCampaignWorkspaceData";
import { asArray, asRecord, formatDateTime, isAuditLockedVersion, versionLockReason } from "./utils/campaignView";
import { cloneJson, ensureCampaignDraft, getBool, getText, updateNested } from "./utils/draftEditor";
import { useRegisterCampaignUnsavedChanges } from "./context/UnsavedChangesContext";
import { useCampaignDraftEditor } from "./hooks/useCampaignDraftEditor";
import { asArray, asRecord, isAuditLockedVersion, versionLockReason } from "./utils/campaignView";
import { cloneJson, getBool, getText } from "./utils/draftEditor";
type BodyMode = "text" | "html";
type EditorTarget = "subject" | "text" | "html";
@@ -30,10 +28,6 @@ type UndefinedPlaceholder = TemplatePlaceholder & {
export default function TemplateDataPage({ settings, campaignId }: { settings: ApiSettings; campaignId: string }) {
const { data, loading, error, reload, setError } = useCampaignWorkspaceData(settings, campaignId);
const [draft, setDraft] = useState<Record<string, unknown> | null>(null);
const [dirty, setDirty] = useState(false);
const [saveState, setSaveState] = useState("Loaded");
const [localError, setLocalError] = useState("");
const [bodyMode, setBodyMode] = useState<BodyMode>("text");
const [activeEditor, setActiveEditor] = useState<EditorTarget>("text");
const [previewOpen, setPreviewOpen] = useState(false);
@@ -45,7 +39,19 @@ export default function TemplateDataPage({ settings, campaignId }: { settings: A
const version = data.currentVersion;
const locked = isAuditLockedVersion(version);
const displayDraft = draft ?? ensureCampaignDraft(null);
const { draft, setDraft, displayDraft, dirty, saveState, localError, patch, markDirty, saveDraft } = useCampaignDraftEditor({
settings,
campaignId,
version,
locked,
reload,
setError,
currentStep: "template",
unsavedTitle: "Unsaved template changes",
unsavedMessage: "The template has unsaved changes. Save them before leaving, or discard them and continue.",
loadedLabel: (loadedVersion) => loadedVersion.autosaved_at ? `Loaded autosave ${new Date(loadedVersion.autosaved_at).toLocaleString()}` : "Loaded",
onLoaded: () => setPreviewIndex(0)
});
const template = asRecord(displayDraft.template);
const fields = useMemo(() => asArray(displayDraft.fields).map(asRecord), [displayDraft.fields]);
const localFieldNames = useMemo(() => fields.map((field) => String(field.name || field.id || "")).filter(Boolean), [fields]);
@@ -70,24 +76,11 @@ export default function TemplateDataPage({ settings, campaignId }: { settings: A
const previewText = renderPreviewText(getText(template, "text"), previewContext, ignoreEmptyFields);
const previewHtml = renderPreviewText(getText(template, "html"), previewContext, ignoreEmptyFields);
useEffect(() => {
if (!version) return;
setDraft(ensureCampaignDraft(version));
setDirty(false);
setPreviewIndex(0);
setSaveState(version.autosaved_at ? `Loaded autosave ${formatDateTime(version.autosaved_at)}` : "Loaded");
}, [version]);
useEffect(() => {
if (previewIndex >= previewEntries.length) setPreviewIndex(Math.max(0, previewEntries.length - 1));
}, [previewIndex, previewEntries.length]);
function patch(path: string[], value: unknown) {
if (locked) return;
setDraft((current) => updateNested(current ?? {}, path, value));
setDirty(true);
setLocalError("");
}
function patchTemplateText(target: EditorTarget, value: string) {
patch(["template", target], value);
@@ -140,42 +133,10 @@ export default function TemplateDataPage({ settings, campaignId }: { settings: A
next.template = nextTemplate;
return next;
});
setDirty(true);
setLocalError("");
markDirty();
setUndefinedDialog(null);
}
async function saveTemplate(mode: "auto" | "manual" = "manual"): Promise<boolean> {
if (!draft || !version || locked) return false;
setSaveState("Saving…");
setError("");
setLocalError("");
try {
const saved = await autosaveCampaignVersion(settings, campaignId, version.id, {
campaign_json: draft,
current_flow: "manual",
current_step: "template",
workflow_state: "editing",
is_complete: false
});
setDraft(ensureCampaignDraft(saved));
setDirty(false);
setSaveState(`Saved ${formatDateTime(saved.autosaved_at ?? saved.updated_at)}`);
await reload();
return true;
} catch (err) {
setLocalError(err instanceof Error ? err.message : String(err));
setSaveState("Save failed");
return false;
}
}
useRegisterCampaignUnsavedChanges(dirty && !locked ? {
title: "Unsaved template changes",
message: "The template has unsaved changes. Save them before leaving, or discard them and continue.",
onSave: () => saveTemplate("manual"),
onDiscard: () => setDirty(false)
} : null);
return (
<div className="content-pad workspace-data-page">
@@ -187,7 +148,7 @@ export default function TemplateDataPage({ settings, campaignId }: { settings: A
<div className="button-row compact-actions">
<Button disabled>Manage templates</Button>
<Button onClick={reload} disabled={loading}>Reload</Button>
<Button variant="primary" onClick={() => saveTemplate("manual")} disabled={!dirty || locked || !draft}>Save</Button>
<Button variant="primary" onClick={() => saveDraft("manual")} disabled={!dirty || locked || !draft}>Save</Button>
</div>
</div>
@@ -277,7 +238,7 @@ export default function TemplateDataPage({ settings, campaignId }: { settings: A
</div>
<div className="button-row page-bottom-actions">
<Button variant="primary" onClick={() => saveTemplate("manual")} disabled={!dirty || locked || !draft}>Save</Button>
<Button variant="primary" onClick={() => saveDraft("manual")} disabled={!dirty || locked || !draft}>Save</Button>
</div>
</>
</LoadingFrame>