diff --git a/src/modules/library/api/useLibraryContent.ts b/src/modules/library/api/useLibraryContent.ts index d40ee1a..ef5adc0 100644 --- a/src/modules/library/api/useLibraryContent.ts +++ b/src/modules/library/api/useLibraryContent.ts @@ -61,8 +61,8 @@ export function useLibraryContent({ const selectMode = useLibrarySelectionStore((s) => s.selectMode); const isSearching = searchQuery.length > 0; const isPrioritySort = sort.field === "priority"; - const dndDisabled = - isSearching || isPatcherActive || !isPrioritySort || hasActiveFilters || selectMode; + const dndDisabled = isSearching || isPatcherActive || hasActiveFilters || selectMode; + const reorderDisabled = !isPrioritySort; const isFlatMode = isSearching || hasActiveFilters; const folderMap = useMemo(() => { @@ -140,6 +140,7 @@ export function useLibraryContent({ return { viewMode, dndDisabled, + reorderDisabled, selectMode, contentView, detailsMod, diff --git a/src/modules/library/api/useReorderMods.ts b/src/modules/library/api/useReorderMods.ts index b9890e2..b1d73f9 100644 --- a/src/modules/library/api/useReorderMods.ts +++ b/src/modules/library/api/useReorderMods.ts @@ -6,7 +6,9 @@ import { unwrapForQuery } from "@/utils/query"; import { libraryKeys } from "./keys"; /** - * Hook to reorder enabled mods in the active profile. + * Hook to reorder mods in the active profile. + * Accepts a partial list of mod IDs (e.g. root mods only) and automatically + * appends any remaining mods from the cache so the backend receives the full set. * Uses optimistic updates for instant UI feedback. */ export function useReorderMods() { @@ -14,7 +16,9 @@ export function useReorderMods() { return useMutation({ mutationFn: async (modIds) => { - const result = await api.reorderMods(modIds); + const allMods = queryClient.getQueryData(libraryKeys.mods()); + const fullOrder = buildFullOrder(modIds, allMods); + const result = await api.reorderMods(fullOrder); return unwrapForQuery(result); }, onMutate: async (modIds) => { @@ -26,7 +30,18 @@ export function useReorderMods() { if (!old) return old; const modMap = new Map(old.map((m) => [m.id, m])); - return modIds.map((id) => modMap.get(id)).filter(Boolean) as InstalledMod[]; + const reorderedSet = new Set(modIds); + const folderMods = old.filter((m) => !reorderedSet.has(m.id)); + const reorderedMods = modIds + .map((id) => { + const mod = modMap.get(id); + if (!mod) { + console.warn(`[useReorderMods] mod ID "${id}" not found in cache, skipping`); + } + return mod; + }) + .filter(Boolean) as InstalledMod[]; + return [...reorderedMods, ...folderMods]; }); return { previous }; @@ -41,3 +56,11 @@ export function useReorderMods() { }, }); } + +/** Build the full mod ID list the backend expects: reordered IDs first, then any remaining. */ +function buildFullOrder(reorderedIds: string[], allMods: InstalledMod[] | undefined): string[] { + if (!allMods) return reorderedIds; + const reorderedSet = new Set(reorderedIds); + const remaining = allMods.filter((m) => !reorderedSet.has(m.id)).map((m) => m.id); + return [...reorderedIds, ...remaining]; +} diff --git a/src/modules/library/api/useRootModDnd.ts b/src/modules/library/api/useRootModDnd.ts index 49774db..b268d88 100644 --- a/src/modules/library/api/useRootModDnd.ts +++ b/src/modules/library/api/useRootModDnd.ts @@ -10,9 +10,10 @@ import { useMoveModToFolder } from "./useMoveMod"; interface UseRootModDndArgs { rootMods: InstalledMod[]; onReorder: (modIds: string[]) => void; + reorderDisabled: boolean; } -export function useRootModDnd({ rootMods, onReorder }: UseRootModDndArgs) { +export function useRootModDnd({ rootMods, onReorder, reorderDisabled }: UseRootModDndArgs) { const moveModToFolder = useMoveModToFolder(); const rootModIds = useMemo(() => rootMods.map((m) => m.id), [rootMods]); @@ -21,6 +22,11 @@ export function useRootModDnd({ rootMods, onReorder }: UseRootModDndArgs) { const [activeId, setActiveId] = useState(null); const [localOrder, setLocalOrder] = useState(rootModIds); const lastPropsOrder = useRef(rootModIds); + const localOrderRef = useRef(localOrder); + + useEffect(() => { + localOrderRef.current = localOrder; + }, [localOrder]); useEffect(() => { if (hasOrderChanged(rootModIds, lastPropsOrder.current)) { @@ -40,20 +46,24 @@ export function useRootModDnd({ rootMods, onReorder }: UseRootModDndArgs) { setActiveId(event.active.id as string); }, []); - const handleDragOver = useCallback((event: DragOverEvent) => { - const { active, over } = event; - if (!over || active.id === over.id) return; - - const overId = over.id as string; - if (resolveFolderId(overId)) return; - - setLocalOrder((prev) => { - const oldIndex = prev.indexOf(active.id as string); - const newIndex = prev.indexOf(overId); - if (oldIndex === -1 || newIndex === -1) return prev; - return arrayMove(prev, oldIndex, newIndex); - }); - }, []); + const handleDragOver = useCallback( + (event: DragOverEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const overId = over.id as string; + if (resolveFolderId(overId)) return; + if (reorderDisabled) return; + + setLocalOrder((prev) => { + const oldIndex = prev.indexOf(active.id as string); + const newIndex = prev.indexOf(overId); + if (oldIndex === -1 || newIndex === -1) return prev; + return arrayMove(prev, oldIndex, newIndex); + }); + }, + [reorderDisabled], + ); const handleDragEnd = useCallback( (event: DragEndEvent) => { @@ -68,11 +78,12 @@ export function useRootModDnd({ rootMods, onReorder }: UseRootModDndArgs) { } } - if (hasOrderChanged(localOrder, rootModIds)) { - onReorder(localOrder); + const currentOrder = localOrderRef.current; + if (!reorderDisabled && hasOrderChanged(currentOrder, rootModIds)) { + onReorder(currentOrder); } }, - [localOrder, rootModIds, onReorder, moveModToFolder], + [rootModIds, onReorder, moveModToFolder, reorderDisabled], ); const handleDragCancel = useCallback(() => { diff --git a/src/modules/library/api/useSortableModDnd.ts b/src/modules/library/api/useSortableModDnd.ts index ecd599e..6ed172a 100644 --- a/src/modules/library/api/useSortableModDnd.ts +++ b/src/modules/library/api/useSortableModDnd.ts @@ -13,9 +13,15 @@ interface UseSortableModDndArgs { mods: InstalledMod[]; onReorder: (modIds: string[]) => void; folderId?: string; + reorderDisabled?: boolean; } -export function useSortableModDnd({ mods, onReorder, folderId }: UseSortableModDndArgs) { +export function useSortableModDnd({ + mods, + onReorder, + folderId, + reorderDisabled, +}: UseSortableModDndArgs) { const moveModToFolder = useMoveModToFolder(); const propsOrder = useMemo(() => mods.map((m) => m.id), [mods]); @@ -43,18 +49,22 @@ export function useSortableModDnd({ mods, onReorder, folderId }: UseSortableModD setActiveId(event.active.id as string); }, []); - const handleDragOver = useCallback((event: DragOverEvent) => { - const { active, over } = event; - if (!over || active.id === over.id) return; - if (over.id === REMOVE_FROM_FOLDER_ID) return; - - setLocalOrder((prev) => { - const oldIndex = prev.indexOf(active.id as string); - const newIndex = prev.indexOf(over.id as string); - if (oldIndex === -1 || newIndex === -1) return prev; - return arrayMove(prev, oldIndex, newIndex); - }); - }, []); + const handleDragOver = useCallback( + (event: DragOverEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + if (over.id === REMOVE_FROM_FOLDER_ID) return; + if (reorderDisabled) return; + + setLocalOrder((prev) => { + const oldIndex = prev.indexOf(active.id as string); + const newIndex = prev.indexOf(over.id as string); + if (oldIndex === -1 || newIndex === -1) return prev; + return arrayMove(prev, oldIndex, newIndex); + }); + }, + [reorderDisabled], + ); const handleDragEnd = useCallback( (event: DragEndEvent) => { @@ -65,11 +75,11 @@ export function useSortableModDnd({ mods, onReorder, folderId }: UseSortableModD return; } - if (localOrder.join() !== propsOrder.join()) { + if (!reorderDisabled && localOrder.join() !== propsOrder.join()) { onReorder(localOrder); } }, - [folderId, localOrder, propsOrder, onReorder, moveModToFolder], + [folderId, localOrder, propsOrder, onReorder, moveModToFolder, reorderDisabled], ); const handleDragCancel = useCallback(() => { diff --git a/src/modules/library/api/useUnifiedDnd.ts b/src/modules/library/api/useUnifiedDnd.ts index 18758bf..61f1d37 100644 --- a/src/modules/library/api/useUnifiedDnd.ts +++ b/src/modules/library/api/useUnifiedDnd.ts @@ -24,9 +24,16 @@ interface UseUnifiedDndArgs { rootMods: InstalledMod[]; modsByFolder: Map; onReorder: (modIds: string[]) => void; + reorderDisabled: boolean; } -export function useUnifiedDnd({ folders, rootMods, modsByFolder, onReorder }: UseUnifiedDndArgs) { +export function useUnifiedDnd({ + folders, + rootMods, + modsByFolder, + onReorder, + reorderDisabled, +}: UseUnifiedDndArgs) { const { localOrder: modLocalOrder, orderedRootMods, @@ -35,7 +42,7 @@ export function useUnifiedDnd({ folders, rootMods, modsByFolder, onReorder }: Us handleDragOver: handleModDragOver, handleDragEnd: handleModDragEnd, handleDragCancel: handleModDragCancel, - } = useRootModDnd({ rootMods, onReorder }); + } = useRootModDnd({ rootMods, onReorder, reorderDisabled }); const { folderLocalOrder, @@ -134,6 +141,7 @@ export function useUnifiedDnd({ folders, rootMods, modsByFolder, onReorder }: Us const overFolderMod = folderModLookup.get(overId); if (overFolderMod && overFolderMod.folderId === activeFolderModSource) { + if (reorderDisabled) return; const currentOrder = (modsByFolder.get(activeFolderModSource) ?? []).map((m) => m.id); const oldIndex = currentOrder.indexOf(id); const newIndex = currentOrder.indexOf(overId); @@ -159,6 +167,7 @@ export function useUnifiedDnd({ folders, rootMods, modsByFolder, onReorder }: Us modsByFolder, moveModToFolder, reorderFolderMods, + reorderDisabled, ], ); @@ -186,12 +195,58 @@ export function useUnifiedDnd({ folders, rootMods, modsByFolder, onReorder }: Us const withoutSource = args.droppableContainers.filter( (c) => c.id !== `sortable-folder:${activeSourceFolderId}`, ); - return closestCenter({ ...args, droppableContainers: withoutSource }); + if (reorderDisabled) { + const hits = pointerWithin({ ...args, droppableContainers: withoutSource }); + return hits + .map((hit) => { + const folderMod = folderModLookup.get(hit.id as string); + if (folderMod && folderMod.folderId !== activeSourceFolderId) { + return { ...hit, id: `sortable-folder:${folderMod.folderId}` }; + } + if (folderMod && folderMod.folderId === activeSourceFolderId) { + return null; + } + return hit; + }) + .filter(Boolean) as ReturnType; + } + const folderHit = pointerWithin({ ...args, droppableContainers: withoutSource }).find((c) => + parseSortableFolderId(c.id as string), + ); + if (folderHit) return [folderHit]; + + const siblingsOnly = withoutSource.filter((c) => { + const mod = folderModLookup.get(c.id as string); + return mod && mod.folderId === activeSourceFolderId; + }); + return closestCenter({ ...args, droppableContainers: siblingsOnly }); } - return closestCenter(args); + if (reorderDisabled) { + const hits = pointerWithin(args); + return hits.map((hit) => { + const folderMod = folderModLookup.get(hit.id as string); + if (folderMod) { + return { ...hit, id: `sortable-folder:${folderMod.folderId}` }; + } + return hit; + }); + } + + const withoutFolderMods = args.droppableContainers.filter( + (c) => !folderModLookup.has(c.id as string), + ); + // Use pointerWithin for folder drops (requires pointer inside folder), + // closestCenter for mod reorder (works by proximity to center) + const folderHit = pointerWithin({ ...args, droppableContainers: withoutFolderMods }).find( + (c) => parseSortableFolderId(c.id as string), + ); + if (folderHit) return [folderHit]; + + const rootModsOnly = withoutFolderMods.filter((c) => !parseSortableFolderId(c.id as string)); + return closestCenter({ ...args, droppableContainers: rootModsOnly }); }, - [folderModLookup], + [folderModLookup, reorderDisabled], ); return { diff --git a/src/modules/library/components/LibraryContent.tsx b/src/modules/library/components/LibraryContent.tsx index 8588bb1..3a41ccc 100644 --- a/src/modules/library/components/LibraryContent.tsx +++ b/src/modules/library/components/LibraryContent.tsx @@ -27,6 +27,7 @@ export function LibraryContent({ const { viewMode, dndDisabled, + reorderDisabled, selectMode, contentView, detailsMod, @@ -117,6 +118,7 @@ export function LibraryContent({ reorderFolderMods.mutate({ folderId: contentView.folder.id, modIds }) } disabled={dndDisabled} + reorderDisabled={reorderDisabled} onViewDetails={setDetailsMod} onEditMetadata={setEditMod} className={`${gridClass(viewMode)} stagger-enter mt-4`} @@ -150,6 +152,7 @@ export function LibraryContent({ modsByFolder={contentView.modsByFolder} viewMode={viewMode} dndDisabled={dndDisabled} + reorderDisabled={reorderDisabled} onReorder={(modIds) => reorderMods.mutate(modIds)} onViewDetails={setDetailsMod} onEditMetadata={setEditMod} diff --git a/src/modules/library/components/SortableFolderRow.tsx b/src/modules/library/components/SortableFolderRow.tsx index f594ea8..928624c 100644 --- a/src/modules/library/components/SortableFolderRow.tsx +++ b/src/modules/library/components/SortableFolderRow.tsx @@ -45,15 +45,17 @@ export function SortableFolderRow({
)}
-
e.stopPropagation()} - {...attributes} - {...listeners} - > - -
+ {!sortDisabled && ( +
e.stopPropagation()} + {...attributes} + {...listeners} + > + +
+ )}
void; onEditMetadata?: (mod: InstalledMod) => void; } @@ -17,11 +18,13 @@ interface SortableModCardProps { export function SortableModCard({ mod, viewMode, + reorderDisabled, onViewDetails, onEditMetadata, }: SortableModCardProps) { const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: mod.id, + disabled: reorderDisabled ? { droppable: true } : false, }); const { data: patcherStatus } = usePatcherStatus(); const disabled = patcherStatus?.running ?? false; diff --git a/src/modules/library/components/SortableModList.tsx b/src/modules/library/components/SortableModList.tsx index 5bf240d..8c7e4ff 100644 --- a/src/modules/library/components/SortableModList.tsx +++ b/src/modules/library/components/SortableModList.tsx @@ -41,6 +41,7 @@ interface SortableModListProps { viewMode: "grid" | "list"; onReorder: (modIds: string[]) => void; disabled?: boolean; + reorderDisabled?: boolean; onViewDetails?: (mod: InstalledMod) => void; onEditMetadata?: (mod: InstalledMod) => void; className?: string; @@ -52,6 +53,7 @@ export function SortableModList({ viewMode, onReorder, disabled, + reorderDisabled, onViewDetails, onEditMetadata, className, @@ -66,7 +68,7 @@ export function SortableModList({ handleDragOver, handleDragEnd, handleDragCancel, - } = useSortableModDnd({ mods, onReorder, folderId }); + } = useSortableModDnd({ mods, onReorder, folderId, reorderDisabled }); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), @@ -109,6 +111,7 @@ export function SortableModList({ key={mod.id} mod={mod} viewMode={viewMode} + reorderDisabled={reorderDisabled} onViewDetails={onViewDetails} onEditMetadata={onEditMetadata} /> diff --git a/src/modules/library/components/UnifiedDndGrid.tsx b/src/modules/library/components/UnifiedDndGrid.tsx index 34553e3..5f5d0f1 100644 --- a/src/modules/library/components/UnifiedDndGrid.tsx +++ b/src/modules/library/components/UnifiedDndGrid.tsx @@ -28,6 +28,7 @@ interface UnifiedDndGridProps { modsByFolder: Map; viewMode: "grid" | "list"; dndDisabled: boolean; + reorderDisabled: boolean; onReorder: (modIds: string[]) => void; onViewDetails?: (mod: InstalledMod) => void; onEditMetadata?: (mod: InstalledMod) => void; @@ -39,6 +40,7 @@ export function UnifiedDndGrid({ modsByFolder, viewMode, dndDisabled, + reorderDisabled, onReorder, onViewDetails, onEditMetadata, @@ -67,6 +69,7 @@ export function UnifiedDndGrid({ rootMods={rootMods} modsByFolder={modsByFolder} viewMode={viewMode} + reorderDisabled={reorderDisabled} onReorder={onReorder} onViewDetails={onViewDetails} onEditMetadata={onEditMetadata} @@ -129,6 +132,7 @@ interface DndGridProps { rootMods: InstalledMod[]; modsByFolder: Map; viewMode: "grid" | "list"; + reorderDisabled: boolean; onReorder: (modIds: string[]) => void; onViewDetails?: (mod: InstalledMod) => void; onEditMetadata?: (mod: InstalledMod) => void; @@ -139,6 +143,7 @@ function DndGrid({ rootMods, modsByFolder, viewMode, + reorderDisabled, onReorder, onViewDetails, onEditMetadata, @@ -156,7 +161,7 @@ function DndGrid({ handleDragOver, handleDragEnd, handleDragCancel, - } = useUnifiedDnd({ folders, rootMods, modsByFolder, onReorder }); + } = useUnifiedDnd({ folders, rootMods, modsByFolder, onReorder, reorderDisabled }); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), @@ -191,7 +196,7 @@ function DndGrid({ sortableId={sortableId} folder={folder} mods={folderMods} - sortDisabled={isDraggingMod || isDraggingFolderMod} + sortDisabled={reorderDisabled || isDraggingMod || isDraggingFolderMod} onViewDetails={onViewDetails} onEditMetadata={onEditMetadata} /> @@ -203,7 +208,7 @@ function DndGrid({ sortableId={sortableId} folder={folder} mods={folderMods} - sortDisabled={isDraggingMod || isDraggingFolderMod} + sortDisabled={reorderDisabled || isDraggingMod || isDraggingFolderMod} /> ); })} @@ -213,6 +218,7 @@ function DndGrid({ key={mod.id} mod={mod} viewMode={viewMode} + reorderDisabled={reorderDisabled} onViewDetails={onViewDetails} onEditMetadata={onEditMetadata} />