minor improvements to routing forms (#3380)
* minor improvements to routing forms * UX improvements and text changes Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Hariom Balhara
kodiakhq[bot]
parent
fd16d734fb
commit
18389f459d
@@ -55,7 +55,7 @@ function Select<
|
||||
IndicatorSeparator: () => null,
|
||||
Input: InputComponent,
|
||||
}}
|
||||
className={classNames("text-sm shadow-sm", className)}
|
||||
className={classNames("text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -93,6 +93,10 @@ const nextConfig = {
|
||||
source: "/team/:teamname/avatar.png",
|
||||
destination: "/api/user/avatar?teamname=:teamname",
|
||||
},
|
||||
{
|
||||
source: "/forms/:formId",
|
||||
destination: "/apps/routing_forms/routing-link/:formId",
|
||||
},
|
||||
/* TODO: have these files being served from another deployment or CDN {
|
||||
source: "/embed/embed.js",
|
||||
destination: process.env.NEXT_PUBLIC_EMBED_LIB_URL?,
|
||||
|
||||
@@ -22,7 +22,7 @@ async function* getResponses(formId: string) {
|
||||
responses.length
|
||||
) {
|
||||
const csv: string[] = [];
|
||||
// Because attributes can be added or removed at any time we can't have fixed columns.
|
||||
// Because fields can be added or removed at any time we can't have fixed columns.
|
||||
// Because there can be huge amount of data we can't keep all that in memory to identify columns from all the data at once.
|
||||
// TODO: So, for now add the field label in front of it. It certainly needs improvement.
|
||||
// TODO: Email CSV when we need to scale it.
|
||||
@@ -38,7 +38,7 @@ async function* getResponses(formId: string) {
|
||||
} else {
|
||||
serializedValue = escapeCsvText(value);
|
||||
}
|
||||
csvLineColumns.push(`"Attribute Label :=> Value"`);
|
||||
csvLineColumns.push(`"Field Label :=> Value"`);
|
||||
csvLineColumns.push(`"${label} :=> ${serializedValue}"`);
|
||||
}
|
||||
csv.push(csvLineColumns.join(","));
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function RoutingNavBar({
|
||||
}) {
|
||||
const tabs = [
|
||||
{
|
||||
name: "Form",
|
||||
name: "Fields",
|
||||
href: `${appUrl}/form-edit/${form?.id}`,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
import { Alert } from "@calcom/ui/Alert";
|
||||
|
||||
import Shell from "@components/Shell";
|
||||
|
||||
import RoutingNavBar from "../components/RoutingNavBar";
|
||||
@@ -16,12 +14,6 @@ const RoutingShell: React.FC<{
|
||||
return (
|
||||
<Shell heading={heading} subtitle={form.description || ""}>
|
||||
<div className="-mx-4 px-4 sm:px-6 md:-mx-8 md:px-8">
|
||||
{!form.routes?.length ? (
|
||||
<Alert severity="warning" title="No routes defined yet" message="" className="mb-4" />
|
||||
) : null}
|
||||
{!form.fields.length ? (
|
||||
<Alert severity="warning" title="No attributes defined yet" message="" className="mb-4" />
|
||||
) : null}
|
||||
<div className="bg-gray-50">
|
||||
<RoutingNavBar appUrl={appUrl} form={form} />
|
||||
{children}
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function SideBar({
|
||||
onCheckedChange={(isChecked) => {
|
||||
mutation.mutate({ ...form, disabled: !isChecked });
|
||||
}}
|
||||
label={!form.disabled ? t("Disable Form") : t("Enable Form")}
|
||||
label={t("Enable Form")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 space-y-1.5">
|
||||
|
||||
@@ -74,7 +74,7 @@ function Field({
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-testid="attribute"
|
||||
data-testid="field"
|
||||
className="group mb-4 flex w-full items-center justify-between hover:bg-neutral-50 ltr:mr-2 rtl:ml-2">
|
||||
{moveUp.check() ? (
|
||||
<button
|
||||
@@ -93,7 +93,7 @@ function Field({
|
||||
<ArrowDownIcon />
|
||||
</button>
|
||||
) : null}
|
||||
<div className="-mx-4 flex flex-1 items-center rounded-sm border border-neutral-200 bg-white p-4 py-6 sm:mx-0 sm:px-8">
|
||||
<div className="-mx-4 flex flex-1 items-center rounded-sm border border-neutral-200 bg-white p-4 sm:mx-0">
|
||||
<div className="w-full">
|
||||
<div className="mt-2 block items-center sm:flex">
|
||||
<div className="min-w-48 mb-4 sm:mb-0">
|
||||
@@ -124,7 +124,7 @@ function Field({
|
||||
const defaultValue = FieldTypes.find((fieldType) => fieldType.value === value);
|
||||
return (
|
||||
<Select
|
||||
className="data-testid-attribute-type"
|
||||
className="data-testid-field-type"
|
||||
options={FieldTypes}
|
||||
onChange={(option) => {
|
||||
if (!option) {
|
||||
@@ -234,7 +234,7 @@ export default function FormEdit({
|
||||
if (!form.fields) {
|
||||
form.fields = [];
|
||||
}
|
||||
const addAttribute = () => {
|
||||
const addField = () => {
|
||||
appendHookFormField({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
@@ -287,7 +287,7 @@ export default function FormEdit({
|
||||
</div>
|
||||
</div>
|
||||
<hr className="mb-5 border-neutral-200" />
|
||||
<h3 className="mb-2 text-base font-medium leading-6 text-gray-900">Attributes</h3>
|
||||
<h3 className="mb-2 text-base font-medium leading-6 text-gray-900">Fields</h3>
|
||||
<div className="flex flex-col">
|
||||
{hookFormFields.map((field, key) => {
|
||||
return (
|
||||
@@ -323,12 +323,12 @@ export default function FormEdit({
|
||||
{hookFormFields.length ? (
|
||||
<div className={classNames("flex")}>
|
||||
<Button
|
||||
data-testid="add-attribute"
|
||||
data-testid="add-field"
|
||||
type="button"
|
||||
StartIcon={PlusIcon}
|
||||
color="secondary"
|
||||
onClick={addAttribute}>
|
||||
Add Attribute
|
||||
onClick={addField}>
|
||||
Add Field
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -346,11 +346,12 @@ export default function FormEdit({
|
||||
<SideBar form={form} appUrl={appUrl} />
|
||||
</div>
|
||||
) : (
|
||||
<button data-testid="add-attribute" onClick={addAttribute} className="w-full">
|
||||
<button data-testid="add-field" onClick={addField} className="w-full">
|
||||
<EmptyScreen
|
||||
Icon={CollectionIcon}
|
||||
headline="Create your first attribute"
|
||||
description="Attributes are the form fields that the booker would see."
|
||||
headline="Create your first field"
|
||||
description="Fields are the form fields that the booker would see."
|
||||
button={<Button>Create Field</Button>}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// TODO: i18n
|
||||
import {
|
||||
TrashIcon,
|
||||
DotsHorizontalIcon,
|
||||
@@ -63,23 +64,28 @@ export default function RoutingForms({
|
||||
},
|
||||
});
|
||||
const formId = uuidv4();
|
||||
|
||||
function NewFormButton() {
|
||||
return (
|
||||
<Button
|
||||
onClick={() => {
|
||||
const form = {
|
||||
id: formId,
|
||||
name: `Form-${formId.slice(0, 8)}`,
|
||||
};
|
||||
mutation.mutate({ ...form, addFallback: true });
|
||||
}}
|
||||
data-testid="new-routing-form"
|
||||
StartIcon={PlusIcon}>
|
||||
New Form
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell
|
||||
heading="Routing Forms"
|
||||
CTA={
|
||||
<Button
|
||||
onClick={() => {
|
||||
const form = {
|
||||
id: formId,
|
||||
name: `Form-${formId.slice(0, 8)}`,
|
||||
};
|
||||
mutation.mutate(form);
|
||||
}}
|
||||
data-testid="new-routing-form"
|
||||
StartIcon={PlusIcon}>
|
||||
New Form
|
||||
</Button>
|
||||
}
|
||||
CTA={<NewFormButton />}
|
||||
subtitle="You can see all routing forms and create one here.">
|
||||
<div className="-mx-4 md:-mx-8">
|
||||
<div className="mb-10 w-full bg-gray-50 px-4 pb-2 sm:px-6 md:px-8">
|
||||
@@ -88,6 +94,7 @@ export default function RoutingForms({
|
||||
Icon={CollectionIcon}
|
||||
headline="Create your first form"
|
||||
description="Forms enable you to allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user"
|
||||
button={<NewFormButton />}
|
||||
/>
|
||||
) : null}
|
||||
{forms.length ? (
|
||||
@@ -129,7 +136,7 @@ export default function RoutingForms({
|
||||
{description.length > 100 && "..."}
|
||||
</h2>
|
||||
<div className="mt-2 text-neutral-500 dark:text-white">
|
||||
{fields.length} attributes, {form.routes.length} routes &{" "}
|
||||
{fields.length} fields, {form.routes.length} routes &{" "}
|
||||
{form._count.responses} Responses
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -195,8 +195,8 @@ const Route = ({
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
<div className="-mx-4 mb-4 flex w-full items-center rounded-sm border border-neutral-200 bg-white sm:mx-0 xl:px-8">
|
||||
<div className="cal-query-builder m-4 my-8 w-full ">
|
||||
<div className="-mx-4 mb-4 flex w-full items-center rounded-sm border border-neutral-200 bg-white p-4 sm:mx-0">
|
||||
<div className="cal-query-builder w-full ">
|
||||
<div>
|
||||
<div className="flex w-full items-center text-sm text-gray-900">
|
||||
<div className="flex flex-grow-0 whitespace-nowrap">
|
||||
|
||||
@@ -235,13 +235,16 @@ export const getServerSideProps = async function getServerSideProps(
|
||||
prisma: AppPrisma
|
||||
) {
|
||||
const { params } = context;
|
||||
console.log("ROUTING LINK");
|
||||
if (!params) {
|
||||
console.log(params);
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
const formId = params.appPages[0];
|
||||
if (!formId || params.appPages.length > 1) {
|
||||
console.log(params.appPages);
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cleanUpForms, todo } from "../lib/testUtils";
|
||||
|
||||
async function addForm(page: Page) {
|
||||
await page.click('[data-testid="new-routing-form"]');
|
||||
await page.waitForSelector('[data-testid="add-attribute"]');
|
||||
await page.waitForSelector('[data-testid="add-field"]');
|
||||
}
|
||||
|
||||
async function verifySelectOptions(
|
||||
@@ -31,19 +31,19 @@ async function fillForm(
|
||||
page: Page,
|
||||
form: { description: string; field?: { typeIndex: number; label: string } }
|
||||
) {
|
||||
await page.click('[data-testid="add-attribute"]');
|
||||
await page.click('[data-testid="add-field"]');
|
||||
await page.fill('[data-testid="description"]', form.description);
|
||||
|
||||
// Verify all Options of SelectBox
|
||||
const { optionsInUi: types } = await verifySelectOptions(
|
||||
{ selector: ".data-testid-attribute-type", nth: 0 },
|
||||
{ selector: ".data-testid-field-type", nth: 0 },
|
||||
["Email", "Long Text", "MultiSelect", "Number", "Phone", "Select", "Short Text"],
|
||||
page
|
||||
);
|
||||
|
||||
if (form.field) {
|
||||
await page.fill('[name="fields.0.label"]', form.field.label);
|
||||
await page.click(".data-testid-attribute-type");
|
||||
await page.click(".data-testid-field-type");
|
||||
await page.locator('[id*="react-select-"][aria-disabled]').nth(form.field.typeIndex).click();
|
||||
}
|
||||
await page.click('[data-testid="update-form"]');
|
||||
@@ -87,11 +87,9 @@ test.describe("Forms", () => {
|
||||
await page.reload();
|
||||
|
||||
expect(await page.inputValue(`[data-testid="description"]`), description);
|
||||
expect(await page.locator('[data-testid="attribute"]').count()).toBe(1);
|
||||
expect(await page.locator('[data-testid="field"]').count()).toBe(1);
|
||||
expect(await page.inputValue('[name="fields.0.label"]')).toBe(field.label);
|
||||
expect(await page.locator(".data-testid-attribute-type").first().innerText()).toBe(
|
||||
types[field.typeIndex]
|
||||
);
|
||||
expect(await page.locator(".data-testid-field-type").first().innerText()).toBe(types[field.typeIndex]);
|
||||
|
||||
await page.click('[href*="/apps/routing_forms/route-builder/"]');
|
||||
await page.click('[data-testid="add-rule"]');
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createProtectedRouter } from "@server/createRouter";
|
||||
@@ -41,11 +42,28 @@ const app_RoutingForms = createProtectedRouter()
|
||||
disabled: z.boolean().optional(),
|
||||
fields: zodFields,
|
||||
routes: zodRoutes,
|
||||
addFallback: z.boolean().optional(),
|
||||
}),
|
||||
async resolve({ ctx: { user, prisma }, input }) {
|
||||
const { name, id, routes, description, disabled } = input;
|
||||
const { name, id, description, disabled, addFallback } = input;
|
||||
let { routes } = input;
|
||||
let { fields } = input;
|
||||
fields = fields || [];
|
||||
|
||||
if (addFallback) {
|
||||
const uuid = uuidv4();
|
||||
routes = routes || [];
|
||||
routes.push({
|
||||
id: uuid,
|
||||
isFallback: true,
|
||||
action: {
|
||||
type: "customPageMessage",
|
||||
value: "Thank you for your interest! We will be in touch soon.",
|
||||
},
|
||||
queryValue: { id: uuid, type: "group" },
|
||||
});
|
||||
}
|
||||
|
||||
return await prisma.app_RoutingForms_Form.upsert({
|
||||
where: {
|
||||
id: id,
|
||||
@@ -73,7 +91,7 @@ const app_RoutingForms = createProtectedRouter()
|
||||
});
|
||||
},
|
||||
})
|
||||
// TODO: Can't se use DELETE method on form?
|
||||
// TODO: Can't we use DELETE method on form?
|
||||
.mutation("deleteForm", {
|
||||
input: z.object({
|
||||
id: z.string(),
|
||||
|
||||
@@ -6,10 +6,12 @@ export default function EmptyScreen({
|
||||
Icon,
|
||||
headline,
|
||||
description,
|
||||
button,
|
||||
}: {
|
||||
Icon: SVGComponent;
|
||||
headline: string;
|
||||
description: string | React.ReactElement;
|
||||
button?: React.ReactElement;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
@@ -21,7 +23,8 @@ export default function EmptyScreen({
|
||||
</div>
|
||||
<div className="max-w-[420px] text-center">
|
||||
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{headline}</h2>
|
||||
<p className="text-sm leading-6 text-gray-600 dark:text-gray-300">{description}</p>
|
||||
<p className="mb-2 text-sm leading-6 text-gray-600 dark:text-gray-300">{description}</p>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -54,7 +54,7 @@ function Select<
|
||||
IndicatorSeparator: () => null,
|
||||
Input: InputComponent,
|
||||
}}
|
||||
className={classNames("text-sm shadow-sm", className)}
|
||||
className={className}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -14384,6 +14384,13 @@ react-easy-crop@^3.5.2:
|
||||
normalize-wheel "^1.0.1"
|
||||
tslib "2.0.1"
|
||||
|
||||
react-feather@^2.0.10:
|
||||
version "2.0.10"
|
||||
resolved "https://registry.yarnpkg.com/react-feather/-/react-feather-2.0.10.tgz#0e9abf05a66754f7b7bb71757ac4da7fb6be3b68"
|
||||
integrity sha512-BLhukwJ+Z92Nmdcs+EMw6dy1Z/VLiJTzEQACDUEnWMClhYnFykJCGWQx+NmwP/qQHGX/5CzQ+TGi8ofg2+HzVQ==
|
||||
dependencies:
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-fit@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-fit/-/react-fit-1.4.0.tgz#6b6e3c75215561cc3cfb9854a6811b4347628666"
|
||||
|
||||
Reference in New Issue
Block a user