From 33e8b5d22746978553ded18d92e222ba46ca1d88 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Thu, 15 Sep 2022 06:49:59 +0100 Subject: [PATCH] Availability / Single View (#4467) * Revamped Schedule before type fixes * Removed t('copy_all'); fake focus state on CopyButton open * Added skeletons, availability single view (wip, still a bit glitchy) * Fixed isDefault switch glitchiness * Fixes changing the default schedule to the created schedule * Fix some type errors, on create set availability to DEFAULT_SCHEDULE * Provided missing translation for copy_times_to * Fix type errors, this may not be possible until v8 RHF * Fix lint error * Some responsiveness fixes Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- .../steps-views/SetupAvailability.tsx | 17 +- apps/web/pages/v2/availability/[schedule].tsx | 243 +++++----- apps/web/public/static/locales/en/common.json | 3 +- .../schedules/components/Schedule.tsx | 450 +++++++++--------- .../server/routers/viewer/availability.tsx | 28 +- 5 files changed, 372 insertions(+), 369 deletions(-) diff --git a/apps/web/components/getting-started/steps-views/SetupAvailability.tsx b/apps/web/components/getting-started/steps-views/SetupAvailability.tsx index 4d22a3e74d..d916aa68ed 100644 --- a/apps/web/components/getting-started/steps-views/SetupAvailability.tsx +++ b/apps/web/components/getting-started/steps-views/SetupAvailability.tsx @@ -3,23 +3,16 @@ import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { Schedule } from "@calcom/features/schedules"; +import { DEFAULT_SCHEDULE } from "@calcom/lib/availability"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc, TRPCClientErrorLike } from "@calcom/trpc/react"; import { AppRouter } from "@calcom/trpc/server/routers/_app"; import { Form } from "@calcom/ui/form/fields"; import { Button } from "@calcom/ui/v2"; -import { DEFAULT_SCHEDULE } from "@lib/availability"; -import type { Schedule as ScheduleType } from "@lib/types/schedule"; - interface ISetupAvailabilityProps { nextStep: () => void; defaultScheduleId?: number | null; - defaultAvailability?: { schedule?: TimeRanges[][] }; -} - -interface ScheduleFormValues { - schedule: ScheduleType; } const SetupAvailability = (props: ISetupAvailabilityProps) => { @@ -37,7 +30,9 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => { } const availabilityForm = useForm({ - defaultValues: { schedule: queryAvailability?.data?.availability || DEFAULT_SCHEDULE }, + defaultValues: { + schedule: queryAvailability?.data?.availability || DEFAULT_SCHEDULE, + }, }); const mutationOptions = { @@ -51,7 +46,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => { const createSchedule = trpc.useMutation("viewer.availability.schedule.create", mutationOptions); const updateSchedule = trpc.useMutation("viewer.availability.schedule.update", mutationOptions); return ( - +
{ @@ -75,7 +70,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => { } } }}> - +
-
- -
- ( - - )} - /> - -
-
- -
- ( - onChange(timezone.value)} - /> - )} - /> -
-
-
- setValue("name", name)} /> + } + subtitle={data?.schedule.availability.map((availability) => ( + + {availabilityAsString(availability, { locale: i18n.language })} +
+
+ ))} + CTA={ +
+
+ + { + form.setValue("isDefault", e); + }} />
-
-
-

{t("something_doesnt_look_right")}

-
- + + + +
+ +
+ }> +
+ {/* TODO: Find a better way to guarantee alignment, but for now this'll do. */} + +
+ { + updateMutation.mutate({ + scheduleId: schedule, + ...values, + }); + }} + className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6"> +
+
+

+ {t("change_start_end")} +

+ +
-
+
+
+
+ + + value ? ( + onChange(timezone.value)} + /> + ) : ( + + ) + } + /> +
+
+
+

{t("something_doesnt_look_right")}

