Let an Owner or Editor delete a place from the detail panel #78
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/delete-place-ui"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes the hole task 95 named:
DELETE /api/places/{id}has existed since task 70 with nothing in the UI calling it.PlaceDetailPanelgains aDeletebutton, gated onCanDelete(courtesy only — the API's ownGroupMayDeletePlacesPolicystill decides, and a stale-role 403 is caught and shown rather than assumed away, the patternPlaceForm/PR #69 established).Homepasses the same caller-role check it already uses forCanEdit.GroupMemberRowalready established for removal/leaving (ADR-0065, ADR-0068): a prompt naming the deletion as permanent, a focused confirm button, Escape to cancel.PlaceDetailPanelis now self-contained for this mutation (injectsPlaceMarkApiClient/MapPlacesStatedirectly), the same shapeGroupMembersPanel.LeaveAsyncuses: on success it callsMapPlacesState.RemovePlaceand closes with no reload.MapPlacesState.RemovePlacemirrorsRemoveGroup's shape — drops one place, clears the selection if it named the removed place. Tested with several places across several groups to prove only the one requested is removed (the PR #70/#76 over/under-removal class of bug).No new ADR: this only applies patterns already decided (ADR-0039, ADR-0065, ADR-0068, ADR-0067's local-patch shape).
Not verified in a browser (task 156), as with every WebUI change so far: focus landing on the confirm button and what a screen reader announces are asserted only as the component's own request to the browser, not observed.
Verdict: changes needed
Real bug: a stale expanded-group id can blank the map after deleting the only place in that group.
MapPlacesState.RemovePlace(src/PlaceMark.WebUI/Maps/MapPlacesState.cs) never removes the group from_expandedGroupIdsthe wayRemoveGroupdoes. If a caller expands group A (its one place), then deletes that place,Groupsrebuilds without A (so the accordion's whole A section, including its toggle, disappears) — but_expandedGroupIdsstill contains A.VisiblePlacesthen filters to_expandedGroupIds(count != 0) and matches nothing, so the map goes blank even though the caller still has places in other, non-expanded groups. Worse, there is no longer any control in the UI to un-expand A, since its accordion section is gone — the caller is stuck until they expand a different group by chance. Reproduced directly: addstate.ExpandedGroupIds.ShouldNotContain(_groupA)after toggling A and deleting its only place, and it reddens.RemovePlaceshould drop the group id from_expandedGroupIdsonce the group has no places left (or unconditionally, mirroringRemoveGroup), the same way it already clears the selection.Everything else checks out:
RemovePlaceitself removes exactly one place — proved multi-group, and a mutation to over-remove (matching by group instead of id) reddens two tests immediately.RemovePlaceclears_selectedPlaceIdwhen it names the removed place, which is what letsHome's own@ifstop renderingPlaceDetailPanelwith no explicitOnClosecall — proved end-to-end inHomeTests.Home_DeleteConfirmed_RemovesTheMarkerAndClosesThePanelWithoutReload.Home.CanManagegates the button for Owner/Editor (courtesy), andPlaceDetailPanel_DeleteRefusedWithA403_ShowsTheErrorAndKeepsThePlaceproves a stale-role 403 is caught and shown, not assumed away — both halves verified.PlaceMark.WebUI.Tests/PlaceMark.Architecture.Testsruns are clean on the pinned 10.0.100 SDK.On the design point (injecting
PlaceMarkApiClient/MapPlacesStateintoPlaceDetailPaneldirectly): I'd have put this in the host rather than the panel. The precedent cited (GroupMembersPanel.LeaveAsync) is real, but it's the wrong precedent to reach for here —GroupMembersPanelis a management surface with no separate "detail" mode, whereasPlaceDetailPanelalready had an established internal convention before this PR:CloseandEditboth bubble as events, and the host (Home) decides what they mean, including owning every other mutation this page makes (PlaceFormPanelpatchesMapPlacesState.UpsertPlaceitself, but it's a distinct componentHomeexplicitly hosts for that reason). Delete is now the one action on this component that skips that bubble-to-host shape and reaches into DI directly, which is an asymmetry future readers of this one file have to notice and remember, not a pattern they can infer from the other two buttons next to it. It also taxes every future test of this component: the class's own doc comment admitsRegisterPlacesStatenow has to run "even [for tests] that never touch delete." I'd have keptPlaceDetailPanelpresentational — confirmation UI only, no I/O — and raised an event forHometo handle the same way it already handlesOnEdit. Not a blocker; it's a genuine judgement call and the precedent isn't unreasonable, but I don't think it's the right one for this specific component.Not verifiable without a browser (task 156, consistent with every prior WebUI PR): whether focus visibly lands on the confirm button, what a screen reader actually announces from
aria-describedby, and whether the panel reads sensibly disappearing off a narrow viewport are all unconfirmed — bUnit only proves the component asks the browser to do the right thing.Verdict: changes needed
Home.razor'sDeleteError="_deleteError"is missing its@prefix, and this is not cosmetic.DeleteErrorisstring?, and a bare identifier is a syntactically valid string literal, so Razor's component-attribute parser treats it as the literal text"_deleteError"rather than a reference to the field — unlikeIsDeleting="_isDeleting"two lines above, which is forced into an expression because a bare identifier can't type-check asbool. The practical effect:PlaceDetailPanel'sErrorNotificationrenders unconditionally onMessage is not null, and the literal string is never null — so every place selected shows a permanent error banner reading the literal text "_deleteError", whether or not a delete was ever attempted, and dismissing it does nothing observable since the bound value never changes. Confirmed directly: added a bUnit test that selects a place and asserts no.error-notification-messagerenders — it fails, showing that exact literal text. Confirmed the fix too — changing toDeleteError="@_deleteError"makes that test pass and also makes a 403-refusal test (also added, since none existed) correctly show the real API detail instead. Both scratch tests were reverted after confirming; neither is committed. This is exactly the shape the "error path gets dropped a level up" concern anticipated, and it's a bigger break than a dropped error path — it's a defect visible on the ordinary, successful path too.Everything else in this delta holds up:
RemovePlace_TheLastPlaceInAnExpandedGroup_DropsItFromExpandedGroupIdsRatherThanBlankingTheMap) reddens against the unfixed code — confirmed by removing thePruneExpandedGroupIdsWithNoPlaces()call fromRemovePlace.RemoveGroup's pre-existing behaviour is unchanged by the shared helper — confirmed by removing the call fromRemoveGroupinstead and watching its own existingRemoveGroup_GroupWasExpanded_DropsItFromExpandedGroupIdsTootest redden.RemovePlace_APlaceRemainsInTheExpandedGroup_LeavesItExpandedproves a group with a place still in it stays expanded — confirmed by mutating the helper to unconditionally clear_expandedGroupIds, which reddens exactly that test. TheUpsertPlaceinteraction is moot in practice: an empty group never has an accordion section (Groupsonly ever contains groups with at least one place), so there is no UI path to expand one before a place exists in it.PlaceDetailPanelinjects nothing now — confirmed by the test class's own setup, which registers no API client or state service anywhere.Home.DeleteSelectedPlaceAsyncowns the call, thetry/catch (ApiException)on the stale-role 403 case is intact in principle (just not correctly wired to the UI, per above), andIsDeletingthreads through correctly since its type forces expression evaluation.PlaceMark.WebUI.Tests/PlaceMark.Architecture.Testsare clean on the pinned SDK — unsurprising, since nothing in the current suite renders aPlaceDetailPaneland checks for the absence of an error notification, which is exactly the gap that let this through.Verdict: mergeable
@fromDeleteError="@_deleteError"again andHome_PlaceSelectedWithNoDeleteAttempted_ShowsNoErrorNotificationreddens (finds a stray.error-notificationdiv), passes clean with it restored.PlaceMark.WebUIfor every[Parameter](all 12 files that declare one, all in code-behind — no inline.razorparameters to miss) and everystring/string?-typed one's call sites:ErrorNotification.Message/AdditionalCssClassare@-prefixed everywhere;FieldErrors.Field/Idare bare but every call site is a genuine literal (kebab-case DOM ids, lowercase field-name keys), not a variable reference;PlaceDetailPanel.DeleteErrorhas exactly one call site, now fixed. NoRenderFragment,RenderFragment<T>, orMarkupStringparameter exists anywhere in this project, so that class of the bug doesn't apply here. The author's report holds.Home.razor,HomeTests.cs, 20 lines) — confirmed by diffing against the prior commit directly.MapPlacesState.cs,PlaceDetailPanel.razor/.razor.cs, and the Maps test files are byte-for-byte unchanged, so the pruning fix and the presentational refactor from the last two rounds are undisturbed.PlaceMark.WebUI.Tests/PlaceMark.Architecture.Testsclean on the pinned SDK.Nothing further to act on.