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
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user