feat: Event availability platform wrapper (#17091)

* moved listSchedules to schedule repository

* Create EventAvailabilityTabPlatformWrapper.tsx

* Revert "moved listSchedules to schedule repository"

This reverts commit b5feacfabbadbf9d75eb205dfad8f70b7773a5cb.

* fix: active state of tabs items

* added event availability platform wrapper

* Update event-types.tsx

* better types

only pick the types we need and omit the rest

* fix: type-check failing

---------

Co-authored-by: Morgan Vernay <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
Somay Chauhan
2024-10-16 13:24:13 +03:00
committed by GitHub
co-authored by Morgan Vernay Morgan
parent 123074f4ba
commit e90e060739
11 changed files with 193 additions and 53 deletions
@@ -42,7 +42,6 @@ const assertSelectOperators = (config: any) => {
expect(config.types.select.widgets.multiselect.operators).not.toContain("select_equals");
expect(config.types.select.widgets.multiselect.operators).not.toContain("select_not_equals");
expect(config.types.select.widgets.select.operators).toContain("select_equals");
expect(config.types.select.widgets.select.operators).toContain("select_not_equals");
@@ -340,10 +340,7 @@ const types: Types = {
defaultOperator: "select_equals",
widgets: {
select: {
operators: [
"select_equals",
"select_not_equals",
],
operators: ["select_equals", "select_not_equals"],
widgetProps: {
customProps: {
showSearch: true,
@@ -351,10 +348,7 @@ const types: Types = {
},
},
multiselect: {
operators: [
"select_any_in",
"select_not_any_in",
],
operators: ["select_any_in", "select_not_any_in"],
},
},
},
@@ -4,10 +4,7 @@ import { Controller, useFormContext } from "react-hook-form";
import type { OptionProps, SingleValueProps } from "react-select";
import { components } from "react-select";
import type {
EventAvailabilityTabWebWrapperProps,
GetAllSchedulesByUserIdQueryType,
} from "@calcom/atoms/event-types/wrappers/EventAvailabilityTabWebWrapper";
import type { GetAllSchedulesByUserIdQueryType } from "@calcom/atoms/event-types/wrappers/EventAvailabilityTabWebWrapper";
import dayjs from "@calcom/dayjs";
import { SelectSkeletonLoader } from "@calcom/features/availability/components/SkeletonLoader";
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
@@ -27,10 +24,14 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import { Avatar, Badge, Button, Icon, Label, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
import { Spinner } from "@calcom/ui/components/icon/Spinner";
type ScheduleQueryData = RouterOutputs["viewer"]["availability"]["schedule"]["get"];
type EventTypeScheduleDetailsProps = {
scheduleQueryData?: RouterOutputs["viewer"]["availability"]["schedule"]["get"];
scheduleQueryData?: Pick<ScheduleQueryData, "timeZone" | "id" | "isManaged" | "readOnly"> & {
schedule: Array<Pick<ScheduleQueryData["schedule"][number], "days" | "startTime" | "endTime">>;
};
isSchedulePending?: boolean;
loggedInUser?: RouterOutputs["viewer"]["me"];
user?: Pick<RouterOutputs["viewer"]["me"], "timeFormat" | "weekStart">;
editAvailabilityRedirectUrl?: string;
};
@@ -40,13 +41,20 @@ type EventTypeTeamScheduleProps = {
};
type EventTypeScheduleProps = {
availabilityQueryData?: RouterOutputs["viewer"]["availability"]["list"];
isAvailabilityPending?: boolean;
schedulesQueryData?: Array<
Omit<RouterOutputs["viewer"]["availability"]["list"]["schedules"][number], "availability">
>;
isSchedulesPending?: boolean;
eventType: EventTypeSetup;
teamMembers: TeamMembers;
} & EventTypeScheduleDetailsProps &
EventTypeTeamScheduleProps;
type EventAvailabilityTabProps = EventAvailabilityTabWebWrapperProps & EventTypeScheduleProps;
export type EventAvailabilityTabBaserProps = {
isTeamEvent: boolean;
};
type EventAvailabilityTabProps = EventAvailabilityTabBaserProps & EventTypeScheduleProps;
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
const { label, isDefault, isManaged = false } = props.data;
@@ -99,16 +107,16 @@ const EventTypeScheduleDetails = memo(
({
scheduleQueryData,
isSchedulePending,
loggedInUser,
user,
editAvailabilityRedirectUrl,
}: EventTypeScheduleDetailsProps) => {
const timeFormat = loggedInUser?.timeFormat;
const timeFormat = user?.timeFormat;
const { t, i18n } = useLocale();
const weekStart = weekStartNum(loggedInUser?.weekStart);
const weekStart = weekStartNum(user?.weekStart);
const filterDays = (dayNum: number) =>
scheduleQueryData?.schedule.filter((item) => item.days.includes((dayNum + weekStart) % 7)) || [];
scheduleQueryData?.schedule?.filter((item) => item.days.includes((dayNum + weekStart) % 7)) || [];
return (
<div>
@@ -157,7 +165,6 @@ const EventTypeScheduleDetails = memo(
!scheduleQueryData.readOnly &&
!!editAvailabilityRedirectUrl && (
<Button
// href={`/availability/${scheduleQueryData.id}`}
href={editAvailabilityRedirectUrl}
disabled={isSchedulePending}
color="minimal"
@@ -177,8 +184,8 @@ EventTypeScheduleDetails.displayName = "EventTypeScheduleDetails";
const EventTypeSchedule = ({
eventType,
availabilityQueryData,
isAvailabilityPending,
schedulesQueryData,
isSchedulesPending,
...rest
}: EventTypeScheduleProps) => {
const { t } = useLocale();
@@ -191,25 +198,21 @@ const EventTypeSchedule = ({
useEffect(() => {
// after data is loaded.
if (availabilityQueryData && scheduleId !== 0 && !scheduleId) {
const newValue = isManagedEventType
? 0
: availabilityQueryData.schedules.find((schedule) => schedule.isDefault)?.id;
if (schedulesQueryData && scheduleId !== 0 && !scheduleId) {
const newValue = isManagedEventType ? 0 : schedulesQueryData.find((schedule) => schedule.isDefault)?.id;
if (!newValue && newValue !== 0) return;
setValue("schedule", newValue, {
shouldDirty: true,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [scheduleId, availabilityQueryData]);
}, [scheduleId, schedulesQueryData]);
if (isAvailabilityPending || !availabilityQueryData) {
if (isSchedulesPending || !schedulesQueryData) {
return <SelectSkeletonLoader />;
}
const schedules = availabilityQueryData.schedules;
const options = schedules.map((schedule) => ({
const options = schedulesQueryData.map((schedule) => ({
value: schedule.id,
label: schedule.name,
isDefault: schedule.isDefault,
@@ -227,7 +230,11 @@ const EventTypeSchedule = ({
}
// We are showing a managed event for a member and team owner selected their own schedule, so adding
// the managed schedule option
if (isChildrenManagedEventType && scheduleId && !schedules.find((schedule) => schedule.id === scheduleId)) {
if (
isChildrenManagedEventType &&
scheduleId &&
!schedulesQueryData.find((schedule) => schedule.id === scheduleId)
) {
options.push({
value: scheduleId,
label: eventType.scheduleName ?? t("default_schedule_name"),
@@ -236,7 +243,7 @@ const EventTypeSchedule = ({
});
}
// We push the selected schedule from the event type if it's not part of the list response. This happens if the user is an admin but not the schedule owner.
else if (eventType.schedule && !schedules.find((schedule) => schedule.id === eventType.schedule)) {
else if (eventType.schedule && !schedulesQueryData.find((schedule) => schedule.id === eventType.schedule)) {
options.push({
value: eventType.schedule,
label: eventType.scheduleName ?? t("default_schedule_name"),
@@ -411,10 +418,7 @@ const TeamAvailability = ({
);
};
const UseCommonScheduleSettingsToggle = ({
eventType,
...rest
}: Omit<EventAvailabilityTabProps, "isTeamEvent">) => {
const UseCommonScheduleSettingsToggle = ({ eventType, ...rest }: EventTypeScheduleProps) => {
const { t } = useLocale();
const { setValue, resetField, getFieldState, getValues, watch } = useFormContext<FormValues>();
@@ -31,6 +31,7 @@ export function transformApiScheduleForAtom(
scheduleId: schedule.id,
days: "days" in entry ? entry.days : [],
date: "date" in entry ? entry.date : null,
id: schedule.id,
};
});
@@ -0,0 +1,36 @@
import { useQuery } from "@tanstack/react-query";
import {
V2_ENDPOINTS,
SUCCESS_STATUS,
VERSION_2024_06_11,
CAL_API_VERSION_HEADER,
} from "@calcom/platform-constants";
import type { GetScheduleOutput_2024_06_11 } from "@calcom/platform-types";
import http from "../../lib/http";
export const QUERY_KEY = "event-type-user-schedule";
export const useSchedule = (id?: number) => {
const pathname = id ? `/${V2_ENDPOINTS.availability}/${id}` : `/${V2_ENDPOINTS.availability}/default`;
const { isLoading, error, data } = useQuery({
queryKey: [QUERY_KEY, id],
queryFn: () => {
return http
.get<GetScheduleOutput_2024_06_11>(pathname, {
headers: { [CAL_API_VERSION_HEADER]: VERSION_2024_06_11 },
})
.then((res) => {
if (res.data.status === SUCCESS_STATUS) {
return res.data.data;
}
throw new Error(res.data.error?.message);
});
},
enabled: !!id,
});
return { isLoading, error, data };
};
@@ -0,0 +1,35 @@
import { useQuery } from "@tanstack/react-query";
import {
V2_ENDPOINTS,
SUCCESS_STATUS,
VERSION_2024_06_11,
CAL_API_VERSION_HEADER,
} from "@calcom/platform-constants";
import type { GetSchedulesOutput_2024_06_11 } from "@calcom/platform-types";
import http from "../../lib/http";
export const QUERY_KEY = "event-type-user-schedules";
export const useSchedules = () => {
const pathname = `/${V2_ENDPOINTS.availability}`;
const { isLoading, error, data } = useQuery({
queryKey: [QUERY_KEY],
queryFn: () => {
return http
.get<GetSchedulesOutput_2024_06_11>(pathname, {
headers: { [CAL_API_VERSION_HEADER]: VERSION_2024_06_11 },
})
.then((res) => {
if (res.data.status === SUCCESS_STATUS) {
return res.data.data;
}
throw new Error(res.data.error?.message);
});
},
});
return { isLoading, error, data };
};
@@ -0,0 +1,66 @@
import { useFormContext } from "react-hook-form";
import { EventAvailabilityTab } from "@calcom/features/eventtypes/components/tabs/availability/EventAvailabilityTab";
import type { EventTypeSetup, FormValues } from "@calcom/features/eventtypes/lib/types";
import type { User } from "@calcom/prisma/client";
import type { Availability } from "../../availability/AvailabilitySettings";
import { transformApiScheduleForAtom } from "../../availability/atom-api-transformers/transformApiScheduleForAtom";
import { useSchedule } from "../hooks/useSchedule";
import { useSchedules } from "../hooks/useSchedules";
type EventAvailabilityTabPlatformWrapperProps = {
user?: Pick<User, "id" | "defaultScheduleId" | "timeZone" | "timeFormat" | "weekStart">;
eventType: EventTypeSetup;
isTeamEvent: boolean;
};
const EventAvailabilityTabPlatformWrapper = ({
user,
...props
}: EventAvailabilityTabPlatformWrapperProps) => {
const formMethods = useFormContext<FormValues>();
const scheduleId = formMethods.watch("schedule");
const { isLoading: isSchedulePending, data: scheduleQueryData } = useSchedule(
scheduleId || user?.defaultScheduleId || undefined
);
const { data: schedulesQueryData, isLoading: isSchedulesPending } = useSchedules();
const atomSchedule = transformApiScheduleForAtom(user, scheduleQueryData, schedulesQueryData?.length || 0);
if (!atomSchedule) {
return <></>;
}
return (
<EventAvailabilityTab
{...props}
user={user}
schedulesQueryData={schedulesQueryData}
isSchedulesPending={isSchedulesPending}
isSchedulePending={isSchedulePending}
scheduleQueryData={{
isManaged: atomSchedule.isManaged,
readOnly: atomSchedule.readOnly,
id: atomSchedule.id,
timeZone: atomSchedule.timeZone,
schedule:
atomSchedule.schedule.reduce(
(acc: Availability[], avail: Availability) => [
...acc,
{
startTime: new Date(avail.startTime),
endTime: new Date(avail.endTime),
days: avail.days,
},
],
[]
) || [],
}}
/>
);
};
export default EventAvailabilityTabPlatformWrapper;
@@ -11,7 +11,7 @@ import { trpc } from "@calcom/trpc/react";
export type EventAvailabilityTabWebWrapperProps = {
eventType: EventTypeSetup;
isTeamEvent: boolean;
loggedInUser?: RouterOutputs["viewer"]["me"];
user?: RouterOutputs["viewer"]["me"];
teamMembers: TeamMembers;
};
@@ -32,13 +32,13 @@ const EventAvailabilityTabWebWrapper = (props: EventAvailabilityTabWebWrapperPro
const { isPending: isSchedulePending, data: scheduleQueryData } =
trpc.viewer.availability.schedule.get.useQuery(
{
scheduleId: scheduleId || props.loggedInUser?.defaultScheduleId || undefined,
scheduleId: scheduleId || props.user?.defaultScheduleId || undefined,
isManagedEventType: isManagedEventType || isChildrenManagedEventType,
},
{ enabled: !!scheduleId || !!props.loggedInUser?.defaultScheduleId }
{ enabled: !!scheduleId || !!props.user?.defaultScheduleId }
);
const { data: availabilityQueryData, isPending: isAvailabilityPending } =
const { data: schedulesQueryData, isPending: isSchedulesPending } =
trpc.viewer.availability.list.useQuery(undefined);
const hostSchedulesQuery = trpc.viewer.availability.schedule.getAllSchedulesByUserId.useQuery;
@@ -46,8 +46,8 @@ const EventAvailabilityTabWebWrapper = (props: EventAvailabilityTabWebWrapperPro
return (
<EventAvailabilityTab
{...props}
availabilityQueryData={availabilityQueryData}
isAvailabilityPending={isAvailabilityPending}
schedulesQueryData={schedulesQueryData?.schedules}
isSchedulesPending={isSchedulesPending}
isSchedulePending={isSchedulePending}
scheduleQueryData={scheduleQueryData}
editAvailabilityRedirectUrl={`/availability/${scheduleQueryData?.id}`}
@@ -20,6 +20,7 @@ import { useEventTypeForm } from "../hooks/useEventTypeForm";
import { useHandleRouteChange } from "../hooks/useHandleRouteChange";
import { usePlatformTabsNavigations } from "../hooks/usePlatformTabsNavigations";
import EventAdvancedPlatformWrapper from "./EventAdvancedPlatformWrapper";
import EventAvailabilityTabPlatformWrapper from "./EventAvailabilityTabPlatformWrapper";
import EventLimitsTabPlatformWrapper from "./EventLimitsTabPlatformWrapper";
import EventRecurringTabPlatformWrapper from "./EventRecurringTabPlatformWrapper";
import SetupTab from "./EventSetupTabPlatformWrapper";
@@ -83,7 +84,7 @@ const EventType = ({
// Reset the form with these values as new default values to ensure the correct comparison for dirtyFields eval
form.reset(currentValues);
toast({ description: t("event_type_updated_successfully", { eventTypeTitle: eventType.title }) });
onSuccess?.(currentValues);
props.onSuccess?.(currentValues);
},
async onSettled() {
return;
@@ -92,7 +93,7 @@ const EventType = ({
const currentValues = form.getValues();
const message = err?.message;
toast({ description: message ? t(message) : t(err.message) });
onError?.(currentValues, err);
props.onError?.(currentValues, err);
},
});
@@ -115,7 +116,11 @@ const EventType = ({
) : (
<></>
),
availability: <></>,
availability: tabs.includes("availability") ? (
<EventAvailabilityTabPlatformWrapper eventType={eventType} isTeamEvent={!!team} user={user?.data} />
) : (
<></>
),
team: <></>,
advanced: tabs.includes("advanced") ? (
<EventAdvancedPlatformWrapper
@@ -160,7 +160,7 @@ const EventTypeWeb = ({
const { t } = useLocale();
const utils = trpc.useUtils();
const { data: loggedInUser, isPending: isLoggedInUserPending } = useMeQuery();
const { data: user, isPending: isLoggedInUserPending } = useMeQuery();
const isTeamEventTypeDeleted = useRef(false);
const leaveWithoutAssigningHosts = useRef(false);
const telemetry = useTelemetry();
@@ -245,7 +245,7 @@ const EventTypeWeb = ({
<EventAvailabilityTab
eventType={eventType}
isTeamEvent={!!team}
loggedInUser={loggedInUser}
user={user}
teamMembers={teamMembers}
/>
),
@@ -255,7 +255,7 @@ const EventTypeWeb = ({
<EventAdvancedTab
eventType={eventType}
team={team}
user={loggedInUser}
user={user}
isUserLoading={isLoggedInUserPending}
showToast={showToast}
/>
@@ -91,7 +91,7 @@ export default function Bookings(props: { calUsername: string; calEmail: string
<EventTypeSettings
allowDelete={true}
id={eventTypeId}
tabs={["setup", "limits", "recurring", "advanced"]}
tabs={["setup", "limits", "recurring", "advanced", "availability"]}
onSuccess={(eventType) => {
console.log(eventType);
setEventTypeId(null);