From 70683a89b9fcf86f4ce140ca121cf6f96b834c71 Mon Sep 17 00:00:00 2001 From: Jamie Pine <32987599+jamiepine@users.noreply.github.com> Date: Wed, 12 Jan 2022 01:29:20 -0800 Subject: [PATCH] Added "New Event Type" button on Team settings (#1411) - Moved CreateNewEventButton in pages/event-types/index to dedicated component as this is used in two places now. - Implemented CreateEventType button on Team settings screen and replaced old markup in on event types page with new component. - Upgrade vanilla JS inputs to library primitives. - Created TextArea & TextAreaField components in components/form. - [Bugfix] Changed back button behavior in Shell to have a specified back path as CreateEventType's modal interfered with the router.goBack behavior. - Ensure modal data is preserved in URL params for router accuracy and removed on exit. --- components/Shell.tsx | 9 +- components/eventtype/CreateEventType.tsx | 242 +++++++++++++++++ components/form/fields.tsx | 68 ++++- components/team/TeamSettingsRightSidebar.tsx | 8 + ee/pages/settings/teams/[id]/availability.tsx | 2 +- pages/event-types/index.tsx | 245 +----------------- pages/integrations/index.tsx | 10 +- pages/settings/teams/[id].tsx | 2 +- 8 files changed, 334 insertions(+), 252 deletions(-) create mode 100644 components/eventtype/CreateEventType.tsx diff --git a/components/Shell.tsx b/components/Shell.tsx index a102128770..3d7f2e47be 100644 --- a/components/Shell.tsx +++ b/components/Shell.tsx @@ -124,7 +124,7 @@ export default function Shell(props: { children: ReactNode; CTA?: ReactNode; HeadingLeftIcon?: ReactNode; - showBackButton?: boolean; + backPath?: string; // renders back button to specified path // use when content needs to expand with flex flexChildrenContainer?: boolean; }) { @@ -289,9 +289,12 @@ export default function Shell(props: { props.flexChildrenContainer && "flex flex-col flex-1", "py-8" )}> - {props.showBackButton && ( + {!!props.backPath && (
-
diff --git a/components/eventtype/CreateEventType.tsx b/components/eventtype/CreateEventType.tsx new file mode 100644 index 0000000000..002231aac0 --- /dev/null +++ b/components/eventtype/CreateEventType.tsx @@ -0,0 +1,242 @@ +import { ChevronDownIcon, PlusIcon } from "@heroicons/react/solid"; +import { SchedulingType } from "@prisma/client"; +import { useRouter } from "next/router"; +import React, { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { useMutation } from "react-query"; + +import { HttpError } from "@lib/core/http/error"; +import { useLocale } from "@lib/hooks/useLocale"; +import { useToggleQuery } from "@lib/hooks/useToggleQuery"; +import createEventType from "@lib/mutations/event-types/create-event-type"; +import showToast from "@lib/notification"; +import { CreateEventType } from "@lib/types/event-type"; + +import { Dialog, DialogClose, DialogContent } from "@components/Dialog"; +import { TextField, InputLeading, TextAreaField, Form } from "@components/form/fields"; +import Avatar from "@components/ui/Avatar"; +import { Button } from "@components/ui/Button"; +import Dropdown, { + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@components/ui/Dropdown"; +import * as RadioArea from "@components/ui/form/radio-area"; + +// this describes the uniform data needed to create a new event type on Profile or Team +interface EventTypeParent { + teamId: number | null | undefined; // if undefined, then it's a profile + name?: string | null; + slug?: string | null; + image?: string | null; +} + +interface Props { + // set true for use on the team settings page + canAddEvents: boolean; + // set true when in use on the team settings page + isIndividualTeam?: boolean; + // EventTypeParent can be a profile (as first option) or a team for the rest. + options: EventTypeParent[]; +} + +export default function CreateEventTypeButton(props: Props) { + const { t } = useLocale(); + const router = useRouter(); + const modalOpen = useToggleQuery("new"); + + const form = useForm({ + defaultValues: { length: 15 }, + }); + const { setValue, watch, register } = form; + + useEffect(() => { + const subscription = watch((value, { name, type }) => { + if (name === "title" && type === "change") { + if (value.title) setValue("slug", value.title.replace(/\s+/g, "-").toLowerCase()); + else setValue("slug", ""); + } + }); + return () => subscription.unsubscribe(); + }, [watch, setValue]); + + // URL encoded params + const teamId: number | null = Number(router.query.teamId) || null; + const pageSlug = router.query.eventPage || props.options[0].slug; + + const hasTeams = !!props.options.find((option) => option.teamId); + + const createMutation = useMutation(createEventType, { + onSuccess: async ({ eventType }) => { + await router.push("/event-types/" + eventType.id); + showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success"); + }, + onError: (err: HttpError) => { + const message = `${err.statusCode}: ${err.message}`; + showToast(message, "error"); + }, + }); + + // inject selection data into url for correct router history + const openModal = (option: EventTypeParent) => { + router.push({ + pathname: router.pathname, + query: { + ...router.query, + new: "1", + eventPage: option.slug, + ...(option.teamId + ? { + teamId: option.teamId, + } + : {}), + }, + }); + }; + + // remove url params after close modal to reset state + const closeModal = () => { + router.replace({ + pathname: router.pathname, + query: { id: router.query.id }, + }); + }; + + return ( + { + router.push(isOpen ? modalOpen.hrefOn : modalOpen.hrefOff); + if (!isOpen) closeModal(); + }}> + {!hasTeams || props.isIndividualTeam ? ( + + ) : ( + + + + + + {t("new_event_subtitle")} + + {props.options.map((option) => ( + openModal(option)}> + + {option.name ? option.name : option.slug} + + ))} + + + )} + + +
+ +
+

