fix: follow-ups of the lot-v018 stack reviews #482
2 changed files with 59 additions and 16 deletions
|
|
@ -281,6 +281,38 @@ describe("useCategories — « Vérifier la hiérarchie » (#393)", () => {
|
||||||
expect(types).not.toContain("SET_LOADING");
|
expect(types).not.toContain("SET_LOADING");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("lets a load started during the check's read win over it", async () => {
|
||||||
|
let answer: (rows: unknown) => void = () => undefined;
|
||||||
|
const NEWER = [...ROWS, { ...ROWS[0], id: 2, name: "Épargne", sort_order: 2 }];
|
||||||
|
vi.mocked(findOrphanCategories).mockResolvedValue([]);
|
||||||
|
let api = await mount();
|
||||||
|
vi.mocked(getAllCategoriesWithCounts)
|
||||||
|
.mockReturnValueOnce(new Promise((r) => (answer = r)) as never)
|
||||||
|
.mockResolvedValueOnce(NEWER as never);
|
||||||
|
const checked = api.checkHierarchy();
|
||||||
|
await flush(); // the check's read is out, still unanswered
|
||||||
|
api = harness.render(useCategories);
|
||||||
|
await api.reinitializeCategories(); // a newer load: NEWER lands
|
||||||
|
answer(ROWS); // the check's older answer comes last
|
||||||
|
await checked;
|
||||||
|
const ids = harness.render(useCategories).state.categories.map((c) => c.id).sort((a, b) => a - b);
|
||||||
|
expect(ids).toEqual([1, 2, 10]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("a reattachment reads the categories again without the loading state: a form behind the window stays", async () => {
|
||||||
|
vi.mocked(findOrphanCategories).mockResolvedValueOnce([CYCLE]).mockResolvedValueOnce([]);
|
||||||
|
vi.mocked(reattachCategory).mockResolvedValue(3);
|
||||||
|
let api = await mount();
|
||||||
|
await api.checkHierarchy();
|
||||||
|
api = harness.render(useCategories);
|
||||||
|
harness.dispatch.mockClear();
|
||||||
|
await api.reattachOrphan(CYCLE, null);
|
||||||
|
const types = harness.dispatch.mock.calls.map(([action]) => (action as { type: string }).type);
|
||||||
|
expect(types).toContain("SET_CATEGORIES");
|
||||||
|
expect(types).not.toContain("SET_LOADING");
|
||||||
|
expect(getAllCategoriesWithCounts).toHaveBeenCalledTimes(3);
|
||||||
|
});
|
||||||
|
|
||||||
it("reads the categories once the drag-and-drop's writes are over, never halfway", async () => {
|
it("reads the categories once the drag-and-drop's writes are over, never halfway", async () => {
|
||||||
let written: () => void = () => undefined;
|
let written: () => void = () => undefined;
|
||||||
vi.mocked(updateCategorySortOrders).mockReturnValue(new Promise<void>((r) => (written = r)));
|
vi.mocked(updateCategorySortOrders).mockReturnValue(new Promise<void>((r) => (written = r)));
|
||||||
|
|
|
||||||
|
|
@ -407,38 +407,49 @@ export function useCategories() {
|
||||||
[state.tree, loadCategories]
|
[state.tree, loadCategories]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the categories again for the « Vérifier la hiérarchie » window
|
||||||
|
* (#393), without the page's loading state: `loadCategories` would show
|
||||||
|
* « Chargement » in place of the tree and the detail panel, and a form open
|
||||||
|
* behind the window would come back from its stored values, its unsaved
|
||||||
|
* changes lost. Waits for a drag-and-drop's writes first (one `UPDATE` per
|
||||||
|
* category: read halfway, the tree would show half the move); a load
|
||||||
|
* started meanwhile wins. Throws when the read fails.
|
||||||
|
*/
|
||||||
|
const readCategoriesAgain = useCallback(async () => {
|
||||||
|
await pendingMoveRef.current;
|
||||||
|
const fetchId = fetchIdRef.current;
|
||||||
|
const rows = await getAllCategoriesWithCounts();
|
||||||
|
if (fetchId !== fetchIdRef.current) return;
|
||||||
|
const flat = rows.map((r) => ({ ...r, children: [] as CategoryTreeNode[] }));
|
||||||
|
dispatch({ type: "SET_CATEGORIES", payload: { flat, tree: buildTree(flat) } });
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the « Vérifier la hiérarchie » window and runs the diagnostic (#393).
|
* Opens the « Vérifier la hiérarchie » window and runs the diagnostic (#393).
|
||||||
* The window names the orphans and offers their parents from
|
* The window names the orphans and offers their parents from
|
||||||
* `state.categories`, which a drag-and-drop leaves with what the tree shows
|
* `state.categories`, which a drag-and-drop leaves with what the tree shows
|
||||||
* only (`SET_TREE`), without the categories no root reaches: the categories
|
* only (`SET_TREE`), without the categories no root reaches: the categories
|
||||||
* are read again first, once the drag-and-drop's writes are over (one
|
* are read again first (`readCategoriesAgain`).
|
||||||
* `UPDATE` per category: read halfway, the tree would show half the move).
|
|
||||||
* Without the page's loading state, so a form open behind the window stays
|
|
||||||
* as it is; a load started meanwhile wins.
|
|
||||||
*/
|
*/
|
||||||
const checkHierarchy = useCallback(async () => {
|
const checkHierarchy = useCallback(async () => {
|
||||||
dispatch({ type: "OPEN_HIERARCHY_CHECK" });
|
dispatch({ type: "OPEN_HIERARCHY_CHECK" });
|
||||||
try {
|
try {
|
||||||
await pendingMoveRef.current;
|
await readCategoriesAgain();
|
||||||
const fetchId = fetchIdRef.current;
|
|
||||||
const rows = await getAllCategoriesWithCounts();
|
|
||||||
if (fetchId === fetchIdRef.current) {
|
|
||||||
const flat = rows.map((r) => ({ ...r, children: [] as CategoryTreeNode[] }));
|
|
||||||
dispatch({ type: "SET_CATEGORIES", payload: { flat, tree: buildTree(flat) } });
|
|
||||||
}
|
|
||||||
const orphans = await findOrphanCategories();
|
const orphans = await findOrphanCategories();
|
||||||
dispatch({ type: "SET_ORPHANS", payload: orphans });
|
dispatch({ type: "SET_ORPHANS", payload: orphans });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: "SET_HIERARCHY_ERROR", payload: categoriesErrorOf(e) });
|
dispatch({ type: "SET_HIERARCHY_ERROR", payload: categoriesErrorOf(e) });
|
||||||
}
|
}
|
||||||
}, []);
|
}, [readCategoriesAgain]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reattaches an orphan under `parentId`, or to the root with `null` (#393).
|
* Reattaches an orphan under `parentId`, or to the root with `null` (#393).
|
||||||
* On success: one Journal line (counts, no name), then the orphan list and
|
* On success: one Journal line (counts, no name), then the orphan list and
|
||||||
* the tree reload; the tree reloads even if the new check fails, since the
|
* the categories are read again; the categories are read even if the new
|
||||||
* write is committed. A refusal shows in the window, by its translated code.
|
* check fails, since the write is committed. Read like at the opening
|
||||||
|
* (`readCategoriesAgain`), so a form open behind the window keeps its
|
||||||
|
* unsaved changes. A refusal, or a failed read, shows in the window.
|
||||||
*/
|
*/
|
||||||
const reattachOrphan = useCallback(
|
const reattachOrphan = useCallback(
|
||||||
async (orphan: OrphanCategory, parentId: number | null) => {
|
async (orphan: OrphanCategory, parentId: number | null) => {
|
||||||
|
|
@ -450,14 +461,14 @@ export function useCategories() {
|
||||||
const orphans = await findOrphanCategories();
|
const orphans = await findOrphanCategories();
|
||||||
dispatch({ type: "SET_ORPHANS", payload: orphans });
|
dispatch({ type: "SET_ORPHANS", payload: orphans });
|
||||||
} finally {
|
} finally {
|
||||||
await loadCategories();
|
await readCategoriesAgain();
|
||||||
}
|
}
|
||||||
dispatch({ type: "SET_SAVING", payload: false });
|
dispatch({ type: "SET_SAVING", payload: false });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dispatch({ type: "SET_HIERARCHY_ERROR", payload: categoriesErrorOf(e) });
|
dispatch({ type: "SET_HIERARCHY_ERROR", payload: categoriesErrorOf(e) });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[loadCategories]
|
[readCategoriesAgain]
|
||||||
);
|
);
|
||||||
|
|
||||||
const closeHierarchyCheck = useCallback(() => {
|
const closeHierarchyCheck = useCallback(() => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue