2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
// eslint-disable-next-line no-restricted-imports
import { noop } from "lodash";
import { useRouter } from "next/navigation";
import type { Dispatch, SetStateAction } from "react";
import { useEffect, useState } from "react";
import classNames from "@calcom/lib/classNames";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
import { Button, Steps } from "../../..";
type DefaultStep = {
title: string;
containerClassname?: string;
contentClassname?: string;
description: string;
content?: ((setIsPending: Dispatch<SetStateAction<boolean>>) => JSX.Element) | JSX.Element;
isEnabled?: boolean;
isPending?: boolean;
};
function WizardForm<T extends DefaultStep>(props: {
href: string;
steps: T[];
disableNavigation?: boolean;
containerClassname?: string;
prevLabel?: string;
nextLabel?: string;
finishLabel?: string;
stepLabel?: React.ComponentProps<typeof Steps>["stepLabel"];
}) {
const searchParams = useCompatSearchParams();
const { href, steps, nextLabel = "Next", finishLabel = "Finish", prevLabel = "Back", stepLabel } = props;
const router = useRouter();
const step = parseInt((searchParams?.get("step") as string) || "1");
const currentStep = steps[step - 1];
const setStep = (newStep: number) => {
router.replace(`${href}?step=${newStep || 1}`);
};
const [currentStepisPending, setCurrentStepisPending] = useState(false);
useEffect(() => {
setCurrentStepisPending(false);
}, [currentStep]);
return (
<div className="mx-auto mt-4 print:w-full" data-testid="wizard-form">
<div className={classNames("overflow-hidden md:mb-2 md:w-[700px]", props.containerClassname)}>
<div className="px-6 py-5 sm:px-14">
<h1 className="font-cal text-emphasis text-2xl" data-testid="step-title">
{currentStep.title}
</h1>
<p className="text-subtle text-sm" data-testid="step-description">
{currentStep.description}
</p>
{!props.disableNavigation && (
<Steps
maxSteps={steps.length}
currentStep={step}
navigateToStep={noop}
stepLabel={stepLabel}
data-testid="wizard-step-component"
/>
)}
</div>
</div>
<div className={classNames("mb-8 overflow-hidden md:w-[700px]", props.containerClassname)}>
<div className={classNames("print:p-none max-w-3xl px-8 py-5 sm:p-6", currentStep.contentClassname)}>
{typeof currentStep.content === "function"
? currentStep.content(setCurrentStepisPending)
: currentStep.content}
</div>
{!props.disableNavigation && (
<div className="flex justify-end px-4 py-4 print:hidden sm:px-6">
{step > 1 && (
<Button
color="secondary"
onClick={() => {
setStep(step - 1);
}}>
{prevLabel}
</Button>
)}
<Button
tabIndex={0}
loading={currentStepisPending}
type="submit"
color="primary"
form={`wizard-step-${step}`}
disabled={currentStep.isEnabled === false}
className="relative ml-2">
{step < steps.length ? nextLabel : finishLabel}
</Button>
</div>
)}
</div>
</div>
);
}
export default WizardForm;

View File

@@ -0,0 +1 @@
export { default as WizardForm } from "./WizardForm";

View File

@@ -0,0 +1,63 @@
import { Canvas, Meta, Story } from "@storybook/addon-docs";
import { CustomArgsTable, Title, VariantsTable, VariantRow } from "@calcom/storybook/components";
import WizardForm from "./WizardForm";
<Meta title="UI/Form/WizardForm" component={WizardForm} />
<Title title="WizardForm" subtitle="Version 1.0 — Last Update: 5 Sep 2023" />
## Definition
The `WizardForm` component provides a structure for creating multi-step forms or wizards.
## Structure
<CustomArgsTable of={WizardForm} />
## Note on Navigation
Please be aware that the steps navigation is managed internally within the `WizardForm` component. As such, when viewing this component in Storybook, clicking the "Next" button will not showcase the transition to the subsequent step.
To observe the actual step navigation behavior, please refer to the Storybook stories for the individual `Steps` component.
## WizardForm Story
<Canvas>
<Story
parameters={{
nextjs: {
appDirectory: true,
},
}}
name="Basic"
args={{
href: "/wizard",
steps: [
{ title: "Step 1", description: "Description for Step 1" },
{ title: "Step 2", description: "Description for Step 2" },
{ title: "Step 3", description: "Description for Step 3" },
],
}}
argTypes={{
href: {
control: {
type: "text",
},
},
steps: {
control: {
type: "object",
},
},
}}>
{({ href, steps }) => (
<VariantsTable titles={["Basic Wizard Form"]} columnMinWidth={150}>
<VariantRow>
<WizardForm href={href} steps={steps} />
</VariantRow>
</VariantsTable>
)}
</Story>
</Canvas>

