Skip to content

Commit ebd5ca6

Browse files
fix: people filter in /bookings and create OOO modal (#17396)
* fix: people filter in /bookings * refactor: lazy loading people filters and OOO * chore: type errr * chore: type errr * fallback to username if no name is set --------- Co-authored-by: sean <sean@brydon.io>
1 parent bcaf067 commit ebd5ca6

File tree

7 files changed

+200
-74
lines changed

7 files changed

+200
-74
lines changed

apps/web/playwright/out-of-office.e2e.ts

+2-6
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,7 @@ test.describe("Out of office", () => {
7979

8080
await page.getByTestId("profile-redirect-switch").click();
8181

82-
await page.getByTestId("team_username_select").click();
83-
84-
await page.locator("#react-select-3-option-0").click();
82+
await page.getByTestId(`team_username_select_${userTo.id}`).click();
8583

8684
// send request
8785
await saveAndWaitForResponse(page);
@@ -157,9 +155,7 @@ test.describe("Out of office", () => {
157155
await page.getByTestId("notes_input").click();
158156
await page.getByTestId("notes_input").fill("Changed notes");
159157

160-
await page.getByTestId("team_username_select").click();
161-
162-
await page.locator("#react-select-3-option-1").click();
158+
await page.getByTestId(`team_username_select_${userToSecond.id}`).click();
163159

164160
// send request
165161
await saveAndWaitForResponse(page);

apps/web/test-results/.last-run.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"status": "failed",
3+
"failedTests": []
4+
}

packages/features/bookings/components/PeopleFilter.tsx

+29-12
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { useState } from "react";
22

33
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
4-
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
54
import {
65
FilterCheckboxField,
76
FilterCheckboxFieldsContainer,
87
} from "@calcom/features/filters/components/TeamsFilter";
8+
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
9+
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
910
import { useLocale } from "@calcom/lib/hooks/useLocale";
1011
import { trpc } from "@calcom/trpc/react";
11-
import { AnimatedPopover, Avatar, Divider, FilterSearchField, Icon } from "@calcom/ui";
12+
import { AnimatedPopover, Avatar, Divider, FilterSearchField, Icon, Button } from "@calcom/ui";
1213

1314
export const PeopleFilter = () => {
1415
const { t } = useLocale();
15-
const orgBranding = useOrgBranding();
1616

1717
const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery();
1818
const isAdmin = currentOrg?.user.role === "ADMIN" || currentOrg?.user.role === "OWNER";
@@ -21,16 +21,23 @@ export const PeopleFilter = () => {
2121
const { data: query, pushItemToKey, removeItemByKeyAndValue, removeAllQueryParams } = useFilterQuery();
2222
const [searchText, setSearchText] = useState("");
2323

24-
const members = trpc.viewer.teams.legacyListMembers.useQuery({});
24+
const debouncedSearch = useDebounce(searchText, 500);
2525

26-
const filteredMembers = members?.data
27-
?.filter((member) => member.accepted)
28-
?.filter((member) =>
29-
searchText.trim() !== ""
30-
? member?.name?.toLowerCase()?.includes(searchText.toLowerCase()) ||
31-
member?.username?.toLowerCase()?.includes(searchText.toLowerCase())
32-
: true
33-
);
26+
const queryMembers = trpc.viewer.teams.legacyListMembers.useInfiniteQuery(
27+
{ limit: 10, searchText: debouncedSearch },
28+
{
29+
enabled: true,
30+
getNextPageParam: (lastPage) => lastPage.nextCursor,
31+
}
32+
);
33+
34+
const { ref: observerRef } = useInViewObserver(() => {
35+
if (queryMembers.hasNextPage && !queryMembers.isFetching) {
36+
queryMembers.fetchNextPage();
37+
}
38+
}, document.querySelector('[role="dialog"]'));
39+
40+
const filteredMembers = queryMembers?.data?.pages.flatMap((page) => page.members);
3441

3542
const getTextForPopover = () => {
3643
const userIds = query.userIds;
@@ -56,6 +63,7 @@ export const PeopleFilter = () => {
5663
/>
5764
<Divider />
5865
<FilterSearchField onChange={(e) => setSearchText(e.target.value)} placeholder={t("search")} />
66+
5967
{filteredMembers?.map((member) => (
6068
<FilterCheckboxField
6169
key={member.id}
@@ -72,6 +80,15 @@ export const PeopleFilter = () => {
7280
icon={<Avatar alt={`${member?.id} avatar`} imageSrc={member.avatarUrl} size="xs" />}
7381
/>
7482
))}
83+
<div className="text-default text-center" ref={observerRef}>
84+
<Button
85+
color="minimal"
86+
loading={queryMembers.isFetchingNextPage}
87+
disabled={!queryMembers.hasNextPage}
88+
onClick={() => queryMembers.fetchNextPage()}>
89+
{queryMembers.hasNextPage ? t("load_more_results") : t("no_more_results")}
90+
</Button>
91+
</div>
7592
{filteredMembers?.length === 0 && (
7693
<h2 className="text-default px-4 py-2 text-sm font-medium">{t("no_options_available")}</h2>
7794
)}

packages/features/settings/outOfOffice/CreateOrEditOutOfOfficeModal.tsx

+75-23
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import { useState } from "react";
22
import { Controller, useForm } from "react-hook-form";
33

44
import dayjs from "@calcom/dayjs";
5+
import { classNames } from "@calcom/lib";
6+
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
57
import { useHasTeamPlan } from "@calcom/lib/hooks/useHasPaidPlan";
8+
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
69
import { useLocale } from "@calcom/lib/hooks/useLocale";
710
import { trpc } from "@calcom/trpc/react";
811
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
@@ -18,6 +21,8 @@ import {
1821
Switch,
1922
TextArea,
2023
UpgradeTeamsBadge,
24+
Label,
25+
Input,
2126
} from "@calcom/ui";
2227

2328
export type BookingRedirectForm = {
@@ -43,21 +48,39 @@ export const CreateOrEditOutOfOfficeEntryModal = ({
4348
const { t } = useLocale();
4449
const utils = trpc.useUtils();
4550

46-
const { data: listMembers } = trpc.viewer.teams.legacyListMembers.useQuery({});
51+
const [searchText, setSearchText] = useState("");
52+
const debouncedSearch = useDebounce(searchText, 500);
53+
54+
const members = trpc.viewer.teams.legacyListMembers.useInfiniteQuery(
55+
{ limit: 10, searchText: debouncedSearch },
56+
{
57+
enabled: true,
58+
getNextPageParam: (lastPage) => lastPage.nextCursor,
59+
}
60+
);
4761

4862
const me = useMeQuery();
4963

5064
const memberListOptions: {
5165
value: number;
5266
label: string;
67+
avatarUrl: string | null;
5368
}[] =
54-
listMembers
69+
members?.data?.pages
70+
.flatMap((page) => page.members)
5571
?.filter((member) => me?.data?.id !== member.id)
5672
.map((member) => ({
5773
value: member.id,
58-
label: member.name || "",
74+
label: member.name || member.username || "",
75+
avatarUrl: member.avatarUrl,
5976
})) || [];
6077

78+
const { ref: observerRef } = useInViewObserver(() => {
79+
if (members.hasNextPage && !members.isFetching) {
80+
members.fetchNextPage();
81+
}
82+
}, document.querySelector('[role="dialog"]'));
83+
6184
const { data: outOfOfficeReasonList } = trpc.viewer.outOfOfficeReasonList.useQuery();
6285

6386
const reasonList = (outOfOfficeReasonList || []).map((reason) => ({
@@ -74,6 +97,7 @@ export const CreateOrEditOutOfOfficeEntryModal = ({
7497
setValue,
7598
control,
7699
register,
100+
watch,
77101
formState: { isSubmitting },
78102
} = useForm<BookingRedirectForm>({
79103
defaultValues: currentlyEditingOutOfOfficeEntry
@@ -89,6 +113,8 @@ export const CreateOrEditOutOfOfficeEntryModal = ({
89113
},
90114
});
91115

116+
const watchedTeamUserId = watch("toTeamUserId");
117+
92118
const createOrEditOutOfOfficeEntry = trpc.viewer.outOfOfficeCreateOrUpdate.useMutation({
93119
onSuccess: () => {
94120
showToast(
@@ -209,28 +235,54 @@ export const CreateOrEditOutOfOfficeEntryModal = ({
209235
</div>
210236

211237
{profileRedirect && (
212-
<div className="mt-4">
213-
<div className="h-16">
214-
<p className="text-emphasis block text-sm font-medium">{t("team_member")}</p>
215-
<Controller
216-
control={control}
217-
name="toTeamUserId"
218-
render={({ field: { onChange, value } }) => (
219-
<Select<Option>
220-
name="toTeamUsername"
221-
data-testid="team_username_select"
222-
value={memberListOptions.find((member) => member.value === value)}
223-
placeholder={t("select_team_member")}
224-
isSearchable
225-
options={memberListOptions}
226-
onChange={(selectedOption) => {
227-
if (selectedOption?.value) {
228-
onChange(selectedOption.value);
238+
<div className="mb-2">
239+
<Label className="text-emphasis mt-6">{t("select_team_member")}</Label>
240+
<div className="mt-2">
241+
<Input
242+
type="text"
243+
placeholder={t("search")}
244+
onChange={(e) => setSearchText(e.target.value)}
245+
value={searchText}
246+
/>
247+
<div className="scroll-bar flex h-[150px] flex-col gap-0.5 overflow-y-scroll rounded-md border p-1">
248+
{memberListOptions.map((member) => (
249+
<label
250+
key={member.value}
251+
data-testid={`team_username_select_${member.value}`}
252+
tabIndex={watchedTeamUserId === member.value ? -1 : 0}
253+
role="radio"
254+
aria-checked={watchedTeamUserId === member.value}
255+
onKeyDown={(e) => {
256+
if (e.key === "Enter" || e.key === " ") {
257+
e.preventDefault();
258+
setValue("toTeamUserId", member.value, { shouldDirty: true });
229259
}
230260
}}
231-
/>
232-
)}
233-
/>
261+
className={classNames(
262+
"hover:bg-subtle focus:bg-subtle focus:ring-emphasis cursor-pointer items-center justify-between gap-0.5 rounded-sm py-2 outline-none focus:ring-2",
263+
watchedTeamUserId === member.value && "bg-subtle"
264+
)}>
265+
<div className="flex flex-1 items-center space-x-3">
266+
<input
267+
type="radio"
268+
className="hidden"
269+
checked={watchedTeamUserId === member.value}
270+
onChange={() => setValue("toTeamUserId", member.value, { shouldDirty: true })}
271+
/>
272+
<span className="text-emphasis w-full px-2 text-sm">{member.label}</span>
273+
</div>
274+
</label>
275+
))}
276+
<div className="text-default text-center" ref={observerRef}>
277+
<Button
278+
color="minimal"
279+
loading={members.isFetchingNextPage}
280+
disabled={!members.hasNextPage}
281+
onClick={() => members.fetchNextPage()}>
282+
{members.hasNextPage ? t("load_more_results") : t("no_more_results")}
283+
</Button>
284+
</div>
285+
</div>
234286
</div>
235287
</div>
236288
)}

packages/trpc/server/routers/viewer/bookings/get.handler.ts

+2
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ export async function getBookings({
114114
userId: {
115115
in: filters.userIds,
116116
},
117+
isFixed: true,
117118
},
118119
},
119120
},
@@ -207,6 +208,7 @@ export async function getBookings({
207208
.map((key) => bookingWhereInputFilters[key])
208209
// On prisma 5.4.2 passing undefined to where "AND" causes an error
209210
.filter(Boolean);
211+
210212
const bookingSelect = {
211213
...bookingMinimalSelect,
212214
uid: true,

0 commit comments

Comments
 (0)