refactor: extract map object actions

This commit is contained in:
Nico 2026-06-04 08:41:54 -07:00
parent 03814b8b4c
commit 95b14b447f
2 changed files with 166 additions and 107 deletions

View File

@ -0,0 +1,136 @@
import { useCallback, useState } from "react";
import {
clampObjectToMap,
createClientObject,
getMapObjectDisplayLabel,
getObjectKey,
objectZoneId,
} from "../lib/locationMapUtils";
const EMPTY_ZONES = [];
export default function useLocationMapObjectActions({
canManage,
hiddenAreaCount,
mapSize,
mapState,
objects,
selectedObject,
selectedObjectKey,
setEditorTool,
setFuture,
setHasUnsavedChanges,
setHistory,
setObjects,
setSelectedObjectKey,
toast,
}) {
const [pendingDeleteObject, setPendingDeleteObject] = useState(null);
const zones = mapState?.zones || EMPTY_ZONES;
const remember = useCallback((currentObjects = objects) => {
setHistory((previous) => [...previous.slice(-19), currentObjects.map((object) => ({ ...object }))]);
setFuture([]);
}, [objects, setFuture, setHistory]);
const updateObjects = useCallback((updater) => {
setHasUnsavedChanges(true);
setObjects((currentObjects) =>
updater(currentObjects).map((object) => clampObjectToMap(object, mapSize))
);
}, [mapSize, setHasUnsavedChanges, setObjects]);
const handleAddObject = useCallback(() => {
if (!canManage) return;
remember();
const usedZoneIds = new Set(objects.map((object) => objectZoneId(object)).filter(Boolean));
const nextZone = zones.find((zone) => !usedZoneIds.has(String(zone.id))) || zones[0];
const nextObject = createClientObject(nextZone, objects.length);
setObjects((currentObjects) => [...currentObjects, nextObject]);
setSelectedObjectKey(getObjectKey(nextObject));
setEditorTool("edit");
setHasUnsavedChanges(true);
}, [canManage, objects, remember, setEditorTool, setHasUnsavedChanges, setObjects, setSelectedObjectKey, zones]);
const handleDuplicateObject = useCallback(() => {
if (!selectedObject) return;
remember();
const displayLabel = getMapObjectDisplayLabel(selectedObject, "Area");
const nextObject = {
...selectedObject,
id: undefined,
client_id: `copy-${Date.now()}`,
label: `${displayLabel} Copy`,
x: selectedObject.x + 28,
y: selectedObject.y + 28,
sort_order: objects.length + 1,
};
setObjects((currentObjects) => [...currentObjects, clampObjectToMap(nextObject, mapSize)]);
setSelectedObjectKey(getObjectKey(nextObject));
setEditorTool("edit");
setHasUnsavedChanges(true);
}, [mapSize, objects.length, remember, selectedObject, setEditorTool, setHasUnsavedChanges, setObjects, setSelectedObjectKey]);
const requestDeleteObject = useCallback(() => {
if (selectedObject) setPendingDeleteObject(selectedObject);
}, [selectedObject]);
const handleDeleteObject = useCallback(() => {
if (!pendingDeleteObject) return;
remember();
setObjects((currentObjects) =>
currentObjects.filter((object) => getObjectKey(object) !== getObjectKey(pendingDeleteObject))
);
setSelectedObjectKey(null);
setHasUnsavedChanges(true);
toast.success("Deleted map area", "Save the draft to keep this change.");
setPendingDeleteObject(null);
}, [pendingDeleteObject, remember, setHasUnsavedChanges, setObjects, setSelectedObjectKey, toast]);
const handleShowHiddenAreas = useCallback(() => {
if (!canManage || hiddenAreaCount === 0) return;
remember();
updateObjects((currentObjects) =>
currentObjects.map((object) => (
object.visible === false ? { ...object, visible: true } : object
))
);
toast.success("Showed hidden areas", "Save the draft to keep this change.");
}, [canManage, hiddenAreaCount, remember, toast, updateObjects]);
const handleObjectField = useCallback((field, value) => {
if (!selectedObject) return;
remember();
updateObjects((currentObjects) =>
currentObjects.map((object) => (
getObjectKey(object) === getObjectKey(selectedObject) ? { ...object, [field]: value } : object
))
);
}, [remember, selectedObject, updateObjects]);
const handleZoneLinkChange = useCallback((zoneId) => {
const zone = zones.find((candidate) => String(candidate.id) === String(zoneId));
handleObjectField("zone_id", zone?.id || null);
setObjects((currentObjects) =>
currentObjects.map((object) => (
getObjectKey(object) === selectedObjectKey
? { ...object, zone_name: zone?.name || null, label: object.label ?? zone?.name ?? "" }
: object
))
);
}, [handleObjectField, selectedObjectKey, setObjects, zones]);
return {
handleAddObject,
handleDeleteObject,
handleDuplicateObject,
handleObjectField,
handleShowHiddenAreas,
handleZoneLinkChange,
pendingDeleteObject,
remember,
requestDeleteObject,
setPendingDeleteObject,
updateObjects,
};
}

View File