{t("new_event_type_to_book_description")}

+
+
+
{ + const payload: CreateEventType = { + title: values.title, + slug: values.slug, + description: values.description, + length: values.length, + }; + if (router.query.teamId) { + payload.teamId = parseInt(`${router.query.teamId}`, 10); + payload.schedulingType = values.schedulingType as SchedulingType; + } + + createMutation.mutate(payload); + }}> +
+ + + + {process.env.NEXT_PUBLIC_APP_URL}/{pageSlug}/ + + } + {...register("slug")} + /> + + + +
+ +
+ {t("minutes")} +
+
+ + {teamId && ( +
+ + form.setValue("schedulingType", val as SchedulingType)} + className="relative flex mt-1 space-x-6 rounded-sm shadow-sm"> + + {t("collective")} +

{t("collective_description")}

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

{t("round_robin_description")}

+
+
+
+ )} +
+
+ + + + +
+
+
+
+ ); +} diff --git a/components/form/fields.tsx b/components/form/fields.tsx index 96e0fe4fa6..ad81c400e7 100644 --- a/components/form/fields.tsx +++ b/components/form/fields.tsx @@ -10,6 +10,7 @@ import showToast from "@lib/notification"; import { Alert } from "@components/ui/Alert"; type InputProps = Omit & { name: string }; + export const Input = forwardRef(function Input(props, ref) { return ( + {props.children} + + ); +} + type InputFieldProps = { label?: ReactNode; addOnLeading?: ReactNode; @@ -50,7 +59,7 @@ const InputField = forwardRef(function InputF : "", className, addOnLeading, - ...passThroughToInput + ...passThrough } = props; return (
@@ -65,13 +74,13 @@ const InputField = forwardRef(function InputF
) : ( - + )} {methods?.formState?.errors[props.name] && ( @@ -112,6 +121,57 @@ export const EmailField = forwardRef(function return ; }); +type TextAreaProps = Omit & { name: string }; + +export const TextArea = forwardRef(function TextAreaInput(props, ref) { + return ( + + defaultValue={useCustomPayloadTemplate && (defaultValues.payloadTemplate || "")} + rows={3} + /> )} diff --git a/pages/settings/teams/[id].tsx b/pages/settings/teams/[id].tsx index 41975fbfdd..cce417b13e 100644 --- a/pages/settings/teams/[id].tsx +++ b/pages/settings/teams/[id].tsx @@ -33,7 +33,7 @@ export function TeamSettingsPage() { return (