From e5b6a3332e3a367a7099ce47bc12bb047d40a2ec Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Fri, 6 Jan 2023 20:05:23 +0000 Subject: [PATCH] Feature/new design radio area group (#6289) * Refactor of the RadioAreaGroup component * Move RadioAreaGroup outside of v2/core * Use useTypedQuery * Removed unused (broken) imports * Fix type for Select --- .../ui/form/radio-area/RadioAreaGroup.tsx | 79 ---- .../components/ui/form/radio-area/Select.tsx | 61 --- .../components/ui/form/radio-area/index.ts | 2 - .../components/CreateEventTypeButton.tsx | 351 +++++++++--------- .../{v2/core => }/form/radio-area/Radio.tsx | 0 .../ui/form/radio-area/RadioAreaGroup.tsx | 53 +++ .../{v2/core => }/form/radio-area/Select.tsx | 18 +- .../ui/{v2/core => }/form/radio-area/index.ts | 0 packages/ui/index.tsx | 9 +- packages/ui/v2/core/Dialog.tsx | 11 +- packages/ui/v2/core/form/index.ts | 7 - .../core/form/radio-area/RadioAreaGroup.tsx | 79 ---- packages/ui/v2/core/index.ts | 2 +- 13 files changed, 260 insertions(+), 412 deletions(-) delete mode 100644 apps/web/components/ui/form/radio-area/RadioAreaGroup.tsx delete mode 100644 apps/web/components/ui/form/radio-area/Select.tsx delete mode 100644 apps/web/components/ui/form/radio-area/index.ts rename packages/ui/{v2/core => }/form/radio-area/Radio.tsx (100%) create mode 100644 packages/ui/form/radio-area/RadioAreaGroup.tsx rename packages/ui/{v2/core => }/form/radio-area/Select.tsx (75%) rename packages/ui/{v2/core => }/form/radio-area/index.ts (100%) delete mode 100644 packages/ui/v2/core/form/radio-area/RadioAreaGroup.tsx diff --git a/apps/web/components/ui/form/radio-area/RadioAreaGroup.tsx b/apps/web/components/ui/form/radio-area/RadioAreaGroup.tsx deleted file mode 100644 index 72b99df551..0000000000 --- a/apps/web/components/ui/form/radio-area/RadioAreaGroup.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { ReactNode, useState } from "react"; - -import classNames from "@lib/classNames"; - -type RadioAreaProps = Omit, "onChange"> & { - onChange?: (value: string) => void; - defaultChecked?: boolean; -}; - -const RadioArea = (props: RadioAreaProps) => { - return ( - - ); -}; - -type ChildrenProps = { - props: RadioAreaProps; - children?: ReactNode; -}; -interface RadioAreaGroupProps extends Omit, "onChange" | "children"> { - children: ChildrenProps | ChildrenProps[]; - name?: string; - onChange?: (value: string) => void; -} - -const RadioAreaGroup = ({ children, name, onChange, ...passThroughProps }: RadioAreaGroupProps) => { - const [checkedIdx, setCheckedIdx] = useState(null); - - const changeHandler = (value: string, idx: number) => { - if (onChange) { - onChange(value); - } - setCheckedIdx(idx); - }; - - return ( -
- {(Array.isArray(children) ? children : [children]).map((child, idx: number) => { - if (checkedIdx === null && child.props.defaultChecked) { - setCheckedIdx(idx); - } - return ( - changeHandler(value, idx)}> - {child.props.children} - - ); - })} -
- ); -}; - -const Item = RadioArea; -const Group = RadioAreaGroup; - -export { RadioArea, RadioAreaGroup, Item, Group }; diff --git a/apps/web/components/ui/form/radio-area/Select.tsx b/apps/web/components/ui/form/radio-area/Select.tsx deleted file mode 100644 index c205ddad05..0000000000 --- a/apps/web/components/ui/form/radio-area/Select.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; -import React from "react"; - -import { Icon } from "@calcom/ui"; - -import classNames from "@lib/classNames"; -import { useLocale } from "@lib/hooks/useLocale"; - -import { RadioArea, RadioAreaGroup } from "@components/ui/form/radio-area/RadioAreaGroup"; - -interface OptionProps - extends Pick, "value" | "label" | "className"> { - description?: string; -} - -interface RadioAreaSelectProps extends Omit, "onChange"> { - options: OptionProps[]; // allow options to be passed programmatically, like options={} - onChange?: (value: string) => void; -} - -export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) { - const { t } = useLocale(); - const { - options, - disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway - placeholder = t("select"), - } = props; - - const getLabel = (value: string | ReadonlyArray | number | undefined) => - options.find((option: OptionProps) => option.value === value)?.label; - - return ( - - - {getLabel(props.value) ?? placeholder} - - - - - {options.map((option) => ( - - {option.label} -

{option.description}

-
- ))} -
-
-
- ); -}; - -export default Select; diff --git a/apps/web/components/ui/form/radio-area/index.ts b/apps/web/components/ui/form/radio-area/index.ts deleted file mode 100644 index 5e1c4577ec..0000000000 --- a/apps/web/components/ui/form/radio-area/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./RadioAreaGroup"; -export * from "./Select"; diff --git a/packages/features/eventtypes/components/CreateEventTypeButton.tsx b/packages/features/eventtypes/components/CreateEventTypeButton.tsx index cce1f15a9e..3ce476bc5c 100644 --- a/packages/features/eventtypes/components/CreateEventTypeButton.tsx +++ b/packages/features/eventtypes/components/CreateEventTypeButton.tsx @@ -1,14 +1,15 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { SchedulingType } from "@prisma/client"; +import { isValidPhoneNumber } from "libphonenumber-js"; import { useSession } from "next-auth/react"; import { useRouter } from "next/router"; -import { useEffect } from "react"; import { useForm } from "react-hook-form"; -import type { z } from "zod"; +import { z } from "zod"; import classNames from "@calcom/lib/classNames"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery"; import { HttpError } from "@calcom/lib/http-error"; import slugify from "@calcom/lib/slugify"; import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype"; @@ -53,58 +54,50 @@ interface CreateEventTypeBtnProps { options: EventTypeParent[]; } -const isValidJSONString = (str: string) => { - try { - JSON.parse(str); - } catch (e) { - return false; - } - return true; -}; +const locationFormSchema = z.array( + z.object({ + locationType: z.string(), + locationAddress: z.string().optional(), + displayLocationPublicly: z.boolean().optional(), + locationPhoneNumber: z + .string() + .refine((val) => isValidPhoneNumber(val)) + .optional(), + locationLink: z.string().url().optional(), // URL validates as new URL() - which requires HTTPS:// In the input field + }) +); -export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) { +const querySchema = z.object({ + eventPage: z.string(), + teamId: z.union([z.string().transform((val) => +val), z.number()]).optional(), + title: z.string().optional(), + slug: z.string().optional(), + length: z + .union([z.string().transform((val) => +val), z.number()]) + .optional() + .default(15), + description: z.string().optional(), + schedulingType: z.nativeEnum(SchedulingType).optional(), + locations: z + .string() + .transform((jsonString) => locationFormSchema.parse(JSON.parse(jsonString))) + .optional(), +}); + +const CreateEventTypeDialog = () => { const { t } = useLocale(); const router = useRouter(); - // URL encoded params - const teamId: number | undefined = - typeof router.query.teamId === "string" && router.query.teamId - ? parseInt(router.query.teamId) - : undefined; - const pageSlug = router.query.eventPage || props.options[0].slug; - const hasTeams = !!props.options.find((option) => option.teamId); - const type: string = typeof router.query.type == "string" && router.query.type ? router.query.type : ""; + const { + data: { teamId, eventPage: pageSlug, ...defaultValues }, + } = useTypedQuery(querySchema); const form = useForm>({ resolver: zodResolver(createEventTypeInput), + defaultValues, }); - const { setValue, register } = form; - useEffect(() => { - if (!router.isReady) return; - - const title: string = - typeof router.query.title === "string" && router.query.title ? router.query.title : ""; - const length: number = - typeof router.query.length === "string" && router.query.length ? parseInt(router.query.length) : 15; - const description: string = - typeof router.query.description === "string" && router.query.description - ? router.query.description - : ""; - const slug: string = typeof router.query.slug === "string" && router.query.slug ? router.query.slug : ""; - const locations = - typeof router.query.locations === "string" && - isValidJSONString(decodeURIComponent(router.query.locations)) - ? JSON.parse(decodeURIComponent(router.query.locations)) - : []; - - setValue("locations", locations); - setValue("title", title); - setValue("length", length); - setValue("description", description); - setValue("slug", slug); - // If query params change, update the form - }, [router.isReady, router.query, setValue]); + const { register } = form; const createMutation = trpc.viewer.eventTypes.create.useMutation({ onSuccess: async ({ eventType }) => { @@ -129,6 +122,142 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) { }, }); + return ( + + +
{ + createMutation.mutate(values); + }}> +
+ {teamId && ( + + )} + { + form.setValue("title", e?.target.value); + if (form.formState.touchedFields["slug"] === undefined) { + form.setValue("slug", slugify(e?.target.value)); + } + }} + /> + + {process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined && + process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? ( + /{pageSlug}/} + {...register("slug")} + onChange={(e) => { + form.setValue("slug", slugify(e?.target.value), { shouldTouch: true }); + }} + /> + ) : ( + + {process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/ + + } + {...register("slug")} + /> + )} + + + +
+ +
+ + {teamId && ( +
+ + {form.formState.errors.schedulingType && ( + + )} + + + {t("collective")} +

{t("collective_description")}

+
+ + {t("round_robin")} +

{t("round_robin_description")}

+
+
+
+ )} +
+
+ + +
+
+
+
+ ); +}; + +export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) { + const { t } = useLocale(); + const router = useRouter(); + + const hasTeams = !!props.options.find((option) => option.teamId); + // inject selection data into url for correct router history const openModal = (option: EventTypeParent) => { const query = { @@ -195,137 +324,7 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) { )} {/* Dialog for duplicate event type */} {router.query.dialog === "duplicate-event-type" && } - {router.query.dialog === "new-eventtype" && ( - - -
{ - createMutation.mutate(values); - }}> -
- {teamId && ( - - )} - { - form.setValue("title", e?.target.value); - if (form.formState.touchedFields["slug"] === undefined) { - form.setValue("slug", slugify(e?.target.value)); - } - }} - /> - - {process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined && - process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? ( - /{pageSlug}/} - {...register("slug")} - onChange={(e) => { - form.setValue("slug", slugify(e?.target.value), { shouldTouch: true }); - }} - /> - ) : ( - - {process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/ - - } - {...register("slug")} - /> - )} - - - -
- -
- - {teamId && ( -
- - {form.formState.errors.schedulingType && ( - - )} - form.setValue("schedulingType", val as SchedulingType)} - className="relative mt-1 flex space-x-6 rounded-sm rtl:space-x-reverse"> - - {t("collective")} -

{t("collective_description")}

-
- - {t("round_robin")} -

{t("round_robin_description")}

-
-
-
- )} -
-
- - -
-
-
-
- )} + {router.query.dialog === "new-eventtype" && } ); } diff --git a/packages/ui/v2/core/form/radio-area/Radio.tsx b/packages/ui/form/radio-area/Radio.tsx similarity index 100% rename from packages/ui/v2/core/form/radio-area/Radio.tsx rename to packages/ui/form/radio-area/Radio.tsx diff --git a/packages/ui/form/radio-area/RadioAreaGroup.tsx b/packages/ui/form/radio-area/RadioAreaGroup.tsx new file mode 100644 index 0000000000..9f241a646b --- /dev/null +++ b/packages/ui/form/radio-area/RadioAreaGroup.tsx @@ -0,0 +1,53 @@ +import React from "react"; + +import classNames from "@calcom/lib/classNames"; + +type RadioAreaProps = React.InputHTMLAttributes; + +const RadioArea = React.forwardRef( + ({ children, className, ...props }, ref) => { + return ( + + ); + } +); + +interface RadioAreaGroupProps extends Omit, "onChange"> { + onChange?: (value: string) => void; +} + +const RadioAreaGroup = ({ children, className, onChange, ...passThroughProps }: RadioAreaGroupProps) => { + const childrenWithProps = React.Children.map(children, (child) => { + if (onChange && React.isValidElement(child)) { + return React.cloneElement(child, { + onChange: (e: React.ChangeEvent) => { + onChange(e.target.value); + }, + }); + } + return child; + }); + return ( +
+ {childrenWithProps} +
+ ); +}; + +RadioAreaGroup.displayName = "RadioAreaGroup"; +RadioArea.displayName = "RadioArea"; + +const Item = RadioArea; +const Group = RadioAreaGroup; + +export { RadioArea, RadioAreaGroup, Item, Group }; diff --git a/packages/ui/v2/core/form/radio-area/Select.tsx b/packages/ui/form/radio-area/Select.tsx similarity index 75% rename from packages/ui/v2/core/form/radio-area/Select.tsx rename to packages/ui/form/radio-area/Select.tsx index 90b8035f80..06a2547d60 100644 --- a/packages/ui/v2/core/form/radio-area/Select.tsx +++ b/packages/ui/form/radio-area/Select.tsx @@ -1,6 +1,7 @@ import { ChevronDownIcon } from "@heroicons/react/solid"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible"; import React from "react"; +import { FieldValues, Path, UseFormReturn } from "react-hook-form"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -12,15 +13,22 @@ interface OptionProps description?: string; } -interface RadioAreaSelectProps extends Omit, "onChange"> { +export type FieldPath = Path; +interface RadioAreaSelectProps + extends Omit, "onChange" | "form"> { options: OptionProps[]; // allow options to be passed programmatically, like options={} onChange?: (value: string) => void; + form: UseFormReturn; + name: FieldPath; } -export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) { +export const Select = function RadioAreaSelect( + props: RadioAreaSelectProps +) { const { t } = useLocale(); const { options, + form, disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway placeholder = t("select"), } = props; @@ -41,12 +49,12 @@ export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) { - + {options.map((option) => ( + key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}> {option.label}

