Skip to content

Commit 25e51c7

Browse files
authored
fix: finish site editor cleanup (#810)
1 parent 1d29cda commit 25e51c7

9 files changed

Lines changed: 766 additions & 2573 deletions

src/components/AccessSettingsEditor.test.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,32 @@ describe("AccessSettingsEditor", () => {
111111
await userEvent.click(within(popover).getByRole("button", { name: /Add Bob/i }));
112112
expect(onAddCollaborator).toHaveBeenCalledWith("user-2");
113113
});
114+
115+
it("can disable collaborator removal without disabling role edits", async () => {
116+
const onRemoveCollaborator = vi.fn();
117+
const onRoleChange = vi.fn();
118+
render(
119+
<AccessSettingsEditor
120+
canRemoveCollaborators={false}
121+
collaborators={collaborators}
122+
directory={directory}
123+
onAddCollaborator={vi.fn()}
124+
onRemoveCollaborator={onRemoveCollaborator}
125+
onRoleChange={onRoleChange}
126+
onVisibilityChange={vi.fn()}
127+
visibility="private"
128+
/>,
129+
);
130+
131+
await userEvent.click(screen.getByRole("button", { name: "Edit collaborators" }));
132+
const popover = await screen.findByRole("dialog", { name: "Edit collaborators" });
133+
134+
await userEvent.selectOptions(within(popover).getByLabelText("Role for Alice"), "editor");
135+
expect(onRoleChange).toHaveBeenCalledWith("user-1", "editor");
136+
137+
const remove = within(popover).getByRole("button", { name: "Remove Alice" });
138+
expect(remove).toBeDisabled();
139+
await userEvent.click(remove);
140+
expect(onRemoveCollaborator).not.toHaveBeenCalled();
141+
});
114142
});

