diff --git a/src/components/admin/catalog/catalog-items-table/catalog-items-table.tsx b/src/components/admin/catalog/catalog-items-table/catalog-items-table.tsx index 74e02a37..9a03ee4f 100644 --- a/src/components/admin/catalog/catalog-items-table/catalog-items-table.tsx +++ b/src/components/admin/catalog/catalog-items-table/catalog-items-table.tsx @@ -90,12 +90,12 @@ import { import { useServerAction } from "@/hooks/use-server-action"; import { getSuggestions, recordMove } from "@/lib/move-suggestions"; import { cn } from "@/lib/utils"; +import { AddItemFormFields } from "./add-item-form-fields"; import { InteractionTypeCombobox, MovePageCombobox, ToggleField, } from "./field-helpers"; -import { AddItemFormFields } from "./add-item-form-fields"; import { FurniIcon } from "./furni-icon"; import { SongPicker } from "./song-picker"; import { diff --git a/src/components/admin/studio/organize-imports-dialog.tsx b/src/components/admin/studio/organize-imports-dialog.tsx index 58795ebe..f848eca7 100644 --- a/src/components/admin/studio/organize-imports-dialog.tsx +++ b/src/components/admin/studio/organize-imports-dialog.tsx @@ -1,13 +1,11 @@ "use client"; -import { useVirtualizer } from "@tanstack/react-virtual"; import { AlertTriangle, ArrowRight, Eye, Folder, FolderTree, - GripVertical, Home, LayoutGrid, Loader2, @@ -21,7 +19,7 @@ import { } from "lucide-react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; import { type OrganizeImportGroup as OrganizeGroupPayload, @@ -29,6 +27,14 @@ import { } from "@/actions/catalog"; import { IconPicker } from "@/components/admin/catalog/icon-picker"; import { CatalogIcon, LAYOUT_COLORS } from "@/components/admin/catalog-tree"; +import type { + OrganizeImportGroup, + OrganizeImportItem, +} from "@/components/admin/studio/organize-imports-dialog/mall-helpers"; +import { + GroupItemList, + LayoutPreview, +} from "@/components/admin/studio/organize-imports-dialog/mall-helpers"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { @@ -51,7 +57,6 @@ import { isValidCatalogLayout, LAYOUT_DESCRIPTIONS, } from "@/lib/catalog-layouts"; -import { getLocalIconUrl } from "@/lib/furni/classname"; import { cn } from "@/lib/utils"; const MAX_ITEMS_PER_RUN = 500; @@ -96,28 +101,6 @@ interface RootPage { caption: string; } -interface OrganizeImportItem { - itemId: number; - catalogItemId: number | null; - alreadyPlaced: boolean; - itemName: string; - publicName: string; - spriteId: number; - type: string; - interactionType: string; - sourcePageCaption: string | null; -} - -interface OrganizeImportGroup { - name: string; - icon: number; - layout: CatalogLayout | null; - total: number; - moved: number; - added: number; - items: OrganizeImportItem[]; -} - type MoveEntry = | { kind: "item"; item: OrganizeImportItem; from: string; to: string } | { @@ -167,16 +150,6 @@ interface OrganizeImportsDialogProps { onOpenChange: (open: boolean) => void; } -/** Best-effort grid dimensions for the better-known catalog layouts. */ -const LAYOUT_GRIDS: Record = { - default_3x3: { cols: 3, rows: 3 }, - sold_ltd_items: { cols: 3, rows: 3 }, - room_bundle: { cols: 3, rows: 2 }, - badge_display: { cols: 3, rows: 3 }, - spaces_new: { cols: 4, rows: 2 }, - single_bundle: { cols: 1, rows: 1 }, -}; - export function OrganizeImportsDialog({ open, onOpenChange, @@ -1598,228 +1571,3 @@ export function OrganizeImportsDialog({ ); } - -interface GroupItemListProps { - items: OrganizeImportItem[]; - moveTargets: OrganizeImportGroup[]; - selection: Set; - destDupes: Set; - onToggleSelect: (itemId: number, checked: boolean) => void; - onMoveItem: (item: OrganizeImportItem, toName: string) => void; - onDragStart: (item: OrganizeImportItem) => void; - onDragEnd: (item: OrganizeImportItem) => void; -} - -// Virtualized row list, scrolled inside its own container so groups with -// hundreds of furniture items stay smooth. -function GroupItemList({ - items, - moveTargets, - selection, - destDupes, - onToggleSelect, - onMoveItem, - onDragStart, - onDragEnd, -}: GroupItemListProps) { - const t = useTranslations("pages.admin.catalog.workspace.organizeImports"); - const scrollRef = useRef(null); - - const rowVirtualizer = useVirtualizer({ - count: items.length, - getScrollElement: () => scrollRef.current, - estimateSize: () => 34, - overscan: 14, - }); - - return ( -
8 ? "max-h-64" : "max-h-none", - )} - style={{ height: items.length > 8 ? undefined : "auto" }} - > - {items.length === 0 ? ( -

- {t("emptyCategory")} -

- ) : ( -
- {rowVirtualizer.getVirtualItems().map((vItem) => { - const item = items[vItem.index]; - const checked = selection.has(item.itemId); - const isDupe = destDupes.has(item.itemName); - return ( - // biome-ignore lint/a11y/noStaticElementInteractions: draggable row with native HTML5 DnD -
{ - e.dataTransfer.effectAllowed = "move"; - e.dataTransfer.setData("text/plain", String(item.itemId)); - onDragStart(item); - }} - onDragEnd={() => onDragEnd(item)} - > - - - onToggleSelect(item.itemId, e.target.checked) - } - /> - -
-

- {item.publicName || item.itemName} -

-

- {item.itemName} -

-
- {item.alreadyPlaced ? ( - - {item.sourcePageCaption - ? item.sourcePageCaption.replace( - new RegExp(`^${t("impPrefix")}`), - "", - ) - : t("isMoved")} - - ) : ( - - {t("isNew")} - - )} - {isDupe && ( - - {t("dupeOnDestShort")} - - )} - {moveTargets.length > 0 && ( - - )} -
- ); - })} -
- )} -
- ); -} - -/** Schematic grid of how a layout places the first furniture items. */ -function LayoutPreview({ - layout, - items, - compact = false, -}: { - layout: string; - items: OrganizeImportItem[]; - compact?: boolean; -}) { - const grid = LAYOUT_GRIDS[layout]; - if (!grid) { - return ( -
- - {layout} -
- ); - } - const cells = grid.cols * grid.rows; - return ( -
- {Array.from({ length: cells }).map((_, index) => { - const item = items[index]; - return item ? ( -
- -
- ) : ( -
- ); - })} -
- ); -} - -function FurniThumb({ - item, - size = 24, -}: { - item: { itemName: string }; - size?: number; -}) { - const primary = getLocalIconUrl(item.itemName); - return ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - {item.itemName} { - const img = e.target as HTMLImageElement; - img.style.opacity = "0"; - }} - loading="lazy" - /> -
- ); -} diff --git a/src/components/admin/studio/organize-imports-dialog/mall-helpers.tsx b/src/components/admin/studio/organize-imports-dialog/mall-helpers.tsx new file mode 100644 index 00000000..30270787 --- /dev/null +++ b/src/components/admin/studio/organize-imports-dialog/mall-helpers.tsx @@ -0,0 +1,264 @@ +"use client"; + +import { useVirtualizer } from "@tanstack/react-virtual"; +import { GripVertical, LayoutGrid } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useRef } from "react"; +import type { CatalogLayout } from "@/lib/catalog-layouts"; +import { getLocalIconUrl } from "@/lib/furni/classname"; +import { cn } from "@/lib/utils"; +export interface OrganizeImportItem { + itemId: number; + catalogItemId: number | null; + alreadyPlaced: boolean; + itemName: string; + publicName: string; + spriteId: number; + type: string; + interactionType: string; + sourcePageCaption: string | null; +} + +export interface OrganizeImportGroup { + name: string; + icon: number; + layout: CatalogLayout | null; + total: number; + moved: number; + added: number; + items: OrganizeImportItem[]; +} + +export const LAYOUT_GRIDS: Record = { + default_3x3: { cols: 3, rows: 3 }, + sold_ltd_items: { cols: 3, rows: 3 }, + room_bundle: { cols: 3, rows: 2 }, + badge_display: { cols: 3, rows: 3 }, + spaces_new: { cols: 4, rows: 2 }, + single_bundle: { cols: 1, rows: 1 }, +}; + +export function FurniThumb({ + item, + size = 24, +}: { + item: { itemName: string }; + size?: number; +}) { + const primary = getLocalIconUrl(item.itemName); + return ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {item.itemName} { + const img = e.target as HTMLImageElement; + img.style.opacity = "0"; + }} + loading="lazy" + /> +
+ ); +} + +/** Schematic grid of how a layout places the first furniture items. */ +export function LayoutPreview({ + layout, + items, + compact = false, +}: { + layout: string; + items: OrganizeImportItem[]; + compact?: boolean; +}) { + const grid = LAYOUT_GRIDS[layout]; + if (!grid) { + return ( +
+ + {layout} +
+ ); + } + const cells = grid.cols * grid.rows; + return ( +
+ {Array.from({ length: cells }).map((_, index) => { + const item = items[index]; + return item ? ( +
+ +
+ ) : ( +
+ ); + })} +
+ ); +} + +interface GroupItemListProps { + items: OrganizeImportItem[]; + moveTargets: OrganizeImportGroup[]; + selection: Set; + destDupes: Set; + onToggleSelect: (itemId: number, checked: boolean) => void; + onMoveItem: (item: OrganizeImportItem, toName: string) => void; + onDragStart: (item: OrganizeImportItem) => void; + onDragEnd: (item: OrganizeImportItem) => void; +} + +// Virtualized row list, scrolled inside its own container so groups with +// hundreds of furniture items stay smooth. +export function GroupItemList({ + items, + moveTargets, + selection, + destDupes, + onToggleSelect, + onMoveItem, + onDragStart, + onDragEnd, +}: GroupItemListProps) { + const t = useTranslations("pages.admin.catalog.workspace.organizeImports"); + const scrollRef = useRef(null); + + const rowVirtualizer = useVirtualizer({ + count: items.length, + getScrollElement: () => scrollRef.current, + estimateSize: () => 34, + overscan: 14, + }); + + return ( +
8 ? "max-h-64" : "max-h-none", + )} + style={{ height: items.length > 8 ? undefined : "auto" }} + > + {items.length === 0 ? ( +

+ {t("emptyCategory")} +

+ ) : ( +
+ {rowVirtualizer.getVirtualItems().map((vItem) => { + const item = items[vItem.index]; + const checked = selection.has(item.itemId); + const isDupe = destDupes.has(item.itemName); + return ( + // biome-ignore lint/a11y/noStaticElementInteractions: draggable row with native HTML5 DnD +
{ + e.dataTransfer.effectAllowed = "move"; + e.dataTransfer.setData("text/plain", String(item.itemId)); + onDragStart(item); + }} + onDragEnd={() => onDragEnd(item)} + > + + + onToggleSelect(item.itemId, e.target.checked) + } + /> + +
+

+ {item.publicName || item.itemName} +

+

+ {item.itemName} +

+
+ {item.alreadyPlaced ? ( + + {item.sourcePageCaption + ? item.sourcePageCaption.replace( + new RegExp(`^${t("impPrefix")}`), + "", + ) + : t("isMoved")} + + ) : ( + + {t("isNew")} + + )} + {isDupe && ( + + {t("dupeOnDestShort")} + + )} + {moveTargets.length > 0 && ( + + )} +
+ ); + })} +
+ )} +
+ ); +}