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:
2025-12-22 16:47:12 +01:00
parent e15b8ecd58
commit 6adcf75ef1
2 changed files with 111 additions and 57 deletions

View File

@@ -79,6 +79,11 @@ export function SwipeTabs<T extends string | number>({
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<T extends string | number>({
);
const handlePointerDown = (event: PointerEvent<HTMLDivElement>) => {
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<T extends string | number>({
};
const handleTouchStart = (event: TouchEvent<HTMLDivElement>) => {
if (swipeDisabled || shouldIgnoreSwipe(event.target)) return;
if (dragRef.current.isActive) return;
const touch = event.changedTouches[0];
if (!touch) return;

View File

@@ -24,39 +24,68 @@ export default function Features() {
const [hasOrderChanges, setHasOrderChanges] = useState(false);
const tabsContainerRef = useRef<HTMLDivElement>(null);
const isUserEditing = useRef(false); // Track if user is actively editing
const initialOrderRef = useRef<string[]>([]);
const initialPositionsRef = useRef<Record<string, 'top' | 'bottom'>>({});
// 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
const buildFullOrder = useCallback((order: string[]) => {
const fullOrder = [...order];
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);
}
});
return fullOrder;
}, []);
setLocalOrder(fullOrder);
setHasOrderChanges(false);
}
}, [moduleOrder]);
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;
}, []);
// Sync positions from moduleStates only on initial load or when not editing
useEffect(() => {
if (!isUserEditing.current) {
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;
});
setLocalPositions(positions);
}
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) {
const fullOrder = buildFullOrder(moduleOrder);
setLocalOrder(fullOrder);
initialOrderRef.current = [...fullOrder];
setHasOrderChanges(false);
}
}, [moduleOrder, buildFullOrder]);
// Sync positions from moduleStates only on initial load or when not editing
useEffect(() => {
if (!isUserEditing.current) {
const positions = buildPositionsFromStates();
setLocalPositions(positions);
initialPositionsRef.current = { ...positions };
}
}, [buildPositionsFromStates]);
// Scroll active tab to center of container
const scrollActiveTabIntoView = useCallback((tabId: string) => {
if (tabsContainerRef.current) {
@@ -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);
const isSameItem = draggedItem === targetModuleId;
const positionChanged = draggedPosition !== targetSection;
if (isSameItem && !positionChanged) return;
beginOrderEdit();
if (positionChanged) {
// Update local positions immediately for UI responsiveness
setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection }));
setHasOrderChanges(true);
}
if (isSameItem) {
setHasOrderChanges(true);
return;
}
// 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);
// Update local positions immediately for UI responsiveness
setLocalPositions(prev => ({ ...prev, [draggedItem]: targetSection }));
}
// 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<string, 'top' | 'bottom'> = {};
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 (
<div className="theme-tab-content">
<div className="theme-tab-content content-narrow">
<div className="theme-section">
<div className="section-header">
<h3 className="section-title">{t.featuresPage?.topSection || 'Sezione Principale'}</h3>
</div>
<div
className="order-cards"
data-swipe-ignore="true"
onDragOver={handleDragOver}
onDrop={(e) => handleSectionDrop(e, 'top')}
>
@@ -333,6 +379,7 @@ export default function Features() {
</div>
<div
className="order-cards"
data-swipe-ignore="true"
onDragOver={handleDragOver}
onDrop={(e) => handleSectionDrop(e, 'bottom')}
>
@@ -486,7 +533,7 @@ export default function Features() {
<span className="material-symbols-outlined">tune</span>
<span>{t.featuresPage?.configTab || 'Configurazione'}</span>
</button>
{[...topOrderModules, ...bottomOrderModules].map((moduleId) => {
{[...topAppliedModules, ...bottomAppliedModules].map((moduleId) => {
const moduleInfo = getModuleInfo(moduleId);
const moduleName = t.sidebar[moduleId as keyof typeof t.sidebar] || moduleId;
return (