From 175f2482e1a3f8ceb83a1e2c2d48733c71bc16a9 Mon Sep 17 00:00:00 2001 From: "GitStart-Cal.com" <121884634+gitstart-calcom@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:51:19 +0800 Subject: [PATCH] fix: Update forms UI (#8093) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Routing Forms: Update forms UI * fix failing test * fix: Routing Forms forms UI Update * fix: Routing Forms forms UI Update * Apply suggestions from code review --------- Co-authored-by: gitstart-calcom Co-authored-by: Peer Richelsen Co-authored-by: Omar López Co-authored-by: Hariom Balhara --- apps/web/public/static/locales/en/common.json | 1 + .../routing-forms/components/SingleForm.tsx | 24 ++- .../pages/form-edit/[...appPages].tsx | 162 +++++++++++++----- .../pages/reporting/[...appPages].tsx | 2 +- .../pages/route-builder/[...appPages].tsx | 2 +- .../playwright/tests/basic.e2e.ts | 10 +- .../ee/teams/pages/team-profile-view.tsx | 2 +- packages/ui/components/alert/Alert.tsx | 75 ++++---- packages/ui/components/card/FormCard.tsx | 4 +- .../form/toggleGroup/BooleanToggleGroup.tsx | 29 +++- .../navigation/tabs/HorizontalTabItem.tsx | 2 +- .../navigation/tabs/HorizontalTabs.tsx | 2 +- 12 files changed, 223 insertions(+), 92 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 369d5bcaf0..45f98d3a35 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1713,6 +1713,7 @@ "spot_popular_event_types": "Spot popular event types", "spot_popular_event_types_description": "See which of your event types are receiving the most clicks and bookings", "no_responses_yet": "No responses yet", + "no_routes_defined": "No routes defined", "this_will_be_the_placeholder": "This will be the placeholder", "error_booking_event": "An error occured when booking the event, please refresh the page and try again", "timeslot_missing_title": "No timeslot selected", diff --git a/packages/app-store/routing-forms/components/SingleForm.tsx b/packages/app-store/routing-forms/components/SingleForm.tsx index 7a6bc17870..8b8e711c1c 100644 --- a/packages/app-store/routing-forms/components/SingleForm.tsx +++ b/packages/app-store/routing-forms/components/SingleForm.tsx @@ -34,10 +34,18 @@ import { Tooltip, VerticalDivider, } from "@calcom/ui"; -import { ExternalLink, Link as LinkIcon, Download, Code, Trash } from "@calcom/ui/components/icon"; +import { + ExternalLink, + Link as LinkIcon, + Download, + Code, + Trash, + MessageCircle, +} from "@calcom/ui/components/icon"; import { RoutingPages } from "../lib/RoutingPages"; import { getSerializableForm } from "../lib/getSerializableForm"; +import { isFallbackRoute } from "../lib/isFallbackRoute"; import { processRoute } from "../lib/processRoute"; import type { Response, Route, SerializableForm } from "../types/types"; import { FormAction, FormActionsDropdown, FormActionsProvider } from "./FormActions"; @@ -371,18 +379,26 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) { {t("test_preview")} + {form.routes?.every(isFallbackRoute) && ( + + )} {!form._count?.responses && ( <> )} -
+
diff --git a/packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx b/packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx index 8a5834983b..9e10195404 100644 --- a/packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/form-edit/[...appPages].tsx @@ -12,11 +12,12 @@ import { Button, EmptyScreen, FormCard, + Label, SelectField, - TextAreaField, + Skeleton, TextField, } from "@calcom/ui"; -import { Plus, FileText } from "@calcom/ui/components/icon"; +import { Plus, FileText, X } from "@calcom/ui/components/icon"; import type { inferSSRProps } from "@lib/types/inferSSRProps"; @@ -27,6 +28,7 @@ import SingleForm, { export { getServerSideProps }; type HookForm = UseFormReturn; +type SelectOption = { placeholder: string; value: string }; export const FieldTypes = [ { @@ -42,11 +44,11 @@ export const FieldTypes = [ value: "textarea", }, { - label: "Select", + label: "Single Selection", value: "select", }, { - label: "MultiSelect", + label: "Multiple Selection", value: "multiselect", }, { @@ -85,30 +87,70 @@ function Field({ }; appUrl: string; }) { - const [identifier, _setIdentifier] = useState(hookForm.getValues(`${hookFieldNamespace}.identifier`)); const { t } = useLocale(); - const setUserChangedIdentifier = (val: string) => { - _setIdentifier(val); - // Also, update the form identifier so tha it can be persisted - hookForm.setValue(`${hookFieldNamespace}.identifier`, val); + const [options, setOptions] = useState([ + { placeholder: "< 10", value: "" }, + { placeholder: "10-100", value: "" }, + { placeholder: "100-500", value: "" }, + { placeholder: "> 500", value: "" }, + ]); + + const handleRemoveOptions = (index: number) => { + const updatedOptions = options.filter((_, i) => i !== index); + setOptions(updatedOptions); + updateSelectText(updatedOptions); }; - const label = hookForm.watch(`${hookFieldNamespace}.label`); + const handleAddOptions = () => { + setOptions((prevState) => [ + ...prevState, + { + placeholder: "New Option", + value: "", + }, + ]); + }; useEffect(() => { - if (!hookForm.getValues(`${hookFieldNamespace}.identifier`)) { - _setIdentifier(label); + const originalValues = hookForm.getValues(`${hookFieldNamespace}.selectText`); + if (originalValues) { + const values: SelectOption[] = originalValues.split("\n").map((fieldValue) => ({ + value: fieldValue, + placeholder: "", + })); + setOptions(values); } - }, [label, hookFieldNamespace, hookForm]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const router = hookForm.getValues(`${hookFieldNamespace}.router`); const routerField = hookForm.getValues(`${hookFieldNamespace}.routerField`); + + const handleChange = (e: React.ChangeEvent, optionIndex: number) => { + const updatedOptions = options.map((opt, index) => ({ + ...opt, + ...(index === optionIndex ? { value: e.target.value } : {}), + })); + setOptions(updatedOptions); + updateSelectText(updatedOptions); + }; + + const updateSelectText = (updatedOptions: SelectOption[]) => { + hookForm.setValue( + `${hookFieldNamespace}.selectText`, + updatedOptions + .filter((opt) => opt.value) + .map((opt) => opt.value) + .join("\n") + ); + }; return (
+ className="bg-default group mb-4 flex w-full items-center justify-between ltr:mr-2 rtl:ml-2"> setUserChangedIdentifier(e.target.value)} - /> -
-
- { + hookForm.setValue(`${hookFieldNamespace}.identifier`, e.target.value); + }} />
@@ -160,6 +201,17 @@ function Field({ const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value); return ( ({ + ...baseStyles, + fontSize: "14px", + }), + option: (baseStyles) => ({ + ...baseStyles, + fontSize: "14px", + }), + }} label="Type" isDisabled={!!router} containerClassName="data-testid-field-type" @@ -177,21 +229,48 @@ function Field({ />
{["select", "multiselect"].includes(hookForm.watch(`${hookFieldNamespace}.type`)) ? ( -
-
- +
+ + {t("options")} + + {options.map((field, index) => ( +
+ handleChange(e, index)} + addOnSuffix={ + + } + /> +
+ ))} +
+
) : null} -
+
{ return (
-
+
{hookFormFields.map((field, key) => { return ( - Add Field + Add field
) : null}
) : ( -
+
["fo ); return (
-
+
+
{mainRoutes.map((route, key) => { return ( diff --git a/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts b/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts index b2e6f9c6c6..c99fc6acab 100644 --- a/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts +++ b/packages/app-store/routing-forms/playwright/tests/basic.e2e.ts @@ -136,7 +136,7 @@ test.describe("Routing Forms", () => { label: "Test Field", }); const queryString = - "firstField=456&Test Field Number=456&Test Field Select=456&Test Field MultiSelect=456&Test Field MultiSelect=789&Test Field Phone=456&Test Field Email=456@example.com"; + "firstField=456&Test Field Number=456&Test Field Single Selection=456&Test Field Multiple Selection=456&Test Field Multiple Selection=789&Test Field Phone=456&Test Field Email=456@example.com"; await gotoRoutingLink({ page, queryString }); @@ -167,8 +167,8 @@ test.describe("Routing Forms", () => { // All other params come from prefill URL expect(url.searchParams.get("Test Field Number")).toBe("456"); expect(url.searchParams.get("Test Field Long Text")).toBe("manual-fill"); - expect(url.searchParams.get("Test Field Select")).toBe("456"); - expect(url.searchParams.getAll("Test Field MultiSelect")).toMatchObject(["456", "789"]); + expect(url.searchParams.get("Test Field Multiple Selection")).toBe("456"); + expect(url.searchParams.getAll("Test Field Multiple Selection")).toMatchObject(["456", "789"]); expect(url.searchParams.get("Test Field Phone")).toBe("456"); expect(url.searchParams.get("Test Field Email")).toBe("456@example.com"); }); @@ -447,7 +447,7 @@ async function addAllTypesOfFieldsAndSaveForm( const { optionsInUi: fieldTypesList } = await verifySelectOptions( { selector: ".data-testid-field-type", nth: 0 }, - ["Email", "Long Text", "MultiSelect", "Number", "Phone", "Select", "Short Text"], + ["Email", "Long Text", "Multiple Selection", "Number", "Phone", "Single Selection", "Short Text"], page ); @@ -507,7 +507,7 @@ export async function addOneFieldAndDescriptionAndSaveForm( // Verify all Options of SelectBox const { optionsInUi: types } = await verifySelectOptions( { selector: ".data-testid-field-type", nth: 0 }, - ["Email", "Long Text", "MultiSelect", "Number", "Phone", "Select", "Short Text"], + ["Email", "Long Text", "Multiple Selection", "Number", "Phone", "Single Selection", "Short Text"], page ); diff --git a/packages/features/ee/teams/pages/team-profile-view.tsx b/packages/features/ee/teams/pages/team-profile-view.tsx index e9cdeecee7..a6bcbec6ab 100644 --- a/packages/features/ee/teams/pages/team-profile-view.tsx +++ b/packages/features/ee/teams/pages/team-profile-view.tsx @@ -264,7 +264,7 @@ const ProfileView = () => { <>
diff --git a/packages/ui/components/alert/Alert.tsx b/packages/ui/components/alert/Alert.tsx index 513ee19778..384805d15a 100644 --- a/packages/ui/components/alert/Alert.tsx +++ b/packages/ui/components/alert/Alert.tsx @@ -1,6 +1,7 @@ import classNames from "classnames"; import type { ReactNode } from "react"; import { forwardRef } from "react"; +import type { IconType } from "react-icons"; import { CheckCircle2, Info, XCircle, AlertTriangle } from "@calcom/ui/components/icon"; @@ -14,9 +15,10 @@ export interface AlertProps { iconClassName?: string; // @TODO: Success and info shouldn't exist as per design? severity: "success" | "warning" | "error" | "info" | "neutral"; + CustomIcon?: IconType; } export const Alert = forwardRef((props, ref) => { - const { severity, iconClassName } = props; + const { severity, iconClassName, CustomIcon } = props; return (
((props, ref) => { severity === "neutral" && "bg-subtle text-default border-none" )}>
-
- {severity === "error" && ( -
+ {CustomIcon ? ( +
+
+ ) : ( +
+ {severity === "error" && ( +
+ )} +

{props.title}

{props.message}
diff --git a/packages/ui/components/card/FormCard.tsx b/packages/ui/components/card/FormCard.tsx index a7ddca0fa8..cac0a11f3d 100644 --- a/packages/ui/components/card/FormCard.tsx +++ b/packages/ui/components/card/FormCard.tsx @@ -5,7 +5,7 @@ import { classNames } from "@calcom/lib"; import type { BadgeProps } from "../.."; import { Badge } from "../.."; import { Divider } from "../divider"; -import { ArrowDown, ArrowUp, Trash } from "../icon"; +import { ArrowDown, ArrowUp, Trash2 } from "../icon"; type Action = { check: () => boolean; fn: () => void }; export default function FormCard({ @@ -68,7 +68,7 @@ export default function FormCard({ deleteField?.fn(); }} color="secondary"> - + ) : null}
diff --git a/packages/ui/components/form/toggleGroup/BooleanToggleGroup.tsx b/packages/ui/components/form/toggleGroup/BooleanToggleGroup.tsx index 01873f5d88..1ff1c27638 100644 --- a/packages/ui/components/form/toggleGroup/BooleanToggleGroup.tsx +++ b/packages/ui/components/form/toggleGroup/BooleanToggleGroup.tsx @@ -10,18 +10,38 @@ import { Label } from "../../../components/form/inputs/Label"; const boolean = (yesNo: "yes" | "no") => (yesNo === "yes" ? true : yesNo === "no" ? false : undefined); const yesNo = (boolean?: boolean) => (boolean === true ? "yes" : boolean === false ? "no" : undefined); +type VariantStyles = { + commonClass?: string; + toggleGroupPrimitiveClass?: string; +}; + +const getVariantStyles = (variant: string) => { + const variants: Record = { + default: { + commonClass: "px-4 w-full py-[10px]", + }, + small: { + commonClass: "w-[49px] px-3 py-1.5", + toggleGroupPrimitiveClass: "space-x-1", + }, + }; + return variants[variant]; +}; + export const BooleanToggleGroup = function BooleanToggleGroup({ defaultValue = true, value, disabled = false, // eslint-disable-next-line @typescript-eslint/no-empty-function onValueChange = () => {}, + variant = "default", ...passThrough }: { defaultValue?: boolean; value?: boolean; onValueChange?: (value?: boolean) => void; disabled?: boolean; + variant?: "default" | "small"; }) { // Maintain a state because it is not necessary that onValueChange the parent component would re-render. Think react-hook-form // Also maintain a string as boolean isn't accepted as ToggleGroupPrimitive value @@ -33,9 +53,11 @@ export const BooleanToggleGroup = function BooleanToggleGroup({ return null; } const commonClass = classNames( - "w-full inline-flex items-center justify-center rounded py-[10px] px-4 text-sm font-medium leading-4", + getVariantStyles(variant).commonClass, + "inline-flex items-center justify-center rounded text-sm font-medium leading-4", disabled && "cursor-not-allowed" ); + const selectedClass = classNames(commonClass, "bg-emphasis text-emphasis"); const unselectedClass = classNames(commonClass, "text-default hover:bg-subtle hover:text-emphasis"); return ( @@ -43,7 +65,10 @@ export const BooleanToggleGroup = function BooleanToggleGroup({ value={yesNoValue} type="single" disabled={disabled} - className="border-subtle flex h-9 space-x-2 rounded-md border p-1 rtl:space-x-reverse" + className={classNames( + "border-subtle flex h-9 space-x-2 rounded-md border p-1 rtl:space-x-reverse", + getVariantStyles(variant).toggleGroupPrimitiveClass + )} onValueChange={(yesNoValue: "yes" | "no") => { setYesNoValue(yesNoValue); onValueChange(boolean(yesNoValue)); diff --git a/packages/ui/components/navigation/tabs/HorizontalTabItem.tsx b/packages/ui/components/navigation/tabs/HorizontalTabItem.tsx index b72a498035..8473b8ee2f 100644 --- a/packages/ui/components/navigation/tabs/HorizontalTabItem.tsx +++ b/packages/ui/components/navigation/tabs/HorizontalTabItem.tsx @@ -31,7 +31,7 @@ const HorizontalTabItem = function ({ name, href, linkProps, avatar, ...props }: href={href} {...linkProps} className={classNames( - isCurrent ? "bg-subtle text-emphasis" : " hover:bg-subtle hover:text-emphasis text-default ", + isCurrent ? "bg-emphasis text-emphasis" : "hover:bg-subtle hover:text-emphasis text-default", "inline-flex items-center justify-center whitespace-nowrap rounded-[6px] p-2 text-sm font-medium leading-4 md:mb-0", props.disabled && "pointer-events-none !opacity-30", props.className diff --git a/packages/ui/components/navigation/tabs/HorizontalTabs.tsx b/packages/ui/components/navigation/tabs/HorizontalTabs.tsx index b8229b7c0b..6ec44cead3 100644 --- a/packages/ui/components/navigation/tabs/HorizontalTabs.tsx +++ b/packages/ui/components/navigation/tabs/HorizontalTabs.tsx @@ -15,7 +15,7 @@ const HorizontalTabs = function ({ tabs, linkProps, actions, ...props }: NavTabP aria-label="Tabs" {...props}> {tabs.map((tab, idx) => ( - + ))} {actions && actions}