Let a member leave a group from the UI #76

Merged
rob merged 2 commits from feat/leave-group-ui into main 2026-08-06 03:11:04 +00:00
Owner

Closes task 160.

GroupMemberRow now offers a "Leave group" control on the caller's own row (any role — the API alone enforces the last-Owner rule for an Owner's own row), with an inline confirmation naming the consequence ("Its places will disappear from your map"). A personal group still offers no roster at all, so no leave control either. An Owner refused with 409 sees a reworded reason ("You're the only Owner...") pointing at the "Make owner" control already visible on another row in the same panel.

Leaving patches MapPlacesState and CallerGroupsState directly (RemoveGroup) rather than reloading — the map, drawer accordion and groups list all drop the group with no page reload.

GroupsSection is folded onto CallerGroupsState (ADR-0068, partially supersedes ADR-0067): leaving is a second mutation path that doesn't go through Drawer's explicit HandleGroupSavedAsync reload, exactly the trigger ADR-0067 named for doing this. Drawer no longer holds a @ref to GroupsSection at all.

Not verified in a browser (task 156): focus landing on the confirm button and what a screen reader announces from aria-describedby are asserted only as "the component asked the browser to do this."

Closes task 160. `GroupMemberRow` now offers a "Leave group" control on the caller's own row (any role — the API alone enforces the last-Owner rule for an Owner's own row), with an inline confirmation naming the consequence ("Its places will disappear from your map"). A personal group still offers no roster at all, so no leave control either. An Owner refused with 409 sees a reworded reason ("You're the only Owner...") pointing at the "Make owner" control already visible on another row in the same panel. Leaving patches `MapPlacesState` and `CallerGroupsState` directly (`RemoveGroup`) rather than reloading — the map, drawer accordion and groups list all drop the group with no page reload. `GroupsSection` is folded onto `CallerGroupsState` (ADR-0068, partially supersedes ADR-0067): leaving is a second mutation path that doesn't go through `Drawer`'s explicit `HandleGroupSavedAsync` reload, exactly the trigger ADR-0067 named for doing this. `Drawer` no longer holds a `@ref` to `GroupsSection` at all. Not verified in a browser (task 156): focus landing on the confirm button and what a screen reader announces from `aria-describedby` are asserted only as "the component asked the browser to do this."
Let a member leave a group from GroupMembersPanel
All checks were successful
CI / build (pull_request) Successful in 2m24s
42d89b420c
rob left a comment

Verdict: changes needed

Build is clean (0 warnings/errors) and PlaceMark.WebUI.Tests passes 351/355 (4 pre-existing skips) under the pinned 10.0.100 SDK. Traced RemoveGroup on both MapPlacesState and CallerGroupsState, GroupMemberRow's self-row rendering, GroupMembersPanel.LeaveAsync, and the GroupsSection fold against every consumer; also mutation-tested by removing the CallerGroups.Changed subscription from GroupsSectionGroupsSectionTests reddened (2 failures) as expected, confirming the fold doesn't bypass the reset.

ADR-0068 is missing its half of the supersession, and the README index wasn't updated (ADR-0001, both required). ADR-0067 correctly gained a Partially superseded by: [ADR-0068] line, but ADR-0068's own header has no reciprocal Partially supersedes: [ADR-0067] field (compare ADR-0017, which states Supersedes: [ADR-0002] in part). And docs/adr/README.md's index row for 0067 still reads plain Accepted — per the README's own convention ("Write its status in the table as Accepted (partly superseded by 00NN)") it should read Accepted (partly superseded by 0068). Only the new 0068 row was added. Fix both before merging.

