From 31875f75352fdcc37ff2eac12c3f7e8c9f83c11b Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Thu, 10 Nov 2022 13:58:07 +0100 Subject: [PATCH] Test Preview for routing forms (#5436) * add first version of testing routing forms * small design changes * use new shared component * readd deleted code for showing result route * add form validation for required fields * design fixes * add scroll * fix design of select * use old dialog with correct overflow behaviour * code clean up * remove unused import * fix mobile view Co-authored-by: CarinaWolli Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- apps/web/public/static/locales/en/common.json | 6 +- .../components/FormInputFields.tsx | 73 ++++++ .../routing-forms/components/SingleForm.tsx | 221 ++++++++++++------ .../react-awesome-query-builder/widgets.tsx | 2 - .../pages/route-builder/[...appPages].tsx | 30 +-- .../pages/routing-link/[...appPages].tsx | 56 +---- packages/ui/Dialog.tsx | 2 +- 7 files changed, 252 insertions(+), 138 deletions(-) create mode 100644 packages/app-store/ee/routing-forms/components/FormInputFields.tsx diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 4aa3fabc65..c2839309eb 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1344,5 +1344,9 @@ "attendee_email_info": "The person booking's email", "invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.", "choose_common_schedule_team_event": "Choose a common schedule", - "choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule." + "choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule.", + "test_routing_form": "Test Routing Form", + "test_preview": "Test Preview", + "route_to": "Route to", + "test_preview_description": "Test your routing form without submitting any data" } diff --git a/packages/app-store/ee/routing-forms/components/FormInputFields.tsx b/packages/app-store/ee/routing-forms/components/FormInputFields.tsx new file mode 100644 index 0000000000..6b3a02e2f0 --- /dev/null +++ b/packages/app-store/ee/routing-forms/components/FormInputFields.tsx @@ -0,0 +1,73 @@ +import { App_RoutingForms_Form } from "@prisma/client"; +import { Dispatch, SetStateAction } from "react"; + +import { getQueryBuilderConfig } from "../pages/route-builder/[...appPages]"; +import { SerializableForm, Response } from "../types/types"; + +type Props = { + form: SerializableForm; + response: Response; + setResponse: Dispatch>; +}; + +export default function FormInputFields(props: Props) { + const { form, response, setResponse } = props; + + const queryBuilderConfig = getQueryBuilderConfig(form); + + return ( + <> + {form.fields?.map((field) => { + const widget = queryBuilderConfig.widgets[field.type]; + if (!("factory" in widget)) { + return null; + } + const Component = widget.factory; + + const optionValues = field.selectText?.trim().split("\n"); + const options = optionValues?.map((value) => { + const title = value; + return { + value, + title, + }; + }); + return ( +
+
+ +
+
+ { + setResponse((response) => { + response = response || {}; + return { + ...response, + [field.id]: { + label: field.label, + value, + }, + }; + }); + }} + /> +
+
+ ); + })} + + ); +} diff --git a/packages/app-store/ee/routing-forms/components/SingleForm.tsx b/packages/app-store/ee/routing-forms/components/SingleForm.tsx index 0ba4f75f80..da730877c3 100644 --- a/packages/app-store/ee/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/ee/routing-forms/components/SingleForm.tsx @@ -1,11 +1,12 @@ import { App_RoutingForms_Form } from "@prisma/client"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useForm, UseFormReturn, Controller } from "react-hook-form"; import useApp from "@calcom/lib/hooks/useApp"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { Icon } from "@calcom/ui"; +import { Dialog, DialogContent, DialogClose, DialogFooter, DialogHeader } from "@calcom/ui/Dialog"; import { Button, ButtonGroup } from "@calcom/ui/components"; import { Form, TextAreaField, TextField } from "@calcom/ui/components/form"; import { showToast, DropdownMenuSeparator, Tooltip, VerticalDivider } from "@calcom/ui/v2"; @@ -14,8 +15,12 @@ import SettingsToggle from "@calcom/ui/v2/core/SettingsToggle"; import { ShellMain } from "@calcom/ui/v2/core/Shell"; import Banner from "@calcom/ui/v2/core/banner"; +import { processRoute } from "../lib/processRoute"; +import { RoutingPages } from "../pages/route-builder/[...appPages]"; import { SerializableForm } from "../types/types"; +import { Response, Route } from "../types/types"; import { FormAction, FormActionsDropdown, FormActionsProvider } from "./FormActions"; +import FormInputFields from "./FormInputFields"; import RoutingNavBar from "./RoutingNavBar"; type RoutingForm = SerializableForm; @@ -190,6 +195,15 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) { const utils = trpc.useContext(); const { t } = useLocale(); + const [isTestPreviewOpen, setIsTestPreviewOpen] = useState(false); + const [response, setResponse] = useState({}); + const [decidedAction, setDecidedAction] = useState(null); + + function testRouting() { + const action = processRoute({ form, response }); + setDecidedAction(action); + } + const hookForm = useForm({ defaultValues: form, }); @@ -210,76 +224,151 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) { }, }); return ( -
{ - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - //@ts-ignore - mutation.mutate({ - ...data, - }); - }}> - - - }> -
-
-
- - + <> + { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + mutation.mutate({ + ...data, + }); + }}> + + + }> +
+
+
+ + -
- { - return ( - onChange(val)} - /> - ); - }} - /> +
+ { + return ( + onChange(val)} + /> + ); + }} + /> +
+
+ +
+ {!form._count?.responses && ( + console.log("dismissed")} + /> + )} +
+
+ +
- {!form._count?.responses && ( - console.log("dismissed")} - /> - )} -
-
- -
+ + + + + + +
+
{ + e.preventDefault(); + testRouting(); + }}> +
+ {form && } +
+
+ {decidedAction && ( +
+
{t("route_to")}:
+
+ {RoutingPages.map((page) => { + if (page.value === decidedAction.type) { + return <>{page.label}; + } + })} + :{" "} + {decidedAction.type === "customPageMessage" ? ( + {decidedAction.value} + ) : decidedAction.type === "externalRedirectUrl" ? ( + + + {decidedAction.value} + + + ) : ( + + + {decidedAction.value} + + + )} +
+
+ )} +
+ + + + + + +
- - - +
+
+ ); } diff --git a/packages/app-store/ee/routing-forms/components/react-awesome-query-builder/widgets.tsx b/packages/app-store/ee/routing-forms/components/react-awesome-query-builder/widgets.tsx index e2e8095d5e..24bbfaf23d 100644 --- a/packages/app-store/ee/routing-forms/components/react-awesome-query-builder/widgets.tsx +++ b/packages/app-store/ee/routing-forms/components/react-awesome-query-builder/widgets.tsx @@ -104,7 +104,6 @@ const MultiSelectWidget = ({ return ( { if (!item) { return; diff --git a/packages/app-store/ee/routing-forms/pages/route-builder/[...appPages].tsx b/packages/app-store/ee/routing-forms/pages/route-builder/[...appPages].tsx index 72e013d549..4d0c1822df 100644 --- a/packages/app-store/ee/routing-forms/pages/route-builder/[...appPages].tsx +++ b/packages/app-store/ee/routing-forms/pages/route-builder/[...appPages].tsx @@ -128,6 +128,21 @@ type SerializableRoute = Pick & { isFallback?: Route["isFallback"]; }; +export const RoutingPages: { label: string; value: Route["action"]["type"] }[] = [ + { + label: "Custom Page", + value: "customPageMessage", + }, + { + label: "External Redirect", + value: "externalRedirectUrl", + }, + { + label: "Event Redirect", + value: "eventTypeRedirectUrl", + }, +]; + const Route = ({ route, routes, @@ -146,20 +161,7 @@ const Route = ({ moveDown?: { fn: () => void; check: () => boolean } | null; }) => { const index = routes.indexOf(route); - const RoutingPages: { label: string; value: Route["action"]["type"] }[] = [ - { - label: "Custom Page", - value: "customPageMessage", - }, - { - label: "External Redirect", - value: "externalRedirectUrl", - }, - { - label: "Event Redirect", - value: "eventTypeRedirectUrl", - }, - ]; + const { data: eventTypesByGroup } = trpc.useQuery(["viewer.eventTypes"]); const eventOptions: { label: string; value: string }[] = []; diff --git a/packages/app-store/ee/routing-forms/pages/routing-link/[...appPages].tsx b/packages/app-store/ee/routing-forms/pages/routing-link/[...appPages].tsx index 974b23287f..1eee26b48f 100644 --- a/packages/app-store/ee/routing-forms/pages/routing-link/[...appPages].tsx +++ b/packages/app-store/ee/routing-forms/pages/routing-link/[...appPages].tsx @@ -18,10 +18,10 @@ import showToast from "@calcom/ui/v2/core/notifications"; import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally"; +import FormInputFields from "../../components/FormInputFields"; import { getSerializableForm } from "../../lib/getSerializableForm"; import { processRoute } from "../../lib/processRoute"; import { Response, Route } from "../../types/types"; -import { getQueryBuilderConfig } from "../route-builder/[...appPages]"; function RoutingForm({ form, profile, ...restProps }: inferSSRProps) { const [customPageMessage, setCustomPageMessage] = useState(""); @@ -89,8 +89,6 @@ function RoutingForm({ form, profile, ...restProps }: inferSSRProps({}); - const queryBuilderConfig = getQueryBuilderConfig(form); - const handleOnSubmit = (e: FormEvent) => { e.preventDefault(); onSubmit(response); @@ -124,57 +122,7 @@ function RoutingForm({ form, profile, ...restProps }: inferSSRProps ) : null}
- {form.fields?.map((field) => { - const widget = queryBuilderConfig.widgets[field.type]; - if (!("factory" in widget)) { - return null; - } - const Component = widget.factory; - - const optionValues = field.selectText?.trim().split("\n"); - const options = optionValues?.map((value) => { - const title = value; - return { - value, - title, - }; - }); - return ( -
-
- -
-
- { - setResponse((response) => { - response = response || {}; - return { - ...response, - [field.id]: { - label: field.label, - value, - }, - }; - }); - }} - /> -
-
- ); - })} +