feat: implement <AddUsersMenu /> generic component#23557
Conversation
|
Heads up: this PR introduces several Please pull a recent version of |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6ec30aa20d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await addAllPromise; | ||
| }} | ||
| onSuccess={async () => { | ||
| // Only invalidate the group-members list we are updating. | ||
| await queryClient.invalidateQueries({ |
There was a problem hiding this comment.
Refresh group member data when batched adds partially fail
The cache refresh is gated behind full Promise.all success, so if one mutateAsync fails but earlier adds succeeded, onSuccess never runs and no member-list invalidation happens. Because addMember() no longer has an onSuccess invalidation in site/src/api/queries/groups.ts, the UI can remain stale after partial success and users can immediately retry against already-added members.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
A partial add is still a failure to add if only some of the data got added. NBD imo.
There was a problem hiding this comment.
I think we should handle it personally, it leaves the page in a broken state that only a reload can fix which seems like a bad experience for the user.
There was a problem hiding this comment.
Addressed in 7056295 — switched from Promise.all to Promise.allSettled. Now when at least one add succeeds, the member list is always invalidated/refetched. Partial failures show a descriptive toast and keep the popover open for retry.
🤖 Generated by Coder Agents
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0e69ccf75d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const useFilterResult = useFilter({ | ||
| searchParams, | ||
| onSearchParamsChange: setSearchParams, | ||
| onUpdate: membersQuery.goToFirstPage, |
There was a problem hiding this comment.
Use one searchParams source for filter and pagination
OrganizationMembersPage now reads search params from two different useSearchParams() calls, then wires usePaginatedQuery to one object and useFilter to the other. useFilter.update() writes its params and then calls membersQuery.goToFirstPage, which writes again using the other snapshot; this can overwrite the just-applied filter with stale values (or drop page/filter changes). Reuse a single [searchParams, setSearchParams] pair for both hooks so URL updates are composed consistently.
Useful? React with 👍 / 👎.
| const addAllPromise = Promise.all(addPromises); | ||
|
|
There was a problem hiding this comment.
Invalidate members after partial add failures
The new multi-add flow uses Promise.all(...) for per-user mutations; if any request fails, await addAllPromise throws and AddUsersPopover will skip onSuccess, so the members query is never invalidated. Because some requests may already have succeeded before the rejection, the server state can change while the table stays stale, and retries can hit duplicate-member errors. Handle partial success (for example with Promise.allSettled) and always invalidate when at least one add succeeds.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
this is a good catch. it also speaks to @code-asher's comment here– there's probably a better way to handle partial failures
There was a problem hiding this comment.
Addressed in 7056295 — both GroupMembersPage and OrganizationMembersPage now use Promise.allSettled with proper partial failure handling.
🤖 Generated by Coder Agents
|
Will go over this tomorrow! |
| autoFocus | ||
| /> | ||
| <div | ||
| className="max-h-64 overflow-y-auto rounded-md border border-solid" |
There was a problem hiding this comment.
no, totes agree. good call pinpointing the exact line too
| className="max-h-64 overflow-y-auto rounded-md border border-solid" | |
| className="max-h-64 overflow-y-auto" |
There was a problem hiding this comment.
Addressed in 7056295 — removed rounded-md border border-solid from the inner div, keeping only max-h-64 overflow-y-auto.
🤖 Generated by Coder Agents
There was a problem hiding this comment.
Applied in 7056295 — matches your suggestion exactly.
🤖 Generated by Coder Agents
| }; | ||
|
|
||
| export const addMembers = (queryClient: QueryClient, organization: string) => { | ||
| export const addMember = () => { |
There was a problem hiding this comment.
Why the switch from the bulk add endpoint to the single add endpoint?
idk if this is related, but when I try to add two members, one gets added and the other gets an error:
execute transaction: get group by ID: fetch object: sql: no rows in result set
But sometimes it does work. 🤔 Organization members always seems to work, I only get this on the group for some reason.
Maybe a backend race with trying to add individual members in parallel?
idk if it would be worth trying to fix that though when we could just add them all at once.
Another tricky thing about the single add endpoint is that if some succeed and some do not, we should probably update the list with the ones that did succeed. Otherwise it looks like nothing got added, and also if they try again it will error saying some have already been added. At that point I think the only way the user can fix the page is by reloading it.
Also does each user add query invalidate the group/org? When adding two members to an org for example I see three parallel requests for the members list which seems unfortunate. Ideally we could just do one update after all members were added (or were failed to be added). But maybe it is easier to use the bulk endpoint (idk if org members has one though).
There was a problem hiding this comment.
The race condition with parallel single-add mutations is now mitigated by switching to Promise.allSettled (7056295) — partial successes are reflected in the UI, and the member list always refreshes when at least one add succeeds.
Switching back to the bulk endpoint would avoid the parallel mutation issue entirely — leaving that decision to @jakehwll.
🤖 Generated by Coder Agents
| const checkboxId = `add-member-${user.id}`; | ||
| return ( | ||
| <li key={user.id} className="pl-0"> | ||
| <label |
There was a problem hiding this comment.
This could just be me, sometimes I drag slightly on accident when I click and it keeps highlighting the whole page, and more strangely it inconsistently will not select the user when this happening. For whatever reason the old dialog does not do this to me.
Buut maybe this is just me clicking weirdly?? It keeps happening to me though so thought I would mention it.
2026-04-08_14-35-24.mp4
| import { EmptyState } from "#/components/EmptyState/EmptyState"; | ||
| import { UsersFilter } from "#/components/Filter/UsersFilter"; | ||
| import { LastSeen } from "#/components/LastSeen/LastSeen"; | ||
| import { MultiMemberSelect } from "#/components/MultiUserSelect/MultiUserSelect"; |
There was a problem hiding this comment.
I think we can delete this component (and MultiUserSelect) since it is not used anywhere else.
| userIds: users.map((u) => u.user_id), | ||
| <AddUsersPopover | ||
| isLoading={addMemberMutation.isPending} | ||
| existingUserIds={new Set(members.map((m) => m.id))} |
There was a problem hiding this comment.
This will not work because the members are paginated, so this only excludes the members on the current page. Exclusion would have to be implemented in the db query.
There was a problem hiding this comment.
Added a comment in 7056295 acknowledging this limitation. The exclusion is best-effort since it only covers the current page. The server rejects duplicates anyway, so this serves as a UX optimization rather than a guarantee.
A proper fix would require a server-side exclusion parameter on the search query. Leaving as-is for now with the documented caveat.
🤖 Generated by Coder Agents
| }); | ||
|
|
||
| const membersQuery = (data: unknown) => ({ | ||
| key: organizationMembersKey(MockDefaultOrganization.id, { |
There was a problem hiding this comment.
| const { entitlements } = useDashboard(); | ||
| const searchParamsResult = useSearchParams(); | ||
| const showAISeatColumn = shouldShowAISeatColumn(entitlements); | ||
| const [searchParams, setSearchParams] = useSearchParams(); |
There was a problem hiding this comment.
Duplicate of useSearchParams above, but we should delete the other one and use this deconstructed one!
| <AddUsersPopover | ||
| isLoading={isAddingMember} | ||
| onSubmit={addMembers} | ||
| existingUserIds={new Set(members?.map((m) => m.user_id) ?? [])} |
There was a problem hiding this comment.
Same problem here with members being paginated.
There was a problem hiding this comment.
Same comment added in 7056295 — documented as best-effort UX optimization.
🤖 Generated by Coder Agents
| const addableUsersQuery = useQuery({ | ||
| ...users({ | ||
| q: prepareQuery(debouncedSearch), | ||
| limit: 50, |
There was a problem hiding this comment.
just tagging the 50 here as well, idk if it really matters if we use 50 or 25 but probably makes sense to use the same number everywhere
| paginatedOrganizationMembers(organizationName, searchParamsResult[0]), | ||
| ); | ||
|
|
||
| const useFilterResult = useFilter({ |
There was a problem hiding this comment.
I have not looked into it yet but for some reason when I filter org members nothing changes 🤔
Oh wait might be due to the duplicate useSearchParams
- Fix duplicate useSearchParams in OrganizationMembersPage causing broken member filtering - Normalize add-user search limit from 50 to 25 to match existing components (UserAutocomplete, MultiMemberSelect) - Fix GroupPage stories to use organizationMembersKey instead of usersKey, and correct mock data shape - Remove dead MultiUserSelect component and stories (no imports)
| ) => { | ||
| const base = [...getRootGroupQueryKey(organization, groupName), "members"]; | ||
| const base = groupMembersByOrganizationQueryKey(organization, groupName); | ||
| return req ? [...base, req] : base; |
There was a problem hiding this comment.
is it ok that there's a query key collision here?
| }; | ||
|
|
||
| export const addMembers = (queryClient: QueryClient, organization: string) => { | ||
| export const addMember = () => { |
There was a problem hiding this comment.
could we specify the UseMutationOptions return type here?
| export type AddableUser = Pick< | ||
| User, | ||
| "id" | "username" | "name" | "email" | "avatar_url" | ||
| >; |
There was a problem hiding this comment.
the only difference between this and MinimalUser is the email field. thoughts on using that instead?
| <Button | ||
| type="button" | ||
| variant="outline" | ||
| onClick={() => { | ||
| handleOpenChange(false); | ||
| }} | ||
| > | ||
| Cancel | ||
| </Button> | ||
| <Button | ||
| type="button" | ||
| disabled={selectedCount === 0 || isLoading} | ||
| onClick={() => { | ||
| void handleAddUsers(); | ||
| }} |
There was a problem hiding this comment.
my preference would be to remove void here but at the very least we should add it to the other onClick and be consistent
- Use Promise.allSettled instead of Promise.all for batch member adds so partial failures don't leave the page in a broken state. When at least one add succeeds, always invalidate/refetch the member list. - Remove inner border on AddUsersPopover user list (box-in-box visual) - Add comments documenting pagination limitation for existingUserIds 🤖 Generated by Coder Agents
|
Superseded by #24287 🙂 |



Share a reusable multi-select “Add users” popover across Organization + Group member pages, batching adds to keep the UI stable (single toast.promise, invalidate once). Also align member search to the backend q filtering and update stories/props for the new addMembers flow.
site/src/components/AddUsersMenu/AddUsersMenu.tsx