Replace direct adapter/persistence imports with context-based injection (AdapterContext + useAdapters) so tests use in-memory implementations instead of vi.mock. Migrate component tests from context mocking to AllProviders with real hooks. Extract export/import logic from ActionBar into useEncounterExportImport hook. Add bestiary-cache and bestiary-index-adapter test suites. Raise adapter coverage thresholds (68→80 lines, 56→62 branches). 77 test files, 891 tests, all passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
140 lines
3.6 KiB
TypeScript
140 lines
3.6 KiB
TypeScript
import type { ExportBundle } from "@initiative/domain";
|
|
import { useCallback, useRef, useState } from "react";
|
|
import { useEncounterContext } from "../contexts/encounter-context.js";
|
|
import { usePlayerCharactersContext } from "../contexts/player-characters-context.js";
|
|
import {
|
|
assembleExportBundle,
|
|
bundleToJson,
|
|
readImportFile,
|
|
triggerDownload,
|
|
validateImportBundle,
|
|
} from "../persistence/export-import.js";
|
|
|
|
export function useEncounterExportImport() {
|
|
const {
|
|
encounter,
|
|
undoRedoState,
|
|
isEmpty: encounterIsEmpty,
|
|
setEncounter,
|
|
setUndoRedoState,
|
|
} = useEncounterContext();
|
|
const { characters: playerCharacters, replacePlayerCharacters } =
|
|
usePlayerCharactersContext();
|
|
|
|
const [importError, setImportError] = useState<string | null>(null);
|
|
const [showExportMethod, setShowExportMethod] = useState(false);
|
|
const [showImportMethod, setShowImportMethod] = useState(false);
|
|
const [showImportConfirm, setShowImportConfirm] = useState(false);
|
|
const pendingBundleRef = useRef<ExportBundle | null>(null);
|
|
const importFileRef = useRef<HTMLInputElement>(null);
|
|
|
|
const handleExportDownload = useCallback(
|
|
(includeHistory: boolean, filename: string) => {
|
|
const bundle = assembleExportBundle(
|
|
encounter,
|
|
undoRedoState,
|
|
playerCharacters,
|
|
includeHistory,
|
|
);
|
|
triggerDownload(bundle, filename);
|
|
},
|
|
[encounter, undoRedoState, playerCharacters],
|
|
);
|
|
|
|
const handleExportClipboard = useCallback(
|
|
(includeHistory: boolean) => {
|
|
const bundle = assembleExportBundle(
|
|
encounter,
|
|
undoRedoState,
|
|
playerCharacters,
|
|
includeHistory,
|
|
);
|
|
void navigator.clipboard.writeText(bundleToJson(bundle));
|
|
},
|
|
[encounter, undoRedoState, playerCharacters],
|
|
);
|
|
|
|
const applyImport = useCallback(
|
|
(bundle: ExportBundle) => {
|
|
setEncounter(bundle.encounter);
|
|
setUndoRedoState({
|
|
undoStack: bundle.undoStack,
|
|
redoStack: bundle.redoStack,
|
|
});
|
|
replacePlayerCharacters([...bundle.playerCharacters]);
|
|
},
|
|
[setEncounter, setUndoRedoState, replacePlayerCharacters],
|
|
);
|
|
|
|
const handleValidatedBundle = useCallback(
|
|
(result: ExportBundle | string) => {
|
|
if (typeof result === "string") {
|
|
setImportError(result);
|
|
return;
|
|
}
|
|
if (encounterIsEmpty) {
|
|
applyImport(result);
|
|
} else {
|
|
pendingBundleRef.current = result;
|
|
setShowImportConfirm(true);
|
|
}
|
|
},
|
|
[encounterIsEmpty, applyImport],
|
|
);
|
|
|
|
const handleImportFile = useCallback(
|
|
async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
if (importFileRef.current) importFileRef.current.value = "";
|
|
|
|
setImportError(null);
|
|
handleValidatedBundle(await readImportFile(file));
|
|
},
|
|
[handleValidatedBundle],
|
|
);
|
|
|
|
const handleImportClipboard = useCallback(
|
|
(text: string) => {
|
|
setImportError(null);
|
|
try {
|
|
const parsed: unknown = JSON.parse(text);
|
|
handleValidatedBundle(validateImportBundle(parsed));
|
|
} catch {
|
|
setImportError("Invalid file format");
|
|
}
|
|
},
|
|
[handleValidatedBundle],
|
|
);
|
|
|
|
const handleImportConfirm = useCallback(() => {
|
|
if (pendingBundleRef.current) {
|
|
applyImport(pendingBundleRef.current);
|
|
pendingBundleRef.current = null;
|
|
}
|
|
setShowImportConfirm(false);
|
|
}, [applyImport]);
|
|
|
|
const handleImportCancel = useCallback(() => {
|
|
pendingBundleRef.current = null;
|
|
setShowImportConfirm(false);
|
|
}, []);
|
|
|
|
return {
|
|
importError,
|
|
showExportMethod,
|
|
showImportMethod,
|
|
showImportConfirm,
|
|
importFileRef,
|
|
setImportError,
|
|
setShowExportMethod,
|
|
setShowImportMethod,
|
|
handleExportDownload,
|
|
handleExportClipboard,
|
|
handleImportFile,
|
|
handleImportClipboard,
|
|
handleImportConfirm,
|
|
handleImportCancel,
|
|
} as const;
|
|
}
|