import type { CampaignVersionDetail } from "../../../api/campaigns"; import { asRecord, getCampaignJson, isRecord, isSafeObjectPathSegment } from "./campaignView"; export type DraftPatch = (draft: Record) => Record; export function cloneJson(value: T): T { return JSON.parse(JSON.stringify(value ?? {})) as T; } export function ensureCampaignDraft(version: CampaignVersionDetail | null): Record { const raw = cloneJson(getCampaignJson(version)); raw.version = typeof raw.version === "string" ? raw.version : "1"; raw.campaign = { id: "", name: "", description: "", mode: "draft", ...asRecord(raw.campaign) }; raw.fields = Array.isArray(raw.fields) ? raw.fields : []; raw.global_values = isRecord(raw.global_values) ? raw.global_values : {}; raw.server = isRecord(raw.server) ? raw.server : {}; raw.recipients = isRecord(raw.recipients) ? raw.recipients : {}; raw.template = isRecord(raw.template) ? raw.template : { subject: "", text: "" }; const sourceAttachments = asRecord(raw.attachments); raw.attachments = { base_path: ".", allow_individual: false, send_without_attachments: true, send_without_attachments_behavior: "continue", reuse_policy: { action: "allow", allow_within: "none" }, global: [], residual_files: { mode: "none", recipient: null, subject: "Unassigned files in campaign {{local:campaign_name}}", text: "The campaign build found {{local:residual_file_count}} file(s) that were not assigned to a recipient.\n\n{{local:residual_file_list}}" }, missing_behavior: "warn", ambiguous_behavior: "ask", ...sourceAttachments }; const normalizedAttachments = asRecord(raw.attachments); if (sourceAttachments.send_without_attachments_behavior === undefined) { normalizedAttachments.send_without_attachments_behavior = getBool(normalizedAttachments, "send_without_attachments", true) ? "continue" : "block"; } raw.entries = isRecord(raw.entries) ? raw.entries : { inline: [] }; raw.validation_policy = { unsent_attachment_files: "warn", ...asRecord(raw.validation_policy) }; raw.delivery = isRecord(raw.delivery) ? raw.delivery : {}; raw.status_tracking = isRecord(raw.status_tracking) ? raw.status_tracking : { enabled: true }; return raw; } export function updateNested( draft: Record, path: string[], value: unknown ): Record { if (!path.length || !path.every(isSafeObjectPathSegment)) return cloneJson(draft); const next = cloneJson(draft); let current: Record = next; for (const [index, segment] of path.entries()) { if (index === path.length - 1) { Object.defineProperty(current, segment, { configurable: true, enumerable: true, value, writable: true }); break; } const existing = Object.getOwnPropertyDescriptor(current, segment)?.value; if (!isRecord(existing)) { Object.defineProperty(current, segment, { configurable: true, enumerable: true, value: {}, writable: true }); } const child = Object.getOwnPropertyDescriptor(current, segment)?.value; if (!isRecord(child)) return next; current = child; } return next; } export function parseJsonTextarea(text: string, fallback: T): { value: T; error: string } { if (!text.trim()) return { value: fallback, error: "" }; try { return { value: JSON.parse(text) as T, error: "" }; } catch (error) { return { value: fallback, error: error instanceof Error ? error.message : String(error) }; } } export function stringifyJson(value: unknown): string { return JSON.stringify(value ?? {}, null, 2); } export function getBool(record: Record, key: string, fallback = false): boolean { const value = record[key]; return typeof value === "boolean" ? value : fallback; } export function getNumber(record: Record, key: string, fallback = 0): number { const value = record[key]; return typeof value === "number" ? value : fallback; } export function getText(record: Record, key: string, fallback = ""): string { const value = record[key]; if (typeof value === "string") return value; if (typeof value === "number" || typeof value === "boolean") return String(value); return fallback; } export function downloadJson(filename: string, data: unknown) { const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); } export function safeFileStem(value?: string | null): string { const stem = (value || "campaign").replace(/[^a-zA-Z0-9_.-]+/g, "-").replace(/^-+|-+$/g, ""); return stem || "campaign"; }