"use client"; /** * MoveToDialog — "Move page to…" (topbar / sidebar-tree entry points). * * A page IS a folder when it has children (`Page.parentId` + fractional * position; there is no separate folder entity). Moving a page is * `PATCH /pages/:id { parentId }` — cycle protection (a page can't become * its own descendant) and trashed-parent rejection are enforced * server-side (page.service.ts `update`); the dialog surfaces those * rejections as a toast. * * The destination picker is the workspace page tree, lazy per level (the * same `usePageTree` hook + sibling-list endpoint the sidebar uses), with a * "Root (top level)" option (parentId = null). The moved page itself is * excluded from the picker; its descendants stay reachable but are never a * useful target (after the move they'd be outside the destination) and the * server rejects any true cycle anyway. * * Mounted at the workspace shell (like SearchDialog / GeneratePageDialog) * and opened through the ui-store (`setMove`), so the topbar menu and the * sidebar tree rows can open it without prop-drilling. */ import { useEffect, useState } from "react"; import { ChevronRight, FolderClosed, FolderOpen, Home, Inbox } from "lucide-react"; import { Dialog, DialogFooter } from "./dialog"; import { isApiError } from "@/lib/api"; import { usePageTree, useUpdatePage } from "@/hooks/use-pages"; import { useUiStore } from "@/stores/ui-store"; import { cn } from "@/lib/utils"; import type { PageTreeEntry } from "@/lib/types"; export function MoveToDialog() { const open = useUiStore((s) => s.moveOpen); const context = useUiStore((s) => s.moveContext); const setMove = useUiStore((s) => s.setMove); const pushToast = useUiStore((s) => s.pushToast); const pageId = context?.pageId ?? null; const workspaceId = context?.workspaceId ?? ""; // undefined = nothing picked yet (Move disabled); null = root; string = page. const [selected, setSelected] = useState(undefined); const [busy, setBusy] = useState(false); const updatePage = useUpdatePage(workspaceId); // Unconditional (hooks rule): disabled while no workspace is in context. const rootQuery = usePageTree(workspaceId, null); // Reset the selection each time the dialog opens for a page. useEffect(() => { if (open) setSelected(undefined); }, [open, pageId]); if (!open || !context || !pageId) return null; const title = context.title || "This page"; const onAction = () => { if (selected === undefined) return; setBusy(true); updatePage.mutate( { id: pageId, parentId: selected ?? null }, { onSuccess: () => { setBusy(false); setMove(false, null); pushToast({ kind: "success", title: "Moved", description: selected === null ? `“${title}” is now at the top level` : `“${title}” moved into its new folder` }); }, onError: (e: Error) => { setBusy(false); const msg = isApiError(e) && e.message && e.message.length < 140 ? e.message : e.message || "Move failed"; pushToast({ kind: "error", title: "Move failed", description: msg }); } } ); }; const entries = (rootQuery.data ?? []).filter((e) => e.id !== pageId); return ( !busy && setMove(false, null)} testId="move-dialog" footer={ setMove(false, null)} onAction={onAction} actionLabel="Move here" busy={busy} disabled={selected === undefined} testId="move-dialog-confirm" /> } >
setSelected(null)} />
or into a page
{rootQuery.isLoading ? (

Loading pages…

) : ( )}
); } /** The "Root (top level)" row — move back to the sidebar top level. */ function RootOption({ selected, onPick }: { selected: boolean; onPick: () => void }) { return ( ); } /** Lazy, recursive destination picker (one level loaded per expand). */ function MoveTreePicker({ entries, workspaceId, selected, onSelect, depth = 0 }: { entries: PageTreeEntry[]; workspaceId: string; selected: string | null | undefined; onSelect: (id: string | null) => void; depth?: number; }) { if (entries.length === 0) { return

No pages here yet.

; } return ( ); } function MovePickerNode({ entry, workspaceId, selected, onSelect, depth }: { entry: PageTreeEntry; workspaceId: string; selected: string | null | undefined; onSelect: (id: string | null) => void; depth: number; }) { const [expanded, setExpanded] = useState(false); const childrenQuery = usePageTree(workspaceId, entry.id); const children = entry.children ?? childrenQuery.data ?? []; const hasChildren = Boolean(entry.hasMore || (entry.children?.length ?? 0) > 0); const isSelected = selected === entry.id; return (
  • {hasChildren ? ( expanded ? ( ) : ( ) ) : ( )} {isSelected ? : null}
    {expanded ? ( children.length > 0 || childrenQuery.isLoading ? ( ) : (

    {childrenQuery.isLoading ? "Loading…" : "Empty"}

    ) ) : null}
  • ); } function PickMark() { return ( ✓ ); }