+
+ +
+
+
+
+
- - ); -} - -const querySchema = z.object({ - schedule: stringOrNumber, -}); - -export default function Availability() { - const router = useRouter(); - const { i18n } = useLocale(); - const { schedule: scheduleId } = router.isReady ? querySchema.parse(router.query) : { schedule: -1 }; - const query = trpc.useQuery(["viewer.availability.schedule", { scheduleId }], { enabled: router.isReady }); - const [name, setName] = useState(); - return ( -
- { - return ( - } - subtitle={data.schedule.availability.map((availability) => ( - - {availabilityAsString(availability, { locale: i18n.language })} -
-
- ))}> - -
- ); - }} - /> -
+ ); } diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index c815ff940f..af2e6d4be0 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -631,8 +631,10 @@ "billing": "Billing", "manage_your_billing_info": "Manage your billing information and cancel your subscription.", "availability": "Availability", + "availability_title": "{{availabilityTitle}} | Availability", "edit_availability": "Edit availability", "configure_availability": "Configure times when you are available for bookings.", + "copy_times_to": "Copy times to", "change_weekly_schedule": "Change your weekly schedule", "logo": "Logo", "error": "Error", @@ -1150,7 +1152,6 @@ "set_availability_getting_started_subtitle_2": "You can customise all of this later in the availability page.", "connect_calendars_from_app_store": "You can add more calendars from the app store", "current_step_of_total": "Step {{currentStep}} of {{maxSteps}}", - "copy_all": "Copy All", "add_variable": "Add variable", "custom_phone_number": "Custom phone number", "message_template": "Message template", diff --git a/packages/features/schedules/components/Schedule.tsx b/packages/features/schedules/components/Schedule.tsx index f0a92761b8..685ee9ecf5 100644 --- a/packages/features/schedules/components/Schedule.tsx +++ b/packages/features/schedules/components/Schedule.tsx @@ -1,113 +1,194 @@ -import classNames from "classnames"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { - Controller, - useFieldArray, - UseFieldArrayAppend, +import { useCallback, useEffect, useMemo, useState, Fragment } from "react"; +import { Controller, useFieldArray, useFormContext } from "react-hook-form"; +import type { UseFieldArrayRemove, - useFormContext, + FieldValues, + FieldPath, + FieldPathValue, + FieldArrayWithId, + ArrayPath, + ControllerRenderProps, + Control, } from "react-hook-form"; import { GroupBase, Props } from "react-select"; import dayjs, { ConfigType, Dayjs } from "@calcom/dayjs"; import { defaultDayRange as DEFAULT_DAY_RANGE } from "@calcom/lib/availability"; +import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { weekdayNames } from "@calcom/lib/weekday"; import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery"; import { TimeRange } from "@calcom/types/schedule"; import { Icon } from "@calcom/ui"; import Dropdown, { DropdownMenuContent, DropdownMenuTrigger } from "@calcom/ui/Dropdown"; -import { Button, Select, Switch, Tooltip } from "@calcom/ui/v2"; +import { Select, Switch } from "@calcom/ui/v2"; +import Button from "@calcom/ui/v2/core/Button"; +import { SkeletonText } from "@calcom/ui/v2/core/skeleton"; -const Schedule = () => { - const { i18n } = useLocale(); - const form = useFormContext(); +export type FieldPathByValue = { + [Key in FieldPath]: FieldPathValue extends TValue ? Key : never; +}[FieldPath]; - const initialValue = form.watch(); +const ScheduleDay = ({ + name, + weekday, + control, + CopyButton, +}: { + name: string; + weekday: string; + control: Control; + CopyButton: JSX.Element; +}) => { + const { watch, setValue } = useFormContext(); + const watchDayRange = watch(name); - const copyAllPosition = (initialValue["schedule"] as Array)?.findIndex( - (item: TimeRange[]) => item.length > 0 + return ( +
+ {/* Label & switch container */} +
+
+ +
+
+ <> + {watchDayRange ? ( +
+ + {!!watchDayRange.length &&
{CopyButton}
} +
+ ) : ( + + )} + +
+
); +}; + +const CopyButton = ({ + getValuesFromDayRange, + weekStart, +}: { + getValuesFromDayRange: string; + weekStart: number; +}) => { + const { t } = useLocale(); + const [open, setOpen] = useState(false); + const fieldArrayName = getValuesFromDayRange.substring(0, getValuesFromDayRange.lastIndexOf(".")); + const { setValue, getValues } = useFormContext(); + return ( + + +
- )} - {index !== 0 && } -
+ )} + {index !== 0 && } +
+ ))} - +
); }; @@ -132,48 +213,26 @@ const RemoveTimeButton = ({ ); }; -interface TimeRangeFieldProps { - name: string; - className?: string; -} - -const TimeRangeField = ({ name, className }: TimeRangeFieldProps) => { - const { watch } = useFormContext(); - - const values = watch(name); - const minEnd = values["start"]; - const maxStart = values["end"]; - +const TimeRangeField = ({ className, value, onChange }: { className?: string } & ControllerRenderProps) => { + // this is a controlled component anyway given it uses LazySelect, so keep it RHF agnostic. return ( -
- { - return ( - { - onChange(new Date(option?.value as number)); - }} - /> - ); +
+ { + onChange({ ...value, start: new Date(option?.value as number) }); }} /> - - ( - { - onChange(new Date(option?.value as number)); - }} - /> - )} + { + onChange({ ...value, end: new Date(option?.value as number) }); + }} />
); @@ -266,136 +325,69 @@ const useOptions = () => { return { options: filteredOptions, filter }; }; -const ActionButtons = ({ - name, - watcher, - setValue, - copyAllShouldRender, -}: { - name: string; - watcher: TimeRange[]; - setValue: (key: string, value: TimeRange[]) => void; - copyAllShouldRender?: boolean; -}) => { - const { t } = useLocale(); - const form = useFormContext(); - - const values = form.watch(); - const { append } = useFieldArray({ - name, - }); - - return ( -
- - - - )} -
- ); -}; - -const handleAppend = ({ - fields = [], - append, -}: { - fields: TimeRange[]; - append: UseFieldArrayAppend; -}) => { - if (fields.length === 0) { - return append(DEFAULT_DAY_RANGE); - } - const nextRangeStart = dayjs((fields[fields.length - 1] as unknown as TimeRange).end); +const getNextRange = (field?: FieldArrayWithId) => { + const nextRangeStart = dayjs((field as unknown as TimeRange).end); const nextRangeEnd = dayjs(nextRangeStart).add(1, "hour"); if (nextRangeEnd.isBefore(nextRangeStart.endOf("day"))) { - return append({ + return { start: nextRangeStart.toDate(), end: nextRangeEnd.toDate(), - }); + }; } }; -const CopyTimes = ({ disabled, onApply }: { disabled: number[]; onApply: (selected: number[]) => void }) => { +const CopyTimes = ({ + disabled, + onClick, + onCancel, + weekStart, +}: { + disabled: number; + onClick: (selected: number[]) => void; + onCancel: () => void; + weekStart: number; +}) => { const [selected, setSelected] = useState([]); const { i18n, t } = useLocale(); return ( -
-

Copy times to

-
    - {weekdayNames(i18n.language).map((weekday, num) => ( -
  1. - -
  2. - ))} -
-
- +
diff --git a/packages/trpc/server/routers/viewer/availability.tsx b/packages/trpc/server/routers/viewer/availability.tsx index 403888affe..5c39d3e919 100644 --- a/packages/trpc/server/routers/viewer/availability.tsx +++ b/packages/trpc/server/routers/viewer/availability.tsx @@ -2,7 +2,7 @@ import { Availability as AvailabilityModel, Prisma, Schedule as ScheduleModel, U import { z } from "zod"; import { getUserAvailability } from "@calcom/core/getUserAvailability"; -import { getAvailabilityFromSchedule } from "@calcom/lib/availability"; +import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; import { PrismaClient } from "@calcom/prisma/client"; import { stringOrNumber } from "@calcom/prisma/zod-utils"; import { Schedule } from "@calcom/types/schedule"; @@ -114,24 +114,22 @@ export const availabilityRouter = createProtectedRouter() }, }; - if (input.schedule) { - const availability = getAvailabilityFromSchedule(input.schedule); - data.availability = { - createMany: { - data: availability.map((schedule) => ({ - days: schedule.days, - startTime: schedule.startTime, - endTime: schedule.endTime, - })), - }, - }; - } + const availability = getAvailabilityFromSchedule(input.schedule || DEFAULT_SCHEDULE); + data.availability = { + createMany: { + data: availability.map((schedule) => ({ + days: schedule.days, + startTime: schedule.startTime, + endTime: schedule.endTime, + })), + }, + }; + const schedule = await prisma.schedule.create({ data, }); const hasDefaultScheduleId = await hasDefaultSchedule(user, prisma); - - if (hasDefaultScheduleId) { + if (!hasDefaultScheduleId) { await setupDefaultSchedule(user.id, schedule.id, prisma); }