140 lines
4.9 KiB
TypeScript
140 lines
4.9 KiB
TypeScript
import type { CampaignVersionDetail } from "../../../api/campaigns";
|
|
import { asRecord, getCampaignJson, isRecord, isSafeObjectPathSegment } from "./campaignView";
|
|
|
|
export type DraftPatch = (draft: Record<string, unknown>) => Record<string, unknown>;
|
|
|
|
export function cloneJson<T>(value: T): T {
|
|
return JSON.parse(JSON.stringify(value ?? {})) as T;
|
|
}
|
|
|
|
export function ensureCampaignDraft(version: CampaignVersionDetail | null): Record<string, unknown> {
|
|
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<string, unknown>,
|
|
path: string[],
|
|
value: unknown
|
|
): Record<string, unknown> {
|
|
if (!path.length || !path.every(isSafeObjectPathSegment)) return cloneJson(draft);
|
|
const next = cloneJson(draft);
|
|
let current: Record<string, unknown> = 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<T>(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<string, unknown>, key: string, fallback = false): boolean {
|
|
const value = record[key];
|
|
return typeof value === "boolean" ? value : fallback;
|
|
}
|
|
|
|
export function getNumber(record: Record<string, unknown>, key: string, fallback = 0): number {
|
|
const value = record[key];
|
|
return typeof value === "number" ? value : fallback;
|
|
}
|
|
|
|
export function getText(record: Record<string, unknown>, 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";
|
|
}
|