Fix Features config tab order management
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -79,6 +79,11 @@ export function SwipeTabs<T extends string | number>({
|
|||||||
isDragging: false
|
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 applyTransform = useCallback((offset: number, animate: boolean) => {
|
||||||
const track = trackRef.current;
|
const track = trackRef.current;
|
||||||
if (!track) return;
|
if (!track) return;
|
||||||
@@ -310,6 +315,7 @@ export function SwipeTabs<T extends string | number>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
|
||||||
|
if (swipeDisabled || shouldIgnoreSwipe(event.target)) return;
|
||||||
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
||||||
if (dragRef.current.isActive) return;
|
if (dragRef.current.isActive) return;
|
||||||
startDrag(event.clientX, event.clientY, event.pointerId, null);
|
startDrag(event.clientX, event.clientY, event.pointerId, null);
|
||||||
@@ -359,6 +365,7 @@ export function SwipeTabs<T extends string | number>({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTouchStart = (event: TouchEvent<HTMLDivElement>) => {
|
const handleTouchStart = (event: TouchEvent<HTMLDivElement>) => {
|
||||||
|
if (swipeDisabled || shouldIgnoreSwipe(event.target)) return;
|
||||||
if (dragRef.current.isActive) return;
|
if (dragRef.current.isActive) return;
|
||||||
const touch = event.changedTouches[0];
|
const touch = event.changedTouches[0];
|
||||||
if (!touch) return;
|
if (!touch) return;
|
||||||
|
|||||||
@@ -24,38 +24,67 @@ export default function Features() {
|
|||||||
const [hasOrderChanges, setHasOrderChanges] = useState(false);
|
const [hasOrderChanges, setHasOrderChanges] = useState(false);
|
||||||
const tabsContainerRef = useRef<HTMLDivElement>(null);
|
const tabsContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const isUserEditing = useRef(false); // Track if user is actively editing
|
const isUserEditing = useRef(false); // Track if user is actively editing
|
||||||
|
const initialOrderRef = useRef<string[]>([]);
|
||||||
|
const initialPositionsRef = useRef<Record<string, 'top' | 'bottom'>>({});
|
||||||
|
|
||||||
|
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<string, 'top' | 'bottom'>) => {
|
||||||
|
const normalized: Record<string, 'top' | 'bottom'> = {};
|
||||||
|
TOGGLEABLE_MODULES.forEach(module => {
|
||||||
|
normalized[module.id] = positions[module.id] || module.defaultPosition;
|
||||||
|
});
|
||||||
|
return normalized;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const buildPositionsFromStates = useCallback(() => {
|
||||||
|
const positions: Record<string, 'top' | 'bottom'> = {};
|
||||||
|
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)
|
// Sync local order with context order (only when moduleOrder changes, not moduleStates)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (moduleOrder.length > 0 && !isUserEditing.current) {
|
if (moduleOrder.length > 0 && !isUserEditing.current) {
|
||||||
// Start with current order
|
const fullOrder = buildFullOrder(moduleOrder);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setLocalOrder(fullOrder);
|
setLocalOrder(fullOrder);
|
||||||
|
initialOrderRef.current = [...fullOrder];
|
||||||
setHasOrderChanges(false);
|
setHasOrderChanges(false);
|
||||||
}
|
}
|
||||||
}, [moduleOrder]);
|
}, [moduleOrder, buildFullOrder]);
|
||||||
|
|
||||||
// Sync positions from moduleStates only on initial load or when not editing
|
// Sync positions from moduleStates only on initial load or when not editing
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isUserEditing.current) {
|
if (!isUserEditing.current) {
|
||||||
const positions: Record<string, 'top' | 'bottom'> = {};
|
const positions = buildPositionsFromStates();
|
||||||
TOGGLEABLE_MODULES.forEach(module => {
|
|
||||||
const state = moduleStates[module.id];
|
|
||||||
positions[module.id] = state?.position || module.defaultPosition;
|
|
||||||
});
|
|
||||||
setLocalPositions(positions);
|
setLocalPositions(positions);
|
||||||
|
initialPositionsRef.current = { ...positions };
|
||||||
}
|
}
|
||||||
}, [moduleStates]);
|
}, [buildPositionsFromStates]);
|
||||||
|
|
||||||
// Scroll active tab to center of container
|
// Scroll active tab to center of container
|
||||||
const scrollActiveTabIntoView = useCallback((tabId: string) => {
|
const scrollActiveTabIntoView = useCallback((tabId: string) => {
|
||||||
@@ -159,18 +188,14 @@ export default function Features() {
|
|||||||
|
|
||||||
// Drag and drop handlers for ordering
|
// Drag and drop handlers for ordering
|
||||||
const handleDragStart = (e: React.DragEvent, moduleId: string) => {
|
const handleDragStart = (e: React.DragEvent, moduleId: string) => {
|
||||||
|
beginOrderEdit();
|
||||||
setDraggedItem(moduleId);
|
setDraggedItem(moduleId);
|
||||||
e.dataTransfer.effectAllowed = 'move';
|
e.dataTransfer.effectAllowed = 'move';
|
||||||
e.dataTransfer.setData('text/plain', moduleId);
|
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);
|
setDraggedItem(null);
|
||||||
(e.target as HTMLElement).classList.remove('dragging');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent) => {
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
@@ -185,30 +210,22 @@ export default function Features() {
|
|||||||
|
|
||||||
const draggedPosition = localPositions[draggedItem] || 'top';
|
const draggedPosition = localPositions[draggedItem] || 'top';
|
||||||
|
|
||||||
// If dropping on same item, just change section if different
|
const isSameItem = draggedItem === targetModuleId;
|
||||||
if (draggedItem === targetModuleId) {
|
const positionChanged = draggedPosition !== targetSection;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change position if moving to different section
|
if (isSameItem && !positionChanged) return;
|
||||||
if (draggedPosition !== targetSection) {
|
|
||||||
isUserEditing.current = true; // Mark as user editing
|
beginOrderEdit();
|
||||||
hasUserMadeChanges.current = true;
|
if (positionChanged) {
|
||||||
setModulePosition(draggedItem as ModuleId, targetSection);
|
|
||||||
// Update local positions immediately for UI responsiveness
|
// Update local positions immediately for UI responsiveness
|
||||||
setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection }));
|
setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection }));
|
||||||
}
|
}
|
||||||
|
if (isSameItem) {
|
||||||
|
setHasOrderChanges(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Reorder within the list
|
// Reorder within the list
|
||||||
isUserEditing.current = true; // Mark as user editing
|
|
||||||
const newOrder = [...localOrder];
|
const newOrder = [...localOrder];
|
||||||
const draggedIndex = newOrder.indexOf(draggedItem);
|
const draggedIndex = newOrder.indexOf(draggedItem);
|
||||||
const targetIndex = newOrder.indexOf(targetModuleId);
|
const targetIndex = newOrder.indexOf(targetModuleId);
|
||||||
@@ -230,9 +247,7 @@ export default function Features() {
|
|||||||
|
|
||||||
// Change position if moving to different section
|
// Change position if moving to different section
|
||||||
if (draggedPosition !== section) {
|
if (draggedPosition !== section) {
|
||||||
isUserEditing.current = true; // Mark as user editing
|
beginOrderEdit();
|
||||||
hasUserMadeChanges.current = true;
|
|
||||||
setModulePosition(draggedItem as ModuleId, section);
|
|
||||||
// Update local positions immediately for UI responsiveness
|
// Update local positions immediately for UI responsiveness
|
||||||
setLocalPositions(prev => ({ ...prev, [draggedItem]: section }));
|
setLocalPositions(prev => ({ ...prev, [draggedItem]: section }));
|
||||||
setHasOrderChanges(true);
|
setHasOrderChanges(true);
|
||||||
@@ -241,12 +256,30 @@ export default function Features() {
|
|||||||
|
|
||||||
const handleApplyOrder = async () => {
|
const handleApplyOrder = async () => {
|
||||||
try {
|
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);
|
setModuleOrder(localOrder);
|
||||||
await saveModuleOrder(localOrder);
|
await saveModuleOrder(localOrder);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save order:', error);
|
console.error('Failed to save order:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setDraggedItem(null); // Reset drag state
|
setDraggedItem(null); // Reset drag state
|
||||||
|
initialOrderRef.current = buildFullOrder(localOrder);
|
||||||
|
initialPositionsRef.current = normalizePositions(localPositions);
|
||||||
isUserEditing.current = false;
|
isUserEditing.current = false;
|
||||||
setHasOrderChanges(false);
|
setHasOrderChanges(false);
|
||||||
}
|
}
|
||||||
@@ -255,14 +288,13 @@ export default function Features() {
|
|||||||
const handleCancelOrder = () => {
|
const handleCancelOrder = () => {
|
||||||
setDraggedItem(null); // Reset drag state
|
setDraggedItem(null); // Reset drag state
|
||||||
isUserEditing.current = false; // Done editing
|
isUserEditing.current = false; // Done editing
|
||||||
setLocalOrder(moduleOrder);
|
// Restore to currently applied state from context
|
||||||
// Reset positions from moduleStates
|
const restoredOrder = buildFullOrder(moduleOrder);
|
||||||
const positions: Record<string, 'top' | 'bottom'> = {};
|
const restoredPositions = buildPositionsFromStates();
|
||||||
TOGGLEABLE_MODULES.forEach(module => {
|
setLocalOrder(restoredOrder);
|
||||||
const state = moduleStates[module.id];
|
setLocalPositions(restoredPositions);
|
||||||
positions[module.id] = state?.position || module.defaultPosition;
|
initialOrderRef.current = restoredOrder;
|
||||||
});
|
initialPositionsRef.current = restoredPositions;
|
||||||
setLocalPositions(positions);
|
|
||||||
setHasOrderChanges(false);
|
setHasOrderChanges(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -272,6 +304,7 @@ export default function Features() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Split modules by position for the config tab (using localPositions for immediate UI updates)
|
// 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 topOrderModules = localOrder.filter(id => {
|
||||||
const position = localPositions[id];
|
const position = localPositions[id];
|
||||||
return !position || position === 'top';
|
return !position || position === 'top';
|
||||||
@@ -282,17 +315,30 @@ export default function Features() {
|
|||||||
return position === 'bottom';
|
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 = () => {
|
const renderConfigTab = () => {
|
||||||
return (
|
return (
|
||||||
<div className="theme-tab-content">
|
<div className="theme-tab-content content-narrow">
|
||||||
<div className="theme-section">
|
<div className="theme-section">
|
||||||
<div className="section-header">
|
<div className="section-header">
|
||||||
<h3 className="section-title">{t.featuresPage?.topSection || 'Sezione Principale'}</h3>
|
<h3 className="section-title">{t.featuresPage?.topSection || 'Sezione Principale'}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="order-cards"
|
className="order-cards"
|
||||||
|
data-swipe-ignore="true"
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={(e) => handleSectionDrop(e, 'top')}
|
onDrop={(e) => handleSectionDrop(e, 'top')}
|
||||||
>
|
>
|
||||||
@@ -333,6 +379,7 @@ export default function Features() {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="order-cards"
|
className="order-cards"
|
||||||
|
data-swipe-ignore="true"
|
||||||
onDragOver={handleDragOver}
|
onDragOver={handleDragOver}
|
||||||
onDrop={(e) => handleSectionDrop(e, 'bottom')}
|
onDrop={(e) => handleSectionDrop(e, 'bottom')}
|
||||||
>
|
>
|
||||||
@@ -486,7 +533,7 @@ export default function Features() {
|
|||||||
<span className="material-symbols-outlined">tune</span>
|
<span className="material-symbols-outlined">tune</span>
|
||||||
<span>{t.featuresPage?.configTab || 'Configurazione'}</span>
|
<span>{t.featuresPage?.configTab || 'Configurazione'}</span>
|
||||||
</button>
|
</button>
|
||||||
{[...topOrderModules, ...bottomOrderModules].map((moduleId) => {
|
{[...topAppliedModules, ...bottomAppliedModules].map((moduleId) => {
|
||||||
const moduleInfo = getModuleInfo(moduleId);
|
const moduleInfo = getModuleInfo(moduleId);
|
||||||
const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId;
|
const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId;
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user