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:
co-authored by
Morgan Vernay
Morgan
parent
123074f4ba
commit
e90e060739
@@ -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");
|
||||
|
||||
|
||||
+2
-8
@@ -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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
+35
-31
@@ -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>();
|
||||
|
||||
|
||||
+1
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user