View File

@@ -0,0 +1,144 @@
/* eslint-disable playwright/missing-playwright-await */
import { render, waitFor } from "@testing-library/react";
import { vi } from "vitest";
import WizardForm from "./WizardForm";
vi.mock("@calcom/lib/hooks/useCompatSearchParams", () => ({
useCompatSearchParams() {
return { get: vi.fn().mockReturnValue(currentStepNavigation) };
},
}));
vi.mock("next/navigation", () => ({
useRouter() {
return { replace: vi.fn() };
},
useSearchParams() {
return { get: vi.fn().mockReturnValue(currentStepNavigation) };
},
}));
const steps = [
{
title: "Step 1",
description: "Description 1",
content: <p data-testid="content-1">Step 1</p>,
isEnabled: false,
},
{
title: "Step 2",
description: "Description 2",
content: (setIsPending: (value: boolean) => void) => (
<button data-testid="content-2" onClick={() => setIsPending(true)}>
Test
</button>
),
isEnabled: true,
},
{ title: "Step 3", description: "Description 3", content: <p data-testid="content-3">Step 3</p> },
];
const props = {
href: "/test/mock",
steps: steps,
nextLabel: "Next step",
prevLabel: "Previous step",
finishLabel: "Finish",
};
let currentStepNavigation: number;
const renderComponent = (extraProps?: { disableNavigation: boolean }) =>
render(<WizardForm {...props} {...extraProps} />);
describe("Tests for WizardForm component", () => {
test("Should handle all the steps correctly", async () => {
currentStepNavigation = 1;
const { queryByTestId, queryByText, rerender } = renderComponent();
const { prevLabel, nextLabel, finishLabel } = props;
const stepInfo = {
title: queryByTestId("step-title"),
description: queryByTestId("step-description"),
};
await waitFor(() => {
steps.forEach((step, index) => {
rerender(<WizardForm {...props} />);
const { title, description } = step;
const buttons = {
prev: queryByText(prevLabel),
next: queryByText(nextLabel),
finish: queryByText(finishLabel),
};
expect(stepInfo.title).toHaveTextContent(title);
expect(stepInfo.description).toHaveTextContent(description);
if (index === 0) {
// case of first step
expect(buttons.prev && buttons.finish).not.toBeInTheDocument();
expect(buttons.next).toBeInTheDocument();
} else if (index === steps.length - 1) {
// case of last step
expect(buttons.prev && buttons.finish).toBeInTheDocument();
expect(buttons.next).not.toBeInTheDocument();
} else {
// case of in-between steps
expect(buttons.prev && buttons.next).toBeInTheDocument();
expect(buttons.finish).not.toBeInTheDocument();
}
currentStepNavigation++;
});
});
});
describe("Should handle the visibility of the content", async () => {
test("Should render JSX content correctly", async () => {
currentStepNavigation = 1;
const { getByTestId, getByText } = renderComponent();
const currentStep = steps[0];
expect(getByTestId("content-1")).toBeInTheDocument();
expect(getByText(currentStep.title && currentStep.description)).toBeInTheDocument();
});
test("Should render function content correctly", async () => {
currentStepNavigation = 2;
const { getByTestId, getByText } = renderComponent();
const currentStep = steps[1];
expect(getByTestId("content-2")).toBeInTheDocument();
expect(getByText(currentStep.title && currentStep.description)).toBeInTheDocument();
});
});
test("Should disable 'Next step' button if current step navigation is not enabled", async () => {
currentStepNavigation = 1;
const { nextLabel } = props;
const { getByText } = renderComponent();
expect(getByText(nextLabel)).toBeDisabled();
});
test("Should handle when navigation is disabled", async () => {
const { queryByText, queryByTestId } = renderComponent({ disableNavigation: true });
const { prevLabel, nextLabel, finishLabel } = props;
const stepComponent = queryByTestId("wizard-step-component");
const stepInfo = {
title: queryByTestId("step-title"),
description: queryByTestId("step-description"),
};
const buttons = {
prev: queryByText(prevLabel),
next: queryByText(nextLabel),
finish: queryByText(finishLabel),
};
expect(stepInfo.title && stepInfo.description).toBeInTheDocument();
expect(stepComponent).not.toBeInTheDocument();
expect(buttons.prev && buttons.next && buttons.finish).not.toBeInTheDocument();
});
});