initial commit after split

This commit is contained in:
2026-06-24 01:43:27 +02:00
parent 2406e70597
commit 7f25a3ccdf
125 changed files with 25551 additions and 0 deletions
@@ -0,0 +1,110 @@
import type { CampaignVersionDetail } from "../../../api/campaigns";
import { asRecord, getCampaignJson, isRecord } 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: "" };
raw.attachments = {
base_path: ".",
allow_individual: false,
send_without_attachments: true,
global: [],
missing_behavior: "ask",
ambiguous_behavior: "ask",
...asRecord(raw.attachments)
};
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> {
const next = cloneJson(draft);
let current: Record<string, unknown> = next;
path.forEach((segment, index) => {
if (index === path.length - 1) {
current[segment] = value;
return;
}
const existing = current[segment];
if (!isRecord(existing)) {
current[segment] = {};
}
current = current[segment] as Record<string, unknown>;
});
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: Record<string, 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";
}