150 lines
5.3 KiB
TypeScript
150 lines
5.3 KiB
TypeScript
import { useCallback, useEffect, useMemo, 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>;
|
|
transformDraftBeforeSave?: (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)}` : "i18n:govoplan-campaign.loaded.6db90a0a";
|
|
}
|
|
|
|
export function useCampaignDraftEditor({
|
|
settings,
|
|
campaignId,
|
|
version,
|
|
locked,
|
|
reload,
|
|
setError,
|
|
currentStep,
|
|
currentFlow = "manual",
|
|
workflowState = "editing",
|
|
isComplete = false,
|
|
unsavedTitle,
|
|
unsavedMessage,
|
|
loadedLabel = defaultLoadedLabel,
|
|
transformLoadedDraft,
|
|
transformDraftBeforeSave,
|
|
extraPayload,
|
|
onLoaded,
|
|
onSaved
|
|
}: UseCampaignDraftEditorOptions) {
|
|
const loadedLabelRef = useRef(loadedLabel);
|
|
const transformLoadedDraftRef = useRef(transformLoadedDraft);
|
|
const transformDraftBeforeSaveRef = useRef(transformDraftBeforeSave);
|
|
const onLoadedRef = useRef(onLoaded);
|
|
|
|
useEffect(() => {
|
|
loadedLabelRef.current = loadedLabel;
|
|
transformLoadedDraftRef.current = transformLoadedDraft;
|
|
transformDraftBeforeSaveRef.current = transformDraftBeforeSave;
|
|
onLoadedRef.current = onLoaded;
|
|
}, [loadedLabel, transformLoadedDraft, transformDraftBeforeSave, onLoaded]);
|
|
|
|
const [draft, setDraft] = useState<Record<string, unknown> | null>(null);
|
|
const [dirty, setDirty] = useState(false);
|
|
const [saveState, setSaveState] = useState("i18n:govoplan-campaign.loaded.6db90a0a");
|
|
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("i18n:govoplan-campaign.saving.56a2285c");
|
|
setError("");
|
|
setLocalError("");
|
|
try {
|
|
const draftToSave = transformDraftBeforeSaveRef.current?.(draft) ?? draft;
|
|
const saved = await autosaveCampaignVersion(settings, campaignId, version.id, {
|
|
campaign_json: draftToSave,
|
|
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("i18n:govoplan-campaign.save_failed.0a444467");
|
|
return false;
|
|
}
|
|
}, [campaignId, currentFlow, currentStep, draft, extraPayload, isComplete, locked, onSaved, reload, setError, settings, version, workflowState]);
|
|
|
|
const unsavedRegistration = useMemo(() => dirty && !locked ? {
|
|
title: unsavedTitle,
|
|
message: unsavedMessage,
|
|
onSave: () => saveDraft("manual"),
|
|
onDiscard: () => setDirty(false)
|
|
} : null, [dirty, locked, saveDraft, unsavedMessage, unsavedTitle]);
|
|
|
|
useRegisterCampaignUnsavedChanges(unsavedRegistration);
|
|
|
|
return {
|
|
draft,
|
|
setDraft,
|
|
displayDraft: draft ?? ensureCampaignDraft(null),
|
|
dirty,
|
|
setDirty,
|
|
saveState,
|
|
setSaveState,
|
|
localError,
|
|
setLocalError,
|
|
patch,
|
|
markDirty,
|
|
saveDraft
|
|
};
|
|
} |