Fix Visual Manager empty categories: seed roots, harden tree API.
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:
SimoandCursor committed 2026-07-17 22:02:53 +02:00
1 parent 385cefd0fa
commit 1d8efefce4
6 files changed
+244 -146

No files matched your search

+17 -8
View File
@@ -232,11 +232,9 @@ export default async function CatalogPagesPage({
prisma.catalogPages.count({ where: { enabled: "1" } }), prisma.catalogPages.count({ where: { enabled: "1" } }),
]); ]);
// ── Tree view data ────────────────────────────────────────────── // ── Root pages (tree view + Visual Manager seed) ────────────────
let rootPages: TreeNodeShape[] = [];
if (viewMode === "tree") {
const rootPagesRaw = await prisma.catalogPages.findMany({ const rootPagesRaw = await prisma.catalogPages.findMany({
where: { parentId: -1 }, where: { OR: [{ parentId: -1 }, { parentId: 0 }] },
orderBy: { orderNum: "asc" }, orderBy: { orderNum: "asc" },
}); });
const rootIds = rootPagesRaw.map((p) => p.id); const rootIds = rootPagesRaw.map((p) => p.id);
@@ -249,11 +247,13 @@ export default async function CatalogPagesPage({
}) })
: [], : [],
rootIds.length > 0 rootIds.length > 0
? prisma.catalogItems.groupBy({ ? prisma.catalogItems
.groupBy({
by: ["pageId"], by: ["pageId"],
where: { pageId: { in: rootIds } }, where: { pageId: { in: rootIds } },
_count: true, _count: true,
}) })
.catch(() => [])
: [], : [],
]); ]);
const rootChildMap = Object.fromEntries( const rootChildMap = Object.fromEntries(
@@ -262,7 +262,7 @@ export default async function CatalogPagesPage({
const rootItemMap = Object.fromEntries( const rootItemMap = Object.fromEntries(
rootItemCounts.map((c) => [c.pageId, c._count]), rootItemCounts.map((c) => [c.pageId, c._count]),
); );
rootPages = rootPagesRaw.map((p) => ({ const rootPages: TreeNodeShape[] = rootPagesRaw.map((p) => ({
id: p.id, id: p.id,
caption: p.caption, caption: p.caption,
parentId: p.parentId, parentId: p.parentId,
@@ -274,7 +274,6 @@ export default async function CatalogPagesPage({
childCount: rootChildMap[p.id] ?? 0, childCount: rootChildMap[p.id] ?? 0,
itemCount: rootItemMap[p.id] ?? 0, itemCount: rootItemMap[p.id] ?? 0,
})); }));
}
// ── Table view data (skip if tree) ──────────────────────────── // ── Table view data (skip if tree) ────────────────────────────
let rows: { let rows: {
@@ -399,7 +398,17 @@ export default async function CatalogPagesPage({
</p> </p>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CatalogViewToggle currentView={viewMode} catalogType="normal" /> <CatalogViewToggle currentView={viewMode} catalogType="normal" />
{canEdit && <CatalogManagerDialog />} {canEdit && (
<CatalogManagerDialog
initialRoots={rootPages.map((p) => ({
id: p.id,
caption: p.caption,
iconImage: p.iconImage,
enabled: p.enabled,
orderNum: 0,
}))}
/>
)}
{canEdit && <CatalogActions />} {canEdit && <CatalogActions />}
</div> </div>
</div> </div>
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