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:
Peer Richelsen
2022-07-15 11:46:05 +00:00
committed by GitHub
co-authored by Hariom Balhara kodiakhq[bot]
parent fd16d734fb
commit 18389f459d
15 changed files with 86 additions and 53 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ function Select<
IndicatorSeparator: () => null,
Input: InputComponent,
}}
className={classNames("text-sm shadow-sm", className)}
className={classNames("text-sm", className)}
{...props}
/>
);
+4
View File
@@ -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(),
+4 -1
View File
@@ -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>
</>
+1 -1
View File
@@ -54,7 +54,7 @@ function Select<
IndicatorSeparator: () => null,
Input: InputComponent,
}}
className={classNames("text-sm shadow-sm", className)}
className={className}
{...props}
/>
);
+7
View File
@@ -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"