Fix Visual Manager empty categories: seed roots, harden tree API.
Local Build and Deploy / deploy (push) Successful in 56s
Local Build and Deploy / deploy (push) Successful in 56s
Seed root tabs from SSR, load the full tree without CLEAR_TREE races, coerce parent ids, and tolerate catalog_items page_id type mismatches so category pages actually appear. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
385cefd0fa
commit
1d8efefce4
6 files changed
+285
-187
No files matched your search
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -120,26 +120,36 @@ export function SortableTree({
|
|||||||
setTreeLoading(true);
|
setTreeLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/admin/catalog/tree?mode=full");
|
const res = await fetch("/api/admin/catalog/tree?mode=full");
|
||||||
if (!res.ok) throw new Error();
|
const data = await res.json().catch(() => ({}));
|
||||||
const data = await res.json();
|
if (!res.ok) throw new Error(data.error || "Tree request failed");
|
||||||
const pages: TreeNodeData[] = data.pages || [];
|
const pages: TreeNodeData[] = data.pages || [];
|
||||||
dispatch({ type: "SET_FULL_TREE", nodes: pages });
|
dispatch({ type: "SET_FULL_TREE", nodes: pages });
|
||||||
|
|
||||||
|
// Auto-expand first level under the active root so categories are visible.
|
||||||
|
if (activeTabId !== null) {
|
||||||
|
const firstLevel = pages
|
||||||
|
.filter((p) => Number(p.parentId) === activeTabId)
|
||||||
|
.map((p) => Number(p.id));
|
||||||
|
if (firstLevel.length > 0) {
|
||||||
|
dispatch({ type: "EXPAND_PATH", ids: firstLevel });
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Failed to load catalog tree");
|
toast.error("Failed to load catalog tree");
|
||||||
} finally {
|
} finally {
|
||||||
setTreeLoading(false);
|
setTreeLoading(false);
|
||||||
}
|
}
|
||||||
}, [dispatch]);
|
}, [dispatch, activeTabId]);
|
||||||
|
|
||||||
// Load full tree when tab changes
|
// Load full tree when tab changes — replace atomically (no CLEAR_TREE race).
|
||||||
// biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code
|
// biome-ignore lint/correctness/useExhaustiveDependencies: explicitly chosen here, see surrounding code
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (activeTabId === null) return;
|
if (activeTabId === null) return;
|
||||||
dispatch({ type: "CLEAR_TREE" });
|
|
||||||
setSearchQuery("");
|
setSearchQuery("");
|
||||||
setSearchResults(null);
|
setSearchResults(null);
|
||||||
setFilter("all");
|
setFilter("all");
|
||||||
loadFullTree();
|
dispatch({ type: "COLLAPSE_ALL" });
|
||||||
|
void loadFullTree();
|
||||||
}, [activeTabId]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [activeTabId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// Debounced search
|
// Debounced search
|
||||||
@@ -198,7 +208,6 @@ export function SortableTree({
|
|||||||
for (const cid of childIds) {
|
for (const cid of childIds) {
|
||||||
const node = nodes.get(cid);
|
const node = nodes.get(cid);
|
||||||
if (!node) continue;
|
if (!node) continue;
|
||||||
// Apply filter
|
|
||||||
if (filter === "enabled" && node.enabled !== "1") continue;
|
if (filter === "enabled" && node.enabled !== "1") continue;
|
||||||
if (filter === "disabled" && node.enabled === "1") continue;
|
if (filter === "disabled" && node.enabled === "1") continue;
|
||||||
const isExpanded = expandedIds.has(cid);
|
const isExpanded = expandedIds.has(cid);
|
||||||
@@ -207,6 +216,20 @@ export function SortableTree({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
traverse(activeTabId, 0);
|
traverse(activeTabId, 0);
|
||||||
|
|
||||||
|
// If this root has no children yet, still show the root page itself
|
||||||
|
// so categories are editable (matches page CatalogTree expectations).
|
||||||
|
if (result.length === 0) {
|
||||||
|
const rootNode = nodes.get(activeTabId);
|
||||||
|
if (rootNode) {
|
||||||
|
if (
|
||||||
|
!(filter === "enabled" && rootNode.enabled !== "1") &&
|
||||||
|
!(filter === "disabled" && rootNode.enabled === "1")
|
||||||
|
) {
|
||||||
|
result.push({ ...rootNode, depth: 0, isExpanded: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}, [activeTabId, nodes, childrenMap, expandedIds, searchResults, filter]);
|
}, [activeTabId, nodes, childrenMap, expandedIds, searchResults, filter]);
|
||||||
|
|
||||||
|
|||||||
@@ -142,12 +142,35 @@ function reducer(
|
|||||||
case "SET_FULL_TREE": {
|
case "SET_FULL_TREE": {
|
||||||
const nodes = new Map<number, TreeNodeData>();
|
const nodes = new Map<number, TreeNodeData>();
|
||||||
const childrenMap = new Map<number, number[]>();
|
const childrenMap = new Map<number, number[]>();
|
||||||
for (const n of action.nodes) {
|
for (const raw of action.nodes) {
|
||||||
nodes.set(n.id, n);
|
const id = Number(raw.id);
|
||||||
|
const parentId = Number(raw.parentId);
|
||||||
|
if (!Number.isFinite(id)) continue;
|
||||||
|
const n: TreeNodeData = {
|
||||||
|
...raw,
|
||||||
|
id,
|
||||||
|
parentId: Number.isFinite(parentId) ? parentId : -1,
|
||||||
|
enabled: String(raw.enabled ?? "0"),
|
||||||
|
visible: String(raw.visible ?? "0"),
|
||||||
|
childCount: Number(raw.childCount) || 0,
|
||||||
|
itemCount: Number(raw.itemCount) || 0,
|
||||||
|
iconImage: Number(raw.iconImage) || 0,
|
||||||
|
iconColor: Number(raw.iconColor) || 0,
|
||||||
|
orderNum: Number(raw.orderNum) || 0,
|
||||||
|
};
|
||||||
|
nodes.set(id, n);
|
||||||
const siblings = childrenMap.get(n.parentId) || [];
|
const siblings = childrenMap.get(n.parentId) || [];
|
||||||
siblings.push(n.id);
|
siblings.push(id);
|
||||||
childrenMap.set(n.parentId, siblings);
|
childrenMap.set(n.parentId, siblings);
|
||||||
}
|
}
|
||||||
|
// Roots may be parent_id -1 or 0 — mirror both keys so tabs resolve children.
|
||||||
|
const underNeg1 = childrenMap.get(-1) || [];
|
||||||
|
const under0 = childrenMap.get(0) || [];
|
||||||
|
if (underNeg1.length || under0.length) {
|
||||||
|
const merged = [...new Set([...underNeg1, ...under0])];
|
||||||
|
childrenMap.set(-1, merged);
|
||||||
|
childrenMap.set(0, merged);
|
||||||
|
}
|
||||||
for (const [parentId, ids] of childrenMap) {
|
for (const [parentId, ids] of childrenMap) {
|
||||||
ids.sort(
|
ids.sort(
|
||||||
(a, b) =>
|
(a, b) =>
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user