Everything else checks out:

  • MapPlacesState.RemoveGroup / CallerGroupsState.RemoveGroup are simple predicate filters — idempotent by construction (removing an absent id is a no-op), covered by tests for both the present and absent cases, and GroupMembersPanel_LeaveConfirmed_... exercises the full path end-to-end against the real component.
  • No code path reloads the roster or GET /api/groups/{groupId} after a successful leave — LeaveAsync patches state and closes; confirmed by reading the method and by the panel-level test.
  • Owner-leaving 409 is reworded correctly, and DefaultMembers() in the test fixture includes a second accepted non-Owner member, so tracing ShowOwnerControls confirms "Make owner" genuinely renders in the same roster list as the reworded error — the test itself only asserts the roster is non-empty, not the "Make owner" text specifically, which would have made this easier to verify from the test alone rather than by tracing.
  • Personal group: GroupMembersPanel gates the entire roster (and thus every row) behind !group.IsPersonal, so no leave control today. As ADR-0068 itself says, GroupMemberRow has no personal-group awareness of its own — if a roster were ever rendered for one, "Leave group" would appear with nothing to stop it. That's a named, accepted gap, not a silent one, so no action needed here.
  • Accessibility (focus, aria-describedby, Escape, native <button>) mirrors the existing remove/promote pattern exactly and is covered the same way.

Not verifiable without a browser (task 156, as the PR body already says): focus actually landing on the confirm button, what a screen reader announces from aria-describedby, and whether leaving reads sensibly if the group's detail is open elsewhere on the page. bUnit only proves the component asks the browser to do these things.

Verdict: changes needed Build is clean (0 warnings/errors) and `PlaceMark.WebUI.Tests` passes 351/355 (4 pre-existing skips) under the pinned 10.0.100 SDK. Traced `RemoveGroup` on both `MapPlacesState` and `CallerGroupsState`, `GroupMemberRow`'s self-row rendering, `GroupMembersPanel.LeaveAsync`, and the `GroupsSection` fold against every consumer; also mutation-tested by removing the `CallerGroups.Changed` subscription from `GroupsSection` — `GroupsSectionTests` reddened (2 failures) as expected, confirming the fold doesn't bypass the reset. **ADR-0068 is missing its half of the supersession, and the README index wasn't updated (ADR-0001, both required).** ADR-0067 correctly gained a `Partially superseded by: [ADR-0068]` line, but ADR-0068's own header has no reciprocal `Partially supersedes: [ADR-0067]` field (compare ADR-0017, which states `Supersedes: [ADR-0002] in part`). And `docs/adr/README.md`'s index row for 0067 still reads plain `Accepted` — per the README's own convention ("Write its status in the table as `Accepted (partly superseded by 00NN)`") it should read `Accepted (partly superseded by 0068)`. Only the new 0068 row was added. Fix both before merging. Everything else checks out: - `MapPlacesState.RemoveGroup` / `CallerGroupsState.RemoveGroup` are simple predicate filters — idempotent by construction (removing an absent id is a no-op), covered by tests for both the present and absent cases, and `GroupMembersPanel_LeaveConfirmed_...` exercises the full path end-to-end against the real component. - No code path reloads the roster or `GET /api/groups/{groupId}` after a successful leave — `LeaveAsync` patches state and closes; confirmed by reading the method and by the panel-level test. - Owner-leaving 409 is reworded correctly, and `DefaultMembers()` in the test fixture includes a second accepted non-Owner member, so tracing `ShowOwnerControls` confirms "Make owner" genuinely renders in the same roster list as the reworded error — the test itself only asserts the roster is non-empty, not the "Make owner" text specifically, which would have made this easier to verify from the test alone rather than by tracing. - Personal group: `GroupMembersPanel` gates the entire roster (and thus every row) behind `!group.IsPersonal`, so no leave control today. As ADR-0068 itself says, `GroupMemberRow` has no personal-group awareness of its own — if a roster were ever rendered for one, "Leave group" would appear with nothing to stop it. That's a named, accepted gap, not a silent one, so no action needed here. - Accessibility (focus, `aria-describedby`, Escape, native `<button>`) mirrors the existing remove/promote pattern exactly and is covered the same way. **Not verifiable without a browser** (task 156, as the PR body already says): focus actually landing on the confirm button, what a screen reader announces from `aria-describedby`, and whether leaving reads sensibly if the group's detail is open elsewhere on the page. bUnit only proves the component asks the browser to do these things.
Record the ADR-0067/0068 supersession in both directions
All checks were successful
CI / build (pull_request) Successful in 2m26s
2a94d38f3e
Author
Owner

