Skip to content

feat: group management UI (BCH-1328)#271

Merged
gusfcarvalho merged 5 commits into
mainfrom
lisa/feat/bch-1328-ui
Jun 24, 2026
Merged

feat: group management UI (BCH-1328)#271
gusfcarvalho merged 5 commits into
mainfrom
lisa/feat/bch-1328-ui

Conversation

@gusfcarvalho

@gusfcarvalho gusfcarvalho commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the UI component for BCH-1328 (native user groups / authz membership model).

  • Group management pages (/admin/groups, /admin/groups/:id) — list, create, edit, delete groups; gated behind admin:manage
  • Members panel — shows both native CCF members and inherited SSO members per group. Inherited members display as locked (blue badge, no Remove button); native members can be added/removed inline via a user-search dialog
  • User detail view enhanced — new "Group Memberships" card on /users/:id with colour-coded chips (blue = SSO-inherited/read-only, grey = native CCF group)
  • TypesCCFGroup, CCFGroupCreate, CCFGroupMember, CCFUserGroup added to src/stores/types.ts
  • Router + nav — new routes under /admin/groups/* and a "Groups" entry in the Admin sidebar section

Addresses the comment on BCH-1328: "Misses UI component for Group management. Inherited Groups MUST be fixed and cannot be removed."

All inherited (SSO-synced) memberships are visually distinct and the Remove action is absent/disabled for them throughout the UI.

API contract assumed

Method Path
GET/POST /api/admin/groups
GET/PUT/DELETE /api/admin/groups/:id
GET/POST /api/admin/groups/:id/members
DELETE /api/admin/groups/:id/members/:userId
GET /api/admin/users/:id/groups

Test plan

  • make reviewable passes (format ✅ typecheck ✅ lint ✅ 754 unit tests ✅)
  • Navigate to Admin > Groups — list renders, Create Group dialog opens and submits
  • Click a group — detail view shows members panel; Add Member opens user search; Remove works for native; inherited members show locked tooltip
  • Navigate to Admin > System Users > a user — "Group Memberships" card appears with correct badges
  • Verify inherited (SSO) group chips show "SSO" label with tooltip and no remove affordance
  • Non-admin user cannot see Groups nav entry or access /admin/groups directly

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added an admin “Groups” section to create, edit, view, and delete groups, including permission-guarded routes and inline status feedback (loading/error toasts).
    • Introduced group member management with an “Add Member” dialog (autocomplete) and safe removal rules for inherited memberships.
    • Updated user profiles to display group memberships via a dedicated card, visually distinguishing inherited (read-only) from native memberships.
    • Added sidebar navigation entry for Admin → Groups.

Add native CCF group management pages and surface group memberships on
the user detail view, completing the UI side of the authz groups feature.

- New types: CCFGroup, CCFGroupCreate, CCFGroupMember, CCFUserGroup
- /admin/groups list view with create-group dialog
- /admin/groups/:id detail view with members panel
- GroupMembersPanel: shows native vs inherited (SSO) members; inherited
  members display as locked (cannot be removed), native members have a
  Remove button; Add Member dialog with live user search
- GroupCreateForm / GroupEditForm reuse existing form patterns
- UserView enhanced with a Group Memberships card using colour-coded
  chips (blue = SSO-inherited, grey = native) with tooltip explaining
  that inherited memberships are read-only
- Router + LeftSideNav wired under Admin > Groups (admin:manage gate)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds group admin screens, member management, group type definitions, admin navigation and routes, and user group membership display.

Changes

Groups Admin Feature

Layer / File(s) Summary
Group types, routes, and nav entry
src/stores/types.ts, src/router/index.ts, src/views/LeftSideNav.vue
Defines the group-related interfaces, adds the authenticated admin routes for listing and viewing groups, and inserts the Admin sidebar entry for Groups.
Group create and edit forms
src/components/groups/GroupCreateForm.vue, src/components/groups/GroupEditForm.vue
Implements the create and edit forms with required-name validation, API submission, emitted events, and Axios-based error toasts.
Group members panel
src/components/groups/GroupMembersPanel.vue
Renders the member table and add-member dialog, fetches members and user directory data, and performs add and remove member requests with toast handling.
Groups list page
src/views/groups/GroupsListView.vue
Fetches the groups list, renders loading/error/empty states and row navigation, and opens the create-group dialog that appends newly created groups.
Group detail page
src/views/groups/GroupView.vue
Loads a single group, renders its details and member panel, supports editing through the modal form, and deletes the group through confirmation and authenticated API calls.
User group memberships
src/views/users/UserView.vue
Adds the Group Memberships card to the user view, fetches the user’s groups, and renders inherited versus native membership pills with tooltip support.

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant GroupsListView
  participant GroupCreateForm
  participant GroupView
  participant GroupEditForm
  participant GroupMembersPanel
  participant API

  Admin->>GroupsListView: open /admin/groups
  GroupsListView->>API: GET /api/admin/groups
  API-->>GroupsListView: CCFGroup[]

  Admin->>GroupsListView: create group
  GroupsListView->>GroupCreateForm: open dialog
  Admin->>GroupCreateForm: submit
  GroupCreateForm->>API: POST /api/admin/groups
  API-->>GroupCreateForm: CCFGroup
  GroupCreateForm-->>GroupsListView: emit create

  Admin->>GroupsListView: open group
  GroupsListView->>GroupView: navigate /admin/groups/:id
  GroupView->>API: GET /api/admin/groups/:id
  API-->>GroupView: CCFGroup
  GroupView->>GroupMembersPanel: render with group.id
  GroupMembersPanel->>API: GET members / users
  API-->>GroupMembersPanel: member data

  Admin->>GroupView: edit group
  GroupView->>GroupEditForm: open dialog
  Admin->>GroupEditForm: save
  GroupEditForm->>API: PUT /api/admin/groups/:id
  API-->>GroupEditForm: updated CCFGroup
  GroupEditForm-->>GroupView: saved
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 Hop, hop—new groups now bloom,
With members, edits, and room to groom.
A sidebar link, a page, a toast,
Each group can dance from coast to coast.
Ears up high, the UI sings,
New admin carrots on bright green strings!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a new group management UI for BCH-1328.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The v-if on availableUsers?.length treated an empty array (0) as falsy,
causing the "Loading users..." placeholder to persist after a successful
but empty user list fetch. Switch to v-if="usersLoading" / v-else so the
list always renders once the request settles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/groups/GroupMembersPanel.vue`:
- Around line 8-13: The template currently only handles loading and empty
states, so when a fetch fails, it incorrectly displays "No members yet" instead
of an error message. Add an error state variable to track fetch failures, then
insert a new v-else-if condition checking the error state between the isLoading
and !members?.length checks in the GroupMembersPanel template. Display an
appropriate error message when the error state is true. Apply the same pattern
to the other template blocks referenced at lines 89-127 and 160-174 to ensure
all member/user load failures show proper error states instead of empty states.

In `@src/views/groups/GroupsListView.vue`:
- Around line 115-117: The onCreated function is using push() to mutate the
groups array, but since groups is a shallowRef from useDataApi, it only tracks
reference changes and mutations won't trigger reactivity. Replace the push
operation by reassigning the entire groups array with a new array that includes
the newGroup, such as creating a new array with the existing items plus the
newGroup using spread syntax or similar approach, to ensure the reactivity
system detects the change.

In `@src/views/users/UserView.vue`:
- Around line 148-150: The useDataApi call for fetching user groups does not
capture error information, so when the API request fails, the template
incorrectly displays the empty-state UI saying "Not a member of any groups"
instead of showing an error message. Modify the destructuring of the useDataApi
response for the `/api/admin/users/${route.params.id}/groups` endpoint to also
capture the error or isError property, then update the template logic at the
empty-state branch (Line 62) to conditionally display an error message when the
error state is true rather than showing the empty-state UI on API failures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ce47fb99-ef32-4112-a300-88514d7851cb

📥 Commits

Reviewing files that changed from the base of the PR and between d00637e and ccdd8a5.

📒 Files selected for processing (9)
  • src/components/groups/GroupCreateForm.vue
  • src/components/groups/GroupEditForm.vue
  • src/components/groups/GroupMembersPanel.vue
  • src/router/index.ts
  • src/stores/types.ts
  • src/views/LeftSideNav.vue
  • src/views/groups/GroupView.vue
  • src/views/groups/GroupsListView.vue
  • src/views/users/UserView.vue

Comment thread src/components/groups/GroupMembersPanel.vue Outdated
Comment thread src/views/groups/GroupsListView.vue Outdated
Comment thread src/views/users/UserView.vue Outdated

@gusfcarvalho gusfcarvalho left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Findings inline. Two items to address before merge: the shallowRef push bug (new groups silently don't appear after creation) and the 403 double-toast (two overlapping permission warnings). Medium items are fixable in the same pass; Low can follow up.

Comment thread src/views/groups/GroupsListView.vue Outdated
Comment thread src/views/groups/GroupsListView.vue Outdated
Comment thread src/components/groups/GroupMembersPanel.vue Outdated
Comment thread src/components/groups/GroupMembersPanel.vue Outdated
Comment thread src/components/groups/GroupCreateForm.vue Outdated
Comment thread src/views/groups/GroupView.vue Outdated
Comment thread src/views/groups/GroupsListView.vue Outdated
- shallowRef push bug: replace groups.value?.push() with array
  reassignment in onCreated so Vue's shallow tracking fires on group
  creation (GroupsListView)
- double-toast on 403: remove local error watcher in GroupsListView;
  the global axios interceptor already throttles a warn toast on 403
- error states: expose membersError and usersError from useDataApi and
  show explicit error messages instead of falling back to empty-state UI
  in GroupMembersPanel; same for groupsError in UserView
- lazy user fetch: load /api/admin/users only when the Add Member dialog
  first opens (watch(showAddMember)) instead of unconditionally on mount
- double-submit guard: addMember is guarded by an adding ref; the Add
  button is disabled while a request is in flight
- Cancel button styling: use SecondaryButton in GroupCreateForm and
  GroupEditForm so Cancel has lower visual weight than Save
- Delete vs Edit visual hierarchy: Edit Group uses SecondaryButton,
  Delete Group uses PrimaryButton so the destructive action is
  visually distinct

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@gusfcarvalho gusfcarvalho left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good pass — shallowRef bug, 403 double-toast, lazy user fetch, Add guard, Cancel styling, and error states all fixed. Two items remain: a regression (success toast dropped) and the Delete/Edit button priority inversion.

Comment thread src/views/groups/GroupsListView.vue
Comment thread src/views/groups/GroupView.vue Outdated
GroupsListView: the success toast was accidentally removed when the
duplicate 403 watcher was dropped. Re-import useToast and re-add the
toast.add() call in onCreated independently of the watcher.

GroupView: making Delete the PrimaryButton and Edit the SecondaryButton
inadvertently put the most prominent affordance on the destructive
action. Revert to Edit = PrimaryButton (the intended main workflow) and
Delete = SecondaryButton with red text class (danger-outlined effect)
so the hierarchy is correct: clear primary CTA on the left, visually
de-emphasised but clearly destructive action on the right.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@gusfcarvalho gusfcarvalho left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All findings resolved across two fix commits. Verified: shallowRef reactivity fix, 403 double-toast removed, lazy user fetch, Add-button inflight guard, Edit=Primary/Delete=SecondaryButton (red text) hierarchy, error states for member/user/groups loads, success toast restored. CI green (tests ✅ type-check ✅ CodeRabbit ✅). LGTM — ready to merge once a human approves.

Signed-off-by: Gustavo Carvalho <gustavo.carvalho@container-solutions.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/components/groups/GroupCreateForm.vue (2)

54-67: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard createGroup against double-submit.

The Save button isn't disabled while the POST is in flight, so rapid double-clicks can create duplicate groups. This PR added an in-flight guard to member-add in GroupMembersPanel.vue (adding ref); apply the same here for consistency.

🛡️ Proposed guard
+const submitting = ref(false);
+
 async function createGroup() {
   if (!group.value.name.trim()) {
     toast.add({ /* ... */ });
     return;
   }
