Skip to content

Commit

Permalink
fix: people filter in /bookings and create OOO modal (#17396)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
Udit-takkar and sean-brydon authored Oct 31, 2024
1 parent bcaf067 commit ebd5ca6
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 74 deletions.
8 changes: 2 additions & 6 deletions apps/web/playwright/out-of-office.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,7 @@ test.describe("Out of office", () => {

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

await page.getByTestId("team_username_select").click();

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

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

await page.getByTestId("team_username_select").click();

await page.locator("#react-select-3-option-1").click();
await page.getByTestId(`team_username_select_${userToSecond.id}`).click();

// send request
await saveAndWaitForResponse(page);
Expand Down
4 changes: 4 additions & 0 deletions apps/web/test-results/.last-run.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"status": "failed",
"failedTests": []
}
41 changes: 29 additions & 12 deletions packages/features/bookings/components/PeopleFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { useState } from "react";

import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import {
FilterCheckboxField,
FilterCheckboxFieldsContainer,
} from "@calcom/features/filters/components/TeamsFilter";
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { AnimatedPopover, Avatar, Divider, FilterSearchField, Icon } from "@calcom/ui";
import { AnimatedPopover, Avatar, Divider, FilterSearchField, Icon, Button } from "@calcom/ui";

export const PeopleFilter = () => {
const { t } = useLocale();
const orgBranding = useOrgBranding();

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

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

const filteredMembers = members?.data
?.filter((member) => member.accepted)
?.filter((member) =>
searchText.trim() !== ""
? member?.name?.toLowerCase()?.includes(searchText.toLowerCase()) ||
member?.username?.toLowerCase()?.includes(searchText.toLowerCase())
: true
);
const queryMembers = trpc.viewer.teams.legacyListMembers.useInfiniteQuery(
{ limit: 10, searchText: debouncedSearch },
{
enabled: true,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);

const { ref: observerRef } = useInViewObserver(() => {
if (queryMembers.hasNextPage && !queryMembers.isFetching) {
queryMembers.fetchNextPage();
}
}, document.querySelector('[role="dialog"]'));

const filteredMembers = queryMembers?.data?.pages.flatMap((page) => page.members);

const getTextForPopover = () => {
const userIds = query.userIds;
Expand All @@ -56,6 +63,7 @@ export const PeopleFilter = () => {
/>
<Divider />
<FilterSearchField onChange={(e) => setSearchText(e.target.value)} placeholder={t("search")} />

{filteredMembers?.map((member) => (
<FilterCheckboxField
key={member.id}
Expand All @@ -72,6 +80,15 @@ export const PeopleFilter = () => {
icon={<Avatar alt={`${member?.id} avatar`} imageSrc={member.avatarUrl} size="xs" />}
/>
))}
<div className="text-default text-center" ref={observerRef}>
<Button
color="minimal"
loading={queryMembers.isFetchingNextPage}
disabled={!queryMembers.hasNextPage}
onClick={() => queryMembers.fetchNextPage()}>
{queryMembers.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
{filteredMembers?.length === 0 && (
<h2 className="text-default px-4 py-2 text-sm font-medium">{t("no_options_available")}</h2>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { useState } from "react";
import { Controller, useForm } from "react-hook-form";

import dayjs from "@calcom/dayjs";
import { classNames } from "@calcom/lib";
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
import { useHasTeamPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
Expand All @@ -18,6 +21,8 @@ import {
Switch,
TextArea,
UpgradeTeamsBadge,
Label,
Input,
} from "@calcom/ui";

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

const { data: listMembers } = trpc.viewer.teams.legacyListMembers.useQuery({});
const [searchText, setSearchText] = useState("");
const debouncedSearch = useDebounce(searchText, 500);

const members = trpc.viewer.teams.legacyListMembers.useInfiniteQuery(
{ limit: 10, searchText: debouncedSearch },
{
enabled: true,
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);

const me = useMeQuery();

const memberListOptions: {
value: number;
label: string;
avatarUrl: string | null;
}[] =
listMembers
members?.data?.pages
.flatMap((page) => page.members)
?.filter((member) => me?.data?.id !== member.id)
.map((member) => ({
value: member.id,
label: member.name || "",
label: member.name || member.username || "",
avatarUrl: member.avatarUrl,
})) || [];

const { ref: observerRef } = useInViewObserver(() => {
if (members.hasNextPage && !members.isFetching) {
members.fetchNextPage();
}
}, document.querySelector('[role="dialog"]'));

const { data: outOfOfficeReasonList } = trpc.viewer.outOfOfficeReasonList.useQuery();

const reasonList = (outOfOfficeReasonList || []).map((reason) => ({
Expand All @@ -74,6 +97,7 @@ export const CreateOrEditOutOfOfficeEntryModal = ({
setValue,
control,
register,
watch,
formState: { isSubmitting },
} = useForm<BookingRedirectForm>({
defaultValues: currentlyEditingOutOfOfficeEntry
Expand All @@ -89,6 +113,8 @@ export const CreateOrEditOutOfOfficeEntryModal = ({
},
});

const watchedTeamUserId = watch("toTeamUserId");

const createOrEditOutOfOfficeEntry = trpc.viewer.outOfOfficeCreateOrUpdate.useMutation({
onSuccess: () => {
showToast(
Expand Down Expand Up @@ -209,28 +235,54 @@ export const CreateOrEditOutOfOfficeEntryModal = ({
</div>

{profileRedirect && (
<div className="mt-4">
<div className="h-16">
<p className="text-emphasis block text-sm font-medium">{t("team_member")}</p>
<Controller
control={control}
name="toTeamUserId"
render={({ field: { onChange, value } }) => (
<Select<Option>
name="toTeamUsername"
data-testid="team_username_select"
value={memberListOptions.find((member) => member.value === value)}
placeholder={t("select_team_member")}
isSearchable
options={memberListOptions}
onChange={(selectedOption) => {
if (selectedOption?.value) {
onChange(selectedOption.value);
<div className="mb-2">
<Label className="text-emphasis mt-6">{t("select_team_member")}</Label>
<div className="mt-2">
<Input
type="text"
placeholder={t("search")}
onChange={(e) => setSearchText(e.target.value)}
value={searchText}
/>
<div className="scroll-bar flex h-[150px] flex-col gap-0.5 overflow-y-scroll rounded-md border p-1">
{memberListOptions.map((member) => (
<label
key={member.value}
data-testid={`team_username_select_${member.value}`}
tabIndex={watchedTeamUserId === member.value ? -1 : 0}
role="radio"
aria-checked={watchedTeamUserId === member.value}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setValue("toTeamUserId", member.value, { shouldDirty: true });
}
}}
/>
)}
/>
className={classNames(
"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",
watchedTeamUserId === member.value && "bg-subtle"
)}>
<div className="flex flex-1 items-center space-x-3">
<input
type="radio"
className="hidden"
checked={watchedTeamUserId === member.value}
onChange={() => setValue("toTeamUserId", member.value, { shouldDirty: true })}
/>
<span className="text-emphasis w-full px-2 text-sm">{member.label}</span>
</div>
</label>
))}
<div className="text-default text-center" ref={observerRef}>
<Button
color="minimal"
loading={members.isFetchingNextPage}
disabled={!members.hasNextPage}
onClick={() => members.fetchNextPage()}>
{members.hasNextPage ? t("load_more_results") : t("no_more_results")}
</Button>
</div>
</div>
</div>
</div>
)}
Expand Down
2 changes: 2 additions & 0 deletions packages/trpc/server/routers/viewer/bookings/get.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export async function getBookings({
userId: {
in: filters.userIds,
},
isFixed: true,
},
},
},
Expand Down Expand Up @@ -207,6 +208,7 @@ export async function getBookings({
.map((key) => bookingWhereInputFilters[key])
// On prisma 5.4.2 passing undefined to where "AND" causes an error
.filter(Boolean);

const bookingSelect = {
...bookingMinimalSelect,
uid: true,
Expand Down
Loading

0 comments on commit ebd5ca6

Please sign in to comment.