+ {subpageParentId !== null ? (
+
+
setSubpageName(e.target.value)}
+ placeholder="Subpage name…"
+ autoFocus
+ className="h-7 text-sm"
+ onKeyDown={(e) => {
+ if (e.key === "Enter") void handleCreateSubpage();
+ if (e.key === "Escape") {
+ setSubpageParentId(null);
+ setSubpageName("");
+ }
+ }}
+ />
+
+
+
+ ) : null}
@@ -927,58 +945,6 @@ export function SortableTree({
)}
-
);
}
diff --git a/src/components/admin/catalog-manager/use-catalog-store.tsx b/src/components/admin/catalog-manager/use-catalog-store.tsx
index 4b1aafca..4b9609e6 100644
--- a/src/components/admin/catalog-manager/use-catalog-store.tsx
+++ b/src/components/admin/catalog-manager/use-catalog-store.tsx
@@ -68,6 +68,7 @@ export type CatalogAction =
| { type: "EXPAND_PATH"; ids: number[] }
| { type: "COLLAPSE_ALL" }
| { type: "SET_NODES"; nodes: TreeNodeData[] }
+ | { type: "SET_FULL_TREE"; nodes: TreeNodeData[] }
| { type: "SET_CHILDREN"; parentId: number; children: TreeNodeData[] }
| { type: "UPDATE_NODE"; update: TreeNodeUpdate }
| { type: "REMOVE_NODE"; id: number }
@@ -138,6 +139,24 @@ function reducer(
for (const n of action.nodes) next.set(n.id, n);
return { ...state, nodes: next };
}
+ case "SET_FULL_TREE": {
+ const nodes = new Map
();
+ const childrenMap = new Map();
+ for (const n of action.nodes) {
+ nodes.set(n.id, n);
+ const siblings = childrenMap.get(n.parentId) || [];
+ siblings.push(n.id);
+ childrenMap.set(n.parentId, siblings);
+ }
+ for (const [parentId, ids] of childrenMap) {
+ ids.sort(
+ (a, b) =>
+ (nodes.get(a)?.orderNum ?? 0) - (nodes.get(b)?.orderNum ?? 0),
+ );
+ childrenMap.set(parentId, ids);
+ }
+ return { ...state, nodes, childrenMap };
+ }
case "SET_CHILDREN": {
const nextNodes = new Map(state.nodes);
const nextChildren = new Map(state.childrenMap);
diff --git a/src/components/admin/confirm-dialog.tsx b/src/components/admin/confirm-dialog.tsx
index 1d5e7d66..2e324d0f 100644
--- a/src/components/admin/confirm-dialog.tsx
+++ b/src/components/admin/confirm-dialog.tsx
@@ -1,15 +1,9 @@
"use client";
-import { useCallback, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { createPortal } from "react-dom";
import { Button } from "@/components/ui/button";
-import {
- Dialog,
- DialogContent,
- DialogDescription,
- DialogFooter,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
+import { cn } from "@/lib/utils";
export type ConfirmDialogProps = {
open: boolean;
@@ -23,6 +17,10 @@ export type ConfirmDialogProps = {
onConfirm: () => void | Promise;
};
+/**
+ * Portal-based confirm modal (not Dialog.Root) so it can safely stack
+ * above Visual Manager / other Base UI dialogs without nested-root bugs.
+ */
export function ConfirmDialog({
open,
onOpenChange,
@@ -35,8 +33,20 @@ export function ConfirmDialog({
onConfirm,
}: ConfirmDialogProps) {
const [busy, setBusy] = useState(false);
+ const [mounted, setMounted] = useState(false);
const isLoading = loading ?? busy;
+ useEffect(() => setMounted(true), []);
+
+ useEffect(() => {
+ if (!open) return;
+ const onKey = (e: KeyboardEvent) => {
+ if (e.key === "Escape" && !isLoading) onOpenChange(false);
+ };
+ document.addEventListener("keydown", onKey);
+ return () => document.removeEventListener("keydown", onKey);
+ }, [open, isLoading, onOpenChange]);
+
async function handleConfirm() {
setBusy(true);
try {
@@ -47,20 +57,38 @@ export function ConfirmDialog({
}
}
- return (
-