mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
fix(sidebar): reorder sidebar nav items by id to stop drag drift (#1206)
* fix(sidebar): reorder nav items by id instead of positional index The Settings sidebar customizer rendered library rows with one filter and reordered them with another, so a hidden/gated item (luckyMix when AudioMuse is unavailable) made the rendered list shorter than the reorder list and drag indices landed one slot off — rows jumped back on drop. Replace the index-based reorder with an id-based primitive shared by the customizer and the in-sidebar long-press DnD, so a render filter can no longer desync the move. Unknown, cross-section, conserved, or no-op drops are rejected. * docs(changelog): note sidebar reorder fix (#1206)
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { SidebarItemConfig } from '../store/sidebarStore';
|
||||
import {
|
||||
applySidebarDropReorder,
|
||||
getLibraryItemsForReorder,
|
||||
getSystemItemsForReorder,
|
||||
applySidebarReorderById,
|
||||
isSidebarNavItemUserHideable,
|
||||
type SidebarNavDropTarget,
|
||||
} from '../utils/componentHelpers/sidebarNavReorder';
|
||||
@@ -15,13 +13,12 @@ import {
|
||||
|
||||
interface NavDndState {
|
||||
section: 'library' | 'system';
|
||||
fromIdx: number;
|
||||
draggedId: string;
|
||||
}
|
||||
|
||||
interface Args {
|
||||
isCollapsed: boolean;
|
||||
sidebarItemsRef: React.MutableRefObject<SidebarItemConfig[]>;
|
||||
randomNavModeRef: React.MutableRefObject<'hub' | 'separate'>;
|
||||
setSidebarItems: (items: SidebarItemConfig[]) => void;
|
||||
}
|
||||
|
||||
@@ -30,12 +27,12 @@ interface Result {
|
||||
navDropTarget: SidebarNavDropTarget | null;
|
||||
navDndTrashHint: { x: number; y: number } | null;
|
||||
suppressNavClickRef: React.MutableRefObject<boolean>;
|
||||
handleNavRowPointerDown: (e: React.PointerEvent, section: 'library' | 'system', sectionIdx: number) => void;
|
||||
navDndRowClass: (section: 'library' | 'system', sectionIdx: number) => string;
|
||||
handleNavRowPointerDown: (e: React.PointerEvent, section: 'library' | 'system', id: string) => void;
|
||||
navDndRowClass: (section: 'library' | 'system', id: string) => string;
|
||||
}
|
||||
|
||||
export function useSidebarNavDnd({
|
||||
isCollapsed, sidebarItemsRef, randomNavModeRef, setSidebarItems,
|
||||
isCollapsed, sidebarItemsRef, setSidebarItems,
|
||||
}: Args): Result {
|
||||
const [navDnd, setNavDnd] = useState<NavDndState | null>(null);
|
||||
const [navDropTarget, setNavDropTarget] = useState<SidebarNavDropTarget | null>(null);
|
||||
@@ -69,13 +66,13 @@ export function useSidebarNavDnd({
|
||||
const section = row.dataset.sidebarSection as 'library' | 'system' | undefined;
|
||||
if (section !== navDnd.section) continue;
|
||||
const rect = row.getBoundingClientRect();
|
||||
const idx = Number(row.dataset.sidebarIdx);
|
||||
if (Number.isNaN(idx)) continue;
|
||||
const id = row.dataset.sidebarId;
|
||||
if (!id) continue;
|
||||
if (clientY < rect.top + rect.height / 2) {
|
||||
target = { idx, before: true, section };
|
||||
target = { id, before: true, section };
|
||||
break;
|
||||
}
|
||||
target = { idx, before: false, section };
|
||||
target = { id, before: false, section };
|
||||
}
|
||||
navDropTargetRef.current = target;
|
||||
setNavDropTarget(target);
|
||||
@@ -99,11 +96,7 @@ export function useSidebarNavDnd({
|
||||
|
||||
const { x, y } = lastPointerDuringNavDndRef.current;
|
||||
if (isPointerOutsideAsideSidebar(x, y)) {
|
||||
const sectionItems =
|
||||
currentDnd.section === 'library'
|
||||
? getLibraryItemsForReorder(sidebarItemsRef.current, randomNavModeRef.current)
|
||||
: getSystemItemsForReorder(sidebarItemsRef.current);
|
||||
const id = sectionItems[currentDnd.fromIdx]?.id;
|
||||
const id = currentDnd.draggedId;
|
||||
if (id && isSidebarNavItemUserHideable(id)) {
|
||||
const nextItems: SidebarItemConfig[] = sidebarItemsRef.current.map(i =>
|
||||
i.id === id ? { ...i, visible: false } : i,
|
||||
@@ -114,12 +107,11 @@ export function useSidebarNavDnd({
|
||||
return;
|
||||
}
|
||||
|
||||
const next = applySidebarDropReorder(
|
||||
const next = applySidebarReorderById(
|
||||
sidebarItemsRef.current,
|
||||
currentDnd.section,
|
||||
currentDnd.fromIdx,
|
||||
currentDnd.draggedId,
|
||||
drop,
|
||||
randomNavModeRef.current,
|
||||
);
|
||||
if (next) {
|
||||
setSidebarItems(next);
|
||||
@@ -132,11 +124,7 @@ export function useSidebarNavDnd({
|
||||
lastPointerDuringNavDndRef.current = { x: e.clientX, y: e.clientY };
|
||||
|
||||
const outside = isPointerOutsideAsideSidebar(e.clientX, e.clientY);
|
||||
const sectionItems =
|
||||
navDnd.section === 'library'
|
||||
? getLibraryItemsForReorder(sidebarItemsRef.current, randomNavModeRef.current)
|
||||
: getSystemItemsForReorder(sidebarItemsRef.current);
|
||||
const draggedId = sectionItems[navDnd.fromIdx]?.id;
|
||||
const draggedId = navDnd.draggedId;
|
||||
const canTrash = Boolean(draggedId && isSidebarNavItemUserHideable(draggedId));
|
||||
if (outside && canTrash) {
|
||||
setNavDndTrashHint({ x: e.clientX, y: e.clientY });
|
||||
@@ -174,10 +162,10 @@ export function useSidebarNavDnd({
|
||||
document.body.style.userSelect = '';
|
||||
setNavDndTrashHint(null);
|
||||
};
|
||||
}, [navDnd, setSidebarItems, sidebarItemsRef, randomNavModeRef]);
|
||||
}, [navDnd, setSidebarItems, sidebarItemsRef]);
|
||||
|
||||
const handleNavRowPointerDown = useCallback(
|
||||
(e: React.PointerEvent, section: 'library' | 'system', sectionIdx: number) => {
|
||||
(e: React.PointerEvent, section: 'library' | 'system', id: string) => {
|
||||
if (isCollapsed || navDnd) return;
|
||||
if (e.pointerType === 'mouse' && e.button !== 0) return;
|
||||
|
||||
@@ -217,9 +205,9 @@ export function useSidebarNavDnd({
|
||||
cleanupEarly();
|
||||
window.getSelection()?.removeAllRanges();
|
||||
lastPointerDuringNavDndRef.current = { x: sx, y: sy };
|
||||
setNavDnd({ section, fromIdx: sectionIdx });
|
||||
navDropTargetRef.current = { idx: sectionIdx, before: true, section };
|
||||
setNavDropTarget({ idx: sectionIdx, before: true, section });
|
||||
setNavDnd({ section, draggedId: id });
|
||||
navDropTargetRef.current = { id, before: true, section };
|
||||
setNavDropTarget({ id, before: true, section });
|
||||
try {
|
||||
(e.currentTarget as HTMLElement).setPointerCapture(pid);
|
||||
} catch {
|
||||
@@ -236,14 +224,14 @@ export function useSidebarNavDnd({
|
||||
);
|
||||
|
||||
const navDndRowClass = useCallback(
|
||||
(section: 'library' | 'system', sectionIdx: number) => {
|
||||
const dragging = navDnd?.section === section && navDnd.fromIdx === sectionIdx;
|
||||
(section: 'library' | 'system', id: string) => {
|
||||
const dragging = navDnd?.section === section && navDnd.draggedId === id;
|
||||
let drop = '';
|
||||
if (
|
||||
navDnd &&
|
||||
navDropTarget?.section === section &&
|
||||
navDropTarget.idx === sectionIdx &&
|
||||
!(navDnd.section === section && navDnd.fromIdx === sectionIdx)
|
||||
navDropTarget.id === id &&
|
||||
!(navDnd.section === section && navDnd.draggedId === id)
|
||||
) {
|
||||
drop = navDropTarget.before
|
||||
? 'sidebar-nav-dnd-row--drop-before'
|
||||
|
||||
Reference in New Issue
Block a user