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
+285 -187

No files matched your search

@@ -17,10 +17,13 @@ import { CatalogManagerProvider, useCatalogManager } from "./use-catalog-store";
/**
* Visual Catalog Manager — matches habbo-next UX (full-viewport editor).
* Uses a portal overlay instead of Base UI Dialog (DialogTitle/Popup break
* the Radix-era layout that habbo-next still uses).
* Portal overlay (not Base UI Dialog) so the tree + editor mount reliably.
*/
export function CatalogManagerDialog() {
export function CatalogManagerDialog({
initialRoots = [],
}: {
initialRoots?: RootTab[];
}) {
const [open, setOpen] = useState(false);
const [mounted, setMounted] = useState(false);
@@ -68,6 +71,7 @@ export function CatalogManagerDialog() {
<CatalogManagerInner
open={open}
onOpenChange={setOpen}
initialRoots={initialRoots}
/>
</CatalogManagerProvider>
</div>
@@ -82,9 +86,11 @@ export function CatalogManagerDialog() {
function CatalogManagerInner({
open,
onOpenChange,
initialRoots,
}: {
open: boolean;
onOpenChange: (v: boolean) => void;
initialRoots: RootTab[];
}) {
const { state, dispatch } = useCatalogManager();
@@ -92,23 +98,23 @@ function CatalogManagerInner({
dispatch({ type: "SET_LOADING_ROOT", loading: true });
try {
const res = await fetch("/api/admin/catalog/tree?parentId=-1");
if (!res.ok) throw new Error();
const data = await res.json();
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.error || "Failed to load roots");
const pages: RootTab[] = (data.pages || []).map(
(p: Record<string, unknown>) => ({
id: p.id as number,
caption: p.caption as string,
iconImage: p.iconImage as number,
enabled: String(p.enabled),
orderNum: (p.orderNum as number) || 0,
id: Number(p.id),
caption: String(p.caption ?? ""),
iconImage: Number(p.iconImage) || 0,
enabled: String(p.enabled ?? "0"),
orderNum: Number(p.orderNum) || 0,
}),
);
dispatch({ type: "SET_ROOT_PAGES", pages });
if (
pages.length > 0 &&
(!state.activeTabId || !pages.find((p) => p.id === state.activeTabId))
) {
const activeStillValid =
state.activeTabId !== null &&
pages.some((p) => p.id === state.activeTabId);
if (pages.length > 0 && !activeStillValid) {
dispatch({ type: "SET_ACTIVE_TAB", id: pages[0].id });
}
} catch {
@@ -117,9 +123,22 @@ function CatalogManagerInner({
}
}, [dispatch, state.activeTabId]);
// biome-ignore lint/correctness/useExhaustiveDependencies: load once when dialog opens
// Seed from SSR immediately, then refresh from API.
useEffect(() => {
if (open) loadRootPages();
if (!open) return;
if (initialRoots.length > 0 && state.rootPages.length === 0) {
const pages = initialRoots.map((p) => ({
...p,
id: Number(p.id),
enabled: String(p.enabled),
}));
dispatch({ type: "SET_ROOT_PAGES", pages });
if (state.activeTabId === null && pages[0]) {
dispatch({ type: "SET_ACTIVE_TAB", id: pages[0].id });
}
}
void loadRootPages();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open]);
useEffect(() => {
@@ -134,7 +153,6 @@ function CatalogManagerInner({
return;
}
dispatch({ type: "SET_ACTIVE_TAB", id });
dispatch({ type: "CLEAR_TREE" });
};
const handleCreateRoot = async () => {
@@ -166,7 +184,6 @@ function CatalogManagerInner({
const handleEditTab = (id: number) => {
if (state.activeTabId !== id) {
dispatch({ type: "SET_ACTIVE_TAB", id });
dispatch({ type: "CLEAR_TREE" });
}
if (
state.editorDirty &&