diff --git a/apps/web/components/availability/Schedule.tsx b/apps/web/components/availability/Schedule.tsx index 05633a59..3e2fc28e 100644 --- a/apps/web/components/availability/Schedule.tsx +++ b/apps/web/components/availability/Schedule.tsx @@ -1,12 +1,16 @@ import { PlusIcon, TrashIcon } from "@heroicons/react/outline"; +import { DuplicateIcon } from "@heroicons/react/solid"; +import classNames from "classnames"; import dayjs, { Dayjs, ConfigType } from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; -import React, { useCallback, useState } from "react"; -import { Controller, useFieldArray } from "react-hook-form"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Controller, useFieldArray, useFormContext } from "react-hook-form"; +import { GroupBase, Props, SingleValue } from "react-select"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import Button from "@calcom/ui/Button"; +import Dropdown, { DropdownMenuTrigger, DropdownMenuContent } from "@calcom/ui/Dropdown"; import { defaultDayRange } from "@lib/availability"; import { weekdayNames } from "@lib/core/i18n/weekday"; @@ -20,84 +24,111 @@ dayjs.extend(timezone); /** Begin Time Increments For Select */ const increment = 15; -/** - * Creates an array of times on a 15 minute interval from - * 00:00:00 (Start of day) to - * 23:45:00 (End of day with enough time for 15 min booking) - */ -const TIMES = (() => { - const end = dayjs().utc().endOf("day"); - let t: Dayjs = dayjs().utc().startOf("day"); - - const times: Dayjs[] = []; - while (t.isBefore(end)) { - times.push(t); - t = t.add(increment, "minutes"); - } - return times; -})(); -/** End Time Increments For Select */ type Option = { readonly label: string; readonly value: number; }; -type TimeRangeFieldProps = { - name: string; -}; - -const TimeRangeField = ({ name }: TimeRangeFieldProps) => { +/** + * Creates an array of times on a 15 minute interval from + * 00:00:00 (Start of day) to + * 23:45:00 (End of day with enough time for 15 min booking) + */ +const useOptions = () => { // Get user so we can determine 12/24 hour format preferences const query = useMeQuery(); - const user = query.data; + const { timeFormat } = query.data || { timeFormat: null }; - // Lazy-loaded options, otherwise adding a field has a noticable redraw delay. - const [options, setOptions] = useState([]); - const [selected, setSelected] = useState(); - // const { i18n } = useLocale(); + const [filteredOptions, setFilteredOptions] = useState([]); - const handleSelected = (value: number | undefined) => { - setSelected(value); - }; + const options = useMemo(() => { + const end = dayjs().utc().endOf("day"); + let t: Dayjs = dayjs().utc().startOf("day"); - const getOption = (time: ConfigType) => ({ - value: dayjs(time).toDate().valueOf(), - label: dayjs(time) - .utc() - .format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm"), - // .toLocaleTimeString(i18n.language, { minute: "numeric", hour: "numeric" }), - }); + const options: Option[] = []; + while (t.isBefore(end)) { + options.push({ + value: t.toDate().valueOf(), + label: dayjs(t) + .utc() + .format(timeFormat === 12 ? "h:mma" : "HH:mm"), + }); + t = t.add(increment, "minutes"); + } + return options; + }, []); - const timeOptions = useCallback( - (offsetOrLimitorSelected: { offset?: number; limit?: number; selected?: number } = {}) => { - const { limit, offset, selected } = offsetOrLimitorSelected; - return TIMES.filter( - (time) => - (!limit || time.isBefore(limit)) && - (!offset || time.isAfter(offset)) && - (!selected || time.isAfter(selected)) - ).map((t) => getOption(t)); + const filter = useCallback( + ({ offset, limit, current }: { offset?: ConfigType; limit?: ConfigType; current?: ConfigType }) => { + if (current) { + setFilteredOptions([options.find((option) => option.value === dayjs(current).toDate().valueOf())!]); + } else + setFilteredOptions( + options.filter((option) => { + const time = dayjs(option.value); + return (!limit || time.isBefore(limit)) && (!offset || time.isAfter(offset)); + }) + ); }, - [] + [options] ); + return { options: filteredOptions, filter }; +}; + +type TimeRangeFieldProps = { + name: string; + className?: string; +}; + +const LazySelect = ({ + value, + min, + max, + ...props +}: Omit>, "value"> & { + value: ConfigType; + min?: ConfigType; + max?: ConfigType; +}) => { + // Lazy-loaded options, otherwise adding a field has a noticable redraw delay. + const { options, filter } = useOptions(); + + useEffect(() => { + filter({ current: value }); + }, [filter, value]); + return ( - <> + setOptions(timeOptions())} - onBlur={() => setOptions([])} - defaultValue={getOption(value)} + { onChange(new Date(option?.value as number)); - handleSelected(option?.value); }} /> ); @@ -107,17 +138,17 @@ const TimeRangeField = ({ name }: TimeRangeFieldProps) => { ( - { + if (e.target.checked && !selected.includes(num)) { + setSelected(selected.concat([num])); + } else if (!e.target.checked && selected.includes(num)) { + setSelected(selected.slice(selected.indexOf(num), 1)); + } + }} + type="checkbox" + className="inline-block rounded-sm border-gray-300 text-neutral-900 focus:ring-neutral-500 disabled:text-neutral-400" + /> + + + ))} + +
+ +
+ + ); +}; + +export const DayRanges = ({ + name, + defaultValue = [defaultDayRange], +}: { + name: string; + defaultValue?: TimeRange[]; +}) => { + const { setValue, watch } = useFormContext(); + // XXX: Hack to make copying times work; `fields` is out of date until save. + const watcher = watch(name); + + const { fields, replace, append, remove } = useFieldArray({ + name, }); + useEffect(() => { + if (defaultValue.length && !fields.length) { + replace(defaultValue); + } + }, [replace, defaultValue, fields.length]); + const handleAppend = () => { // FIXME: Fix type-inference, can't get this to work. @see https://github.com/react-hook-form/react-hook-form/issues/4499 const nextRangeStart = dayjs((fields[fields.length - 1] as unknown as TimeRange).end); @@ -147,47 +231,95 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => { }; return ( -
-
- -
-
- {fields.map((field, index) => ( -
-
- -
+
+ {fields.map((field, index) => ( +
+
+
- ))} - {!fields.length && t("no_availability")} -
-
-
+ {index === 0 && ( +
+
+ )} +
+ ))} +
+ ); +}; + +const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => { + const { t } = useLocale(); + + const form = useFormContext(); + const watchAvailable = form.watch(`${name}.${day}`, []); + + return ( +
+ + {!!watchAvailable.length && ( +
+ +
+ )}
); }; diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 323d302d..7ade26b4 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -445,6 +445,7 @@ "danger_zone": "Danger Zone", "back": "Back", "cancel": "Cancel", + "apply": "Apply", "cancel_event": "Cancel this event", "continue": "Continue", "confirm": "Confirm",