+  if (submitting.value) return;
+  submitting.value = true;
   try {
     await execute({ data: group.value });
     if (!createdGroup.value) return;
     emit('create', createdGroup.value);
   } catch (error) {
     // ...
+  } finally {
+    submitting.value = false;
   }
 }

Bind :disabled="submitting" on the Save button.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/groups/GroupCreateForm.vue` around lines 54 - 67, The
createGroup flow in GroupCreateForm.vue needs an in-flight submit guard to
prevent duplicate group creation on rapid clicks. Add a submitting ref around
createGroup, set it true before execute({ data: group.value }) and clear it in a
finally block, then bind the Save button’s disabled state to that flag so the
action cannot be triggered twice while the POST is pending, mirroring the adding
guard used in GroupMembersPanel.vue.

68-77: 🩺 Stability & Availability | 🟡 Minor

Chain through data and errors in group error handlers. errorResponse.response?.data.errors.body can still throw when the response body doesn’t match ErrorResponse<ErrorBody>. Use errorResponse.response?.data?.errors?.body ?? 'Unknown error occurred' in src/components/groups/GroupCreateForm.vue, src/components/groups/GroupEditForm.vue, src/components/groups/GroupMembersPanel.vue, and src/views/groups/GroupView.vue.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/groups/GroupCreateForm.vue` around lines 68 - 77, The group
error handlers are still assuming a fully-shaped Axios response and can throw
when the payload is missing or differently structured. Update the error detail
lookup in GroupCreateForm, GroupEditForm, GroupMembersPanel, and GroupView to
safely chain through response, data, and errors before reading body, using the
existing errorResponse variable in each catch block and keeping the same
fallback message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/groups/GroupMembersPanel.vue`:
- Around line 172-174: The GroupMembersPanel setup still eagerly calls
fetchAllUsers() to populate emailByUserId, which defeats the server-side
typeahead scalability claim. Remove the unconditional full /api/admin/users
fetch and instead get email from the members data source or load member emails
on demand for the rows actually displayed, using the existing
useUserSearch/searchUsers and member table rendering paths. Also update the
nearby comment so it no longer says there is no need to download every user up
front unless that is actually true.

In `@src/stores/types.ts`:
- Around line 68-71: The doc comment on CCFGroupMember still mentions
client-side enrichment for email, but that field is not part of this interface
and is handled separately via emailByUserId in GroupMembersPanel.vue. Update the
comment next to CCFGroupMember to remove the email parenthetical and keep only
the fields actually represented here, namely userId, displayName, and optional
inherited.

---

Outside diff comments:
In `@src/components/groups/GroupCreateForm.vue`:
- Around line 54-67: The createGroup flow in GroupCreateForm.vue needs an
in-flight submit guard to prevent duplicate group creation on rapid clicks. Add
a submitting ref around createGroup, set it true before execute({ data:
group.value }) and clear it in a finally block, then bind the Save button’s
disabled state to that flag so the action cannot be triggered twice while the
POST is pending, mirroring the adding guard used in GroupMembersPanel.vue.
- Around line 68-77: The group error handlers are still assuming a fully-shaped
Axios response and can throw when the payload is missing or differently
structured. Update the error detail lookup in GroupCreateForm, GroupEditForm,
GroupMembersPanel, and GroupView to safely chain through response, data, and
errors before reading body, using the existing errorResponse variable in each
catch block and keeping the same fallback message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 1ab5647d-9738-44eb-ad93-c8c735f86272

📥 Commits

Reviewing files that changed from the base of the PR and between ccdd8a5 and 68b15d4.

📒 Files selected for processing (7)
  • src/components/groups/GroupCreateForm.vue
  • src/components/groups/GroupEditForm.vue
  • src/components/groups/GroupMembersPanel.vue
  • src/stores/types.ts
  • src/views/groups/GroupView.vue
  • src/views/groups/GroupsListView.vue
  • src/views/users/UserView.vue

Comment thread src/components/groups/GroupMembersPanel.vue
Comment thread src/stores/types.ts
@gusfcarvalho gusfcarvalho merged commit 938ec8c into main Jun 24, 2026
3 checks passed
@gusfcarvalho gusfcarvalho deleted the lisa/feat/bch-1328-ui branch June 24, 2026 12:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant