Add store location map manager #20
@ -138,11 +138,34 @@ export default function LocationMapCanvas({
|
|||||||
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fittedLabel = (object, fallback) => {
|
const truncateLabel = (value, maxCharacters) => {
|
||||||
const rawLabel = object.label || object.zone_name || fallback;
|
if (value.length <= maxCharacters) return value;
|
||||||
const maxCharacters = Math.max(8, Math.floor((Number(object.width) - 28) / 14));
|
return `${value.slice(0, Math.max(5, maxCharacters - 3)).trimEnd()}...`;
|
||||||
if (rawLabel.length <= maxCharacters) return rawLabel;
|
};
|
||||||
return `${rawLabel.slice(0, Math.max(5, maxCharacters - 3))}...`;
|
|
||||||
|
const fittedLabelLines = (object, fallback) => {
|
||||||
|
const rawLabel = String(object.label || object.zone_name || fallback).trim();
|
||||||
|
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) => {
|
const renderMapObject = (object, index) => {
|
||||||
@ -152,6 +175,8 @@ export default function LocationMapCanvas({
|
|||||||
const zoneItems = itemsForZone(mapItems, object.zone_id, filters, username);
|
const zoneItems = itemsForZone(mapItems, object.zone_id, filters, username);
|
||||||
const count = zoneItems.length;
|
const count = zoneItems.length;
|
||||||
const countLabel = `${count} item${count === 1 ? "" : "s"}`;
|
const countLabel = `${count} item${count === 1 ? "" : "s"}`;
|
||||||
|
const labelLines = fittedLabelLines(object, "Area");
|
||||||
|
const countY = labelLines.length > 1 ? object.y + 76 : object.y + 52;
|
||||||
const pinDots = filters.showPins
|
const pinDots = filters.showPins
|
||||||
? zoneItems.slice(0, 12).map((item, itemIndex) => {
|
? zoneItems.slice(0, 12).map((item, itemIndex) => {
|
||||||
const column = itemIndex % 4;
|
const column = itemIndex % 4;
|
||||||
@ -192,11 +217,15 @@ export default function LocationMapCanvas({
|
|||||||
/>
|
/>
|
||||||
{filters.showLabels ? (
|
{filters.showLabels ? (
|
||||||
<text x={object.x + 16} y={object.y + 28} className="location-map-label">
|
<text x={object.x + 16} y={object.y + 28} className="location-map-label">
|
||||||
{fittedLabel(object, "Area")}
|
{labelLines.map((line, lineIndex) => (
|
||||||
|
<tspan key={`${line}-${lineIndex}`} x={object.x + 16} dy={lineIndex === 0 ? 0 : 24}>
|
||||||
|
{line}
|
||||||
|
</tspan>
|
||||||
|
))}
|
||||||
</text>
|
</text>
|
||||||
) : null}
|
) : null}
|
||||||
{filters.showCounts ? (
|
{filters.showCounts ? (
|
||||||
<text x={object.x + 16} y={object.y + 52} className="location-map-count">
|
<text x={object.x + 16} y={countY} className="location-map-count">
|
||||||
{countLabel}
|
{countLabel}
|
||||||
</text>
|
</text>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -423,6 +423,46 @@ test("admin status follows the visible map when a saved draft exists", async ({
|
|||||||
await expect(page.locator(".location-map-object", { hasText: "Live Bakery" })).toBeVisible();
|
await expect(page.locator(".location-map-object", { hasText: "Live Bakery" })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("viewer wraps long zone labels and keeps item counts visible", async ({ page }) => {
|
||||||
|
await mockMapShell(page);
|
||||||
|
|
||||||
|
const mapState = publishedMapState([
|
||||||
|
{
|
||||||
|
id: 1501,
|
||||||
|
location_map_id: 901,
|
||||||
|
zone_id: 501,
|
||||||
|
zone_name: "Produce & Fresh Vegetables",
|
||||||
|
type: "zone",
|
||||||
|
label: "Produce & Fresh Vegetables",
|
||||||
|
x: 40,
|
||||||
|
y: 40,
|
||||||
|
width: 260,
|
||||||
|
height: 120,
|
||||||
|
rotation: 0,
|
||||||
|
locked: false,
|
||||||
|
visible: true,
|
||||||
|
sort_order: 1,
|
||||||
|
},
|
||||||
|
], true);
|
||||||
|
|
||||||
|
await page.route("**/households/1/locations/10/map", async (route) => {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(mapState),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/stores/100/locations/10/map");
|
||||||
|
|
||||||
|
const produceArea = page.locator('[data-object-key="1501"]');
|
||||||
|
await expect(produceArea.locator(".location-map-label tspan")).toHaveCount(2);
|
||||||
|
await expect(produceArea.locator(".location-map-label tspan").nth(0)).toHaveText("Produce & Fresh");
|
||||||
|
await expect(produceArea.locator(".location-map-label tspan").nth(1)).toHaveText("Vegetables");
|
||||||
|
await expect(produceArea.locator(".location-map-count")).toHaveText("2 items");
|
||||||
|
await expect(page.locator(".location-map-pin")).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
test("members can view a published map but cannot edit it", async ({ page }) => {
|
test("members can view a published map but cannot edit it", async ({ page }) => {
|
||||||
await mockMapShell(page, memberHousehold);
|
await mockMapShell(page, memberHousehold);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user