From 58de9209512c45e253ba3737311a0e21e0d1cf10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Thu, 7 Oct 2021 09:43:20 -0600 Subject: [PATCH] Refactors custom input form & dialog (#853) --- .prettierrc.js | 2 +- components/Modal.tsx | 4 +- components/eventtype/CustomInputTypeForm.tsx | 126 +++++++++++ lib/trpc.ts | 6 +- package.json | 1 + pages/[user].tsx | 3 +- pages/_app.tsx | 8 +- pages/api/availability/eventtype.ts | 78 ++++--- pages/api/trpc/[trpc].ts | 3 +- pages/api/user/profile.ts | 3 +- pages/event-types/[type].tsx | 224 ++++++------------- pages/event-types/index.tsx | 1 + server/routers/_app.ts | 4 +- yarn.lock | 8 + 14 files changed, 266 insertions(+), 205 deletions(-) create mode 100644 components/eventtype/CustomInputTypeForm.tsx diff --git a/.prettierrc.js b/.prettierrc.js index 5a9e76d98c..132044b0ed 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -7,6 +7,6 @@ module.exports = { semi: true, printWidth: 110, arrowParens: "always", - importOrder: ["^@ee/(.*)$", "^@lib/(.*)$", "^@components/(.*)$", "^[./]"], + importOrder: ["^@ee/(.*)$", "^@lib/(.*)$", "^@components/(.*)$", "^@(server|trcp)/(.*)$", "^[./]"], importOrderSeparation: true, }; diff --git a/components/Modal.tsx b/components/Modal.tsx index a04a2c3ee6..5f8daf9f9f 100644 --- a/components/Modal.tsx +++ b/components/Modal.tsx @@ -1,10 +1,12 @@ -/* legacy and soon deprecated, please refactor to use only */ import { Dialog, Transition } from "@headlessui/react"; import { CheckIcon, InformationCircleIcon } from "@heroicons/react/outline"; import { Fragment, ReactNode } from "react"; import classNames from "@lib/classNames"; +/** + * @deprecated please refactor to use only + */ export default function Modal(props: { heading: ReactNode; description: ReactNode; diff --git a/components/eventtype/CustomInputTypeForm.tsx b/components/eventtype/CustomInputTypeForm.tsx new file mode 100644 index 0000000000..7e7a000b75 --- /dev/null +++ b/components/eventtype/CustomInputTypeForm.tsx @@ -0,0 +1,126 @@ +import { EventTypeCustomInput, EventTypeCustomInputType } from "@prisma/client"; +import React, { FC } from "react"; +import { Controller, SubmitHandler, useForm, useWatch } from "react-hook-form"; +import Select, { OptionTypeBase } from "react-select"; + +const inputOptions: OptionTypeBase[] = [ + { value: EventTypeCustomInputType.TEXT, label: "Text" }, + { value: EventTypeCustomInputType.TEXTLONG, label: "Multiline Text" }, + { value: EventTypeCustomInputType.NUMBER, label: "Number" }, + { value: EventTypeCustomInputType.BOOL, label: "Checkbox" }, +]; + +interface Props { + onSubmit: SubmitHandler; + onCancel: () => void; + selectedCustomInput?: EventTypeCustomInput; +} + +type IFormInput = EventTypeCustomInput; + +const CustomInputTypeForm: FC = (props) => { + const { selectedCustomInput } = props; + const defaultValues = selectedCustomInput || { type: inputOptions[0].value }; + const { register, control, handleSubmit } = useForm({ + defaultValues, + }); + const selectedInputType = useWatch({ name: "type", control }); + const selectedInputOption = inputOptions.find((e) => selectedInputType === e.value)!; + + const onCancel = () => { + props.onCancel(); + }; + + return ( +
+
+ + ( + +
+ + {(selectedInputType === EventTypeCustomInputType.TEXT || + selectedInputType === EventTypeCustomInputType.TEXTLONG) && ( +
+ +
+ +
+
+ )} +
+ + +
+ + +
+ + +
+
+ ); +}; + +export default CustomInputTypeForm; diff --git a/lib/trpc.ts b/lib/trpc.ts index a008c73f19..99caec41ff 100644 --- a/lib/trpc.ts +++ b/lib/trpc.ts @@ -1,8 +1,10 @@ // ℹ️ Type-only import: // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export -import type { AppRouter } from "@server/routers/_app"; import { createReactQueryHooks } from "@trpc/react"; import type { inferProcedureOutput, inferProcedureInput } from "@trpc/server"; +import superjson from "superjson"; + +import type { AppRouter } from "@server/routers/_app"; /** * A set of strongly-typed React hooks from your `AppRouter` type signature with `createReactQueryHooks`. @@ -10,7 +12,7 @@ import type { inferProcedureOutput, inferProcedureInput } from "@trpc/server"; */ export const trpc = createReactQueryHooks(); -// export const transformer = superjson; +export const transformer = superjson; /** * This is a helper method to infer the output of a query resolver * @example type HelloOutput = inferQueryOutput<'hello'> diff --git a/package.json b/package.json index 55b289fb01..b54e9d485b 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "react-use-intercom": "1.4.0", "short-uuid": "^4.2.0", "stripe": "^8.168.0", + "superjson": "1.7.5", "tsdav": "1.0.6", "tslog": "^3.2.1", "uuid": "^8.3.2", diff --git a/pages/[user].tsx b/pages/[user].tsx index cf6f536e78..aa6607cc93 100644 --- a/pages/[user].tsx +++ b/pages/[user].tsx @@ -1,5 +1,4 @@ import { ArrowRightIcon } from "@heroicons/react/outline"; -import { ssg } from "@server/ssg"; import { GetStaticPaths, GetStaticPropsContext } from "next"; import Link from "next/link"; import React from "react"; @@ -13,6 +12,8 @@ import EventTypeDescription from "@components/eventtype/EventTypeDescription"; import { HeadSeo } from "@components/seo/head-seo"; import Avatar from "@components/ui/Avatar"; +import { ssg } from "@server/ssg"; + export default function User(props: inferSSRProps) { const { username } = props; // data of query below will be will be prepopulated b/c of `getStaticProps` diff --git a/pages/_app.tsx b/pages/_app.tsx index 527b25d80f..a34869fe5d 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,4 +1,3 @@ -import type { AppRouter } from "@server/routers/_app"; import { httpBatchLink } from "@trpc/client/links/httpBatchLink"; import { loggerLink } from "@trpc/client/links/loggerLink"; import { withTRPC } from "@trpc/next"; @@ -7,10 +6,13 @@ import { Maybe } from "@trpc/server"; import { appWithTranslation } from "next-i18next"; import { DefaultSeo } from "next-seo"; import type { AppProps as NextAppProps } from "next/app"; +import superjson from "superjson"; import AppProviders from "@lib/app-providers"; import { seoConfig } from "@lib/config/next-seo.config"; +import type { AppRouter } from "@server/routers/_app"; + import "../styles/globals.css"; // Workaround for https://github.com/vercel/next.js/issues/8592 @@ -77,6 +79,10 @@ export default withTRPC({ }, }, }, + /** + * @link https://trpc.io/docs/data-transformers + */ + transformer: superjson, }; }, /** diff --git a/pages/api/availability/eventtype.ts b/pages/api/availability/eventtype.ts index f68dca3c59..afa8588f91 100644 --- a/pages/api/availability/eventtype.ts +++ b/pages/api/availability/eventtype.ts @@ -1,10 +1,50 @@ +import { EventTypeCustomInput, Prisma } from "@prisma/client"; import type { NextApiRequest, NextApiResponse } from "next"; import { getSession } from "@lib/auth"; import prisma from "@lib/prisma"; +function handleCustomInputs(customInputs: EventTypeCustomInput[], eventTypeId: number) { + if (!customInputs || customInputs?.length) return undefined; + const cInputsIdsToDelete = customInputs.filter((input) => input.id > 0).map((e) => e.id); + const cInputsToCreate = customInputs + .filter((input) => input.id < 0) + .map((input) => ({ + type: input.type, + label: input.label, + required: input.required, + placeholder: input.placeholder, + })); + const cInputsToUpdate = customInputs + .filter((input) => input.id > 0) + .map((input) => ({ + data: { + type: input.type, + label: input.label, + required: input.required, + placeholder: input.placeholder, + }, + where: { + id: input.id, + }, + })); + + return { + deleteMany: { + eventTypeId, + NOT: { + id: { in: cInputsIdsToDelete }, + }, + }, + createMany: { + data: cInputsToCreate, + }, + update: cInputsToUpdate, + }; +} + export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const session = await getSession({ req: req }); + const session = await getSession({ req }); if (!session) { res.status(401).json({ message: "Not authenticated" }); @@ -41,7 +81,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } if (req.method == "PATCH" || req.method == "POST") { - const data = { + const data: Prisma.EventTypeUpdateInput = { title: req.body.title, slug: req.body.slug.trim(), description: req.body.description, @@ -51,39 +91,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) disableGuests: req.body.disableGuests, locations: req.body.locations, eventName: req.body.eventName, - customInputs: !req.body.customInputs - ? undefined - : { - deleteMany: { - eventTypeId: req.body.id, - NOT: { - id: { in: req.body.customInputs.filter((input) => !!input.id).map((e) => e.id) }, - }, - }, - createMany: { - data: req.body.customInputs - .filter((input) => !input.id) - .map((input) => ({ - type: input.type, - label: input.label, - required: input.required, - placeholder: input.placeholder, - })), - }, - update: req.body.customInputs - .filter((input) => !!input.id) - .map((input) => ({ - data: { - type: input.type, - label: input.label, - required: input.required, - placeholder: input.placeholder, - }, - where: { - id: input.id, - }, - })), - }, + customInputs: handleCustomInputs(req.body.customInputs as EventTypeCustomInput[], req.body.id), periodType: req.body.periodType, periodDays: req.body.periodDays, periodStartDate: req.body.periodStartDate, diff --git a/pages/api/trpc/[trpc].ts b/pages/api/trpc/[trpc].ts index e0b9531d0e..588e112a0a 100644 --- a/pages/api/trpc/[trpc].ts +++ b/pages/api/trpc/[trpc].ts @@ -1,9 +1,10 @@ /** * This file contains tRPC's HTTP response handler */ +import * as trpcNext from "@trpc/server/adapters/next"; + import { createContext } from "@server/createContext"; import { appRouter } from "@server/routers/_app"; -import * as trpcNext from "@trpc/server/adapters/next"; export default trpcNext.createNextApiHandler({ router: appRouter, diff --git a/pages/api/user/profile.ts b/pages/api/user/profile.ts index 50a1075fce..95593f0117 100644 --- a/pages/api/user/profile.ts +++ b/pages/api/user/profile.ts @@ -1,10 +1,11 @@ -import { resizeBase64Image } from "@server/lib/resizeBase64Image"; import { pick } from "lodash"; import type { NextApiRequest, NextApiResponse } from "next"; import { getSession } from "@lib/auth"; import prisma from "@lib/prisma"; +import { resizeBase64Image } from "@server/lib/resizeBase64Image"; + export default async function handler(req: NextApiRequest, res: NextApiResponse) { const session = await getSession({ req: req }); diff --git a/pages/event-types/[type].tsx b/pages/event-types/[type].tsx index 9ee26de9f4..218c17785e 100644 --- a/pages/event-types/[type].tsx +++ b/pages/event-types/[type].tsx @@ -14,7 +14,7 @@ import { UserAddIcon, UsersIcon, } from "@heroicons/react/solid"; -import { EventTypeCustomInput, EventTypeCustomInputType, Prisma, SchedulingType } from "@prisma/client"; +import { EventTypeCustomInput, Prisma, SchedulingType } from "@prisma/client"; import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; @@ -48,10 +48,11 @@ import { defaultAvatarSrc } from "@lib/profile"; import { AdvancedOptions, EventTypeInput } from "@lib/types/event-type"; import { inferSSRProps } from "@lib/types/inferSSRProps"; -import { Dialog, DialogTrigger } from "@components/Dialog"; +import { Dialog, DialogContent, DialogTrigger } from "@components/Dialog"; import Modal from "@components/Modal"; import Shell from "@components/Shell"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; +import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm"; import Button from "@components/ui/Button"; import { Scheduler } from "@components/ui/Scheduler"; import Switch from "@components/ui/Switch"; @@ -86,13 +87,6 @@ const EventTypePage = (props: inferSSRProps) => { const router = useRouter(); const [successModalOpen, setSuccessModalOpen] = useState(false); - const inputOptions: OptionTypeBase[] = [ - { value: EventTypeCustomInputType.TEXT, label: "Text" }, - { value: EventTypeCustomInputType.TEXTLONG, label: "Multiline Text" }, - { value: EventTypeCustomInputType.NUMBER, label: "Number" }, - { value: EventTypeCustomInputType.BOOL, label: "Checkbox" }, - ]; - const updateMutation = useMutation(updateEventType, { onSuccess: async ({ eventType }) => { await router.push("/event-types"); @@ -121,12 +115,11 @@ const EventTypePage = (props: inferSSRProps) => { const [enteredAvailability, setEnteredAvailability] = useState(); const [showLocationModal, setShowLocationModal] = useState(false); - const [showAddCustomModal, setShowAddCustomModal] = useState(false); const [selectedTimeZone, setSelectedTimeZone] = useState(""); const [selectedLocation, setSelectedLocation] = useState(undefined); - const [selectedInputOption, setSelectedInputOption] = useState(inputOptions[0]); const [locations, setLocations] = useState(eventType.locations || []); const [selectedCustomInput, setSelectedCustomInput] = useState(undefined); + const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false); const [customInputs, setCustomInputs] = useState( eventType.customInputs.sort((a, b) => a.id - b.id) || [] ); @@ -217,12 +210,6 @@ const EventTypePage = (props: inferSSRProps) => { setShowLocationModal(false); }; - const closeAddCustomModal = () => { - setSelectedInputOption(inputOptions[0]); - setShowAddCustomModal(false); - setSelectedCustomInput(undefined); - }; - const closeSuccessModal = () => { setSuccessModalOpen(false); }; @@ -252,12 +239,6 @@ const EventTypePage = (props: inferSSRProps) => { setLocations(locations.filter((location) => location.type !== selectedLocation.type)); }; - const openEditCustomModel = (customInput: EventTypeCustomInput) => { - setSelectedCustomInput(customInput); - setSelectedInputOption(inputOptions.find((e) => e.value === customInput.type)!); - setShowAddCustomModal(true); - }; - const LocationOptions = () => { if (!selectedLocation) { return null; @@ -293,29 +274,6 @@ const EventTypePage = (props: inferSSRProps) => { return null; }; - const updateCustom = (e: React.FormEvent) => { - e.preventDefault(); - - const customInput: EventTypeCustomInput = { - id: -1, - eventTypeId: -1, - label: e.currentTarget.label.value, - placeholder: e.currentTarget.placeholder?.value, - required: e.currentTarget.required.checked, - type: e.currentTarget.type.value, - }; - - if (selectedCustomInput) { - selectedCustomInput.label = customInput.label; - selectedCustomInput.placeholder = customInput.placeholder; - selectedCustomInput.required = customInput.required; - selectedCustomInput.type = customInput.type; - } else { - setCustomInputs(customInputs.concat(customInput)); - } - closeAddCustomModal(); - }; - const removeCustom = (index: number) => { customInputs.splice(index, 1); setCustomInputs([...customInputs]); @@ -422,7 +380,7 @@ const EventTypePage = (props: inferSSRProps) => { id="length" required placeholder="15" - defaultValue={eventType.length} + defaultValue={eventType.length || 15} />
@@ -679,12 +637,15 @@ const EventTypePage = (props: inferSSRProps) => {
- + @@ -693,15 +654,16 @@ const EventTypePage = (props: inferSSRProps) => { ))}
  • - + StartIcon={PlusIcon}> + Add an input +
  • @@ -1035,111 +997,51 @@ const EventTypePage = (props: inferSSRProps) => { )} - {showAddCustomModal && ( -
    -
    -
    ); diff --git a/pages/event-types/index.tsx b/pages/event-types/index.tsx index 7316685366..613ab14556 100644 --- a/pages/event-types/index.tsx +++ b/pages/event-types/index.tsx @@ -520,6 +520,7 @@ const CreateNewEventDialog = ({ required className="block w-full pr-20 border-gray-300 rounded-sm focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm" placeholder="15" + defaultValue={15} />
    minutes diff --git a/server/routers/_app.ts b/server/routers/_app.ts index 2093a8b5c0..e497c036a0 100644 --- a/server/routers/_app.ts +++ b/server/routers/_app.ts @@ -1,6 +1,8 @@ /** * This file contains the root router of your tRPC-backend */ +import superjson from "superjson"; + import { createRouter } from "../createRouter"; import { bookingRouter } from "./booking"; import { viewerRouter } from "./viewer"; @@ -16,7 +18,7 @@ export const appRouter = createRouter() * Add data transformers * @link https://trpc.io/docs/data-transformers */ - // .transformer(superjson) + .transformer(superjson) /** * Optionally do custom error (type safe!) formatting * @link https://trpc.io/docs/error-formatting diff --git a/yarn.lock b/yarn.lock index a652bed7b0..f005a4fe9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7911,6 +7911,14 @@ stylis@^4.0.3: version "4.0.10" resolved "https://registry.npmjs.org/stylis/-/stylis-4.0.10.tgz" +superjson@1.7.5: + version "1.7.5" + resolved "https://registry.yarnpkg.com/superjson/-/superjson-1.7.5.tgz#596d080fd3c010f6d991c53a292c03704f160649" + integrity sha512-AHuFroOcMTK6LdG/irwXIHwH6Gof5nh42iywnhhf7hMZ6UJqFDRtJ82ViJg14UX3AG8vWRf4Dh3oPIJcqu16Nw== + dependencies: + debug "^4.3.1" + lodash.clonedeep "^4.5.0" + supports-color@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz"