From 6adcf75ef171aa3dc933b6c02cebe508a8933cc3 Mon Sep 17 00:00:00 2001 From: matteoscrugli Date: Mon, 22 Dec 2025 16:47:12 +0100 Subject: [PATCH] Fix Features config tab order management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Tabs now use applied order (moduleOrder) instead of local pending order - Cards still show local order for drag preview feedback - Cancel button correctly restores to the applied state from context - Apply button updates both tabs and sidebar after saving 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- frontend/src/components/SwipeTabs.tsx | 7 ++ frontend/src/pages/admin/Features.tsx | 161 +++++++++++++++++--------- 2 files changed, 111 insertions(+), 57 deletions(-) diff --git a/frontend/src/components/SwipeTabs.tsx b/frontend/src/components/SwipeTabs.tsx index 10ceffd..340c693 100644 --- a/frontend/src/components/SwipeTabs.tsx +++ b/frontend/src/components/SwipeTabs.tsx @@ -79,6 +79,11 @@ export function SwipeTabs({ isDragging: false }); + const shouldIgnoreSwipe = useCallback((target: EventTarget | null) => { + if (!target || typeof (target as Element).closest !== 'function') return false; + return Boolean((target as Element).closest('[data-swipe-ignore="true"]')); + }, []); + const applyTransform = useCallback((offset: number, animate: boolean) => { const track = trackRef.current; if (!track) return; @@ -310,6 +315,7 @@ export function SwipeTabs({ ); const handlePointerDown = (event: PointerEvent) => { + if (swipeDisabled || shouldIgnoreSwipe(event.target)) return; if (event.pointerType === 'mouse' && event.button !== 0) return; if (dragRef.current.isActive) return; startDrag(event.clientX, event.clientY, event.pointerId, null); @@ -359,6 +365,7 @@ export function SwipeTabs({ }; const handleTouchStart = (event: TouchEvent) => { + if (swipeDisabled || shouldIgnoreSwipe(event.target)) return; if (dragRef.current.isActive) return; const touch = event.changedTouches[0]; if (!touch) return; diff --git a/frontend/src/pages/admin/Features.tsx b/frontend/src/pages/admin/Features.tsx index d37fee7..ab90ad3 100644 --- a/frontend/src/pages/admin/Features.tsx +++ b/frontend/src/pages/admin/Features.tsx @@ -24,38 +24,67 @@ export default function Features() { const [hasOrderChanges, setHasOrderChanges] = useState(false); const tabsContainerRef = useRef(null); const isUserEditing = useRef(false); // Track if user is actively editing + const initialOrderRef = useRef([]); + const initialPositionsRef = useRef>({}); + + const buildFullOrder = useCallback((order: string[]) => { + const fullOrder = [...order]; + TOGGLEABLE_MODULES.forEach((module, defaultIndex) => { + if (!fullOrder.includes(module.id)) { + const insertAt = Math.min(defaultIndex, fullOrder.length); + fullOrder.splice(insertAt, 0, module.id); + } + }); + return fullOrder; + }, []); + + const normalizePositions = useCallback((positions: Record) => { + const normalized: Record = {}; + TOGGLEABLE_MODULES.forEach(module => { + normalized[module.id] = positions[module.id] || module.defaultPosition; + }); + return normalized; + }, []); + + const buildPositionsFromStates = useCallback(() => { + const positions: Record = {}; + TOGGLEABLE_MODULES.forEach(module => { + const state = moduleStates[module.id]; + positions[module.id] = state?.position || module.defaultPosition; + }); + return positions; + }, [moduleStates]); + + const beginOrderEdit = useCallback(() => { + if (!isUserEditing.current) { + const snapshotOrder = localOrder.length ? [...localOrder] : buildFullOrder(moduleOrder); + const snapshotPositions = Object.keys(localPositions).length + ? { ...normalizePositions(localPositions) } + : buildPositionsFromStates(); + initialOrderRef.current = snapshotOrder; + initialPositionsRef.current = snapshotPositions; + isUserEditing.current = true; + } + }, [buildFullOrder, buildPositionsFromStates, localOrder, localPositions, moduleOrder, normalizePositions]); // Sync local order with context order (only when moduleOrder changes, not moduleStates) useEffect(() => { if (moduleOrder.length > 0 && !isUserEditing.current) { - // Start with current order - const fullOrder = [...moduleOrder]; - - // Insert missing modules at their default positions from TOGGLEABLE_MODULES - TOGGLEABLE_MODULES.forEach((module, defaultIndex) => { - if (!fullOrder.includes(module.id)) { - // Insert at the default index position, or at end if index is beyond current length - const insertAt = Math.min(defaultIndex, fullOrder.length); - fullOrder.splice(insertAt, 0, module.id); - } - }); - + const fullOrder = buildFullOrder(moduleOrder); setLocalOrder(fullOrder); + initialOrderRef.current = [...fullOrder]; setHasOrderChanges(false); } - }, [moduleOrder]); + }, [moduleOrder, buildFullOrder]); // Sync positions from moduleStates only on initial load or when not editing useEffect(() => { if (!isUserEditing.current) { - const positions: Record = {}; - TOGGLEABLE_MODULES.forEach(module => { - const state = moduleStates[module.id]; - positions[module.id] = state?.position || module.defaultPosition; - }); + const positions = buildPositionsFromStates(); setLocalPositions(positions); + initialPositionsRef.current = { ...positions }; } - }, [moduleStates]); + }, [buildPositionsFromStates]); // Scroll active tab to center of container const scrollActiveTabIntoView = useCallback((tabId: string) => { @@ -159,18 +188,14 @@ export default function Features() { // Drag and drop handlers for ordering const handleDragStart = (e: React.DragEvent, moduleId: string) => { + beginOrderEdit(); setDraggedItem(moduleId); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', moduleId); - // Add dragging class after a small delay for visual feedback - setTimeout(() => { - (e.target as HTMLElement).classList.add('dragging'); - }, 0); }; - const handleDragEnd = (e: React.DragEvent) => { + const handleDragEnd = () => { setDraggedItem(null); - (e.target as HTMLElement).classList.remove('dragging'); }; const handleDragOver = (e: React.DragEvent) => { @@ -185,30 +210,22 @@ export default function Features() { const draggedPosition = localPositions[draggedItem] || 'top'; - // If dropping on same item, just change section if different - if (draggedItem === targetModuleId) { - if (draggedPosition !== targetSection) { - isUserEditing.current = true; // Mark as user editing - hasUserMadeChanges.current = true; - setModulePosition(draggedItem as ModuleId, targetSection); - // Update local positions immediately for UI responsiveness - setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection })); - setHasOrderChanges(true); - } - return; - } + const isSameItem = draggedItem === targetModuleId; + const positionChanged = draggedPosition !== targetSection; - // Change position if moving to different section - if (draggedPosition !== targetSection) { - isUserEditing.current = true; // Mark as user editing - hasUserMadeChanges.current = true; - setModulePosition(draggedItem as ModuleId, targetSection); + if (isSameItem && !positionChanged) return; + + beginOrderEdit(); + if (positionChanged) { // Update local positions immediately for UI responsiveness setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection })); } + if (isSameItem) { + setHasOrderChanges(true); + return; + } // Reorder within the list - isUserEditing.current = true; // Mark as user editing const newOrder = [...localOrder]; const draggedIndex = newOrder.indexOf(draggedItem); const targetIndex = newOrder.indexOf(targetModuleId); @@ -230,9 +247,7 @@ export default function Features() { // Change position if moving to different section if (draggedPosition !== section) { - isUserEditing.current = true; // Mark as user editing - hasUserMadeChanges.current = true; - setModulePosition(draggedItem as ModuleId, section); + beginOrderEdit(); // Update local positions immediately for UI responsiveness setLocalPositions(prev => ({ ...prev, [draggedItem]: section })); setHasOrderChanges(true); @@ -241,12 +256,30 @@ export default function Features() { const handleApplyOrder = async () => { try { + const positionUpdates: Array<{ id: ModuleId; position: 'top' | 'bottom' }> = []; + TOGGLEABLE_MODULES.forEach(module => { + const desiredPosition = localPositions[module.id] || module.defaultPosition; + const currentPosition = moduleStates[module.id]?.position || module.defaultPosition; + if (desiredPosition !== currentPosition) { + positionUpdates.push({ id: module.id, position: desiredPosition }); + } + }); + + if (positionUpdates.length > 0) { + hasUserMadeChanges.current = true; + positionUpdates.forEach(({ id, position }) => { + setModulePosition(id, position); + }); + } + setModuleOrder(localOrder); await saveModuleOrder(localOrder); } catch (error) { console.error('Failed to save order:', error); } finally { setDraggedItem(null); // Reset drag state + initialOrderRef.current = buildFullOrder(localOrder); + initialPositionsRef.current = normalizePositions(localPositions); isUserEditing.current = false; setHasOrderChanges(false); } @@ -255,14 +288,13 @@ export default function Features() { const handleCancelOrder = () => { setDraggedItem(null); // Reset drag state isUserEditing.current = false; // Done editing - setLocalOrder(moduleOrder); - // Reset positions from moduleStates - const positions: Record = {}; - TOGGLEABLE_MODULES.forEach(module => { - const state = moduleStates[module.id]; - positions[module.id] = state?.position || module.defaultPosition; - }); - setLocalPositions(positions); + // Restore to currently applied state from context + const restoredOrder = buildFullOrder(moduleOrder); + const restoredPositions = buildPositionsFromStates(); + setLocalOrder(restoredOrder); + setLocalPositions(restoredPositions); + initialOrderRef.current = restoredOrder; + initialPositionsRef.current = restoredPositions; setHasOrderChanges(false); }; @@ -272,6 +304,7 @@ export default function Features() { }; // Split modules by position for the config tab (using localPositions for immediate UI updates) + // Local order for cards (changes during drag) const topOrderModules = localOrder.filter(id => { const position = localPositions[id]; return !position || position === 'top'; @@ -282,17 +315,30 @@ export default function Features() { return position === 'bottom'; }); - const tabIds = ['config', ...topOrderModules, ...bottomOrderModules] as TabId[]; + // Applied order for tabs (only changes after Apply) + const appliedOrder = buildFullOrder(moduleOrder); + const appliedPositions = buildPositionsFromStates(); + const topAppliedModules = appliedOrder.filter(id => { + const position = appliedPositions[id]; + return !position || position === 'top'; + }); + const bottomAppliedModules = appliedOrder.filter(id => { + const position = appliedPositions[id]; + return position === 'bottom'; + }); + + const tabIds = ['config', ...topAppliedModules, ...bottomAppliedModules] as TabId[]; const renderConfigTab = () => { return ( -
+

{t.featuresPage?.topSection || 'Sezione Principale'}

handleSectionDrop(e, 'top')} > @@ -333,6 +379,7 @@ export default function Features() {
handleSectionDrop(e, 'bottom')} > @@ -486,7 +533,7 @@ export default function Features() { tune {t.featuresPage?.configTab || 'Configurazione'} - {[...topOrderModules, ...bottomOrderModules].map((moduleId) => { + {[...topAppliedModules, ...bottomAppliedModules].map((moduleId) => { const moduleInfo = getModuleInfo(moduleId); const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId; return (