src/components/AccessSettingsEditor.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type AccessSettingsEditorProps = {
2424
onAddCollaborator: (userId: string) => void;
2525
onRemoveCollaborator: (userId: string) => void;
2626
onRoleChange: (userId: string, role: AccessRole) => void;
27+
canRemoveCollaborators?: boolean;
2728
disabled?: boolean;
2829
ownerUserId?: string;
2930
directoryBusy?: boolean;
@@ -44,6 +45,7 @@ export function AccessSettingsEditor({
4445
onAddCollaborator,
4546
onRemoveCollaborator,
4647
onRoleChange,
48+
canRemoveCollaborators = true,
4749
disabled = false,
4850
ownerUserId = "",
4951
directoryBusy = false,
@@ -193,7 +195,7 @@ export function AccessSettingsEditor({
193195
<ActionButton
194196
aria-label={`Remove ${label}`}
195197
className="access-collaborator-remove"
196-
disabled={disabled}
198+
disabled={disabled || !canRemoveCollaborators}
197199
onClick={() => onRemoveCollaborator(user.id)}
198200
size="icon"
199201
title={`Remove ${label}`}

src/components/MapView.tsx

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,7 @@ export function MapView({
806806
bearing: number;
807807
pitch: number;
808808
} | null>(null);
809+
const editorDraftAnimationKeyRef = useRef("");
809810

810811
const stopUserLocation = useCallback(() => {
811812
if (userLocationWatchIdRef.current !== null && navigator.geolocation) {
@@ -2094,6 +2095,34 @@ export function MapView({
20942095
latitude: viewport.center.lat,
20952096
zoom: viewport.zoom,
20962097
};
2098+
useEffect(() => {
2099+
if (!isMapLoaded || mapEditor?.kind !== "site" || !mapEditorSiteDraft) return;
2100+
const animationKey = `${mapEditor.resourceId ?? "new"}:${mapEditorSiteDraft.lat.toFixed(6)}:${mapEditorSiteDraft.lon.toFixed(6)}`;
2101+
if (editorDraftAnimationKeyRef.current === animationKey) return;
2102+
editorDraftAnimationKeyRef.current = animationKey;
2103+
setInteractionViewState(null);
2104+
const didAnimate = animateMapToCenter(mapRef, {
2105+
center: { lat: mapEditorSiteDraft.lat, lon: mapEditorSiteDraft.lon },
2106+
zoom: viewport.zoom,
2107+
padding: resolveMapCameraPadding(fitChromePadding, fitBottomInset),
2108+
duration: 420,
2109+
});
2110+
if (!didAnimate) {
2111+
updateMapViewport({
2112+
center: { lat: mapEditorSiteDraft.lat, lon: mapEditorSiteDraft.lon },
2113+
zoom: viewport.zoom,
2114+
});
2115+
}
2116+
}, [
2117+
fitBottomInset,
2118+
fitChromePadding,
2119+
isMapLoaded,
2120+
mapEditor?.kind,
2121+
mapEditor?.resourceId,
2122+
mapEditorSiteDraft,
2123+
updateMapViewport,
2124+
viewport.zoom,
2125+
]);
20972126
const mqttNodesInView = useMemo(() => {
20982127
const lonSpan = Math.max(0.12, 360 / Math.pow(2, activeViewState.zoom) * 2.2);
20992128
const latSpan = Math.max(0.12, 170 / Math.pow(2, activeViewState.zoom) * 1.8);
@@ -3424,9 +3453,17 @@ export function MapView({
34243453
</Source>
34253454

34263455
{sites.map((site) => {
3427-
const isSelected = !armAddSiteOnNextEmptyMapClick && selectedSiteSet.has(site.id);
3456+
const isEditedSiteInSimulation =
3457+
mapEditor?.kind === "site" &&
3458+
!mapEditor.isNew &&
3459+
mapEditor.resourceId !== null &&
3460+
site.libraryEntryId === mapEditor.resourceId &&
3461+
mapEditorSiteDraft !== null;
3462+
const isSelected = isEditedSiteInSimulation || (!armAddSiteOnNextEmptyMapClick && selectedSiteSet.has(site.id));
34283463
const pendingMove = pendingSiteMoves[site.id];
3429-
const markerPosition = pendingMove?.currentPosition ?? site.position;
3464+
const markerPosition = isEditedSiteInSimulation
3465+
? { lat: mapEditorSiteDraft.lat, lon: mapEditorSiteDraft.lon }
3466+
: pendingMove?.currentPosition ?? site.position;
34303467
const isTemporarilyMoved = Boolean(pendingMove);
34313468
const isPassFailMode = coverageVizMode === "passfail" && Boolean(selectedFromSite);
34323469
const isRelayMode = coverageVizMode === "relay" && Boolean(selectedFromSite) && Boolean(selectedToSite);
@@ -3445,8 +3482,8 @@ export function MapView({
34453482
longitude={markerPosition.lon}
34463483
offset={SITE_PIN_MARKER_OFFSET}
34473484
style={{ zIndex: markerZIndex }}
3448-
onDrag={(event) => onSiteDrag(site.id, event)}
3449-
onDragEnd={(event) => onSiteDragEnd(site.id, event)}
3485+
onDrag={isEditedSiteInSimulation ? undefined : (event) => onSiteDrag(site.id, event)}
3486+
onDragEnd={isEditedSiteInSimulation ? onEditorSiteDraftDragEnd : (event) => onSiteDragEnd(site.id, event)}
34503487
>
34513488
<MarkerActionButton
34523489
ariaLabel={site.name}
@@ -3556,7 +3593,9 @@ export function MapView({
35563593
</Marker>
35573594
) : null}
35583595

3559-
{mapEditor?.kind === "site" && mapEditorSiteDraft ? (
3596+
{mapEditor?.kind === "site" &&
3597+
mapEditorSiteDraft &&
3598+
(mapEditor.isNew || !sites.some((site) => site.libraryEntryId === mapEditor.resourceId)) ? (
35603599
<Marker
35613600
anchor="bottom"
35623601
draggable={canPersist}

src/components/MapView.userLocation.test.tsx

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
55

66
const mapMock = vi.hoisted(() => ({
77
easeTo: vi.fn(),
8+
markerProps: [] as Array<{ latitude?: number; longitude?: number }>,
89
latestProps: null as null | {
910
onMove?: (event: { originalEvent?: unknown; viewState: { longitude: number; latitude: number; zoom: number } }) => void;
1011
},
@@ -47,7 +48,18 @@ vi.mock("react-map-gl/maplibre", async () => {
4748
return <div data-testid="mock-map">{props.children}</div>;
4849
}),
4950
Layer: () => null,
50-
Marker: ({ children }: { children?: React.ReactNode }) => <div>{children}</div>,
51+
Marker: ({
52+
children,
53+
latitude,
54+
longitude,
55+
}: {
56+
children?: React.ReactNode;
57+
latitude?: number;
58+
longitude?: number;
59+
}) => {
60+
mapMock.markerProps.push({ latitude, longitude });
61+
return <div>{children}</div>;
62+
},
5163
Source: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
5264
};
5365
});
@@ -98,6 +110,7 @@ describe("MapView user location flow", () => {
98110
beforeEach(() => {
99111
vi.clearAllMocks();
100112
mapMock.latestProps = null;
113+
mapMock.markerProps = [];
101114
installGeolocation();
102115
watchPosition.mockReturnValue(42);
103116
Object.defineProperty(HTMLCanvasElement.prototype, "getContext", {
@@ -274,6 +287,42 @@ describe("MapView user location flow", () => {
274287
).toBeInTheDocument();
275288
});
276289

290+
it("uses the existing simulation marker as the editor marker when editing its library site", () => {
291+
useAppStore.setState({
292+
sites: [
293+
{
294+
id: "site-alpha",
295+
name: "Alpha Site",
296+
libraryEntryId: "lib-alpha",
297+
position: { lat: 60.5, lon: 11.5 },
298+
groundElevationM: 120,
299+
antennaHeightM: 2,
300+
txPowerDbm: 20,
301+
txGainDbi: 2,
302+
rxGainDbi: 2,
303+
cableLossDb: 1,
304+
},
305+
],
306+
selectedSiteId: "site-alpha",
307+
selectedSiteIds: ["site-alpha"],
308+
mapEditor: {
309+
kind: "site",
310+
resourceId: "lib-alpha",
311+
isNew: false,
312+
label: "Alpha Site",
313+
anchorRect: { top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 },
314+
},
315+
mapEditorSiteDraft: { lat: 61.25, lon: 12.75, groundElevationM: 130 },
316+
});
317+
318+
renderMapView();
319+
320+
expect(screen.getAllByRole("button", { name: "Alpha Site" })).toHaveLength(1);
321+
expect(mapMock.markerProps).toEqual(
322+
expect.arrayContaining([expect.objectContaining({ latitude: 61.25, longitude: 12.75 })]),
323+
);
324+
});
325+
277326
it("publishes plain location failure notifications", () => {
278327
const onPublishNotice = vi.fn();
279328
const consoleError = vi.spyOn(console, "error").mockImplementation(() => undefined);

0 commit comments

Comments
 (0)