Batch bestiary add produces a single undo entry
All checks were successful
CI / check (push) Successful in 1m10s
CI / build-image (push) Successful in 15s

Extract addOneFromBestiary (no undo) and build addMultipleFromBestiary
on top so confirming N creatures from the bestiary panel creates one
undo entry that restores the entire batch, not N individual entries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-27 00:07:25 +01:00
parent 541e04b732
commit 9437272fe0
3 changed files with 70 additions and 12 deletions

View File

@@ -52,6 +52,7 @@ function mockContext(overrides: Partial<Encounter> = {}) {
toggleCondition: vi.fn(), toggleCondition: vi.fn(),
toggleConcentration: vi.fn(), toggleConcentration: vi.fn(),
addFromBestiary: vi.fn(), addFromBestiary: vi.fn(),
addMultipleFromBestiary: vi.fn(),
addFromPlayerCharacter: vi.fn(), addFromPlayerCharacter: vi.fn(),
makeStore: vi.fn(), makeStore: vi.fn(),
withUndo: vi.fn((action: () => unknown) => action()), withUndo: vi.fn((action: () => unknown) => action()),

View File

@@ -26,8 +26,12 @@ export function creatureKey(r: SearchResult): string {
} }
export function useActionBarState() { export function useActionBarState() {
const { addCombatant, addFromBestiary, addFromPlayerCharacter } = const {
useEncounterContext(); addCombatant,
addFromBestiary,
addMultipleFromBestiary,
addFromPlayerCharacter,
} = useEncounterContext();
const { search: bestiarySearch, isLoaded: bestiaryLoaded } = const { search: bestiarySearch, isLoaded: bestiaryLoaded } =
useBestiaryContext(); useBestiaryContext();
const { characters: playerCharacters } = usePlayerCharactersContext(); const { characters: playerCharacters } = usePlayerCharactersContext();
@@ -92,11 +96,24 @@ export function useActionBarState() {
const confirmQueued = useCallback(() => { const confirmQueued = useCallback(() => {
if (!queued) return; if (!queued) return;
for (let i = 0; i < queued.count; i++) { if (queued.count === 1) {
handleAddFromBestiary(queued.result); handleAddFromBestiary(queued.result);
} else {
const creatureId = addMultipleFromBestiary(queued.result, queued.count);
const isDesktop = globalThis.matchMedia("(min-width: 1024px)").matches;
if (creatureId && panelView.mode === "closed" && isDesktop) {
showCreature(creatureId);
}
} }
clearInput(); clearInput();
}, [queued, handleAddFromBestiary, clearInput]); }, [
queued,
handleAddFromBestiary,
addMultipleFromBestiary,
panelView.mode,
showCreature,
clearInput,
]);
const parseNum = (v: string): number | undefined => { const parseNum = (v: string): number | undefined => {
if (v.trim() === "") return undefined; if (v.trim() === "") return undefined;

View File

@@ -298,9 +298,10 @@ export function useEncounter() {
setEvents((prev) => [...prev, ...result]); setEvents((prev) => [...prev, ...result]);
}, [makeStore]); }, [makeStore]);
const addFromBestiary = useCallback( const addOneFromBestiary = useCallback(
(entry: BestiaryIndexEntry): CreatureId | null => { (
const snapshot = encounterRef.current; entry: BestiaryIndexEntry,
): { cId: CreatureId; events: DomainEvent[] } | null => {
const store = makeStore(); const store = makeStore();
const existingNames = store.get().combatants.map((c) => c.name); const existingNames = store.get().combatants.map((c) => c.name);
const { newName, renames } = resolveCreatureName( const { newName, renames } = resolveCreatureName(
@@ -328,8 +329,20 @@ export function useEncounter() {
creatureId: cId, creatureId: cId,
}); });
if (isDomainError(result)) { if (isDomainError(result)) return null;
store.save(snapshot);
return { cId, events: result };
},
[makeStore],
);
const addFromBestiary = useCallback(
(entry: BestiaryIndexEntry): CreatureId | null => {
const snapshot = encounterRef.current;
const added = addOneFromBestiary(entry);
if (!added) {
makeStore().save(snapshot);
return null; return null;
} }
@@ -337,10 +350,36 @@ export function useEncounter() {
undoRedoRef.current = newState; undoRedoRef.current = newState;
setUndoRedoState(newState); setUndoRedoState(newState);
setEvents((prev) => [...prev, ...result]); setEvents((prev) => [...prev, ...added.events]);
return cId; return added.cId;
}, },
[makeStore], [makeStore, addOneFromBestiary],
);
const addMultipleFromBestiary = useCallback(
(entry: BestiaryIndexEntry, count: number): CreatureId | null => {
const snapshot = encounterRef.current;
const allEvents: DomainEvent[] = [];
let lastCId: CreatureId | null = null;
for (let i = 0; i < count; i++) {
const added = addOneFromBestiary(entry);
if (!added) {
makeStore().save(snapshot);
return null;
}
allEvents.push(...added.events);
lastCId = added.cId;
}
const newState = pushUndo(undoRedoRef.current, snapshot);
undoRedoRef.current = newState;
setUndoRedoState(newState);
setEvents((prev) => [...prev, ...allEvents]);
return lastCId;
},
[makeStore, addOneFromBestiary],
); );
const addFromPlayerCharacter = useCallback( const addFromPlayerCharacter = useCallback(
@@ -426,6 +465,7 @@ export function useEncounter() {
toggleCondition, toggleCondition,
toggleConcentration, toggleConcentration,
addFromBestiary, addFromBestiary,
addMultipleFromBestiary,
addFromPlayerCharacter, addFromPlayerCharacter,
undo: undoAction, undo: undoAction,
redo: redoAction, redo: redoAction,