Files
rack-planner/client/src/store/useMapStore.ts
jason 7c04c4633f Fix service map nodes disappearing on page reload
Root cause: Zustand store resets to activeMap=null on every page load.
fetchMaps() only loaded the summary list — nodes were never reloaded
because the user had to manually re-select their map each time.

Fixes:
1. Persist last active map ID in localStorage (key: rackmapper:lastMapId)
   - loadMap() saves the ID on successful load
   - setActiveMap() saves/clears the ID
   - deleteMap() clears the ID if the deleted map was active

2. Auto-restore on mount inside fetchMaps():
   - If the saved map ID is still in the list, auto-load it
   - If there is exactly one map, auto-load it as a convenience

3. Block spurious position saves during map load (blockSaveRef):
   - fitView fires position NodeChanges for all nodes after load
   - Without a guard these would overwrite stored positions with
     React Flow's fitted coordinates immediately on every reload
   - blockSaveRef is set true on activeMap change, cleared after 800ms

4. Tighten the position-change filter:
   - Require dragging === false (strict equality, not just falsy)
   - Require position != null before saving
   - Both conditions must be true to queue a save

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 00:05:01 -05:00

89 lines
2.4 KiB
TypeScript

import { create } from 'zustand';
import type { ServiceMap, ServiceMapSummary } from '../types';
import { apiClient } from '../api/client';
const LAST_MAP_KEY = 'rackmapper:lastMapId';
function saveLastMapId(id: string | null) {
if (id) localStorage.setItem(LAST_MAP_KEY, id);
else localStorage.removeItem(LAST_MAP_KEY);
}
function getLastMapId(): string | null {
return localStorage.getItem(LAST_MAP_KEY);
}
interface MapState {
maps: ServiceMapSummary[];
activeMap: ServiceMap | null;
loading: boolean;
fetchMaps: () => Promise<void>;
loadMap: (id: string) => Promise<void>;
createMap: (name: string, description?: string) => Promise<ServiceMap>;
deleteMap: (id: string) => Promise<void>;
setActiveMap: (map: ServiceMap | null) => void;
}
export const useMapStore = create<MapState>((set, get) => ({
maps: [],
activeMap: null,
loading: false,
fetchMaps: async () => {
set({ loading: true });
try {
const maps = await apiClient.maps.list();
set({ maps, loading: false });
// Auto-restore the last active map after loading the list
const lastId = getLastMapId();
if (lastId && maps.some((m) => m.id === lastId)) {
await get().loadMap(lastId);
} else if (maps.length === 1) {
// Convenience: auto-load if there's only one map
await get().loadMap(maps[0].id);
}
} catch {
set({ loading: false });
throw new Error('Failed to load maps');
}
},
loadMap: async (id) => {
set({ loading: true });
try {
const map = await apiClient.maps.get(id);
saveLastMapId(id);
set({ activeMap: map, loading: false });
} catch {
set({ loading: false });
throw new Error('Failed to load map');
}
},
createMap: async (name, description) => {
const map = await apiClient.maps.create({ name, description });
set((s) => ({
maps: [
{ id: map.id, name: map.name, description: map.description, createdAt: map.createdAt, updatedAt: map.updatedAt },
...s.maps,
],
}));
return map;
},
deleteMap: async (id) => {
await apiClient.maps.delete(id);
if (getLastMapId() === id) saveLastMapId(null);
set((s) => ({
maps: s.maps.filter((m) => m.id !== id),
activeMap: s.activeMap?.id === id ? null : s.activeMap,
}));
},
setActiveMap: (map) => {
saveLastMapId(map?.id ?? null);
set({ activeMap: map });
},
}));