From 19d345ac9a49d703956713f866e146018f7765d4 Mon Sep 17 00:00:00 2001 From: Nico Date: Thu, 4 Jun 2026 07:33:48 -0700 Subject: [PATCH] refactor: split map object rendering --- .../src/components/maps/LocationMapCanvas.jsx | 139 ++-------------- .../src/components/maps/LocationMapObject.jsx | 148 ++++++++++++++++++ 2 files changed, 164 insertions(+), 123 deletions(-) create mode 100644 frontend/src/components/maps/LocationMapObject.jsx diff --git a/frontend/src/components/maps/LocationMapCanvas.jsx b/frontend/src/components/maps/LocationMapCanvas.jsx index 929c50e..5d72929 100644 --- a/frontend/src/components/maps/LocationMapCanvas.jsx +++ b/frontend/src/components/maps/LocationMapCanvas.jsx @@ -1,10 +1,10 @@ import { useMemo, useRef, useState } from "react"; import { clientPointToMap, - getMapObjectDisplayLabel, getObjectKey, itemMatchesMapFilters, } from "../../lib/locationMapUtils"; +import LocationMapObject from "./LocationMapObject"; const DRAG_CHANGE_THRESHOLD = 2; const NUDGE_KEYS = new Set(["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"]); @@ -246,135 +246,28 @@ export default function LocationMapCanvas({ event.currentTarget.releasePointerCapture?.(event.pointerId); }; - const truncateLabel = (value, maxCharacters) => { - if (value.length <= maxCharacters) return value; - return `${value.slice(0, Math.max(5, maxCharacters - 3)).trimEnd()}...`; - }; - - const fittedLabelLines = (object, fallback) => { - const rawLabel = getMapObjectDisplayLabel(object, fallback); - const maxCharacters = Math.max(8, Math.floor((Number(object.width) - 28) / 13)); - const canUseTwoLines = Number(object.height) >= 92 && rawLabel.length > maxCharacters; - if (!canUseTwoLines) return [truncateLabel(rawLabel, maxCharacters)]; - - const words = rawLabel.split(/\s+/); - let firstLine = ""; - let splitIndex = 0; - for (let index = 0; index < words.length; index += 1) { - const candidate = firstLine ? `${firstLine} ${words[index]}` : words[index]; - if (candidate.length > maxCharacters && firstLine) break; - firstLine = candidate; - splitIndex = index + 1; - if (candidate.length >= maxCharacters) break; - } - - const secondLine = words.slice(splitIndex).join(" "); - if (!secondLine) return [truncateLabel(firstLine, maxCharacters)]; - return [ - truncateLabel(firstLine, maxCharacters), - truncateLabel(secondLine, maxCharacters), - ]; - }; - const renderMapObject = (object, index) => { - if (!object.visible || !filters.showZones) return null; const key = getObjectKey(object); - const isSelected = key === selectedObjectKey; const zoneKey = String(object.zone_id || ""); const zoneItems = zoneKey ? zoneItemSummary.visibleItemsByZone.get(zoneKey) || [] : []; const assignedZoneItemCount = zoneKey ? zoneItemSummary.assignedCounts.get(zoneKey) || 0 : 0; - const count = zoneItems.length; - const hasHiddenZoneItems = assignedZoneItemCount > count; - const countLabel = hasHiddenZoneItems - ? `${count} shown` - : `${count} item${count === 1 ? "" : "s"}`; - const labelLines = fittedLabelLines(object, "Area"); - const countY = filters.showLabels && labelLines.length > 1 ? object.y + 76 : object.y + 52; - const resizeHandleSize = Math.min(96, Math.max(44, Math.ceil(48 / zoom))); - const resizeHandleInset = 6; - const resizeHandleX = Math.min( - Math.max(0, mapSize.width - resizeHandleSize), - Math.max( - object.x + resizeHandleInset, - object.x + object.width - resizeHandleSize - resizeHandleInset - ) - ); - const resizeHandleY = Math.min( - Math.max(0, mapSize.height - resizeHandleSize), - Math.max( - object.y + resizeHandleInset, - object.y + object.height - resizeHandleSize - resizeHandleInset - ) - ); - const pinDots = filters.showPins - ? zoneItems.slice(0, 12).map((item, itemIndex) => { - const column = itemIndex % 4; - const row = Math.floor(itemIndex / 4); - return ( - - ); - }) - : null; - return ( - - startDrag(event, object, "move")} - onClick={(event) => handleObjectClick(event, object)} - onKeyDown={(event) => handleObjectKeyDown(event, object)} - /> - {filters.showLabels ? ( - - {labelLines.map((line, lineIndex) => ( - - {line} - - ))} - - ) : null} - {filters.showCounts ? ( - - {countLabel} - - ) : null} - {pinDots} - {canEditObjects && isSelected && !object.locked ? ( - startDrag(event, object, "resize")} - onClick={(event) => event.stopPropagation()} - /> - ) : null} - + object={object} + index={index} + filters={filters} + zoneItems={zoneItems} + assignedZoneItemCount={assignedZoneItemCount} + isSelected={key === selectedObjectKey} + canEditObjects={canEditObjects} + canSelect={mode === "view" || canEditObjects} + zoom={zoom} + mapSize={mapSize} + onStartDrag={startDrag} + onObjectClick={handleObjectClick} + onObjectKeyDown={handleObjectKeyDown} + /> ); }; diff --git a/frontend/src/components/maps/LocationMapObject.jsx b/frontend/src/components/maps/LocationMapObject.jsx new file mode 100644 index 0000000..ce276f1 --- /dev/null +++ b/frontend/src/components/maps/LocationMapObject.jsx @@ -0,0 +1,148 @@ +import { + getMapObjectDisplayLabel, + getObjectKey, +} from "../../lib/locationMapUtils"; + +const PIN_PREVIEW_LIMIT = 12; + +function truncateLabel(value, maxCharacters) { + if (value.length <= maxCharacters) return value; + return `${value.slice(0, Math.max(5, maxCharacters - 3)).trimEnd()}...`; +} + +function fittedLabelLines(object, fallback) { + const rawLabel = getMapObjectDisplayLabel(object, fallback); + const maxCharacters = Math.max(8, Math.floor((Number(object.width) - 28) / 13)); + const canUseTwoLines = Number(object.height) >= 92 && rawLabel.length > maxCharacters; + if (!canUseTwoLines) return [truncateLabel(rawLabel, maxCharacters)]; + + const words = rawLabel.split(/\s+/); + let firstLine = ""; + let splitIndex = 0; + for (let index = 0; index < words.length; index += 1) { + const candidate = firstLine ? `${firstLine} ${words[index]}` : words[index]; + if (candidate.length > maxCharacters && firstLine) break; + firstLine = candidate; + splitIndex = index + 1; + if (candidate.length >= maxCharacters) break; + } + + const secondLine = words.slice(splitIndex).join(" "); + if (!secondLine) return [truncateLabel(firstLine, maxCharacters)]; + return [ + truncateLabel(firstLine, maxCharacters), + truncateLabel(secondLine, maxCharacters), + ]; +} + +export default function LocationMapObject({ + object, + index, + filters, + zoneItems, + assignedZoneItemCount, + isSelected, + canEditObjects, + canSelect, + zoom, + mapSize, + onStartDrag, + onObjectClick, + onObjectKeyDown, +}) { + if (!object.visible || !filters.showZones) return null; + + const key = getObjectKey(object); + const count = zoneItems.length; + const hasHiddenZoneItems = assignedZoneItemCount > count; + const countLabel = hasHiddenZoneItems + ? `${count} shown` + : `${count} item${count === 1 ? "" : "s"}`; + const labelLines = fittedLabelLines(object, "Area"); + const countY = filters.showLabels && labelLines.length > 1 ? object.y + 76 : object.y + 52; + const resizeHandleSize = Math.min(96, Math.max(44, Math.ceil(48 / zoom))); + const resizeHandleInset = 6; + const resizeHandleX = Math.min( + Math.max(0, mapSize.width - resizeHandleSize), + Math.max( + object.x + resizeHandleInset, + object.x + object.width - resizeHandleSize - resizeHandleInset + ) + ); + const resizeHandleY = Math.min( + Math.max(0, mapSize.height - resizeHandleSize), + Math.max( + object.y + resizeHandleInset, + object.y + object.height - resizeHandleSize - resizeHandleInset + ) + ); + const pinDots = filters.showPins + ? zoneItems.slice(0, PIN_PREVIEW_LIMIT).map((item, itemIndex) => { + const column = itemIndex % 4; + const row = Math.floor(itemIndex / 4); + return ( + + ); + }) + : null; + + return ( + + onStartDrag(event, object, "move")} + onClick={(event) => onObjectClick(event, object)} + onKeyDown={(event) => onObjectKeyDown(event, object)} + /> + {filters.showLabels ? ( + + {labelLines.map((line, lineIndex) => ( + + {line} + + ))} + + ) : null} + {filters.showCounts ? ( + + {countLabel} + + ) : null} + {pinDots} + {canEditObjects && isSelected && !object.locked ? ( + onStartDrag(event, object, "resize")} + onClick={(event) => event.stopPropagation()} + /> + ) : null} + + ); +}