@ -1,4 +1,4 @@
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { AuthContext } from "../context/AuthContext";
import { HouseholdContext } from "../context/HouseholdContext";
@ -12,19 +12,17 @@ import ConfirmSlideModal from "../components/modals/ConfirmSlideModal";
import useActionToast from "../hooks/useActionToast";
import useLocationMapDraftActions from "../hooks/useLocationMapDraftActions";
import useLocationMapDraftState from "../hooks/useLocationMapDraftState";
import useLocationMapObjectActions from "../hooks/useLocationMapObjectActions";
import useUnsavedMapLeaveGuard from "../hooks/useUnsavedMapLeaveGuard";
import {
DEFAULT_MAP_SIZE,
clampMapZoom,
clampObjectToMap,
createClientObject,
getMapObjectDisplayLabel,
getMapStatus,
getObjectKey,
itemMatchesMapFilters,
itemsForZone,
mapForMode,
objectZoneId,
unmappedItems,
} from "../lib/locationMapUtils";
import "../styles/pages/LocationMapManager.css";
@ -84,7 +82,6 @@ export default function LocationMapManager() {
locationId,
toast,
});
const [pendingDeleteObject, setPendingDeleteObject] = useState(null);
const svgRef = useRef(null);
const location = mapState?.location || stores.find((store) => String(store.id) === String(locationId));
@ -168,19 +165,6 @@ export default function LocationMapManager() {
}
}, [filters.showZones, objects, selectedObjectKey]);
const remember = (currentObjects = objects) => {
setHistory((previous) => [...previous.slice(-19), currentObjects.map((object) => ({ ...object }))]);
setFuture([]);
};
const updateObjects = (updater) => {
setHasUnsavedChanges(true);
setObjects((currentObjects) => {
const nextObjects = updater(currentObjects).map((object) => clampObjectToMap(object, mapSize));
return nextObjects;
});
};
const {
handleCreateBlank,
handleCreateFromZones,
@ -202,95 +186,34 @@ export default function LocationMapManager() {
toast,
});
const handleAddObject = () => {
if (!canManage) return;
remember();
const usedZoneIds = new Set(objects.map((object) => objectZoneId(object)).filter(Boolean));
const nextZone = (mapState?.zones || []).find((zone) => !usedZoneIds.has(String(zone.id))) || mapState?.zones?.[0];
const nextObject = createClientObject(nextZone, objects.length);
setObjects((currentObjects) => [...currentObjects, nextObject]);
setSelectedObjectKey(getObjectKey(nextObject));
setEditorTool("edit");
setHasUnsavedChanges(true);
};
const handleDuplicateObject = () => {
if (!selectedObject) return;
remember();
const displayLabel = getMapObjectDisplayLabel(selectedObject, "Area");
const nextObject = {
...selectedObject,
id: undefined,
client_id: `copy-${Date.now()}`,
label: `${displayLabel} Copy`,
x: selectedObject.x + 28,
y: selectedObject.y + 28,
sort_order: objects.length + 1,
};
setObjects((currentObjects) => [...currentObjects, clampObjectToMap(nextObject, mapSize)]);
setSelectedObjectKey(getObjectKey(nextObject));
setEditorTool("edit");
setHasUnsavedChanges(true);
};
const requestDeleteObject = () => {
if (!selectedObject) return;
setPendingDeleteObject(selectedObject);
};
const handleDeleteObject = () => {
if (!pendingDeleteObject) return;
remember();
setObjects((currentObjects) =>
currentObjects.filter((object) => getObjectKey(object) !== getObjectKey(pendingDeleteObject))
);
setSelectedObjectKey(null);
setHasUnsavedChanges(true);
toast.success(
"Deleted map area",
"Save the draft to keep this change."
);
setPendingDeleteObject(null);
};
const handleShowHiddenAreas = () => {
if (!canManage || hiddenAreaCount === 0) return;
remember();
updateObjects((currentObjects) =>
currentObjects.map((object) =>
object.visible === false ? { ...object, visible: true } : object
)
);
toast.success("Showed hidden areas", "Save the draft to keep this change.");
};
const handleObjectField = (field, value) => {
if (!selectedObject) return;
remember();
updateObjects((currentObjects) =>
currentObjects.map((object) =>
getObjectKey(object) === getObjectKey(selectedObject)
? { ...object, [field]: value }
: object
)
);
};
const handleZoneLinkChange = (zoneId) => {
const zone = (mapState?.zones || []).find((candidate) => String(candidate.id) === String(zoneId));
handleObjectField("zone_id", zone?.id || null);
setObjects((currentObjects) =>
currentObjects.map((object) =>
getObjectKey(object) === selectedObjectKey
? {
...object,
zone_name: zone?.name || null,
label: object.label ?? zone?.name ?? "",
}
: object
)
);
};
const {
handleAddObject,
handleDeleteObject,
handleDuplicateObject,
handleObjectField,
handleShowHiddenAreas,
handleZoneLinkChange,
pendingDeleteObject,
remember,
requestDeleteObject,
setPendingDeleteObject,
updateObjects,
} = useLocationMapObjectActions({
canManage,
hiddenAreaCount,
mapSize,
mapState,
objects,
selectedObject,
selectedObjectKey,
setEditorTool,
setFuture,
setHasUnsavedChanges,
setHistory,
setObjects,
setSelectedObjectKey,
toast,
});
const handleUndo = () => {
setHistory((previous) => {