refactor: extract map draft state hook
This commit is contained in:
parent
d5cfd22014
commit
3f6eade576
142
frontend/src/hooks/useLocationMapDraftState.js
Normal file
142
frontend/src/hooks/useLocationMapDraftState.js
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { getLocationMap } from "../api/locationMaps";
|
||||||
|
import getApiErrorMessage from "../lib/getApiErrorMessage";
|
||||||
|
import { DEFAULT_MAP_FILTERS, DEFAULT_MAP_SIZE, mapForMode, normalizeMapObject, objectsForMode } from "../lib/locationMapUtils";
|
||||||
|
|
||||||
|
const DEFAULT_DRAFT_MAP = {
|
||||||
|
name: "Store Map",
|
||||||
|
width: DEFAULT_MAP_SIZE.width,
|
||||||
|
height: DEFAULT_MAP_SIZE.height,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function useLocationMapDraftState({
|
||||||
|
activeHousehold,
|
||||||
|
hasLoaded,
|
||||||
|
householdLoading,
|
||||||
|
locationId,
|
||||||
|
toast,
|
||||||
|
}) {
|
||||||
|
const [mapState, setMapState] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [loadError, setLoadError] = useState(null);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [savingAction, setSavingAction] = useState(null);
|
||||||
|
const [mode, setMode] = useState("setup");
|
||||||
|
const [editorTool, setEditorTool] = useState("pan");
|
||||||
|
const [previewDraft, setPreviewDraft] = useState(false);
|
||||||
|
const [mapDraft, setMapDraft] = useState(DEFAULT_DRAFT_MAP);
|
||||||
|
const [objects, setObjects] = useState([]);
|
||||||
|
const [selectedObjectKey, setSelectedObjectKey] = useState(null);
|
||||||
|
const [filters, setFilters] = useState(DEFAULT_MAP_FILTERS);
|
||||||
|
const [layersOpen, setLayersOpen] = useState(false);
|
||||||
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
||||||
|
const [zoom, setZoom] = useState(0.75);
|
||||||
|
const [history, setHistory] = useState([]);
|
||||||
|
const [future, setFuture] = useState([]);
|
||||||
|
const [dragState, setDragState] = useState(null);
|
||||||
|
const toastRef = useRef(toast);
|
||||||
|
const loadRequestRef = useRef(0);
|
||||||
|
|
||||||
|
const resetDraftState = useCallback(() => {
|
||||||
|
setObjects([]);
|
||||||
|
setSelectedObjectKey(null);
|
||||||
|
setHistory([]);
|
||||||
|
setFuture([]);
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
|
setMapDraft(DEFAULT_DRAFT_MAP);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const syncMapDraftFromState = useCallback((nextState, nextMode, nextPreviewDraft) => {
|
||||||
|
const nextMap = mapForMode(nextState, nextMode, nextPreviewDraft);
|
||||||
|
const nextObjects = objectsForMode(nextState, nextMode, nextPreviewDraft);
|
||||||
|
if (!nextMap) return;
|
||||||
|
|
||||||
|
setMapDraft({
|
||||||
|
name: nextMap.name || "Store Map",
|
||||||
|
width: nextMap.width || DEFAULT_MAP_SIZE.width,
|
||||||
|
height: nextMap.height || DEFAULT_MAP_SIZE.height,
|
||||||
|
});
|
||||||
|
setObjects(nextObjects.map(normalizeMapObject));
|
||||||
|
setSelectedObjectKey(null);
|
||||||
|
setHistory([]);
|
||||||
|
setFuture([]);
|
||||||
|
setHasUnsavedChanges(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadMap = useCallback(async () => {
|
||||||
|
const requestId = loadRequestRef.current + 1;
|
||||||
|
loadRequestRef.current = requestId;
|
||||||
|
|
||||||
|
if (!activeHousehold?.id || !locationId) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setLoadError(null);
|
||||||
|
try {
|
||||||
|
const response = await getLocationMap(activeHousehold.id, locationId);
|
||||||
|
if (loadRequestRef.current !== requestId) return;
|
||||||
|
|
||||||
|
const nextState = response.data;
|
||||||
|
const nextCanManage = Boolean(
|
||||||
|
nextState.can_manage ?? ["owner", "admin"].includes(activeHousehold?.role)
|
||||||
|
);
|
||||||
|
setMapState(nextState);
|
||||||
|
setEditorTool("pan");
|
||||||
|
|
||||||
|
if (!nextState.draft_map && !nextState.published_map) {
|
||||||
|
setMode("setup");
|
||||||
|
setPreviewDraft(false);
|
||||||
|
resetDraftState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextState.published_map) {
|
||||||
|
setMode("view");
|
||||||
|
setPreviewDraft(false);
|
||||||
|
syncMapDraftFromState(nextState, "view", false);
|
||||||
|
} else if (nextState.draft_map && nextCanManage) {
|
||||||
|
setMode("setup");
|
||||||
|
setPreviewDraft(true);
|
||||||
|
syncMapDraftFromState(nextState, "edit", true);
|
||||||
|
} else {
|
||||||
|
setMode("setup");
|
||||||
|
setPreviewDraft(false);
|
||||||
|
resetDraftState();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (loadRequestRef.current !== requestId) return;
|
||||||
|
|
||||||
|
const message = getApiErrorMessage(error, "Failed to load map");
|
||||||
|
toastRef.current.error("Load map failed", message);
|
||||||
|
setMapState(null);
|
||||||
|
resetDraftState();
|
||||||
|
setLoadError(message);
|
||||||
|
} finally {
|
||||||
|
if (loadRequestRef.current === requestId) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeHousehold?.id, activeHousehold?.role, locationId, resetDraftState, syncMapDraftFromState]);
|
||||||
|
|
||||||
|
useEffect(() => { toastRef.current = toast; }, [toast]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!householdLoading && hasLoaded) {
|
||||||
|
loadMap();
|
||||||
|
}
|
||||||
|
}, [hasLoaded, householdLoading, loadMap]);
|
||||||
|
|
||||||
|
const beginSaving = useCallback((action) => { setSavingAction(action); setSaving(true); }, []);
|
||||||
|
|
||||||
|
const endSaving = useCallback(() => { setSaving(false); setSavingAction(null); }, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
beginSaving, dragState, editorTool, endSaving, filters, future, hasUnsavedChanges, history, layersOpen,
|
||||||
|
loadError, loadMap, loading, mapDraft, mapState, mode, objects, previewDraft, saving,
|
||||||
|
savingAction, selectedObjectKey, setDragState, setEditorTool, setFilters, setFuture,
|
||||||
|
setHasUnsavedChanges, setHistory, setLayersOpen, setMapState, setMode, setObjects,
|
||||||
|
setPreviewDraft, setSelectedObjectKey, setZoom, syncMapDraftFromState, zoom,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@ import { useLocation, useNavigate, useParams } from "react-router-dom";
|
|||||||
import {
|
import {
|
||||||
createBlankLocationMap,
|
createBlankLocationMap,
|
||||||
createLocationMapFromZones,
|
createLocationMapFromZones,
|
||||||
getLocationMap,
|
|
||||||
publishLocationMapDraft,
|
publishLocationMapDraft,
|
||||||
saveLocationMapDraft,
|
saveLocationMapDraft,
|
||||||
} from "../api/locationMaps";
|
} from "../api/locationMaps";
|
||||||
@ -17,10 +16,10 @@ import LocationMapToolbar from "../components/maps/LocationMapToolbar";
|
|||||||
import LocationMapTopbar from "../components/maps/LocationMapTopbar";
|
import LocationMapTopbar from "../components/maps/LocationMapTopbar";
|
||||||
import ConfirmSlideModal from "../components/modals/ConfirmSlideModal";
|
import ConfirmSlideModal from "../components/modals/ConfirmSlideModal";
|
||||||
import useActionToast from "../hooks/useActionToast";
|
import useActionToast from "../hooks/useActionToast";
|
||||||
|
import useLocationMapDraftState from "../hooks/useLocationMapDraftState";
|
||||||
import useUnsavedMapLeaveGuard from "../hooks/useUnsavedMapLeaveGuard";
|
import useUnsavedMapLeaveGuard from "../hooks/useUnsavedMapLeaveGuard";
|
||||||
import getApiErrorMessage from "../lib/getApiErrorMessage";
|
import getApiErrorMessage from "../lib/getApiErrorMessage";
|
||||||
import {
|
import {
|
||||||
DEFAULT_MAP_FILTERS,
|
|
||||||
DEFAULT_MAP_SIZE,
|
DEFAULT_MAP_SIZE,
|
||||||
clampMapZoom,
|
clampMapZoom,
|
||||||
clampObjectToMap,
|
clampObjectToMap,
|
||||||
@ -31,9 +30,7 @@ import {
|
|||||||
itemMatchesMapFilters,
|
itemMatchesMapFilters,
|
||||||
itemsForZone,
|
itemsForZone,
|
||||||
mapForMode,
|
mapForMode,
|
||||||
normalizeMapObject,
|
|
||||||
objectZoneId,
|
objectZoneId,
|
||||||
objectsForMode,
|
|
||||||
prepareObjectsForSave,
|
prepareObjectsForSave,
|
||||||
unmappedItems,
|
unmappedItems,
|
||||||
} from "../lib/locationMapUtils";
|
} from "../lib/locationMapUtils";
|
||||||
@ -51,32 +48,51 @@ export default function LocationMapManager() {
|
|||||||
const { activeHousehold, loading: householdLoading, hasLoaded } = useContext(HouseholdContext);
|
const { activeHousehold, loading: householdLoading, hasLoaded } = useContext(HouseholdContext);
|
||||||
const { stores, activeStore, setActiveStore } = useContext(StoreContext);
|
const { stores, activeStore, setActiveStore } = useContext(StoreContext);
|
||||||
|
|
||||||
const [mapState, setMapState] = useState(null);
|
const {
|
||||||
const [loading, setLoading] = useState(true);
|
beginSaving,
|
||||||
const [loadError, setLoadError] = useState(null);
|
dragState,
|
||||||
const [saving, setSaving] = useState(false);
|
editorTool,
|
||||||
const [savingAction, setSavingAction] = useState(null);
|
endSaving,
|
||||||
const [mode, setMode] = useState("setup");
|
filters,
|
||||||
const [editorTool, setEditorTool] = useState("pan");
|
future,
|
||||||
const [previewDraft, setPreviewDraft] = useState(false);
|
hasUnsavedChanges,
|
||||||
const [mapDraft, setMapDraft] = useState({
|
history,
|
||||||
name: "Store Map",
|
layersOpen,
|
||||||
width: DEFAULT_MAP_SIZE.width,
|
loadError,
|
||||||
height: DEFAULT_MAP_SIZE.height,
|
loadMap,
|
||||||
|
loading,
|
||||||
|
mapDraft,
|
||||||
|
mapState,
|
||||||
|
mode,
|
||||||
|
objects,
|
||||||
|
previewDraft,
|
||||||
|
saving,
|
||||||
|
savingAction,
|
||||||
|
selectedObjectKey,
|
||||||
|
setDragState,
|
||||||
|
setEditorTool,
|
||||||
|
setFilters,
|
||||||
|
setFuture,
|
||||||
|
setHasUnsavedChanges,
|
||||||
|
setHistory,
|
||||||
|
setLayersOpen,
|
||||||
|
setMapState,
|
||||||
|
setMode,
|
||||||
|
setObjects,
|
||||||
|
setPreviewDraft,
|
||||||
|
setSelectedObjectKey,
|
||||||
|
setZoom,
|
||||||
|
syncMapDraftFromState,
|
||||||
|
zoom,
|
||||||
|
} = useLocationMapDraftState({
|
||||||
|
activeHousehold,
|
||||||
|
hasLoaded,
|
||||||
|
householdLoading,
|
||||||
|
locationId,
|
||||||
|
toast,
|
||||||
});
|
});
|
||||||
const [objects, setObjects] = useState([]);
|
|
||||||
const [selectedObjectKey, setSelectedObjectKey] = useState(null);
|
|
||||||
const [filters, setFilters] = useState(DEFAULT_MAP_FILTERS);
|
|
||||||
const [layersOpen, setLayersOpen] = useState(false);
|
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
|
|
||||||
const [zoom, setZoom] = useState(0.75);
|
|
||||||
const [history, setHistory] = useState([]);
|
|
||||||
const [future, setFuture] = useState([]);
|
|
||||||
const [dragState, setDragState] = useState(null);
|
|
||||||
const [pendingDeleteObject, setPendingDeleteObject] = useState(null);
|
const [pendingDeleteObject, setPendingDeleteObject] = useState(null);
|
||||||
const svgRef = useRef(null);
|
const svgRef = useRef(null);
|
||||||
const toastRef = useRef(toast);
|
|
||||||
const loadRequestRef = useRef(0);
|
|
||||||
|
|
||||||
const location = mapState?.location || stores.find((store) => String(store.id) === String(locationId));
|
const location = mapState?.location || stores.find((store) => String(store.id) === String(locationId));
|
||||||
const canManage = Boolean(mapState?.can_manage ?? ["owner", "admin"].includes(activeHousehold?.role));
|
const canManage = Boolean(mapState?.can_manage ?? ["owner", "admin"].includes(activeHousehold?.role));
|
||||||
@ -132,117 +148,6 @@ export default function LocationMapManager() {
|
|||||||
[activeMap?.height, activeMap?.width, mapDraft.height, mapDraft.width]
|
[activeMap?.height, activeMap?.width, mapDraft.height, mapDraft.width]
|
||||||
);
|
);
|
||||||
|
|
||||||
const syncMapDraftFromState = useCallback((nextState, nextMode, nextPreviewDraft) => {
|
|
||||||
const nextMap = mapForMode(nextState, nextMode, nextPreviewDraft);
|
|
||||||
const nextObjects = objectsForMode(nextState, nextMode, nextPreviewDraft);
|
|
||||||
if (!nextMap) return;
|
|
||||||
|
|
||||||
setMapDraft({
|
|
||||||
name: nextMap.name || "Store Map",
|
|
||||||
width: nextMap.width || DEFAULT_MAP_SIZE.width,
|
|
||||||
height: nextMap.height || DEFAULT_MAP_SIZE.height,
|
|
||||||
});
|
|
||||||
setObjects(nextObjects.map(normalizeMapObject));
|
|
||||||
setSelectedObjectKey(null);
|
|
||||||
setHistory([]);
|
|
||||||
setFuture([]);
|
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadMap = useCallback(async () => {
|
|
||||||
const requestId = loadRequestRef.current + 1;
|
|
||||||
loadRequestRef.current = requestId;
|
|
||||||
|
|
||||||
if (!activeHousehold?.id || !locationId) {
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
setLoadError(null);
|
|
||||||
try {
|
|
||||||
const response = await getLocationMap(activeHousehold.id, locationId);
|
|
||||||
if (loadRequestRef.current !== requestId) return;
|
|
||||||
|
|
||||||
const nextState = response.data;
|
|
||||||
const nextCanManage = Boolean(
|
|
||||||
nextState.can_manage ?? ["owner", "admin"].includes(activeHousehold?.role)
|
|
||||||
);
|
|
||||||
setMapState(nextState);
|
|
||||||
|
|
||||||
if (!nextState.draft_map && !nextState.published_map) {
|
|
||||||
setMode("setup");
|
|
||||||
setEditorTool("pan");
|
|
||||||
setPreviewDraft(false);
|
|
||||||
setObjects([]);
|
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
setMapDraft({
|
|
||||||
name: "Store Map",
|
|
||||||
width: DEFAULT_MAP_SIZE.width,
|
|
||||||
height: DEFAULT_MAP_SIZE.height,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState.published_map) {
|
|
||||||
setMode("view");
|
|
||||||
setEditorTool("pan");
|
|
||||||
setPreviewDraft(false);
|
|
||||||
} else if (nextState.draft_map && nextCanManage) {
|
|
||||||
setMode("setup");
|
|
||||||
setEditorTool("pan");
|
|
||||||
setPreviewDraft(true);
|
|
||||||
} else {
|
|
||||||
setMode("setup");
|
|
||||||
setEditorTool("pan");
|
|
||||||
setPreviewDraft(false);
|
|
||||||
setObjects([]);
|
|
||||||
setSelectedObjectKey(null);
|
|
||||||
setHistory([]);
|
|
||||||
setFuture([]);
|
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
setMapDraft({
|
|
||||||
name: "Store Map",
|
|
||||||
width: DEFAULT_MAP_SIZE.width,
|
|
||||||
height: DEFAULT_MAP_SIZE.height,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
syncMapDraftFromState(
|
|
||||||
nextState,
|
|
||||||
nextState.published_map ? "view" : "edit",
|
|
||||||
!nextState.published_map
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
if (loadRequestRef.current !== requestId) return;
|
|
||||||
|
|
||||||
const message = getApiErrorMessage(error, "Failed to load map");
|
|
||||||
toastRef.current.error("Load map failed", message);
|
|
||||||
setMapState(null);
|
|
||||||
setObjects([]);
|
|
||||||
setSelectedObjectKey(null);
|
|
||||||
setHistory([]);
|
|
||||||
setFuture([]);
|
|
||||||
setHasUnsavedChanges(false);
|
|
||||||
setLoadError(message);
|
|
||||||
} finally {
|
|
||||||
if (loadRequestRef.current === requestId) {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [activeHousehold?.id, activeHousehold?.role, locationId, syncMapDraftFromState]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
toastRef.current = toast;
|
|
||||||
}, [toast]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!householdLoading && hasLoaded) {
|
|
||||||
loadMap();
|
|
||||||
}
|
|
||||||
}, [hasLoaded, householdLoading, loadMap]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const matchingLocation = stores.find((store) => String(store.id) === String(locationId));
|
const matchingLocation = stores.find((store) => String(store.id) === String(locationId));
|
||||||
if (matchingLocation && String(activeStore?.id) !== String(matchingLocation.id)) {
|
if (matchingLocation && String(activeStore?.id) !== String(matchingLocation.id)) {
|
||||||
@ -283,16 +188,6 @@ export default function LocationMapManager() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const beginSaving = (action) => {
|
|
||||||
setSavingAction(action);
|
|
||||||
setSaving(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const endSaving = () => {
|
|
||||||
setSaving(false);
|
|
||||||
setSavingAction(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCreateBlank = async () => {
|
const handleCreateBlank = async () => {
|
||||||
if (!activeHousehold?.id || !locationId) return;
|
if (!activeHousehold?.id || !locationId) return;
|
||||||
beginSaving("create-blank");
|
beginSaving("create-blank");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user