{option.description}

diff --git a/packages/ui/v2/core/form/radio-area/index.ts b/packages/ui/form/radio-area/index.ts similarity index 100% rename from packages/ui/v2/core/form/radio-area/index.ts rename to packages/ui/form/radio-area/index.ts diff --git a/packages/ui/index.tsx b/packages/ui/index.tsx index 31861b6ef7..51af1629be 100644 --- a/packages/ui/index.tsx +++ b/packages/ui/index.tsx @@ -114,7 +114,14 @@ export { DropdownMenuSeparator, DropdownMenuTrigger, } from "./v2/core/Dropdown"; -export { RadioGroup, Radio, Group, RadioField } from "./v2/core/form"; +export { + RadioGroup, + /* TODO: solve this conflict -> Select, */ + Radio, + Group, + RadioField, +} from "./form/radio-area"; + export { BooleanToggleGroupField } from "./v2/core/form/BooleanToggleGroup"; export { DateRangePickerLazy as DateRangePicker } from "./v2/core/form/date-range-picker"; export { default as DatePickerField } from "./v2/core/form/DatePicker"; diff --git a/packages/ui/v2/core/Dialog.tsx b/packages/ui/v2/core/Dialog.tsx index 112b7dbefa..0e39159929 100644 --- a/packages/ui/v2/core/Dialog.tsx +++ b/packages/ui/v2/core/Dialog.tsx @@ -28,9 +28,18 @@ export function Dialog(props: DialogProps) { if (open) { router.query["dialog"] = name; } else { + const query = router.query; clearQueryParamsOnClose.forEach((queryParam) => { - delete router.query[queryParam]; + delete query[queryParam]; }); + router.push( + { + pathname: router.pathname, + query, + }, + undefined, + { shallow: true } + ); } setOpen(open); }; diff --git a/packages/ui/v2/core/form/index.ts b/packages/ui/v2/core/form/index.ts index bc5dd19320..7fd8bf7488 100644 --- a/packages/ui/v2/core/form/index.ts +++ b/packages/ui/v2/core/form/index.ts @@ -1,10 +1,3 @@ -export { - RadioGroup, - /* TODO: solve this conflict -> Select, */ - Radio, - Group, - RadioField, -} from "./radio-area"; export { default as Checkbox } from "../../../components/form/checkbox/Checkbox"; export { default as DatePicker } from "./DatePicker"; diff --git a/packages/ui/v2/core/form/radio-area/RadioAreaGroup.tsx b/packages/ui/v2/core/form/radio-area/RadioAreaGroup.tsx deleted file mode 100644 index 7aa20d2a7b..0000000000 --- a/packages/ui/v2/core/form/radio-area/RadioAreaGroup.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import React, { ReactNode, useState } from "react"; - -import classNames from "@calcom/lib/classNames"; - -type RadioAreaProps = Omit, "onChange"> & { - onChange?: (value: string) => void; - defaultChecked?: boolean; -}; - -const RadioArea = (props: RadioAreaProps) => { - return ( - - ); -}; - -type ChildrenProps = { - props: RadioAreaProps; - children?: ReactNode; -}; -interface RadioAreaGroupProps extends Omit, "onChange" | "children"> { - children: ChildrenProps | ChildrenProps[]; - name?: string; - onChange?: (value: string) => void; -} - -const RadioAreaGroup = ({ children, name, onChange, ...passThroughProps }: RadioAreaGroupProps) => { - const [checkedIdx, setCheckedIdx] = useState(null); - - const changeHandler = (value: string, idx: number) => { - if (onChange) { - onChange(value); - } - setCheckedIdx(idx); - }; - - return ( -
- {(Array.isArray(children) ? children : [children]).map((child, idx: number) => { - if (checkedIdx === null && child.props.defaultChecked) { - setCheckedIdx(idx); - } - return ( - changeHandler(value, idx)}> - {child.props.children} - - ); - })} -
- ); -}; - -const Item = RadioArea; -const Group = RadioAreaGroup; - -export { RadioArea, RadioAreaGroup, Item, Group }; diff --git a/packages/ui/v2/core/index.ts b/packages/ui/v2/core/index.ts index 0e9ac0fb68..12170000c0 100644 --- a/packages/ui/v2/core/index.ts +++ b/packages/ui/v2/core/index.ts @@ -19,7 +19,7 @@ export { DropdownMenuTrigger, DropdownMenuTriggerItem, } from "./Dropdown"; -export { Checkbox, DatePicker, FormStep, Radio, RadioGroup } from "./form"; +export { Checkbox, DatePicker, FormStep } from "./form"; export { default as LinkIconButton } from "./LinkIconButton"; export { default as Loader } from "./Loader"; export { default as MeetingTimeInTimezones } from "./MeetingTimeInTimezones";