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

View File

@@ -0,0 +1,143 @@
import { useCallback, useEffect, useRef, useState } from "react";
import type { ApiSettings } from "../../../types";
import { autosaveCampaignVersion, type CampaignVersionDetail, type CampaignVersionUpdatePayload } from "../../../api/campaigns";
import { formatDateTime, getCampaignJson } from "../utils/campaignView";
import { ensureCampaignDraft, updateNested } from "../utils/draftEditor";
import { useRegisterCampaignUnsavedChanges } from "../context/UnsavedChangesContext";
type StepValue = string | (() => string | null | undefined);
type UseCampaignDraftEditorOptions = {
settings: ApiSettings;
campaignId: string;
version: CampaignVersionDetail | null;
locked: boolean;
reload: () => Promise<void>;
setError: (message: string) => void;
currentStep: StepValue;
currentFlow?: string;
workflowState?: string;
isComplete?: boolean;
unsavedTitle: string;
unsavedMessage: string;
loadedLabel?: (version: CampaignVersionDetail) => string;
transformLoadedDraft?: (version: CampaignVersionDetail, draft: Record<string, unknown>) => Record<string, unknown>;
extraPayload?: () => Partial<CampaignVersionUpdatePayload>;
onLoaded?: (version: CampaignVersionDetail, draft: Record<string, unknown>) => void;
onSaved?: (version: CampaignVersionDetail) => void;
};
function resolveStep(value: StepValue): string | null | undefined {
return typeof value === "function" ? value() : value;
}
function defaultLoadedLabel(version: CampaignVersionDetail): string {
return version.autosaved_at ? `Loaded saved draft ${formatDateTime(version.autosaved_at)}` : "Loaded";
}
export function useCampaignDraftEditor({
settings,
campaignId,
version,
locked,
reload,
setError,
currentStep,
currentFlow = "manual",
workflowState = "editing",
isComplete = false,
unsavedTitle,
unsavedMessage,
loadedLabel = defaultLoadedLabel,
transformLoadedDraft,
extraPayload,
onLoaded,
onSaved
}: UseCampaignDraftEditorOptions) {
const loadedLabelRef = useRef(loadedLabel);
const transformLoadedDraftRef = useRef(transformLoadedDraft);
const onLoadedRef = useRef(onLoaded);
useEffect(() => {
loadedLabelRef.current = loadedLabel;
transformLoadedDraftRef.current = transformLoadedDraft;
onLoadedRef.current = onLoaded;
}, [loadedLabel, transformLoadedDraft, onLoaded]);
const [draft, setDraft] = useState<Record<string, unknown> | null>(null);
const [dirty, setDirty] = useState(false);
const [saveState, setSaveState] = useState("Loaded");
const [localError, setLocalError] = useState("");
useEffect(() => {
if (!version) return;
const initialDraft = ensureCampaignDraft(version);
const loadedDraft = transformLoadedDraftRef.current?.(version, initialDraft) ?? initialDraft;
setDraft(loadedDraft);
setDirty(false);
setLocalError("");
setSaveState(loadedLabelRef.current(version));
onLoadedRef.current?.(version, loadedDraft);
}, [version]);
const markDirty = useCallback(() => {
setDirty(true);
setLocalError("");
}, []);
const patch = useCallback((path: string[], value: unknown) => {
if (locked) return;
setDraft((current) => updateNested(current ?? {}, path, value));
markDirty();
}, [locked, markDirty]);
const saveDraft = useCallback(async (_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: currentFlow,
current_step: resolveStep(currentStep),
workflow_state: workflowState,
is_complete: isComplete,
...(extraPayload?.() ?? {})
});
setDraft(getCampaignJson(saved));
setDirty(false);
setSaveState(`Saved ${formatDateTime(saved.autosaved_at ?? saved.updated_at)}`);
onSaved?.(saved);
await reload();
return true;
} catch (err) {
const text = err instanceof Error ? err.message : String(err);
setLocalError(text);
setSaveState("Save failed");
return false;
}
}, [campaignId, currentFlow, currentStep, draft, extraPayload, isComplete, locked, onSaved, reload, setError, settings, version, workflowState]);
useRegisterCampaignUnsavedChanges(dirty && !locked ? {
title: unsavedTitle,
message: unsavedMessage,
onSave: () => saveDraft("manual"),
onDiscard: () => setDirty(false)
} : null);
return {
draft,
setDraft,
displayDraft: draft ?? ensureCampaignDraft(null),
dirty,
setDirty,
saveState,
setSaveState,
localError,
setLocalError,
patch,
markDirty,
saveDraft
};
}

View File

@@ -0,0 +1,80 @@
import { useCallback, useEffect, useState } from "react";
import { useSearchParams } from "react-router-dom";
import type { ApiSettings } from "../../../types";
import {
getCampaign,
getCampaignSummary,
getCampaignVersion,
listCampaignVersions,
type CampaignSummary,
type CampaignVersionDetail
} from "../../../api/campaigns";
import type { CampaignWorkspaceData } from "../utils/campaignView";
const initialData: CampaignWorkspaceData = {
campaign: null,
versions: [],
currentVersion: null,
summary: null
};
type UseCampaignWorkspaceDataOptions = {
includeCurrentVersion?: boolean;
includeSummary?: boolean;
includeVersions?: boolean;
};
export function useCampaignWorkspaceData(
settings: ApiSettings,
campaignId: string,
options: UseCampaignWorkspaceDataOptions = {}
) {
const {
includeCurrentVersion = true,
includeSummary = false,
includeVersions = false
} = options;
const [searchParams] = useSearchParams();
const selectedVersionId = searchParams.get("version");
const [data, setData] = useState<CampaignWorkspaceData>(initialData);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const reload = useCallback(async () => {
if (!campaignId) return;
setLoading(true);
setError("");
try {
const needsCampaign = includeCurrentVersion || includeVersions;
const shouldLoadVersions = includeCurrentVersion || includeVersions;
const [campaign, versions] = await Promise.all([
needsCampaign ? getCampaign(settings, campaignId) : Promise.resolve(null),
shouldLoadVersions ? listCampaignVersions(settings, campaignId) : Promise.resolve([]),
]);
const wantedVersionId = selectedVersionId || campaign?.current_version_id || versions[0]?.id;
const [versionResult, summaryResult] = await Promise.allSettled([
includeCurrentVersion && wantedVersionId ? getCampaignVersion(settings, campaignId, wantedVersionId) : Promise.resolve(null),
includeSummary ? getCampaignSummary(settings, campaignId, wantedVersionId) : Promise.resolve(null)
]);
setData({
campaign,
versions,
currentVersion: versionResult.status === "fulfilled" ? (versionResult.value as CampaignVersionDetail | null) : null,
summary: summaryResult.status === "fulfilled" ? (summaryResult.value as CampaignSummary | null) : null
});
} catch (err) {
setData(initialData);
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
}
}, [settings, campaignId, includeCurrentVersion, includeSummary, includeVersions, selectedVersionId]);
useEffect(() => {
reload();
}, [reload]);
return { data, loading, error, reload, setError };
}