Add group sharing and member management UI #73
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/member-management-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?
Implements task 96: invite by email, roster (roles + invitation status), role changes, promote to Owner, and member removal with confirmation.
Where it lives: a new "Groups" section in the drawer, below the place accordion (task 91 is still held, ADR-0062 already set the panel-not-page precedent). Selecting a group opens
GroupMembersPanel, floating over the map outside the drawer's owninertsubtree, the same shapePlaceDetailPanelestablished.API/Infra change:
GroupMemberResponsegainedUserId. The existing roster response (ADR-0046) had no way to name a specific row —PUT/DELETE/promote-to-ownerall key by{userId}, and two members can share a display name. ADR-0065 records why this is additive to ADR-0046, not a reopening of it (email stays withheld).Invite wording: the API answers
202uniformly whether or not the address resolves (ADR-0050). The confirmation text is deliberately conditional — "If {email} has a PlaceMark account, they've been invited to this group — you won't see a difference either way" — and the roster reloads immediately after, which is the indirect signal ADR-0050 itself names.Promotion: the role selector never offers Owner (Editor/Viewer only); a separate "Make owner" button routes to
promote-to-owner, with its own confirmation — not required by the acceptance criteria, but justified by ADR-0050/0052/0054 all treating a second Owner as a materially bigger, harder-to-reverse grant.Confirmation: inline per-row (swap to prompt + Yes/Cancel), not a modal — the confirming button is focused immediately and carries
aria-describedbynaming what it does; Escape cancels. Reasoning and alternatives (including why not<dialog>) are in ADR-0065.Scope cut: "leave group" (removing yourself) is not built — the acceptance criteria frame removal as Owner-only, and self-service leaving is a distinct, unbuilt feature on the same endpoint (ADR-0053). Also not built: focus returning to the specific drawer button that opened the panel on close (no supported way to
@refa dynamic per-row element;PlaceDetailPanelsets the same no-return-focus precedent).Not verified: anything needing a real browser — actual focus movement, screen-reader announcement, visual layout on a narrow viewport (task 156, same limit every WebUI ADR since 0059 has carried). bUnit proves the component asks the browser to do the right thing, not that it lands.
See ADR-0065 for full reasoning, including the alternatives considered.
Verdict: mergeable
Build and full test suite (WebUI, Api, Infrastructure, Architecture — 1105 tests) pass clean with the pinned 10.0.100 SDK.
Contract change (
UserId): genuinely populated end to end —SelectMembersForGroupselectsgm.user_id, flows throughGroupMemberForResponse/GroupMemberResponse, and is covered both at the repository level (GroupMembershipRepositoryTests) and the API integration level (GetGroupEndpointTests), against real PostgreSQL.GetGroup_TheRoster_NeverCarriesAnEmailAddressstill passes and nothing else leaked in — the roster's shape is otherwise unchanged. Exposing an opaqueUserIdis the right call: it's what the mutating routes key by, and nothing about it is more sensitive than the display name already on the wire.Anti-enumeration wording: checked the actual strings. "Request sent. If {email} has a PlaceMark account..." is unconditionally true and never branches on the outcome; the roster reload afterwards is not a new leak — an Owner already has standing to read the roster at will, this just automates a check ADR-0050's own Consequences section already names as available. No success/failure divergence, no timing tell (one awaited call either way). Test
GroupMembersPanel_InviteSubmittedSuccessfully_ShowsTheUniformWordingRatherThanClaimingSuccesspins the wording.Promotion seam: confirmed "Make owner" calls
POST .../promote-to-owner, never the role PUT (separate code path, separate test asserting the POST hits/promote-to-owner). The role selector never offers Owner. Reads as one action ("make this person an Owner") plus a two-option role dropdown, not three unrelated controls.Last Owner, personal groups, courtesy controls, confirmation: all as claimed. The 409 detail ("This member is the group's last Owner...") is shown verbatim, not genericised.
GroupsSectionomits personal groups outright (tested).CallerIsOwneris derived from a live roster fetch each load, so a stale-role 403 is just anotherApiExceptioncaught and shown — no special-casing needed or missing. Removal confirmation is a real two-step flow with its own tests asserting the intermediate state (click ≠ immediate removal),aria-describedby, and a focus call — a regression here would fail those tests, not just silently pass.One defect:
src/PlaceMark.Contracts/Groups/GroupMemberResponse.csandsrc/PlaceMark.Infrastructure/Groups/GroupMemberForResponse.csboth link ADR-0065 asdocs/adr/0065-put-a-members-own-user-id-on-the-roster.md, which doesn't exist — the file this PR actually adds is0065-group-membership-panel-roster-user-id-and-invite-wording.md(correctly referenced from the two WebUI files that cite it). Fix the two dead links.Stated omissions: both read as reasonable trade-offs rather than gaps to fix now. No self-service leave matches the acceptance criteria's own Owner-only framing for removal, and ADR-0065 names it explicitly rather than leaving it to be rediscovered — though it's worth a follow-up ticket, since a non-Owner currently has no in-app way to leave a group they no longer want to be in. Not returning focus to the drawer's per-row trigger is a real, argued technical constraint (no supported per-iteration
@ref), consistent withPlaceDetailPanel's precedent, and not silently dropped.Not verifiable here: real focus movement, what a screen reader actually announces from
aria-describedby, and layout/z-index behaviour on a narrow viewport — bUnit proves the component asks the browser to do the right thing, not that it lands. Consistent with every WebUI ADR since 0059 and named as such in ADR-0065 itself.rob referenced this pull request2026-08-06 06:20:01 +00:00
rob referenced this pull request2026-08-14 14:07:00 +00:00