diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 74b182d..d7d0b9f 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -161,6 +161,14 @@ function Dashboard() { // Widget order from preferences, falling back to defaults const [localOrder, setLocalOrder] = useState>(DEFAULT_ORDER); + const [localCustomGroups, setLocalCustomGroups] = useState<{ name: string; title: string }[]>([]); + + // Always persist the FULL preferences object to avoid wiping unrelated fields (widgets, menuOrder, etc.) + const savePreferences = useCallback((patch: Record) => { + const full = { ...(preferences ?? {}), ...patch }; + queryClient.setQueryData(['user-preferences'], full); + preferencesApi.update(full); + }, [preferences, queryClient]); useEffect(() => { if (preferences?.widgetOrder) { @@ -188,6 +196,9 @@ function Dashboard() { return merged; }); + // Sync local custom groups + setLocalCustomGroups((preferences.customGroups as { name: string; title: string }[]) ?? []); + // Sync group order from preferences const customGroupNames = (preferences.customGroups as { name: string; title: string }[] | undefined ?? []).map(g => g.name); const allGroupNames = [...BUILTIN_GROUPS.map(g => g.name), ...customGroupNames]; @@ -289,9 +300,7 @@ function Dashboard() { if (oldIndex === -1 || newIndex === -1) return; const newOrder = arrayMove(localGroupOrder, oldIndex, newIndex); setLocalGroupOrder(newOrder); - preferencesApi.update({ widgetOrder: localOrder, groupsOrder: newOrder }).then(() => { - queryClient.invalidateQueries({ queryKey: ['user-preferences'] }); - }); + savePreferences({ widgetOrder: localOrder, groupsOrder: newOrder }); return; } @@ -318,18 +327,14 @@ function Dashboard() { } } // Cross-group move was already handled in handleDragOver - - // Persist current state - // Use a timeout to read the latest localOrder after state updates + // Persist current state using a timeout to read the latest localOrder after state updates setTimeout(() => { setLocalOrder((current) => { - preferencesApi.update({ widgetOrder: current }).then(() => { - queryClient.invalidateQueries({ queryKey: ['user-preferences'] }); - }); + savePreferences({ widgetOrder: current }); return current; }); }, 0); - }, [localOrder, localGroupOrder, queryClient, findGroupForWidget]); + }, [localOrder, localGroupOrder, savePreferences, findGroupForWidget]); // ── Add custom group ── const handleAddGroup = useCallback(() => { @@ -338,20 +343,19 @@ function Dashboard() { const name = title.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_'); // Guard duplicate - const existingGroups = [...BUILTIN_GROUPS, ...((preferences?.customGroups as { name: string; title: string }[]) || [])]; + const existingGroups = [...BUILTIN_GROUPS, ...localCustomGroups]; if (existingGroups.some((g) => g.name === name)) return; - const updatedCustomGroups = [...((preferences?.customGroups as { name: string; title: string }[]) || []), { name, title }]; + const updatedCustomGroups = [...localCustomGroups, { name, title }]; const updatedOrder = { ...localOrder, [name]: [] }; const updatedGroupOrder = [...localGroupOrder, name]; setLocalOrder(updatedOrder); setLocalGroupOrder(updatedGroupOrder); - preferencesApi.update({ widgetOrder: updatedOrder, customGroups: updatedCustomGroups, groupsOrder: updatedGroupOrder }).then(() => { - queryClient.invalidateQueries({ queryKey: ['user-preferences'] }); - }); + setLocalCustomGroups(updatedCustomGroups); + savePreferences({ widgetOrder: updatedOrder, customGroups: updatedCustomGroups, groupsOrder: updatedGroupOrder }); setNewGroupTitle(''); setAddGroupOpen(false); - }, [newGroupTitle, preferences?.customGroups, localOrder, queryClient]); + }, [newGroupTitle, localCustomGroups, localOrder, localGroupOrder, savePreferences]); // ── Delete custom group — move widgets back to their default groups ── const handleDeleteGroup = useCallback((groupName: string) => { @@ -369,25 +373,23 @@ function Dashboard() { updatedOrder[targetGroup].push(widgetKey); } - const updatedCustomGroups = ((preferences?.customGroups as { name: string; title: string }[]) || []).filter((g) => g.name !== groupName); + const updatedCustomGroups = localCustomGroups.filter((g) => g.name !== groupName); const updatedGroupOrder = localGroupOrder.filter(n => n !== groupName); setLocalOrder(updatedOrder); setLocalGroupOrder(updatedGroupOrder); - preferencesApi.update({ widgetOrder: updatedOrder, customGroups: updatedCustomGroups, groupsOrder: updatedGroupOrder }).then(() => { - queryClient.invalidateQueries({ queryKey: ['user-preferences'] }); - }); - }, [localOrder, localGroupOrder, preferences?.customGroups, queryClient]); + setLocalCustomGroups(updatedCustomGroups); + savePreferences({ widgetOrder: updatedOrder, customGroups: updatedCustomGroups, groupsOrder: updatedGroupOrder }); + }, [localOrder, localGroupOrder, localCustomGroups, savePreferences]); // ── Reset to defaults ── const handleReset = useCallback(() => { const defaultGroupOrder = BUILTIN_GROUPS.map(g => g.name); setLocalOrder(DEFAULT_ORDER); setLocalGroupOrder(defaultGroupOrder); - preferencesApi.update({ widgetOrder: DEFAULT_ORDER, customGroups: [], groupsOrder: defaultGroupOrder }).then(() => { - queryClient.invalidateQueries({ queryKey: ['user-preferences'] }); - }); + setLocalCustomGroups([]); + savePreferences({ widgetOrder: DEFAULT_ORDER, customGroups: [], groupsOrder: defaultGroupOrder }); setResetOpen(false); - }, [queryClient]); + }, [savePreferences]); useEffect(() => { const timer = setTimeout(() => { @@ -463,20 +465,13 @@ function Dashboard() { // Compute the full group list: built-in + custom groups, ordered by localGroupOrder const groupOrder = useMemo(() => { - const allGroups = [...BUILTIN_GROUPS]; - if (preferences?.customGroups) { - for (const cg of preferences.customGroups as { name: string; title: string }[]) { - if (!allGroups.some((g) => g.name === cg.name)) { - allGroups.push(cg); - } - } - } + const allGroups = [...BUILTIN_GROUPS, ...localCustomGroups.filter(cg => !BUILTIN_GROUPS.some(g => g.name === cg.name))]; const orderedGroups = localGroupOrder .map(name => allGroups.find(g => g.name === name)) .filter((g): g is { name: string; title: string } => !!g); const remaining = allGroups.filter(g => !localGroupOrder.includes(g.name)); return [...orderedGroups, ...remaining]; - }, [preferences?.customGroups, localGroupOrder]); + }, [localCustomGroups, localGroupOrder]); return (