Skip to content

Commit 6d1ce99

Browse files
committed
Use users paginated request
1 parent 76224c3 commit 6d1ce99

File tree

3 files changed

+175
-40
lines changed

3 files changed

+175
-40
lines changed

site/src/api/queries/users.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export const authMethods = () => {
124124
};
125125
};
126126

127-
const meKey = ["me"];
127+
export const meKey = ["me"];
128128

129129
export const me = (metadata: MetadataState<User>) => {
130130
return cachedQuery({

site/src/pages/WorkspacesPage/WorkspaceSearch/UserMenu.stories.tsx

+80-16
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ import { useState } from "react";
44
import type { User } from "api/typesGenerated";
55
import { UserMenu } from "./UserMenu";
66

7+
const defaultQueries = [
8+
{
9+
key: ["users", { limit: 100, q: "" }],
10+
data: {
11+
users: generateUsers(50),
12+
},
13+
},
14+
];
15+
716
const meta: Meta<typeof UserMenu> = {
817
title: "pages/WorkspacesPage/UserMenu",
918
component: UserMenu,
1019
parameters: {
11-
queries: [
12-
{
13-
key: ["users", {}],
14-
data: {
15-
users: generateUsers(50),
16-
},
17-
},
18-
],
20+
queries: defaultQueries,
1921
},
2022
};
2123

@@ -32,9 +34,17 @@ export const Open: Story = {
3234
},
3335
};
3436

35-
export const Default: Story = {
37+
export const Selected: Story = {
3638
args: {
37-
selected: "2",
39+
selected: user(2).email,
40+
},
41+
parameters: {
42+
queries: [
43+
{
44+
key: ["users", { limit: 1, q: user(2).email }],
45+
data: user(2),
46+
},
47+
],
3848
},
3949
};
4050

@@ -50,6 +60,15 @@ export const SelectOption: Story = {
5060
const option = canvas.getByText("User 4");
5161
await userEvent.click(option);
5262
},
63+
parameters: {
64+
queries: [
65+
...defaultQueries,
66+
{
67+
key: ["users", { limit: 1, q: user(4).email }],
68+
data: user(4),
69+
},
70+
],
71+
},
5372
};
5473

5574
export const SearchStickyOnTop: Story = {
@@ -65,14 +84,22 @@ export const SearchStickyOnTop: Story = {
6584

6685
export const ScrollToSelectedOption: Story = {
6786
args: {
68-
selected: "30",
87+
selected: user(30).email,
6988
},
70-
7189
play: async ({ canvasElement }) => {
7290
const canvas = within(canvasElement);
7391
const button = canvas.getByRole("button", { name: /Select user/i });
7492
await userEvent.click(button);
7593
},
94+
parameters: {
95+
queries: [
96+
...defaultQueries,
97+
{
98+
key: ["users", { limit: 1, q: user(30).email }],
99+
data: user(30),
100+
},
101+
],
102+
},
76103
};
77104

78105
export const Filter: Story = {
@@ -81,7 +108,18 @@ export const Filter: Story = {
81108
const button = canvas.getByRole("button", { name: /Select user/i });
82109
await userEvent.click(button);
83110
const filter = canvas.getByLabelText("Search user");
84-
await userEvent.type(filter, "user23@coder.com");
111+
await userEvent.type(filter, user(23).email!);
112+
},
113+
parameters: {
114+
queries: [
115+
...defaultQueries,
116+
{
117+
key: ["users", { limit: 100, q: user(23).email }],
118+
data: {
119+
users: [user(23)],
120+
},
121+
},
122+
],
85123
},
86124
};
87125

@@ -93,6 +131,17 @@ export const EmptyResults: Story = {
93131
const filter = canvas.getByLabelText("Search user");
94132
await userEvent.type(filter, "invalid-user@coder.com");
95133
},
134+
parameters: {
135+
queries: [
136+
...defaultQueries,
137+
{
138+
key: ["users", { limit: 100, q: "invalid-user@coder.com" }],
139+
data: {
140+
users: [],
141+
},
142+
},
143+
],
144+
},
96145
};
97146

98147
export const FocusOnFirstResultWhenPressArrowDown: Story = {
@@ -101,17 +150,32 @@ export const FocusOnFirstResultWhenPressArrowDown: Story = {
101150
const button = canvas.getByRole("button", { name: /Select user/i });
102151
await userEvent.click(button);
103152
const filter = canvas.getByLabelText("Search user");
104-
await userEvent.type(filter, "user1");
153+
await userEvent.type(filter, user(1).email!);
105154
await userEvent.type(filter, "{arrowdown}");
106155
},
156+
parameters: {
157+
queries: [
158+
...defaultQueries,
159+
{
160+
key: ["users", { limit: 100, q: user(1).email }],
161+
data: {
162+
users: [user(1)],
163+
},
164+
},
165+
],
166+
},
107167
};
108168

109169
function generateUsers(amount: number): Partial<User>[] {
110-
return Array.from({ length: amount }, (_, i) => ({
170+
return Array.from({ length: amount }, (_, i) => user(i));
171+
}
172+
173+
function user(i: number): Partial<User> {
174+
return {
111175
id: i.toString(),
112176
name: `User ${i}`,
113177
username: `user${i}`,
114178
avatar_url: "",
115179
email: `user${i}@coder.com`,
116-
}));
180+
};
117181
}

site/src/pages/WorkspacesPage/WorkspaceSearch/UserMenu.tsx

+94-23
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import MenuItem from "@mui/material/MenuItem";
22
import MenuList from "@mui/material/MenuList";
33
import { useState } from "react";
4-
import { useQuery } from "react-query";
5-
import { users } from "api/queries/users";
4+
import { type QueryClient, useQuery, useQueryClient } from "react-query";
5+
import { API } from "api/api";
6+
import { meKey, usersKey, users as usersQuery } from "api/queries/users";
7+
import type { User } from "api/typesGenerated";
68
import { Loader } from "components/Loader/Loader";
79
import { MenuButton } from "components/Menu/MenuButton";
810
import { MenuCheck } from "components/Menu/MenuCheck";
@@ -15,35 +17,39 @@ import {
1517
withPopover,
1618
} from "components/Popover/Popover";
1719
import { UserAvatar } from "components/UserAvatar/UserAvatar";
20+
import { useDebouncedValue } from "hooks/debounce";
21+
22+
type UserOption = {
23+
label: string;
24+
value: string;
25+
avatar: JSX.Element;
26+
};
1827

1928
type UserMenuProps = {
20-
selected: string | undefined;
21-
onSelect: (value: string) => void;
29+
// The currently selected user email or undefined if no user is selected
30+
selected: UserOption["value"] | undefined;
31+
onSelect: (value: UserOption["value"]) => void;
2232
};
2333

2434
export const UserMenu = withPopover<UserMenuProps>((props) => {
35+
const queryClient = useQueryClient();
2536
const popover = usePopover();
2637
const { selected, onSelect } = props;
2738
const [filter, setFilter] = useState("");
28-
const userOptionsQuery = useQuery({
29-
...users({}),
30-
enabled: selected !== undefined || popover.isOpen,
39+
const debouncedFilter = useDebouncedValue(filter, 300);
40+
const usersQueryResult = useQuery({
41+
...usersQuery({ limit: 100, q: debouncedFilter }),
42+
enabled: popover.isOpen,
3143
});
32-
const options = userOptionsQuery.data?.users
33-
.filter((u) => {
34-
const f = filter.toLowerCase();
35-
return (
36-
u.name?.toLowerCase().includes(f) ||
37-
u.username.toLowerCase().includes(f) ||
38-
u.email.toLowerCase().includes(f)
39-
);
40-
})
41-
.map((u) => ({
42-
label: u.name ?? u.username,
43-
value: u.id,
44-
avatar: <UserAvatar size="xs" username={u.username} src={u.avatar_url} />,
45-
}));
46-
const selectedOption = options?.find((option) => option.value === selected);
44+
const { data: selectedUser } = useQuery({
45+
queryKey: selectedUserKey(selected ?? ""),
46+
queryFn: () => getSelectedUser(selected ?? "", queryClient),
47+
enabled: selected !== undefined,
48+
});
49+
const options = mountOptions(usersQueryResult.data?.users, selectedUser);
50+
const selectedOption = selectedUser
51+
? optionFromUser(selectedUser)
52+
: undefined;
4753

4854
return (
4955
<>
@@ -76,6 +82,17 @@ export const UserMenu = withPopover<UserMenuProps>((props) => {
7682
selected={isSelected}
7783
key={option.value}
7884
onClick={() => {
85+
const user = usersQueryResult.data?.users.find(
86+
(u) => u.email === option.value,
87+
);
88+
89+
if (!user) {
90+
return;
91+
}
92+
93+
// This avoid the need to refetch the selected user query
94+
// when the user is selected
95+
setSelectedUserQueryData(user, queryClient);
7996
popover.setIsOpen(false);
8097
onSelect(option.value);
8198
}}
@@ -91,9 +108,63 @@ export const UserMenu = withPopover<UserMenuProps>((props) => {
91108
<MenuNoResults />
92109
)
93110
) : (
94-
<Loader />
111+
<Loader size={20} />
95112
)}
96113
</PopoverContent>
97114
</>
98115
);
99116
});
117+
118+
function selectedUserKey(email: string) {
119+
return usersKey({ limit: 1, q: email });
120+
}
121+
122+
async function getSelectedUser(
123+
email: string,
124+
queryClient: QueryClient,
125+
): Promise<User | undefined> {
126+
const loggedInUser = queryClient.getQueryData<User>(meKey);
127+
128+
if (loggedInUser && loggedInUser.email === email) {
129+
return loggedInUser;
130+
}
131+
132+
const usersRes = await API.getUsers({ q: email, limit: 1 });
133+
return usersRes.users.at(0);
134+
}
135+
136+
function setSelectedUserQueryData(user: User, queryClient: QueryClient) {
137+
queryClient.setQueryData(selectedUserKey(user.email), user);
138+
}
139+
140+
function optionFromUser(user: User): UserOption {
141+
return {
142+
label: user.name ?? user.username,
143+
value: user.email,
144+
avatar: (
145+
<UserAvatar size="xs" username={user.username} src={user.avatar_url} />
146+
),
147+
};
148+
}
149+
150+
function mountOptions(
151+
users: readonly User[] | undefined,
152+
selectedUser: User | undefined,
153+
): UserOption[] | undefined {
154+
if (!users) {
155+
return undefined;
156+
}
157+
158+
let usersToDisplay = [...users];
159+
160+
if (selectedUser) {
161+
const usersIncludeSelectedUser = users.some(
162+
(u) => u.id === selectedUser.id,
163+
);
164+
if (!usersIncludeSelectedUser) {
165+
usersToDisplay = [selectedUser, ...usersToDisplay];
166+
}
167+
}
168+
169+
return usersToDisplay.map(optionFromUser);
170+
}

0 commit comments

Comments
 (0)