Addressed: ADR-0068 now carries Partially supersedes: ADR-0067, ADR-0067's own metadata names the exact paragraphs it displaces, and the README index row for 0067 reads Accepted (partly superseded by 0068). Also fixed a wording slip in ADR-0068's own Consequences found while doing this (miscounted GET /api/groups readers). Full CI sequence re-run clean, including dotnet format --verify-no-changes.

Addressed: ADR-0068 now carries `Partially supersedes: ADR-0067`, ADR-0067's own metadata names the exact paragraphs it displaces, and the README index row for 0067 reads `Accepted (partly superseded by 0068)`. Also fixed a wording slip in ADR-0068's own Consequences found while doing this (miscounted `GET /api/groups` readers). Full CI sequence re-run clean, including `dotnet format --verify-no-changes`.
rob left a comment

Verdict: mergeable

Delta since 42d89b4 is exactly the three files expected (docs/adr/0067, docs/adr/0068, docs/adr/README.md), 12/-7 lines, no code touched.

  • ADR-0068 now carries Partially supersedes: [ADR-0067], naming the specific displaced paragraphs by their opening words — matches the established Partially supersedes precedent (ADR-0038's own field, superseding ADR-0033), which ADR-0001 permits alongside the Supersedes ... in part style used elsewhere (ADR-0017, ADR-0022, ADR-0034).
  • ADR-0067 reciprocates with a matching Partially superseded by line naming ADR-0068 and the same paragraphs, plus what replaces them. Its frozen body (the "Two GET /api/groups readers exist..." paragraph) is untouched, as ADR-0001 requires — the metadata line is what tells a reader it's now displaced, not an edit to the paragraph itself.
  • README index row for 0067 reads Accepted (partly superseded by 0068), matching the exact format used elsewhere in the same table (e.g. 0029, 0033).
  • The reader count is now correct: before this record, GET /api/groups had two independent readers (CallerGroupsState, GroupsSection); after folding, one (CallerGroupsState) — MapPlacesState's GET /api/places was never a GET /api/groups reader and is now correctly pulled out of that count rather than counted toward it. Checked against the source: CallerGroupsState is the only production type that independently loads GET /api/groups; GroupsSection, PlaceAccordion and Home all read it via that shared state. Both mentions of the count inside ADR-0068 (Decision and Consequences) were updated consistently, and no stale "three" reference remains anywhere in the file.

Nothing else to act on.

Verdict: mergeable Delta since 42d89b4 is exactly the three files expected (docs/adr/0067, docs/adr/0068, docs/adr/README.md), 12/-7 lines, no code touched. - ADR-0068 now carries `Partially supersedes: [ADR-0067]`, naming the specific displaced paragraphs by their opening words — matches the established `Partially supersedes` precedent (ADR-0038's own field, superseding ADR-0033), which ADR-0001 permits alongside the `Supersedes ... in part` style used elsewhere (ADR-0017, ADR-0022, ADR-0034). - ADR-0067 reciprocates with a matching `Partially superseded by` line naming ADR-0068 and the same paragraphs, plus what replaces them. Its frozen body (the "Two `GET /api/groups` readers exist..." paragraph) is untouched, as ADR-0001 requires — the metadata line is what tells a reader it's now displaced, not an edit to the paragraph itself. - README index row for 0067 reads `Accepted (partly superseded by 0068)`, matching the exact format used elsewhere in the same table (e.g. 0029, 0033). - The reader count is now correct: before this record, `GET /api/groups` had two independent readers (`CallerGroupsState`, `GroupsSection`); after folding, one (`CallerGroupsState`) — `MapPlacesState`'s `GET /api/places` was never a `GET /api/groups` reader and is now correctly pulled out of that count rather than counted toward it. Checked against the source: `CallerGroupsState` is the only production type that independently loads `GET /api/groups`; `GroupsSection`, `PlaceAccordion` and `Home` all read it via that shared state. Both mentions of the count inside ADR-0068 (Decision and Consequences) were updated consistently, and no stale "three" reference remains anywhere in the file. Nothing else to act on.
rob merged commit 02f01c7787 into main 2026-08-06 03:11:04 +00:00
rob deleted branch feat/leave-group-ui 2026-08-06 03:11:05 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rob/PlaceMark!76
No description provided.