"use client"; import { closestCenter, DndContext, type DragEndEvent, DragOverlay, type DragStartEvent, PointerSensor, useSensor, useSensors, } from "@dnd-kit/core"; import { arrayMove, horizontalListSortingStrategy, SortableContext, useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { Copy, Loader2, Pencil, Plus, Power, PowerOff, Trash2, } from "lucide-react"; import { useTranslations } from "next-intl"; import { useCallback, useState } from "react"; import { toast } from "sonner"; import { deleteTreePage, reorderCatalogPages, toggleCatalogPage, } from "@/actions/catalog"; import { deleteBcTreePage, reorderBcCatalogPages, toggleBcPage, } from "@/actions/catalog-bc"; import { CatalogIcon } from "@/components/admin/catalog-tree"; import { useConfirmDialog } from "@/components/admin/confirm-dialog"; import { Button } from "@/components/ui/button"; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuTrigger, } from "@/components/ui/context-menu"; import { requestCatalogNavigation } from "@/features/catalog/client/use-catalog-navigation"; import { DuplicateCategoryDialog } from "@/features/catalog/components/duplicate-category-dialog"; import type { DuplicateInput } from "@/features/catalog/domain/duplicate"; import { cn } from "@/lib/utils"; import { useCatalogManager } from "./use-catalog-store"; export type { RootTab } from "@/types/catalog"; import type { RootTab } from "@/types/catalog"; interface RootTabsProps { tabs: RootTab[]; activeTabId: number | null; onTabClick: (id: number) => void | Promise; onEditTab: (id: number) => void | Promise; onCreateRoot: () => void; loading?: boolean; onMutation?: () => void; } export function RootTabs({ tabs, activeTabId, onTabClick, onEditTab, onCreateRoot, loading, onMutation, }: RootTabsProps) { const { catalogType, canEdit, state } = useCatalogManager(); const t = useTranslations("pages.admin.catalog.workspace.duplicate"); const { confirm, dialog: confirmDialog } = useConfirmDialog(); const [duplicateSource, setDuplicateSource] = useState( null, ); const [activeDragId, setActiveDragId] = useState(null); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), ); const handleDragStart = useCallback((event: DragStartEvent) => { setActiveDragId(event.active.id as number); }, []); const handleDragEnd = useCallback( async (event: DragEndEvent) => { const { active, over } = event; setActiveDragId(null); if (!over || active.id === over.id) return; const oldIndex = tabs.findIndex((t) => t.id === active.id); const newIndex = tabs.findIndex((t) => t.id === over.id); if (oldIndex === -1 || newIndex === -1) return; if (!canEdit || !requestCatalogNavigation()) return; const newOrder = arrayMove(tabs, oldIndex, newIndex); // Batch reorder try { const reorder = catalogType === "bc" ? reorderBcCatalogPages : reorderCatalogPages; const result = await reorder({ parentId: -1, ids: newOrder.map((tab) => tab.id), expectedIds: tabs.map((tab) => tab.id), }); if (!result.ok) throw Error("Failed to reorder tabs"); toast.success("Tabs reordered"); onMutation?.(); } catch { toast.error("Failed to reorder tabs"); } }, [tabs, canEdit, catalogType, onMutation], ); const handleToggle = async (tab: RootTab) => { if (!canEdit) return; try { if (catalogType === "bc") { await toggleBcPage({ id: tab.id, field: "enabled" }); } else { await toggleCatalogPage({ id: tab.id, action: "toggleEnabled" }); } toast.success( `${tab.caption} ${tab.enabled === "1" ? "disabled" : "enabled"}`, ); onMutation?.(); } catch { toast.error("Failed to toggle"); } }; const handleDelete = async (tab: RootTab) => { if (!canEdit) return; const ok = await confirm({ title: "Delete root page", description: `Delete root page "${tab.caption}"? Child pages (if any) will be moved to root; items on this page will be deleted.`, confirmLabel: "Delete", }); if (!ok) return; try { if (catalogType === "bc") { await deleteBcTreePage({ pageId: tab.id, mode: "reparent" }); } else { await deleteTreePage({ pageId: tab.id, mode: "reparent" }); } toast.success(`"${tab.caption}" deleted`); onMutation?.(); } catch { toast.error("Failed to delete"); } }; const draggedTab = activeDragId ? tabs.find((t) => t.id === activeDragId) : null; return (
{confirmDialog} {duplicateSource && ( setDuplicateSource(null)} onComplete={() => onMutation?.()} /> )} {loading && ( )} t.id)} strategy={horizontalListSortingStrategy} > {tabs.map((tab) => ( onTabClick(tab.id)} onEdit={() => onEditTab(tab.id)} onToggle={() => handleToggle(tab)} onDelete={() => handleDelete(tab)} onDuplicate={() => { if (state.editorDirty) { toast.error(t("draftBlocked")); return; } if (canEdit && requestCatalogNavigation()) setDuplicateSource({ kind: catalogType, sourceId: tab.id, parentId: -1, name: `${tab.caption.slice(0, 121)} (copy)`, }); }} /> ))} {draggedTab && ( )} {canEdit && ( )}
); } // ── Sortable root tab ───────────────────────────────────────── function SortableRootTab({ tab, isActive, onClick, onEdit, onToggle, onDelete, onDuplicate, }: { tab: RootTab; isActive: boolean; onClick: () => void; onEdit: () => void; onToggle: () => void; onDelete: () => void; onDuplicate: () => void; }) { const { attributes, listeners, setNodeRef, transform, transition, isDragging, } = useSortable({ id: tab.id, }); const t = useTranslations("pages.admin.catalog.workspace.duplicate"); const { canEdit } = useCatalogManager(); const style: React.CSSProperties = { transform: CSS.Transform.toString(transform), transition, opacity: isDragging ? 0.3 : 1, }; return (
{canEdit && ( {t("title")} )} Edit page {tab.enabled === "1" ? ( ) : ( )} {tab.enabled === "1" ? "Disable" : "Enable"} Delete
); } // ── Tab button render ───────────────────────────────────────── function TabButton({ tab, isActive, isOverlay, onClick, onDoubleClick, }: { tab: RootTab; isActive: boolean; isOverlay?: boolean; onClick?: () => void; onDoubleClick?: () => void; }) { return ( ); }