Skip to content

Commit 49a709f

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 49a709f

File tree

4 files changed

+142
-51
lines changed

4 files changed

+142
-51
lines changed
Lines changed: 26 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 95 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ 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 { getErrorMessage } from "api/errors";
6+
import { organizationMembers } from "api/queries/organizations";
57
import { users } from "api/queries/users";
6-
import type { User } from "api/typesGenerated";
8+
import type { OrganizationMemberWithUserData, User } from "api/typesGenerated";
79
import { Avatar } from "components/Avatar/Avatar";
810
import { AvatarData } from "components/AvatarData/AvatarData";
911
import { useDebouncedFunction } from "hooks/debounce";
@@ -16,71 +18,124 @@ import {
1618
import { useQuery } from "react-query";
1719
import { prepareQuery } from "utils/filters";
1820

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

28-
export const UserAutocomplete: FC<UserAutocompleteProps> = ({
29-
value,
30-
onChange,
31-
label,
32-
className,
33-
size = "small",
34-
required,
35-
}) => {
36-
const [autoComplete, setAutoComplete] = useState<{
37-
value: string;
38-
open: boolean;
39-
}>({
40-
value: value?.email ?? "",
41-
open: false,
42-
});
37+
export type UserAutocompleteProps = CommonAutocompleteProps<User>;
38+
39+
export const UserAutocomplete: FC<UserAutocompleteProps> = (props) => {
40+
const [filter, setFilter] = useState<string>();
41+
4342
const usersQuery = useQuery({
4443
...users({
45-
q: prepareQuery(encodeURI(autoComplete.value)),
44+
q: filter !== undefined ? prepareQuery(encodeURI(filter)) : "",
4645
limit: 25,
4746
}),
48-
enabled: autoComplete.open,
47+
enabled: filter !== undefined,
48+
keepPreviousData: true,
49+
});
50+
return (
51+
<InnerAutocomplete<User>
52+
users={usersQuery.data?.users}
53+
error={usersQuery.error}
54+
setFilter={setFilter}
55+
{...props}
56+
/>
57+
);
58+
};
59+
60+
export type MemberAutocompleteProps =
61+
CommonAutocompleteProps<OrganizationMemberWithUserData> & {
62+
organizationId: string;
63+
};
64+
65+
export const MemberAutocomplete: FC<MemberAutocompleteProps> = ({
66+
organizationId,
67+
...props
68+
}) => {
69+
const [filter, setFilter] = useState<string>();
70+
71+
// Currently this queries all members, as there is no pagination.
72+
const membersQuery = useQuery({
73+
...organizationMembers(organizationId),
74+
enabled: filter !== undefined,
4975
keepPreviousData: true,
5076
});
77+
return (
78+
<InnerAutocomplete<OrganizationMemberWithUserData>
79+
users={membersQuery.data}
80+
error={membersQuery.error}
81+
setFilter={setFilter}
82+
{...props}
83+
/>
84+
);
85+
};
86+
87+
type InnerAutocompleteProps<T extends SelectedUser> =
88+
CommonAutocompleteProps<T> & {
89+
/** Filter is undefined if the autocomplete is closed. */
90+
setFilter: (filter: string | undefined) => void;
91+
/** Users are undefined if not loaded or errored. */
92+
users: readonly T[] | undefined;
93+
/** The error is null if not loaded or no error. */
94+
error: unknown;
95+
};
96+
97+
const InnerAutocomplete = <T extends SelectedUser>({
98+
className,
99+
error,
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
);
61116

62117
return (
63118
<Autocomplete
64-
noOptionsText="No users found"
119+
noOptionsText={
120+
error
121+
? getErrorMessage(error, "Unable to fetch users")
122+
: "No users found"
123+
}
65124
className={className}
66-
options={usersQuery.data?.users ?? []}
67-
loading={usersQuery.isLoading}
125+
options={users ?? []}
126+
loading={!users && !error}
68127
value={value}
69128
data-testid="user-autocomplete"
70-
open={autoComplete.open}
129+
open={open}
71130
isOptionEqualToValue={(a, b) => a.username === b.username}
72131
getOptionLabel={(option) => option.email}
73132
onOpen={() => {
74-
setAutoComplete((state) => ({
75-
...state,
76-
open: true,
77-
}));
133+
setOpen(true);
134+
setFilter(value?.email ?? "");
78135
}}
79136
onClose={() => {
80-
setAutoComplete({
81-
value: value?.email ?? "",
82-
open: false,
83-
});
137+
setOpen(false);
138+
setFilter(undefined);
84139
}}
85140
onChange={(_, newValue) => {
86141
onChange(newValue);
@@ -117,9 +172,7 @@ export const UserAutocomplete: FC<UserAutocompleteProps> = ({
117172
),
118173
endAdornment: (
119174
<>
120-
{usersQuery.isFetching && autoComplete.open && (
121-
<CircularProgress size={16} />
122-
)}
175+
{!users && !error && open && <CircularProgress size={16} />}
123176
{params.InputProps.endAdornment}
124177
</>
125178
),

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

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ import {
1818
groupPermissions,
1919
removeMember,
2020
} from "api/queries/groups";
21-
import type { Group, ReducedUser, User } from "api/typesGenerated";
21+
import type {
22+
Group,
23+
OrganizationMemberWithUserData,
24+
ReducedUser,
25+
} from "api/typesGenerated";
2226
import { ErrorAlert } from "components/Alert/ErrorAlert";
2327
import { AvatarData } from "components/AvatarData/AvatarData";
2428
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog";
@@ -39,7 +43,7 @@ import {
3943
PaginationStatus,
4044
TableToolbar,
4145
} from "components/TableToolbar/TableToolbar";
42-
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
46+
import { MemberAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
4347
import { UserAvatar } from "components/UserAvatar/UserAvatar";
4448
import { type FC, useState } from "react";
4549
import { Helmet } from "react-helmet-async";
@@ -133,11 +137,12 @@ export const GroupPage: FC = () => {
133137
{canUpdateGroup && groupData && !isEveryoneGroup(groupData) && (
134138
<AddGroupMember
135139
isLoading={addMemberMutation.isLoading}
136-
onSubmit={async (user, reset) => {
140+
organizationId={groupData.organization_id}
141+
onSubmit={async (member, reset) => {
137142
try {
138143
await addMemberMutation.mutateAsync({
139144
groupId,
140-
userId: user.id,
145+
userId: member.user_id,
141146
});
142147
reset();
143148
await groupQuery.refetch();
@@ -231,11 +236,17 @@ export const GroupPage: FC = () => {
231236

232237
interface AddGroupMemberProps {
233238
isLoading: boolean;
234-
onSubmit: (user: User, reset: () => void) => void;
239+
onSubmit: (user: OrganizationMemberWithUserData, reset: () => void) => void;
240+
organizationId: string;
235241
}
236242

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

240251
const resetValues = () => {
241252
setSelectedUser(null);
@@ -252,9 +263,10 @@ const AddGroupMember: FC<AddGroupMemberProps> = ({ isLoading, onSubmit }) => {
252263
}}
253264
>
254265
<Stack direction="row" alignItems="center" spacing={1}>
255-
<UserAutocomplete
266+
<MemberAutocomplete
256267
css={styles.autoComplete}
257268
value={selectedUser}
269+
organizationId={organizationId}
258270
onChange={(newValue) => {
259271
setSelectedUser(newValue);
260272
}}

0 commit comments

Comments
 (0)