Skip to content

Commit 8b7185d

Browse files
committed
fix: filter add group member by organization
This is accomplished by using the members endpoint instead of the users endpoint, and to that end the UserAutocomplete component has been reworked to support either endpoint as separate components with a shared base.
1 parent 9f4f88f commit 8b7185d

File tree

4 files changed

+135
-47
lines changed

4 files changed

+135
-47
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { MockOrganizationMember } from "testHelpers/entities";
3+
import { MemberAutocomplete } from "./UserAutocomplete";
4+
5+
const meta: Meta<typeof MemberAutocomplete> = {
6+
title: "components/MemberAutocomplete",
7+
component: MemberAutocomplete,
8+
};
9+
10+
export default meta;
11+
type Story = StoryObj<typeof MemberAutocomplete>;
12+
13+
export const WithLabel: Story = {
14+
args: {
15+
value: MockOrganizationMember,
16+
organizationId: MockOrganizationMember.organization_id,
17+
label: "Member",
18+
},
19+
};
20+
21+
export const NoLabel: Story = {
22+
args: {
23+
value: MockOrganizationMember,
24+
organizationId: MockOrganizationMember.organization_id,
25+
},
26+
};

site/src/components/UserAutocomplete/UserAutocomplete.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const meta: Meta<typeof UserAutocomplete> = {
1010
export default meta;
1111
type Story = StoryObj<typeof UserAutocomplete>;
1212

13-
export const Example: Story = {
13+
export const WithLabel: Story = {
1414
args: {
1515
value: MockUser,
1616
label: "User",

site/src/components/UserAutocomplete/UserAutocomplete.tsx

+87-38
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import { css } from "@emotion/css";
22
import Autocomplete from "@mui/material/Autocomplete";
33
import CircularProgress from "@mui/material/CircularProgress";
44
import TextField from "@mui/material/TextField";
5+
import { organizationMembers } from "api/queries/organizations";
56
import { users } from "api/queries/users";
6-
import type { User } from "api/typesGenerated";
7+
import type { OrganizationMemberWithUserData, User } from "api/typesGenerated";
78
import { Avatar } from "components/Avatar/Avatar";
89
import { AvatarData } from "components/AvatarData/AvatarData";
910
import { useDebouncedFunction } from "hooks/debounce";
@@ -16,45 +17,99 @@ import {
1617
import { useQuery } from "react-query";
1718
import { prepareQuery } from "utils/filters";
1819

19-
export type UserAutocompleteProps = {
20-
value: User | null;
21-
onChange: (user: User | null) => void;
22-
label?: string;
20+
// The common properties between users and org members that we need.
21+
export type SelectedUser = {
22+
avatar_url: string;
23+
email: string;
24+
username: string;
25+
};
26+
27+
export type CommonAutocompleteProps<T extends SelectedUser> = {
2328
className?: string;
24-
size?: ComponentProps<typeof TextField>["size"];
29+
label?: string;
30+
onChange: (user: T | null) => void;
2531
required?: boolean;
32+
size?: ComponentProps<typeof TextField>["size"];
33+
value: T | null;
2634
};
2735

36+
export type UserAutocompleteProps = CommonAutocompleteProps<User>;
37+
2838
export const UserAutocomplete: FC<UserAutocompleteProps> = ({
2939
value,
30-
onChange,
31-
label,
32-
className,
33-
size = "small",
34-
required,
40+
...props
3541
}) => {
36-
const [autoComplete, setAutoComplete] = useState<{
37-
value: string;
38-
open: boolean;
39-
}>({
40-
value: value?.email ?? "",
41-
open: false,
42-
});
42+
const [filter, setFilter] = useState<string>();
43+
4344
const usersQuery = useQuery({
4445
...users({
45-
q: prepareQuery(encodeURI(autoComplete.value)),
46+
q: filter !== undefined ? prepareQuery(encodeURI(filter)) : "",
4647
limit: 25,
4748
}),
48-
enabled: autoComplete.open,
49+
enabled: filter !== undefined,
4950
keepPreviousData: true,
5051
});
52+
return (
53+
<InnerAutocomplete<User>
54+
users={usersQuery.data?.users}
55+
value={value}
56+
setFilter={setFilter}
57+
{...props}
58+
/>
59+
);
60+
};
61+
62+
export type MemberAutocompleteProps =
63+
CommonAutocompleteProps<OrganizationMemberWithUserData> & {
64+
organizationId: string;
65+
};
66+
67+
export const MemberAutocomplete: FC<MemberAutocompleteProps> = ({
68+
value,
69+
organizationId,
70+
...props
71+
}) => {
72+
const [filter, setFilter] = useState<string>();
73+
74+
// Currently this queries all members, as there is no pagination.
75+
const membersQuery = useQuery({
76+
...organizationMembers(organizationId),
77+
enabled: filter !== undefined,
78+
keepPreviousData: true,
79+
});
80+
return (
81+
<InnerAutocomplete<OrganizationMemberWithUserData>
82+
users={membersQuery.data}
83+
value={value}
84+
setFilter={setFilter}
85+
{...props}
86+
/>
87+
);
88+
};
89+
90+
type InnerAutocompleteProps<T extends SelectedUser> =
91+
CommonAutocompleteProps<T> & {
92+
/** Filter is undefined if the autocomplete is closed. */
93+
setFilter: (filter: string | undefined) => void;
94+
/** Users are undefined if not loaded. */
95+
users: readonly T[] | undefined;
96+
};
97+
98+
const InnerAutocomplete = <T extends SelectedUser>({
99+
className,
100+
label,
101+
onChange,
102+
required,
103+
setFilter,
104+
size = "small",
105+
users,
106+
value,
107+
}: InnerAutocompleteProps<T>) => {
108+
const [open, setOpen] = useState(false);
51109

52110
const { debounced: debouncedInputOnChange } = useDebouncedFunction(
53111
(event: ChangeEvent<HTMLInputElement>) => {
54-
setAutoComplete((state) => ({
55-
...state,
56-
value: event.target.value,
57-
}));
112+
setFilter(event.target.value ?? "");
58113
},
59114
750,
60115
);
@@ -63,24 +118,20 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
63118
<Autocomplete
64119
noOptionsText="No users found"
65120
className={className}
66-
options={usersQuery.data?.users ?? []}
67-
loading={usersQuery.isLoading}
121+
options={users ?? []}
122+
loading={users !== undefined}
68123
value={value}
69124
data-testid="user-autocomplete"
70-
open={autoComplete.open}
125+
open={open}
71126
isOptionEqualToValue={(a, b) => a.username === b.username}
72127
getOptionLabel={(option) => option.email}
73128
onOpen={() => {
74-
setAutoComplete((state) => ({
75-
...state,
76-
open: true,
77-
}));
129+
setOpen(true);
130+
setFilter(value?.email ?? "");
78131
}}
79132
onClose={() => {
80-
setAutoComplete({
81-
value: value?.email ?? "",
82-
open: false,
83-
});
133+
setOpen(false);
134+
setFilter(undefined);
84135
}}
85136
onChange={(_, newValue) => {
86137
onChange(newValue);
@@ -117,9 +168,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
117168
),
118169
endAdornment: (
119170
<>
120-
{usersQuery.isFetching && autoComplete.open && (
121-
<CircularProgress size={16} />
122-
)}
171+
{users === undefined && open && <CircularProgress size={16} />}
123172
{params.InputProps.endAdornment}
124173
</>
125174
),

site/src/pages/ManagementSettingsPage/GroupsPage/GroupPage.tsx

+21-8
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ import {
1818
groupPermissions,
1919
removeMember,
2020
} from "api/queries/groups";
21-
import type { Group, ReducedUser, User } from "api/typesGenerated";
21+
import type {
22+
OrganizationMemberWithUserData,
23+
Group,
24+
ReducedUser,
25+
User,
26+
} from "api/typesGenerated";
2227
import { ErrorAlert } from "components/Alert/ErrorAlert";
2328
import { AvatarData } from "components/AvatarData/AvatarData";
2429
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
@@ -39,7 +44,7 @@ import {
3944
PaginationStatus,
4045
TableToolbar,
4146
} from "components/TableToolbar/TableToolbar";
42-
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
47+
import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
4348
import { UserAvatar } from "components/UserAvatar/UserAvatar";
4449
import { type FC, useState } from "react";
4550
import { Helmet } from "react-helmet-async";
@@ -133,11 +138,12 @@ export const GroupPage: FC = () => {
133138
{canUpdateGroup && groupData && !isEveryoneGroup(groupData) && (
134139
<AddGroupMember
135140
isLoading={addMemberMutation.isLoading}
136-
onSubmit={async (user, reset) => {
141+
organizationId={groupData.organization_id}
142+
onSubmit={async (member, reset) => {
137143
try {
138144
await addMemberMutation.mutateAsync({
139145
groupId,
140-
userId: user.id,
146+
userId: member.user_id,
141147
});
142148
reset();
143149
await groupQuery.refetch();
@@ -231,11 +237,17 @@ export const GroupPage: FC = () => {
231237

232238
interface AddGroupMemberProps {
233239
isLoading: boolean;
234-
onSubmit: (user: User, reset: () => void) => void;
240+
onSubmit: (user: OrganizationMemberWithUserData, reset: () => void) => void;
241+
organizationId: string;
235242
}
236243

237-
const AddGroupMember: FC<AddGroupMemberProps> = ({ isLoading, onSubmit }) => {
238-
const [selectedUser, setSelectedUser] = useState<User | null>(null);
244+
const AddGroupMember: FC<AddGroupMemberProps> = ({
245+
isLoading,
246+
onSubmit,
247+
organizationId,
248+
}) => {
249+
const [selectedUser, setSelectedUser] =
250+
useState<OrganizationMemberWithUserData | null>(null);
239251

240252
const resetValues = () => {
241253
setSelectedUser(null);
@@ -252,9 +264,10 @@ const AddGroupMember: FC<AddGroupMemberProps> = ({ isLoading, onSubmit }) => {
252264
}}
253265
>
254266
<Stack direction="row" alignItems="center" spacing={1}>
255-
<UserAutocomplete
267+
<MemberAutocomplete
256268
css={styles.autoComplete}
257269
value={selectedUser}
270+
organizationId={organizationId}
258271
onChange={(newValue) => {
259272
setSelectedUser(newValue);
260273
}}

0 commit comments

Comments
 (0)