diff --git a/frontend/src/hooks/useLocationMapDraftActions.js b/frontend/src/hooks/useLocationMapDraftActions.js index 1ce69df..c5d8bc6 100644 --- a/frontend/src/hooks/useLocationMapDraftActions.js +++ b/frontend/src/hooks/useLocationMapDraftActions.js @@ -6,7 +6,7 @@ import { saveLocationMapDraft, } from "../api/locationMaps"; import getApiErrorMessage from "../lib/getApiErrorMessage"; -import { prepareObjectsForSave } from "../lib/locationMapUtils"; +import { getObjectKey, prepareObjectsForSave } from "../lib/locationMapUtils"; export default function useLocationMapDraftActions({ activeHouseholdId, @@ -16,6 +16,7 @@ export default function useLocationMapDraftActions({ locationId, mapDraft, objects, + selectedObjectKey, setEditorTool, setMapState, setMode, @@ -23,11 +24,15 @@ export default function useLocationMapDraftActions({ syncMapDraftFromState, toast, }) { - const applyDraftResponse = useCallback((response, nextMode = "edit", nextPreviewDraft = true) => { + const selectedObjectIndex = selectedObjectKey + ? objects.findIndex((object) => getObjectKey(object) === selectedObjectKey) + : -1; + + const applyDraftResponse = useCallback((response, nextMode = "edit", nextPreviewDraft = true, options = {}) => { setMapState(response.data); setMode(nextMode); setPreviewDraft(nextPreviewDraft); - syncMapDraftFromState(response.data, nextMode, nextPreviewDraft); + syncMapDraftFromState(response.data, nextMode, nextPreviewDraft, options); }, [setMapState, setMode, setPreviewDraft, syncMapDraftFromState]); const handleCreateBlank = useCallback(async () => { @@ -68,14 +73,17 @@ export default function useLocationMapDraftActions({ map: mapDraft, objects: prepareObjectsForSave(objects), }); - applyDraftResponse(response); + applyDraftResponse(response, "edit", true, { preserveSelectedIndex: selectedObjectIndex }); toast.success("Saved draft", "Map draft saved"); } catch (error) { toast.error("Save draft failed", getApiErrorMessage(error, "Failed to save map draft")); } finally { endSaving(); } - }, [activeHouseholdId, applyDraftResponse, beginSaving, endSaving, locationId, mapDraft, objects, toast]); + }, [ + activeHouseholdId, applyDraftResponse, beginSaving, endSaving, locationId, + mapDraft, objects, selectedObjectIndex, toast, + ]); const handlePublish = useCallback(async () => { if (!activeHouseholdId || !locationId) return; @@ -88,7 +96,7 @@ export default function useLocationMapDraftActions({ objects: prepareObjectsForSave(objects), }); savedPendingDraft = true; - applyDraftResponse(saveResponse); + applyDraftResponse(saveResponse, "edit", true, { preserveSelectedIndex: selectedObjectIndex }); } const response = await publishLocationMapDraft(activeHouseholdId, locationId); @@ -108,7 +116,7 @@ export default function useLocationMapDraftActions({ } }, [ activeHouseholdId, applyDraftResponse, beginSaving, endSaving, hasUnsavedChanges, - locationId, mapDraft, objects, setEditorTool, toast, + locationId, mapDraft, objects, selectedObjectIndex, setEditorTool, toast, ]); return { handleCreateBlank, handleCreateFromZones, handlePublish, handleSaveDraft }; diff --git a/frontend/src/hooks/useLocationMapDraftState.js b/frontend/src/hooks/useLocationMapDraftState.js index 63e62f3..5021b31 100644 --- a/frontend/src/hooks/useLocationMapDraftState.js +++ b/frontend/src/hooks/useLocationMapDraftState.js @@ -1,7 +1,14 @@ 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"; +import { + DEFAULT_MAP_FILTERS, + DEFAULT_MAP_SIZE, + getObjectKey, + mapForMode, + normalizeMapObject, + objectsForMode, +} from "../lib/locationMapUtils"; const DEFAULT_DRAFT_MAP = { name: "Store Map", @@ -46,18 +53,25 @@ export default function useLocationMapDraftState({ setMapDraft(DEFAULT_DRAFT_MAP); }, []); - const syncMapDraftFromState = useCallback((nextState, nextMode, nextPreviewDraft) => { + const syncMapDraftFromState = useCallback((nextState, nextMode, nextPreviewDraft, options = {}) => { const nextMap = mapForMode(nextState, nextMode, nextPreviewDraft); const nextObjects = objectsForMode(nextState, nextMode, nextPreviewDraft); if (!nextMap) return; + const normalizedObjects = nextObjects.map(normalizeMapObject); + const preserveSelectedIndex = Number.isInteger(options.preserveSelectedIndex) + ? options.preserveSelectedIndex + : -1; + const preservedSelectedObject = preserveSelectedIndex >= 0 + ? normalizedObjects[preserveSelectedIndex] + : null; 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); + setObjects(normalizedObjects); + setSelectedObjectKey(preservedSelectedObject ? getObjectKey(preservedSelectedObject) : null); setHistory([]); setFuture([]); setHasUnsavedChanges(false); diff --git a/frontend/src/pages/LocationMapManager.jsx b/frontend/src/pages/LocationMapManager.jsx index e4627ad..db414ad 100644 --- a/frontend/src/pages/LocationMapManager.jsx +++ b/frontend/src/pages/LocationMapManager.jsx @@ -254,6 +254,7 @@ export default function LocationMapManager() { locationId, mapDraft, objects, + selectedObjectKey, setEditorTool, setMapState, setMode, diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts index 93680b0..1f96683 100644 --- a/frontend/tests/location-map-manager.spec.ts +++ b/frontend/tests/location-map-manager.spec.ts @@ -1418,6 +1418,9 @@ test("admin save progress locks draft controls", async ({ page }) => { releaseSave(); await expect(page.locator(".location-map-status")).toHaveText("Draft"); await expect(page.getByRole("button", { name: "Save Draft" })).toBeDisabled(); + await expect(page.getByRole("textbox", { name: "Label" })).toBeEnabled(); + await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("Saving Bakery"); + await expect(page.getByRole("button", { name: "Map area Saving Bakery" })).toHaveAttribute("aria-pressed", "true"); }); test("admin save progress locks empty-canvas recovery actions", async ({ page }) => {