fix(dashboard): fix group/widget state loss due to partial preference saves and stale closures
This commit is contained in:
@@ -161,6 +161,14 @@ function Dashboard() {
|
||||
|
||||
// Widget order from preferences, falling back to defaults
|
||||
const [localOrder, setLocalOrder] = useState<Record<string, string[]>>(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<string, unknown>) => {
|
||||
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 (
|
||||
<DashboardLayout>
|
||||
|
||||
Reference in New Issue
Block a user