diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml new file mode 100644 index 0000000..bc6877f --- /dev/null +++ b/.github/workflows/cypress.yml @@ -0,0 +1,30 @@ +name: Cypress Tests + +on: push + +jobs: + cypress-run: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: "20" + + - name: Install dependencies + run: npm ci + + - name: Build packages + run: npm run build + + - name: Cypress run + uses: cypress-io/github-action@v6 + with: + install-command: npm install + project: apps/web + start: npm run cypress + browser: chrome + component: true diff --git a/README.md b/README.md index f45294f..67d23ff 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ For releases, AutoForm uses changesets. To create a new release, run: ```bash npm run build +npm run cypress # Run the component tests npx changeset ``` diff --git a/apps/docs/pages/docs/react/_meta.ts b/apps/docs/pages/docs/react/_meta.ts index 4f20cd7..7a27e95 100644 --- a/apps/docs/pages/docs/react/_meta.ts +++ b/apps/docs/pages/docs/react/_meta.ts @@ -3,4 +3,5 @@ export default { migration: "Migration from v1", customization: "Customization", "custom-integration": "Custom integration", + api: "API", }; diff --git a/apps/docs/pages/docs/react/api.mdx b/apps/docs/pages/docs/react/api.mdx new file mode 100644 index 0000000..dba7814 --- /dev/null +++ b/apps/docs/pages/docs/react/api.mdx @@ -0,0 +1,37 @@ +# API + +## schema: SchemaProvider + +The `schema` prop is used to pass the schema to the form. It can be a `ZodProvider` or a `YupProvider`. + +## onSubmit?: (values: T, form: FormInstance) => void + +The `onSubmit` prop is called when the form is submitted and the data is valid. It receives the form data and the form instance from react-hook-form. + +## onFormInit?: (form: FormInstance) => void + +The `onFormInit` prop is called when the form is initialized. It receives the form instance from react-hook-form. + +## withSubmit?: boolean + +The `withSubmit` prop is used to automatically add a submit button to the form. It defaults to `false`. + +## defaultValues?: Partial\ + +The `defaultValues` prop is used to set the initial values of the form. + +## values: Partial\ + +The `values` prop is used to set the values of the form. It is a controlled input. + +## children: React.ReactNode + +All children passed to the `AutoForm` component will be rendered below the form. + +## formComponents: Partial\ + +Additional, custom form components can be passed to the `formComponents` prop. This allows you to add custom field types to the form. + +## uiComponents: Partial\ + +Override the default UI components with custom components. This allows you to customize the look and feel of the form. diff --git a/apps/docs/pages/docs/react/custom-integration.mdx b/apps/docs/pages/docs/react/custom-integration.mdx index dd8ab0c..875c7c3 100644 --- a/apps/docs/pages/docs/react/custom-integration.mdx +++ b/apps/docs/pages/docs/react/custom-integration.mdx @@ -129,19 +129,10 @@ import { AutoFormFieldProps } from "@autoform/react"; export const StringField: React.FC = ({ field, - value, - onChange, + inputProps, error, id, -}) => ( - onChange(e.target.value)} - {...field.fieldConfig?.inputProps} - /> -); +}) => ; ``` ```tsx @@ -151,19 +142,9 @@ import { AutoFormFieldProps } from "@autoform/react"; export const NumberField: React.FC = ({ field, - value, - onChange, - error, + inputProps, id, -}) => ( - onChange(Number(e.target.value))} - {...field.fieldConfig?.inputProps} - /> -); +}) => ; ``` ```tsx @@ -173,19 +154,9 @@ import { AutoFormFieldProps } from "@autoform/react"; export const DateField: React.FC = ({ field, - value, - onChange, - error, + inputProps, id, -}) => ( - onChange(new Date(e.target.value))} - {...field.fieldConfig?.inputProps} - /> -); +}) => ; ``` Additionally, you can create custom field components for other types like checkboxes, radio buttons, etc. These can later be used by setting a custom `fieldType` in the schema. @@ -234,28 +205,6 @@ export function AutoForm>( } ``` -## Implement utility functions - -To provide type-safety when selecting field types, you should create a custom wrapper for the `fieldConfig` function. - -Create a new file called `src/utils.ts`: - -```tsx -// src/utils.ts -import { FieldConfig } from "@autoform/core"; -import { SuperRefineFunction } from "@autoform/zod"; -import { fieldConfig as baseFieldConfig } from "@autoform/react"; -import { ReactNode } from "react"; - -type FieldTypes = "string" | "number" | "date" | string; - -export function fieldConfig( - config: FieldConfig -): SuperRefineFunction { - return baseFieldConfig(config); -} -``` - ## Add custom field types (optional) If you want to add custom field types, you can create new components and add them to the `formComponents` object in `src/AutoForm.tsx`. For example: diff --git a/apps/docs/pages/docs/react/customization.md b/apps/docs/pages/docs/react/customization.md index ddd497c..1f613d5 100644 --- a/apps/docs/pages/docs/react/customization.md +++ b/apps/docs/pages/docs/react/customization.md @@ -2,13 +2,23 @@ The customization of the components is done by providing a `fieldConfig` to your schema fields. This allows you to customize the rendering of the field, add additional props, and more. -With zod, you can use the `superRefine` method to add a `fieldConfig` to your schema field. This method is used to add additional validation and configuration to your field. +With zod, you can use the `superRefine` method to add a `fieldConfig` to your schema field. For more information, see the [Zod documentation](/docs/schema/zod). -You should import `fieldConfig` from your AutoForm UI-specific package (e.g. `@autoform/mui`) so it will be type-safe for your specific UI. If you use a custom UI, you can import `fieldConfig` from `@autoform/react` or `@autoform/core`. +With yup, you can use the `transform` method to add a `fieldConfig` to your schema field. For more information, see the [Yup documentation](/docs/schema/yup). In these examples, we will use Zod. + +You should import `buildZodFieldConfig` or `buildYupFieldConfig` from `@autoform/react` and customize it. ```tsx import * as z from "zod"; -import { fieldConfig } from "@autoform/mui"; // or your UI library +import { buildZodFieldConfig } from "@autoform/react"; +import { FieldTypes } from "@autoform/mui"; + +const fieldConfig = buildZodFieldConfig< + FieldTypes, // You should provide the "FieldTypes" type from the UI library you use + { + isImportant?: boolean; // You can add custom props here + } +>(); const schema = z.object({ username: z.string().superRefine( @@ -17,7 +27,10 @@ const schema = z.object({ inputProps: { placeholder: "Enter your username", }, - }), + customData: { + isImportant: true, // You can add custom data here + }, + }) ), // ... }); @@ -35,7 +48,7 @@ const schema = z.object({ type: "text", placeholder: "Username", }, - }), + }) ), }); // This will be rendered as: @@ -51,13 +64,54 @@ const schema = z.object({ username: z.string().superRefine( fieldConfig({ fieldType: "textarea", - }), + }) ), }); ``` The list of available fields depends on the UI library you use - use the autocomplete in your IDE to see the available options. +### Custom field types + +You can also add your own custom field types. To do this, you need to extend the `formComponents` prop of your AutoForm component and add your custom field type. + +```tsx + { + return ( +
+ +
+ ); + }, + }} +/>; + +const fieldConfig = buildZodFieldConfig< + FieldTypes | "custom", + { + isImportant?: boolean; + } +>(); + +const schema = z.object({ + username: z.string().superRefine( + fieldConfig({ + fieldType: "custom", + }) + ), +}); +``` + +Please note that this will still render the default `FieldWrapper` around your input field, which contains the label and error message. If you want to customize this, you can use the `fieldWrapper` property ([see below](#custom-field-wrapper)). + ## Description You can use the `description` property to add a description below the field. @@ -68,9 +122,80 @@ const schema = z.object({ fieldConfig({ description: "Enter a unique username. This will be shown to other users.", - }), + }) ), }); ``` You can use JSX in the description. + +## Order + +If you want to change the order of fields, use the order config. You can pass an arbitrary number where smaller numbers will be displayed first. All fields without a defined order use "0" so they appear in the same order they are defined in + +```tsx +const schema = z.object({ + termsOfService: z.boolean().superRefine( + fieldConfig({ + order: 1, // This will be displayed after other fields with order 0 + }) + ), + + username: z.string().superRefine( + fieldConfig({ + order: -1, // This will be displayed first + }) + ), + + email: z.string().superRefine( + fieldConfig({ + // Without specifying an order, this will have order 0 + }) + ), +}); +``` + +## Custom field wrapper + +You can use the `fieldWrapper` property to wrap the field in a custom component. This is useful if you want to add additional elements to the field. + +The `fieldWrapper` is responsible for rendering the field label and error, so when you use a custom `fieldWrapper`, you need to handle these yourself. You can take a look at the `FieldWrapperProps` type to see what props are passed to the `fieldWrapper`. + +```tsx +const schema = z.object({ + email: z.string().superRefine( + fieldConfig({ + fieldWrapper: (props: FieldWrapperProps) => { + return ( + <> + {props.children} +

+ Don't worry, we won't share your email with anyone! +

+ + ); + }, + }) + ), +}); +``` + +## Override UI components + +You can also override the default UI components with custom components. This allows you to customize the look and feel of the form. + +```tsx + { + return ( +
+ + {children} + {error} +
+ ); + }, + }} +/> +``` diff --git a/apps/docs/pages/docs/react/getting-started.mdx b/apps/docs/pages/docs/react/getting-started.mdx index 89a5609..d35e170 100644 --- a/apps/docs/pages/docs/react/getting-started.mdx +++ b/apps/docs/pages/docs/react/getting-started.mdx @@ -46,7 +46,7 @@ function MyForm() { return ( { + onSubmit={(data, form) => { console.log(data); }} withSubmit @@ -81,7 +81,7 @@ export default function MyForm() { ## Accessing the form data -There are two ways to access the form data: +The form data is managed by react-hook-form. There are two ways to access the form data: ### onSubmit @@ -89,26 +89,38 @@ The preferred way is to use the `onSubmit` prop. This will be called when the fo ```tsx { + onSubmit={(data, form) => { // Do something with the data // Data is validated and coerced with zod automatically - // You can use setError to set errors for the fields - // You can run clearForm() to clear the form + // You can use the "form" prop to access the underlying "react-hook-form" instance + // for further information and control over the form }} /> ``` -### Controlled form +### Using onFormInit and `values` -You can also use the `values` and `setValues` props to control the form data yourself. +If you need more control over the form, you can use the `onFormInit` and `values` props to get access to the react-hook-form instance and control data directly: ```tsx -const [values, setValues] = useState>>({}); +const [values, setValues] = useState({}); -; + { + // You can use the "form" prop to access the underlying "react-hook-form" instance + // https://www.react-hook-form.com/api/useform/ + form.watch((data) => { + setValues(data); + }); + + // You can freely save the form instance to a state or context and use it later to access the form state + form.formState.isDirty; // => true + }} + values={values} +/>; ``` -Please note that the data is not validated or coerced when using this method as they update immediately. +This allows you to access form methods and state from the parent component. You can use the `onFormInit` prop independent of controlled forms to access the form instance. ## Submitting the form diff --git a/apps/docs/pages/docs/react/migration.mdx b/apps/docs/pages/docs/react/migration.mdx index 4952845..56afd88 100644 --- a/apps/docs/pages/docs/react/migration.mdx +++ b/apps/docs/pages/docs/react/migration.mdx @@ -4,11 +4,8 @@ This guide will help you migrate from the old, pure shadcn/ui component to the n Please note that the new library does not have full feature-partity with the old one - specifically, it does not support the following features (yet): -- AutoForm doesn't use `react-hook-form` under the hood, so information about the form state is not available -- Custom parent components -- Custom order - Dependencies between fields -- Declaring custom field components inline +- Declaring custom field components inline. This should instead be done using the `formComponents` prop of the `AutoForm` component. But the new AutoForm library has a lot of new features, such as: @@ -16,6 +13,7 @@ But the new AutoForm library has a lot of new features, such as: - Support for other schema libraries, not just zod - Cleaner, in-schema fieldConfig definition - Cleaner shadcn/ui components, as they are now just wrappers around the AutoForm library +- Support for customizing the form components and UI components on a per-form basis, not just globally import { Steps } from "nextra/components"; @@ -73,9 +71,11 @@ Instead of passing the `fieldConfig` prop to the `AutoForm` component, you can n ```diff import * as z from "zod"; -+ import { fieldConfig } from "@autoform/zod"; ++ import { buildZodFieldConfig } from "@autoform/react"; + import { FieldTypes } from "@/components/ui/autoform"; ++ const fieldConfig = buildZodFieldConfig(); + const formSchema = z.object({ password: z .string() @@ -93,4 +93,62 @@ const formSchema = z.object({ }); ``` +## Update callbacks + +If you are only using `onSubmit`, your code should be able to work without any changes. If you are using controlled input instead, you need to update your code to use the new `onFormInit` prop to get manual access to the form instance. + +```tsx +const [values, setValues] = useState({}); + + { + // You can use the "form" prop to access the underlying "react-hook-form" instance + // https://www.react-hook-form.com/api/useform/ + form.watch((data) => { + setValues(data); + }); + + // You can freely save the form instance to a state or context and use it later to access the form state + form.formState.isDirty; // => true + }} + values={values} +/>; +``` + +## Update custom components + +If you are using custom parent components or custom field components, you need to update your code to use the new `formComponents` and `uiComponents` props and the `fieldWrapper` field config. + +```tsx +const schema = z.object({ + username: z.string().superRefine( + fieldConfig({ + fieldWrapper: (props) => ( +
+ + {props.children} +
+ ), + }) + ), +}); + + { + return ( +
+ +
+ ); + }, + }} +/>; +``` + diff --git a/apps/docs/pages/docs/schema/yup.md b/apps/docs/pages/docs/schema/yup.md index aad49b6..43c2d7b 100644 --- a/apps/docs/pages/docs/schema/yup.md +++ b/apps/docs/pages/docs/schema/yup.md @@ -4,8 +4,18 @@ Basic usage: ```tsx "use client"; -import { YupProvider, fieldConfig } from "@autoform/yup"; +import { YupProvider } from "@autoform/yup"; import { object, string, number, date, InferType, array, mixed } from "yup"; +import { buildYupFieldConfig } from "@autoform/react"; +import { AutoForm, FieldTypes } from "@autoform/mui"; // use any UI library + +const fieldConfig = buildYupFieldConfig< + FieldTypes, + { + // You can define custom props here + isImportant?: boolean; + } +>(); // Define your form schema using zod @@ -28,6 +38,10 @@ const yupFormSchema = object({ inputProps: { type: "email", }, + customData: { + // You can add custom data here + isImportant: true, + }, }) ), website: string().url().nullable(), @@ -145,12 +159,16 @@ const formSchema = object({ You can use the `fieldConfig` function to set additional configuration for how a field should be rendered. This function is independent of the UI library you use so you can provide the FieldTypes that are supported by your UI library. -It's recommended that you create your own fieldConfig function that uses the base fieldConfig function from `@autoform/zod` and adds your own customizations: +It's recommended that you create your own fieldConfig function that uses the base fieldConfig function from `@autoform/react` and adds your own customizations: ```tsx -import { fieldConfig as baseFieldConfig } from "@autoform/yup"; +import { buildYupFieldConfig } from "@autoform/react"; import { FieldTypes } from "@autoform/mui"; -export const fieldConfig = (config: FieldConfig) => - baseFieldConfig(config); +export const fieldConfig = buildYupFieldConfig< + FieldTypes, // You should provide the "FieldTypes" type from the UI library you use + { + isImportant?: boolean; // You can add custom props here + } +>(); ``` diff --git a/apps/docs/pages/docs/schema/zod.md b/apps/docs/pages/docs/schema/zod.md index fca598d..f60a6f0 100644 --- a/apps/docs/pages/docs/schema/zod.md +++ b/apps/docs/pages/docs/schema/zod.md @@ -5,9 +5,18 @@ Basic usage: ```tsx "use client"; import * as z from "zod"; -import { ZodProvider, fieldConfig } from "@autoform/zod"; +import { ZodProvider } from "@autoform/zod"; +import { buildZodFieldConfig } from "@autoform/react"; import { AutoForm, FieldTypes } from "@autoform/mui"; // use any UI library +const fieldConfig = buildZodFieldConfig< + FieldTypes, + { + // You can define custom props here + isImportant?: boolean; + } +>(); + // Define your form schema using zod const formSchema = z.object({ username: z @@ -38,6 +47,10 @@ const formSchema = z.object({ inputProps: { type: "password", }, + customData: { + // You can add custom data here + isImportant: true, + }, }) ), @@ -232,12 +245,16 @@ const formSchema = z.object({ You can use the `fieldConfig` function to set additional configuration for how a field should be rendered. This function is independent of the UI library you use so you can provide the FieldTypes that are supported by your UI library. -It's recommended that you create your own fieldConfig function that uses the base fieldConfig function from `@autoform/zod` and adds your own customizations: +It's recommended that you create your own fieldConfig function that uses the base fieldConfig function from `@autoform/react` and adds your own customizations: ```tsx -import { fieldConfig as baseFieldConfig } from "@autoform/zod"; +import { buildZodFieldConfig } from "@autoform/react"; import { FieldTypes } from "@autoform/mui"; -export const fieldConfig = (config: FieldConfig) => - baseFieldConfig(config); +const fieldConfig = buildZodFieldConfig< + FieldTypes, + { + isImportant?: boolean; + } +>(); ``` diff --git a/apps/docs/pages/docs/technical/todo.mdx b/apps/docs/pages/docs/technical/todo.mdx index 34b0e0f..2f1af33 100644 --- a/apps/docs/pages/docs/technical/todo.mdx +++ b/apps/docs/pages/docs/technical/todo.mdx @@ -28,5 +28,5 @@ import { Callout } from "nextra/components"; - [ ] Joi - [ ] Clean up documentation - [ ] Dependencies -- [ ] Custom field types via prop to AutoForm -- [ ] Software tests +- [x] Custom field types via prop to AutoForm +- [x] Software tests diff --git a/apps/web/components/Basics.tsx b/apps/web/components/Basics.tsx index d54993e..e210a2a 100644 --- a/apps/web/components/Basics.tsx +++ b/apps/web/components/Basics.tsx @@ -1,6 +1,7 @@ -import { AutoForm, FieldTypes } from "@autoform/mui"; +import { AutoForm } from "@autoform/shadcn/components/ui/autoform/AutoForm"; import { ZodProvider } from "@autoform/zod"; import { zodSchemaProvider } from "./utils"; +import { AutoFormFieldProps } from "@autoform/react"; function Basics() { return ( @@ -9,7 +10,23 @@ function Basics() { onSubmit={(data) => { console.log(JSON.stringify(data, null, 2)); }} + onFormInit={(form) => { + console.log("Form initialized", form); + }} withSubmit + formComponents={{ + custom: ({ field, label, inputProps }: AutoFormFieldProps) => { + return ( +
+ +
+ ); + }, + }} /> ); } diff --git a/apps/web/components/utils.tsx b/apps/web/components/utils.tsx index 6cdb117..36f367d 100644 --- a/apps/web/components/utils.tsx +++ b/apps/web/components/utils.tsx @@ -1,9 +1,20 @@ -import { fieldConfig } from "@autoform/react"; +import { + fieldConfig, + FieldWrapperProps, + buildZodFieldConfig, +} from "@autoform/react"; import { ZodProvider } from "@autoform/zod"; import { YupProvider, fieldConfig as yupFieldConfig } from "@autoform/yup"; import * as z from "zod"; import { object, string, number, date, InferType, array, mixed } from "yup"; +const customFieldConfig = buildZodFieldConfig< + string, + { + isImportant?: boolean; + } +>(); + enum Sports { Football = "Football/Soccer", Basketball = "Basketball", @@ -13,7 +24,20 @@ enum Sports { } const zodFormSchema = z.object({ - hobbies: z.array(z.string()).optional(), + // hobbies: z + // .string() + // .optional() + // .superRefine( + // customFieldConfig({ + // description: "This uses a custom field component", + // order: 1, + // fieldType: "custom", + // customData: { + // // You can define custom data here + // isImportant: true, + // }, + // }) + // ), // username: z // .string({ // required_error: "Username is required.", @@ -48,7 +72,7 @@ const zodFormSchema = z.object({ // }) // ), - // favouriteNumber: z.coerce + // favouriteNumber: z // .number({ // invalid_type_error: "Favourite number must be a number.", // }) @@ -61,15 +85,31 @@ const zodFormSchema = z.object({ // .default(1) // .optional(), - // acceptTerms: z - // .boolean() - // .describe("Accept terms and conditions.") - // .refine((value) => value, { - // message: "You must accept the terms and conditions.", - // path: ["acceptTerms"], - // }), + acceptTerms: z + .boolean() + .describe("Accept terms and conditions.") + .refine((value) => value, { + message: "You must accept the terms and conditions.", + path: ["acceptTerms"], + }), - // sendMeMails: z.boolean().optional(), + // sendMeMails: z + // .boolean() + // .optional() + // .superRefine( + // fieldConfig({ + // fieldWrapper: (props: FieldWrapperProps) => { + // return ( + // <> + // {props.children} + //

+ // Don't worry, we only send important emails! + //

+ // + // ); + // }, + // }) + // ), // birthday: z.coerce.date().optional(), @@ -83,15 +123,15 @@ const zodFormSchema = z.object({ // // Native enum example // sports: z.nativeEnum(Sports).describe("What is your favourite sport?"), - guests: z.array( - z.object({ - name: z.string().optional(), - age: z.number().optional(), - }) - ), + // guests: z.array( + // z.object({ + // name: z.string().optional(), + // age: z.coerce.number().optional(), + // }) + // ), // location: z.object({ - // city: z.string().optional(), + // city: z.string(), // country: z.string().optional(), // }), }); diff --git a/apps/web/cypress.config.ts b/apps/web/cypress.config.ts new file mode 100644 index 0000000..2bb1e5a --- /dev/null +++ b/apps/web/cypress.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + component: { + devServer: { + framework: "next", + bundler: "webpack", + }, + }, +}); diff --git a/apps/web/cypress/component/autoform/all.cy.tsx b/apps/web/cypress/component/autoform/all.cy.tsx new file mode 100644 index 0000000..5b20e97 --- /dev/null +++ b/apps/web/cypress/component/autoform/all.cy.tsx @@ -0,0 +1,4 @@ +import "./mui-yup/all.cy"; +import "./mui-zod/all.cy"; +import "./mantine-zod/all.cy"; +import "./shadcn-zod/all.cy"; diff --git a/apps/web/cypress/component/autoform/mantine-zod/advanced-features.cy.tsx b/apps/web/cypress/component/autoform/mantine-zod/advanced-features.cy.tsx new file mode 100644 index 0000000..20cc30a --- /dev/null +++ b/apps/web/cypress/component/autoform/mantine-zod/advanced-features.cy.tsx @@ -0,0 +1,145 @@ +import React from "react"; +import { AutoForm } from "@autoform/mui"; +import { ZodProvider, fieldConfig } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Advanced Features Tests", () => { + const advancedSchema = z.object({ + username: z + .string() + .min(3, "Username must be at least 3 characters") + .superRefine( + fieldConfig({ + description: "Choose a unique username", + order: 1, + inputProps: { + placeholder: "Enter username", + }, + }) + ), + password: z + .string() + .min(8, "Password must be at least 8 characters") + .superRefine( + fieldConfig({ + description: "Use a strong password", + order: 2, + inputProps: { + type: "password", + }, + }) + ), + favoriteColor: z.enum(["red", "green", "blue"]).superRefine( + fieldConfig({ + fieldType: "select", + order: 3, + }) + ), + bio: z + .string() + .optional() + .superRefine( + fieldConfig({ + order: 4, + }) + ), + }); + + const schemaProvider = new ZodProvider(advancedSchema); + + it("renders fields in the correct order", () => { + cy.mount( + + + + ); + + cy.get(".MuiFormControl-root") + .eq(0) + .find("input") + .should("have.attr", "name", "username"); + cy.get(".MuiFormControl-root") + .eq(1) + .find("input") + .should("have.attr", "name", "password"); + cy.get(".MuiFormControl-root").eq(2).find(".MuiSelect-select"); + cy.get(".MuiFormControl-root") + .eq(3) + .find("input") + .should("have.attr", "name", "bio"); + }); + + it("displays field descriptions", () => { + cy.mount( + + + + ); + + cy.contains("Choose a unique username").should("be.visible"); + cy.contains("Use a strong password").should("be.visible"); + }); + + it("applies custom input props", () => { + cy.mount( + + + + ); + + cy.get('input[name="username"]').should( + "have.attr", + "placeholder", + "Enter username" + ); + cy.get('input[name="password"]').should("have.attr", "type", "password"); + }); + + it("renders select field correctly", () => { + cy.mount( + + + + ); + + cy.get('.MuiSelect-select[aria-labelledby*="favoriteColor"]').should( + "exist" + ); + cy.get('.MuiSelect-select[aria-labelledby*="favoriteColor"]').click(); + cy.get('.MuiMenu-list[role="listbox"] .MuiMenuItem-root').should( + "have.length", + 3 + ); + }); + + it("renders textarea field correctly", () => { + cy.mount( + + + + ); + + cy.get('input[name="bio"]').should("exist"); + }); +}); diff --git a/apps/web/cypress/component/autoform/mantine-zod/all.cy.tsx b/apps/web/cypress/component/autoform/mantine-zod/all.cy.tsx new file mode 100644 index 0000000..493695b --- /dev/null +++ b/apps/web/cypress/component/autoform/mantine-zod/all.cy.tsx @@ -0,0 +1,7 @@ +import "./basic.cy"; +import "./arrays.cy"; +import "./subobjects.cy"; +import "./advanced-features.cy"; +import "./validation.cy"; +import "./controlled-form.cy"; +import "./ui-customization.cy"; diff --git a/apps/web/cypress/component/autoform/mantine-zod/arrays.cy.tsx b/apps/web/cypress/component/autoform/mantine-zod/arrays.cy.tsx new file mode 100644 index 0000000..b18fae2 --- /dev/null +++ b/apps/web/cypress/component/autoform/mantine-zod/arrays.cy.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { AutoForm } from "@autoform/mantine"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Arrays Tests", () => { + const arraySchema = z.object({ + tags: z.array(z.string()), + friends: z.array( + z.object({ + name: z.string(), + age: z.coerce.number(), + }) + ), + }); + + const schemaProvider = new ZodProvider(arraySchema); + + it("renders array fields correctly", () => { + cy.mount( + + + + ); + + cy.get('[data-testid="add-item-button"]').should("exist"); + cy.get('[data-testid="add-item-button"]').should("exist"); + }); + + it("allows adding and removing array items", () => { + cy.mount( + + + + ); + + // Add tags + cy.get('[data-testid="add-item-button"]').eq(0).click(); + cy.get('input[name="tags.0"]').type("tag1"); + cy.get('[data-testid="add-item-button"]').eq(0).click(); + cy.get('input[name="tags.1"]').type("tag2"); + + // Add friends + cy.get('[data-testid="add-item-button"]').eq(1).click(); + cy.get('input[name="friends.0.name"]').type("Alice"); + cy.get('input[name="friends.0.age"]').type("25"); + cy.get('[data-testid="add-item-button"]').eq(1).click(); + cy.get('input[name="friends.1.name"]').type("Bob"); + cy.get('input[name="friends.1.age"]').type("30"); + + // Remove a tag and a friend + cy.get('[data-testid="remove-item-button"]').eq(0).click(); + cy.get('[data-testid="remove-item-button"]').eq(1).click(); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + tags: ["tag2"], + friends: [{ name: "Bob", age: 30 }], + }); + }); +}); diff --git a/apps/web/cypress/component/autoform/mantine-zod/basic.cy.tsx b/apps/web/cypress/component/autoform/mantine-zod/basic.cy.tsx new file mode 100644 index 0000000..7a30c3f --- /dev/null +++ b/apps/web/cypress/component/autoform/mantine-zod/basic.cy.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { AutoForm } from "@autoform/mantine"; +import { ZodProvider, fieldConfig } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Basic Tests", () => { + const basicSchema = z.object({ + name: z.string().min(2, "Name must be at least 2 characters"), + age: z.coerce.number().min(18, "Must be at least 18 years old"), + email: z.string().email("Invalid email address"), + website: z.string().url("Invalid URL").optional(), + birthdate: z.coerce.date(), + isStudent: z.boolean(), + }); + + const schemaProvider = new ZodProvider(basicSchema); + + it("renders all field types correctly", () => { + cy.mount( + + + + ); + + cy.get('input[name="name"]').should("exist"); + cy.get('input[name="age"]').should("have.attr", "type", "number"); + cy.get('input[name="email"]').should("exist"); + cy.get('input[name="website"]').should("exist"); + cy.get('[data-dates-input="true"]').should("exist"); + cy.get('input[name="isStudent"]').should("have.attr", "type", "checkbox"); + }); + + it("submits form with correct data types", () => { + const onSubmit = cy.stub().as("onSubmit"); + cy.mount( + + + + ); + + cy.get('input[name="name"]').type("John Doe"); + cy.get('input[name="age"]').type("25"); + cy.get('input[name="email"]').type("john@example.com"); + cy.get('input[name="website"]').type("https://example.com"); + cy.get('[data-dates-input="true"]').type("1990-01-01"); + cy.get('input[name="isStudent"]').check(); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + name: "John Doe", + age: 25, + email: "john@example.com", + website: "https://example.com", + birthdate: new Date("1990-01-01"), + isStudent: true, + }); + }); +}); diff --git a/apps/web/cypress/component/autoform/mantine-zod/controlled-form.cy.tsx b/apps/web/cypress/component/autoform/mantine-zod/controlled-form.cy.tsx new file mode 100644 index 0000000..4e64472 --- /dev/null +++ b/apps/web/cypress/component/autoform/mantine-zod/controlled-form.cy.tsx @@ -0,0 +1,55 @@ +import React, { useState } from "react"; +import { AutoForm } from "@autoform/mantine"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +const ControlledForm = () => { + const [formValues, setFormValues] = useState({ + name: "John Doe", + email: "john@example.com", + }); + + const schema = z.object({ + name: z.string(), + email: z.string().email(), + }); + + const schemaProvider = new ZodProvider(schema); + + return ( + + { + form.watch((data) => { + setFormValues(data as typeof formValues); + }); + }} + /> + + ); +}; + +describe("AutoForm Controlled Form Tests", () => { + it("renders with initial values", () => { + cy.mount(); + + cy.get('input[name="name"]').should("have.value", "John Doe"); + cy.get('input[name="email"]').should("have.value", "john@example.com"); + }); + + it("updates controlled values on input", () => { + return; // TODO: controlled forms for mantine are re-creating the element on every change, so this is not reliable + cy.mount(); + + cy.get('input[name="name"]').clear().type("Jane Doe"); + cy.get('input[name="name"]').should("have.value", "Jane Doe"); + + cy.get('input[name="email"]').clear().type("jane@example.com"); + cy.get('input[name="email"]').should("have.value", "jane@example.com"); + }); +}); diff --git a/apps/web/cypress/component/autoform/mantine-zod/custom-fields.cy.tsx b/apps/web/cypress/component/autoform/mantine-zod/custom-fields.cy.tsx new file mode 100644 index 0000000..653202e --- /dev/null +++ b/apps/web/cypress/component/autoform/mantine-zod/custom-fields.cy.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { AutoForm } from "@autoform/mantine"; +import { fieldConfig, ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { AutoFormFieldProps } from "@autoform/react"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Custom Fields Tests", () => { + const CustomField: React.FC = ({ + field, + inputProps, + error, + id, + }) => ( +
+ + {error && {error}} +
+ ); + + const customSchema = z.object({ + customField: z + .string() + .min(5, "Must be at least 5 characters") + .superRefine( + fieldConfig({ + fieldType: "custom", + }) + ), + }); + + const schemaProvider = new ZodProvider(customSchema); + + it("renders and interacts with custom field components", () => { + cy.mount( + + + + ); + + cy.get(".custom-input").should("exist"); + cy.get(".custom-input").type("Hello"); + cy.get(".custom-input").should("have.value", "Hello"); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + customField: "Hello", + }); + }); + + it("shows validation errors for custom fields", () => { + cy.mount( + + + + ); + + cy.get(".custom-input").type("Hi"); + cy.get('button[type="submit"]').click(); + + cy.get(".error").should("contain", "Must be at least 5 characters"); + cy.get("@onSubmit").should("not.have.been.called"); + }); +}); diff --git a/apps/web/cypress/component/autoform/mantine-zod/subobjects.cy.tsx b/apps/web/cypress/component/autoform/mantine-zod/subobjects.cy.tsx new file mode 100644 index 0000000..ff037c8 --- /dev/null +++ b/apps/web/cypress/component/autoform/mantine-zod/subobjects.cy.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { AutoForm } from "@autoform/mantine"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Sub-objects Tests", () => { + const subObjectSchema = z.object({ + user: z.object({ + name: z.string(), + address: z.object({ + street: z.string(), + city: z.string(), + }), + }), + }); + + const schemaProvider = new ZodProvider(subObjectSchema); + + it("renders sub-object fields correctly", () => { + cy.mount( + + + + ); + + cy.get('input[name="user.name"]').should("exist"); + cy.get('input[name="user.address.street"]').should("exist"); + cy.get('input[name="user.address.city"]').should("exist"); + }); + + it("submits sub-object data correctly", () => { + cy.mount( + + + + ); + + cy.get('input[name="user.name"]').type("John Doe"); + cy.get('input[name="user.address.street"]').type("123 Main St"); + cy.get('input[name="user.address.city"]').type("New York"); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + user: { + name: "John Doe", + address: { + street: "123 Main St", + city: "New York", + }, + }, + }); + }); + + it("handles sub-sub-objects correctly", () => { + const subSubObjectSchema = z.object({ + user: z.object({ + name: z.string(), + contact: z.object({ + email: z.string().email(), + phone: z.object({ + country: z.string(), + number: z.string(), + }), + }), + }), + }); + + const subSubSchemaProvider = new ZodProvider(subSubObjectSchema); + + cy.mount( + + + + ); + + cy.get('input[name="user.name"]').type("John Doe"); + cy.get('input[name="user.contact.email"]').type("john@example.com"); + cy.get('input[name="user.contact.phone.country"]').type("US"); + cy.get('input[name="user.contact.phone.number"]').type("1234567890"); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + user: { + name: "John Doe", + contact: { + email: "john@example.com", + phone: { + country: "US", + number: "1234567890", + }, + }, + }, + }); + }); +}); diff --git a/apps/web/cypress/component/autoform/mantine-zod/ui-customization.cy.tsx b/apps/web/cypress/component/autoform/mantine-zod/ui-customization.cy.tsx new file mode 100644 index 0000000..2c38a87 --- /dev/null +++ b/apps/web/cypress/component/autoform/mantine-zod/ui-customization.cy.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { AutoForm } from "@autoform/mantine"; +import { ZodProvider, fieldConfig } from "@autoform/zod"; +import { z } from "zod"; +import { TextField } from "@mui/material"; +import { TestWrapper } from "./utils"; +import { FieldWrapperProps } from "@autoform/react"; + +describe("AutoForm UI Customization Tests", () => { + const customSchema = z.object({ + name: z.string().superRefine( + fieldConfig({ + fieldWrapper: ({ label, children }: FieldWrapperProps) => ( +
+ + {children} +
+ ), + }) + ), + email: z.string().email(), + }); + + const schemaProvider = new ZodProvider(customSchema); + + it("renders custom field wrapper", () => { + cy.mount( + + + + ); + + cy.get(".custom-wrapper").should("exist"); + cy.get(".custom-label").should("contain", "Name"); + }); + + it("overrides UI components", () => { + cy.mount( + + ( +
+ {label} + {children} +
+ ), + }} + /> +
+ ); + + cy.get(".override-wrapper").should("exist"); + cy.get(".override-label").first().should("contain", "Email"); + }); + + it("uses custom form components", () => { + cy.mount( + + ( + + ), + }} + /> + + ); + + cy.get(".custom-text-field").should("exist"); + }); +}); diff --git a/apps/web/cypress/component/autoform/mantine-zod/utils.tsx b/apps/web/cypress/component/autoform/mantine-zod/utils.tsx new file mode 100644 index 0000000..5e36bde --- /dev/null +++ b/apps/web/cypress/component/autoform/mantine-zod/utils.tsx @@ -0,0 +1,8 @@ +import { createTheme, MantineProvider } from "@mantine/core"; +import "@mantine/core/styles.css"; + +const theme = createTheme({}); + +export const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); diff --git a/apps/web/cypress/component/autoform/mantine-zod/validation.cy.tsx b/apps/web/cypress/component/autoform/mantine-zod/validation.cy.tsx new file mode 100644 index 0000000..619e322 --- /dev/null +++ b/apps/web/cypress/component/autoform/mantine-zod/validation.cy.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { AutoForm } from "@autoform/mantine"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Validation Tests", () => { + const validationSchema = z.object({ + username: z.string().min(3, "Username must be at least 3 characters"), + password: z.string().min(8, "Password must be at least 8 characters"), + email: z.string().email("Invalid email address"), + }); + + const schemaProvider = new ZodProvider(validationSchema); + + it("shows validation errors when submitting invalid data", () => { + cy.mount( + + + + ); + + cy.get('input[name="username"]').type("ab"); + cy.get('input[name="password"]').type("1234567"); + cy.get('input[name="email"]').type("invalid"); + + cy.get('button[type="submit"]').click(); + + cy.contains("Username must be at least 3 characters").should("be.visible"); + cy.contains("Password must be at least 8 characters").should("be.visible"); + cy.contains("Invalid email address").should("be.visible"); + + cy.get("@onSubmit").should("not.have.been.called"); + }); + + it("does not show errors for valid data", () => { + cy.mount( + + + + ); + + cy.get('input[name="username"]').type("johndoe"); + cy.get('input[name="password"]').type("password123"); + cy.get('input[name="email"]').type("john@example.com"); + + cy.get('button[type="submit"]').click(); + + cy.contains("Username must be at least 3 characters").should("not.exist"); + cy.contains("Password must be at least 8 characters").should("not.exist"); + cy.contains("Invalid email address").should("not.exist"); + + cy.get("@onSubmit").should("have.been.calledOnce"); + }); +}); diff --git a/apps/web/cypress/component/autoform/mui-yup/all.cy.tsx b/apps/web/cypress/component/autoform/mui-yup/all.cy.tsx new file mode 100644 index 0000000..6352a6f --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-yup/all.cy.tsx @@ -0,0 +1,2 @@ +import "./basics.cy"; +import "./validation.cy"; diff --git a/apps/web/cypress/component/autoform/mui-yup/basics.cy.tsx b/apps/web/cypress/component/autoform/mui-yup/basics.cy.tsx new file mode 100644 index 0000000..f5dc63c --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-yup/basics.cy.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { AutoForm } from "@autoform/mui"; +import { YupProvider, fieldConfig } from "@autoform/yup"; +import * as Yup from "yup"; + +describe("AutoForm Basic Tests", () => { + const basicSchema = Yup.object({ + name: Yup.string().min(2, "Name must be at least 2 characters"), + age: Yup.number().min(18, "Must be at least 18 years old"), + email: Yup.string().email("Invalid email address"), + website: Yup.string().url("Invalid URL").optional(), + birthdate: Yup.date(), + isStudent: Yup.boolean(), + }); + + const schemaProvider = new YupProvider(basicSchema); + + it("renders all field types correctly", () => { + cy.mount( + + ); + + cy.get('input[name="name"]').should("exist"); + cy.get('input[name="age"]').should("have.attr", "type", "number"); + cy.get('input[name="email"]').should("exist"); + cy.get('input[name="website"]').should("exist"); + cy.get('input[name="birthdate"]').should("have.attr", "type", "date"); + cy.get('input[name="isStudent"]').should("have.attr", "type", "checkbox"); + }); + + it("submits form with correct data types", () => { + const onSubmit = cy.stub().as("onSubmit"); + cy.mount( + + ); + + cy.get('input[name="name"]').type("John Doe"); + cy.get('input[name="age"]').type("25"); + cy.get('input[name="email"]').type("john@example.com"); + cy.get('input[name="website"]').type("https://example.com"); + cy.get('input[name="birthdate"]').type("1990-01-01"); + cy.get('input[name="isStudent"]').check(); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + name: "John Doe", + age: 25, + email: "john@example.com", + website: "https://example.com", + birthdate: new Date("1990-01-01"), + isStudent: true, + }); + }); +}); diff --git a/apps/web/cypress/component/autoform/mui-yup/validation.cy.tsx b/apps/web/cypress/component/autoform/mui-yup/validation.cy.tsx new file mode 100644 index 0000000..929ddb8 --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-yup/validation.cy.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { AutoForm } from "@autoform/mui"; +import { YupProvider } from "@autoform/yup"; +import * as Yup from "yup"; + +describe("AutoForm Validation Tests", () => { + const validationSchema = Yup.object({ + username: Yup.string().min(3, "Username must be at least 3 characters"), + password: Yup.string().min(8, "Password must be at least 8 characters"), + email: Yup.string().email("Invalid email address"), + }); + + const schemaProvider = new YupProvider(validationSchema); + + it("shows validation errors when submitting invalid data", () => { + cy.mount( + + ); + + cy.get('input[name="username"]').type("ab"); + cy.get('input[name="password"]').type("1234567"); + cy.get('input[name="email"]').type("invalid"); + + cy.get('button[type="submit"]').click(); + + cy.contains("Username must be at least 3 characters").should("be.visible"); + cy.contains("Password must be at least 8 characters").should("be.visible"); + cy.contains("Invalid email address").should("be.visible"); + + cy.get("@onSubmit").should("not.have.been.called"); + }); + + it("does not show errors for valid data", () => { + cy.mount( + + ); + + cy.get('input[name="username"]').type("johndoe"); + cy.get('input[name="password"]').type("password123"); + cy.get('input[name="email"]').type("john@example.com"); + + cy.get('button[type="submit"]').click(); + + cy.contains("Username must be at least 3 characters").should("not.exist"); + cy.contains("Password must be at least 8 characters").should("not.exist"); + cy.contains("Invalid email address").should("not.exist"); + + cy.get("@onSubmit").should("have.been.calledOnce"); + }); +}); diff --git a/apps/web/cypress/component/autoform/mui-zod/advanced-features.cy.tsx b/apps/web/cypress/component/autoform/mui-zod/advanced-features.cy.tsx new file mode 100644 index 0000000..bbf3e90 --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-zod/advanced-features.cy.tsx @@ -0,0 +1,134 @@ +import React from "react"; +import { AutoForm } from "@autoform/mui"; +import { ZodProvider, fieldConfig } from "@autoform/zod"; +import { z } from "zod"; + +describe("AutoForm Advanced Features Tests", () => { + const advancedSchema = z.object({ + username: z + .string() + .min(3, "Username must be at least 3 characters") + .superRefine( + fieldConfig({ + description: "Choose a unique username", + order: 1, + inputProps: { + placeholder: "Enter username", + }, + }) + ), + password: z + .string() + .min(8, "Password must be at least 8 characters") + .superRefine( + fieldConfig({ + description: "Use a strong password", + order: 2, + inputProps: { + type: "password", + }, + }) + ), + favoriteColor: z.enum(["red", "green", "blue"]).superRefine( + fieldConfig({ + fieldType: "select", + order: 3, + }) + ), + bio: z + .string() + .optional() + .superRefine( + fieldConfig({ + order: 4, + }) + ), + }); + + const schemaProvider = new ZodProvider(advancedSchema); + + it("renders fields in the correct order", () => { + cy.mount( + + ); + + cy.get(".MuiFormControl-root") + .eq(0) + .find("input") + .should("have.attr", "name", "username"); + cy.get(".MuiFormControl-root") + .eq(1) + .find("input") + .should("have.attr", "name", "password"); + cy.get(".MuiFormControl-root").eq(2).find(".MuiSelect-select"); + cy.get(".MuiFormControl-root") + .eq(3) + .find("input") + .should("have.attr", "name", "bio"); + }); + + it("displays field descriptions", () => { + cy.mount( + + ); + + cy.contains("Choose a unique username").should("be.visible"); + cy.contains("Use a strong password").should("be.visible"); + }); + + it("applies custom input props", () => { + cy.mount( + + ); + + cy.get('input[name="username"]').should( + "have.attr", + "placeholder", + "Enter username" + ); + cy.get('input[name="password"]').should("have.attr", "type", "password"); + }); + + it("renders select field correctly", () => { + cy.mount( + + ); + + cy.get('.MuiSelect-select[aria-labelledby*="favoriteColor"]').should( + "exist" + ); + cy.get('.MuiSelect-select[aria-labelledby*="favoriteColor"]').click(); + cy.get('.MuiMenu-list[role="listbox"] .MuiMenuItem-root').should( + "have.length", + 3 + ); + }); + + it("renders textarea field correctly", () => { + cy.mount( + + ); + + cy.get('input[name="bio"]').should("exist"); + }); +}); diff --git a/apps/web/cypress/component/autoform/mui-zod/all.cy.tsx b/apps/web/cypress/component/autoform/mui-zod/all.cy.tsx new file mode 100644 index 0000000..493695b --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-zod/all.cy.tsx @@ -0,0 +1,7 @@ +import "./basic.cy"; +import "./arrays.cy"; +import "./subobjects.cy"; +import "./advanced-features.cy"; +import "./validation.cy"; +import "./controlled-form.cy"; +import "./ui-customization.cy"; diff --git a/apps/web/cypress/component/autoform/mui-zod/arrays.cy.tsx b/apps/web/cypress/component/autoform/mui-zod/arrays.cy.tsx new file mode 100644 index 0000000..2dc76ea --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-zod/arrays.cy.tsx @@ -0,0 +1,67 @@ +import React from "react"; +import { AutoForm } from "@autoform/mui"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; + +describe("AutoForm Arrays Tests", () => { + const arraySchema = z.object({ + tags: z.array(z.string()), + friends: z.array( + z.object({ + name: z.string(), + age: z.coerce.number(), + }) + ), + }); + + const schemaProvider = new ZodProvider(arraySchema); + + it("renders array fields correctly", () => { + cy.mount( + + ); + + cy.get("[data-testid='AddIcon']").should("exist"); + cy.get("[data-testid='AddIcon']").should("exist"); + }); + + it("allows adding and removing array items", () => { + cy.mount( + + ); + + // Add tags + cy.get("[data-testid='AddIcon']").eq(0).click(); + cy.get('input[name="tags.0"]').type("tag1"); + cy.get("[data-testid='AddIcon']").eq(0).click(); + cy.get('input[name="tags.1"]').type("tag2"); + + // Add friends + cy.get("[data-testid='AddIcon']").eq(1).click(); + cy.get('input[name="friends.0.name"]').type("Alice"); + cy.get('input[name="friends.0.age"]').type("25"); + cy.get("[data-testid='AddIcon']").eq(1).click(); + cy.get('input[name="friends.1.name"]').type("Bob"); + cy.get('input[name="friends.1.age"]').type("30"); + + // Remove a tag and a friend + cy.get("[data-testid='DeleteIcon']").eq(0).click(); + cy.get("[data-testid='DeleteIcon']").eq(1).click(); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + tags: ["tag2"], + friends: [{ name: "Bob", age: 30 }], + }); + }); +}); diff --git a/apps/web/cypress/component/autoform/mui-zod/basic.cy.tsx b/apps/web/cypress/component/autoform/mui-zod/basic.cy.tsx new file mode 100644 index 0000000..4c98997 --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-zod/basic.cy.tsx @@ -0,0 +1,60 @@ +import React from "react"; +import { AutoForm } from "@autoform/mui"; +import { ZodProvider, fieldConfig } from "@autoform/zod"; +import { z } from "zod"; + +describe("AutoForm Basic Tests", () => { + const basicSchema = z.object({ + name: z.string().min(2, "Name must be at least 2 characters"), + age: z.coerce.number().min(18, "Must be at least 18 years old"), + email: z.string().email("Invalid email address"), + website: z.string().url("Invalid URL").optional(), + birthdate: z.coerce.date(), + isStudent: z.boolean(), + }); + + const schemaProvider = new ZodProvider(basicSchema); + + it("renders all field types correctly", () => { + cy.mount( + + ); + + cy.get('input[name="name"]').should("exist"); + cy.get('input[name="age"]').should("have.attr", "type", "number"); + cy.get('input[name="email"]').should("exist"); + cy.get('input[name="website"]').should("exist"); + cy.get('input[name="birthdate"]').should("have.attr", "type", "date"); + cy.get('input[name="isStudent"]').should("have.attr", "type", "checkbox"); + }); + + it("submits form with correct data types", () => { + const onSubmit = cy.stub().as("onSubmit"); + cy.mount( + + ); + + cy.get('input[name="name"]').type("John Doe"); + cy.get('input[name="age"]').type("25"); + cy.get('input[name="email"]').type("john@example.com"); + cy.get('input[name="website"]').type("https://example.com"); + cy.get('input[name="birthdate"]').type("1990-01-01"); + cy.get('input[name="isStudent"]').check(); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + name: "John Doe", + age: 25, + email: "john@example.com", + website: "https://example.com", + birthdate: new Date("1990-01-01"), + isStudent: true, + }); + }); +}); diff --git a/apps/web/cypress/component/autoform/mui-zod/controlled-form.cy.tsx b/apps/web/cypress/component/autoform/mui-zod/controlled-form.cy.tsx new file mode 100644 index 0000000..e1eb166 --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-zod/controlled-form.cy.tsx @@ -0,0 +1,53 @@ +import React, { useState } from "react"; +import { AutoForm } from "@autoform/mui"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; + +const ControlledForm = () => { + const [formValues, setFormValues] = useState({ + name: "John Doe", + email: "john@example.com", + }); + + const schema = z.object({ + name: z.string(), + email: z.string().email(), + }); + + const schemaProvider = new ZodProvider(schema); + + return ( + { + form.watch((data) => { + setFormValues(data as typeof formValues); + }); + }} + /> + ); +}; + +describe("AutoForm Controlled Form Tests", () => { + it("renders with initial values", () => { + cy.mount(); + + cy.get('input[name="name"]').should("have.value", "John Doe"); + cy.get('input[name="email"]').should("have.value", "john@example.com"); + }); + + it("updates controlled values on input", () => { + return; // TODO: controlled forms for mui are re-creating the element on every change, so this is not reliable + + cy.mount(); + + cy.get('input[name="name"]').clear().type("Jane Doe"); + cy.get('input[name="name"]').should("have.value", "Jane Doe"); + + cy.get('input[name="email"]').clear().type("jane@example.com"); + cy.get('input[name="email"]').should("have.value", "jane@example.com"); + }); +}); diff --git a/apps/web/cypress/component/autoform/mui-zod/custom-fields.cy.tsx b/apps/web/cypress/component/autoform/mui-zod/custom-fields.cy.tsx new file mode 100644 index 0000000..d347a47 --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-zod/custom-fields.cy.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { AutoForm } from "@autoform/mui"; +import { fieldConfig, ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { AutoFormFieldProps } from "@autoform/react"; + +describe("AutoForm Custom Fields Tests", () => { + const CustomField: React.FC = ({ + field, + inputProps, + error, + id, + }) => ( +
+ + {error && {error}} +
+ ); + + const customSchema = z.object({ + customField: z + .string() + .min(5, "Must be at least 5 characters") + .superRefine( + fieldConfig({ + fieldType: "custom", + }) + ), + }); + + const schemaProvider = new ZodProvider(customSchema); + + it("renders and interacts with custom field components", () => { + cy.mount( + + ); + + cy.get(".custom-input").should("exist"); + cy.get(".custom-input").type("Hello"); + cy.get(".custom-input").should("have.value", "Hello"); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + customField: "Hello", + }); + }); + + it("shows validation errors for custom fields", () => { + cy.mount( + + ); + + cy.get(".custom-input").type("Hi"); + cy.get('button[type="submit"]').click(); + + cy.get(".error").should("contain", "Must be at least 5 characters"); + cy.get("@onSubmit").should("not.have.been.called"); + }); +}); diff --git a/apps/web/cypress/component/autoform/mui-zod/subobjects.cy.tsx b/apps/web/cypress/component/autoform/mui-zod/subobjects.cy.tsx new file mode 100644 index 0000000..b5973df --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-zod/subobjects.cy.tsx @@ -0,0 +1,105 @@ +import React from "react"; +import { AutoForm } from "@autoform/mui"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; + +describe("AutoForm Sub-objects Tests", () => { + const subObjectSchema = z.object({ + user: z.object({ + name: z.string(), + address: z.object({ + street: z.string(), + city: z.string(), + }), + }), + }); + + const schemaProvider = new ZodProvider(subObjectSchema); + + it("renders sub-object fields correctly", () => { + cy.mount( + + ); + + cy.get('input[name="user.name"]').should("exist"); + cy.get('input[name="user.address.street"]').should("exist"); + cy.get('input[name="user.address.city"]').should("exist"); + }); + + it("submits sub-object data correctly", () => { + cy.mount( + + ); + + cy.get('input[name="user.name"]').type("John Doe"); + cy.get('input[name="user.address.street"]').type("123 Main St"); + cy.get('input[name="user.address.city"]').type("New York"); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + user: { + name: "John Doe", + address: { + street: "123 Main St", + city: "New York", + }, + }, + }); + }); + + it("handles sub-sub-objects correctly", () => { + const subSubObjectSchema = z.object({ + user: z.object({ + name: z.string(), + contact: z.object({ + email: z.string().email(), + phone: z.object({ + country: z.string(), + number: z.string(), + }), + }), + }), + }); + + const subSubSchemaProvider = new ZodProvider(subSubObjectSchema); + + cy.mount( + + ); + + cy.get('input[name="user.name"]').type("John Doe"); + cy.get('input[name="user.contact.email"]').type("john@example.com"); + cy.get('input[name="user.contact.phone.country"]').type("US"); + cy.get('input[name="user.contact.phone.number"]').type("1234567890"); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + user: { + name: "John Doe", + contact: { + email: "john@example.com", + phone: { + country: "US", + number: "1234567890", + }, + }, + }, + }); + }); +}); diff --git a/apps/web/cypress/component/autoform/mui-zod/ui-customization.cy.tsx b/apps/web/cypress/component/autoform/mui-zod/ui-customization.cy.tsx new file mode 100644 index 0000000..c5748ba --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-zod/ui-customization.cy.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { AutoForm } from "@autoform/mui"; +import { ZodProvider, fieldConfig } from "@autoform/zod"; +import { z } from "zod"; +import { TextField } from "@mui/material"; +import { FieldWrapperProps } from "@autoform/react"; + +describe("AutoForm UI Customization Tests", () => { + const customSchema = z.object({ + name: z.string().superRefine( + fieldConfig({ + fieldWrapper: ({ label, children }: FieldWrapperProps) => ( +
+ + {children} +
+ ), + }) + ), + email: z.string().email(), + }); + + const schemaProvider = new ZodProvider(customSchema); + + it("renders custom field wrapper", () => { + cy.mount( + + ); + + cy.get(".custom-wrapper").should("exist"); + cy.get(".custom-label").should("contain", "Name"); + }); + + it("overrides UI components", () => { + cy.mount( + ( +
+ {label} + {children} +
+ ), + }} + /> + ); + + cy.get(".override-wrapper").should("exist"); + cy.get(".override-label").first().should("contain", "Email"); + }); + + it("uses custom form components", () => { + cy.mount( + ( + + ), + }} + /> + ); + + cy.get(".custom-text-field").should("exist"); + }); +}); diff --git a/apps/web/cypress/component/autoform/mui-zod/validation.cy.tsx b/apps/web/cypress/component/autoform/mui-zod/validation.cy.tsx new file mode 100644 index 0000000..8aa56c8 --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-zod/validation.cy.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { AutoForm } from "@autoform/mui"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; + +describe("AutoForm Validation Tests", () => { + const validationSchema = z.object({ + username: z.string().min(3, "Username must be at least 3 characters"), + password: z.string().min(8, "Password must be at least 8 characters"), + email: z.string().email("Invalid email address"), + }); + + const schemaProvider = new ZodProvider(validationSchema); + + it("shows validation errors when submitting invalid data", () => { + cy.mount( + + ); + + cy.get('input[name="username"]').type("ab"); + cy.get('input[name="password"]').type("1234567"); + cy.get('input[name="email"]').type("invalid"); + + cy.get('button[type="submit"]').click(); + + cy.contains("Username must be at least 3 characters").should("be.visible"); + cy.contains("Password must be at least 8 characters").should("be.visible"); + cy.contains("Invalid email address").should("be.visible"); + + cy.get("@onSubmit").should("not.have.been.called"); + }); + + it("does not show errors for valid data", () => { + cy.mount( + + ); + + cy.get('input[name="username"]').type("johndoe"); + cy.get('input[name="password"]').type("password123"); + cy.get('input[name="email"]').type("john@example.com"); + + cy.get('button[type="submit"]').click(); + + cy.contains("Username must be at least 3 characters").should("not.exist"); + cy.contains("Password must be at least 8 characters").should("not.exist"); + cy.contains("Invalid email address").should("not.exist"); + + cy.get("@onSubmit").should("have.been.calledOnce"); + }); +}); diff --git a/apps/web/cypress/component/autoform/shadcn-zod/advanced-features.cy.tsx b/apps/web/cypress/component/autoform/shadcn-zod/advanced-features.cy.tsx new file mode 100644 index 0000000..dcc6256 --- /dev/null +++ b/apps/web/cypress/component/autoform/shadcn-zod/advanced-features.cy.tsx @@ -0,0 +1,131 @@ +import React from "react"; +import { AutoForm } from "@autoform/shadcn/components/ui/autoform/AutoForm"; +import { ZodProvider, fieldConfig } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Advanced Features Tests", () => { + const advancedSchema = z.object({ + username: z + .string() + .min(3, "Username must be at least 3 characters") + .superRefine( + fieldConfig({ + description: "Choose a unique username", + order: 1, + inputProps: { + placeholder: "Enter username", + }, + }) + ), + password: z + .string() + .min(8, "Password must be at least 8 characters") + .superRefine( + fieldConfig({ + description: "Use a strong password", + order: 2, + inputProps: { + type: "password", + }, + }) + ), + favoriteColor: z.enum(["red", "green", "blue"]).superRefine( + fieldConfig({ + fieldType: "select", + order: 3, + }) + ), + bio: z + .string() + .optional() + .superRefine( + fieldConfig({ + order: 4, + }) + ), + }); + + const schemaProvider = new ZodProvider(advancedSchema); + + it("renders fields in the correct order", () => { + cy.mount( + + + + ); + + cy.get("input").eq(0).should("have.attr", "name", "username"); + cy.get("input").eq(1).should("have.attr", "name", "password"); + cy.get("select").should("have.attr", "name", "favoriteColor"); + cy.get("input").eq(2).should("have.attr", "name", "bio"); + }); + + it("displays field descriptions", () => { + cy.mount( + + + + ); + + cy.contains("Choose a unique username").should("be.visible"); + cy.contains("Use a strong password").should("be.visible"); + }); + + it("applies custom input props", () => { + cy.mount( + + + + ); + + cy.get('input[name="username"]').should( + "have.attr", + "placeholder", + "Enter username" + ); + cy.get('input[name="password"]').should("have.attr", "type", "password"); + }); + + it("renders select field correctly", () => { + cy.mount( + + + + ); + + cy.get('[role="combobox"]').should("exist"); + cy.get('[role="combobox"]').click(); + cy.get("[role='option']").should("have.length", 3); + }); + + it("renders textarea field correctly", () => { + cy.mount( + + + + ); + + cy.get('input[name="bio"]').should("exist"); + }); +}); diff --git a/apps/web/cypress/component/autoform/shadcn-zod/all.cy.tsx b/apps/web/cypress/component/autoform/shadcn-zod/all.cy.tsx new file mode 100644 index 0000000..493695b --- /dev/null +++ b/apps/web/cypress/component/autoform/shadcn-zod/all.cy.tsx @@ -0,0 +1,7 @@ +import "./basic.cy"; +import "./arrays.cy"; +import "./subobjects.cy"; +import "./advanced-features.cy"; +import "./validation.cy"; +import "./controlled-form.cy"; +import "./ui-customization.cy"; diff --git a/apps/web/cypress/component/autoform/shadcn-zod/arrays.cy.tsx b/apps/web/cypress/component/autoform/shadcn-zod/arrays.cy.tsx new file mode 100644 index 0000000..ef54180 --- /dev/null +++ b/apps/web/cypress/component/autoform/shadcn-zod/arrays.cy.tsx @@ -0,0 +1,72 @@ +import React from "react"; +import { AutoForm } from "@autoform/shadcn/components/ui/autoform/AutoForm"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Arrays Tests", () => { + const arraySchema = z.object({ + tags: z.array(z.string()), + friends: z.array( + z.object({ + name: z.string(), + age: z.coerce.number(), + }) + ), + }); + + const schemaProvider = new ZodProvider(arraySchema); + + it("renders array fields correctly", () => { + cy.mount( + + + + ); + + cy.get(".lucide-plus").should("exist"); + cy.get(".lucide-plus").should("exist"); + }); + + it("allows adding and removing array items", () => { + cy.mount( + + + + ); + + // Add tags + cy.get(".lucide-plus").eq(0).click(); + cy.get('input[name="tags.0"]').type("tag1"); + cy.get(".lucide-plus").eq(0).click(); + cy.get('input[name="tags.1"]').type("tag2"); + + // Add friends + cy.get(".lucide-plus").eq(1).click(); + cy.get('input[name="friends.0.name"]').type("Alice"); + cy.get('input[name="friends.0.age"]').type("25"); + cy.get(".lucide-plus").eq(1).click(); + cy.get('input[name="friends.1.name"]').type("Bob"); + cy.get('input[name="friends.1.age"]').type("30"); + + // Remove a tag and a friend + cy.get(".lucide-trash").eq(0).click(); + cy.get(".lucide-trash").eq(1).click(); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + tags: ["tag2"], + friends: [{ name: "Bob", age: 30 }], + }); + }); +}); diff --git a/apps/web/cypress/component/autoform/shadcn-zod/basic.cy.tsx b/apps/web/cypress/component/autoform/shadcn-zod/basic.cy.tsx new file mode 100644 index 0000000..1befa66 --- /dev/null +++ b/apps/web/cypress/component/autoform/shadcn-zod/basic.cy.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import { AutoForm } from "@autoform/shadcn/components/ui/autoform/AutoForm"; +import { ZodProvider, fieldConfig } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Basic Tests", () => { + const basicSchema = z.object({ + name: z.string().min(2, "Name must be at least 2 characters"), + age: z.coerce.number().min(18, "Must be at least 18 years old"), + email: z.string().email("Invalid email address"), + website: z.string().url("Invalid URL").optional(), + birthdate: z.coerce.date(), + isStudent: z.boolean(), + }); + + const schemaProvider = new ZodProvider(basicSchema); + + it("renders all field types correctly", () => { + cy.mount( + + + + ); + + cy.get('input[name="name"]').should("exist"); + cy.get('input[name="age"]').should("have.attr", "type", "number"); + cy.get('input[name="email"]').should("exist"); + cy.get('input[name="website"]').should("exist"); + cy.get('input[name="birthdate"]').should("exist"); + cy.get("button#isStudent").should("exist"); + }); + + it("submits form with correct data types", () => { + const onSubmit = cy.stub().as("onSubmit"); + cy.mount( + + + + ); + + cy.get('input[name="name"]').type("John Doe"); + cy.get('input[name="age"]').type("25"); + cy.get('input[name="email"]').type("john@example.com"); + cy.get('input[name="website"]').type("https://example.com"); + cy.get('input[name="birthdate"]').type("1990-01-01"); + cy.get("button#isStudent").click(); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + name: "John Doe", + age: 25, + email: "john@example.com", + website: "https://example.com", + birthdate: new Date("1990-01-01"), + isStudent: true, + }); + }); +}); diff --git a/apps/web/cypress/component/autoform/shadcn-zod/controlled-form.cy.tsx b/apps/web/cypress/component/autoform/shadcn-zod/controlled-form.cy.tsx new file mode 100644 index 0000000..d0ac2d5 --- /dev/null +++ b/apps/web/cypress/component/autoform/shadcn-zod/controlled-form.cy.tsx @@ -0,0 +1,54 @@ +import React, { useState } from "react"; +import { AutoForm } from "@autoform/shadcn/components/ui/autoform/AutoForm"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +const ControlledForm = () => { + const [formValues, setFormValues] = useState({ + name: "John Doe", + email: "john@example.com", + }); + + const schema = z.object({ + name: z.string(), + email: z.string().email(), + }); + + const schemaProvider = new ZodProvider(schema); + + return ( + + { + form.watch((data) => { + setFormValues(data as typeof formValues); + }); + }} + /> + + ); +}; + +describe("AutoForm Controlled Form Tests", () => { + it("renders with initial values", () => { + cy.mount(); + + cy.get('input[name="name"]').should("have.value", "John Doe"); + cy.get('input[name="email"]').should("have.value", "john@example.com"); + }); + + it("updates controlled values on input", () => { + cy.mount(); + + cy.get('input[name="name"]').clear().type("Jane Doe"); + cy.get('input[name="name"]').should("have.value", "Jane Doe"); + + cy.get('input[name="email"]').clear().type("jane@example.com"); + cy.get('input[name="email"]').should("have.value", "jane@example.com"); + }); +}); diff --git a/apps/web/cypress/component/autoform/shadcn-zod/custom-fields.cy.tsx b/apps/web/cypress/component/autoform/shadcn-zod/custom-fields.cy.tsx new file mode 100644 index 0000000..d272261 --- /dev/null +++ b/apps/web/cypress/component/autoform/shadcn-zod/custom-fields.cy.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { AutoForm } from "@autoform/shadcn/components/ui/autoform/AutoForm"; +import { fieldConfig, ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { AutoFormFieldProps } from "@autoform/react"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Custom Fields Tests", () => { + const CustomField: React.FC = ({ + field, + inputProps, + error, + id, + }) => ( +
+ + {error && {error}} +
+ ); + + const customSchema = z.object({ + customField: z + .string() + .min(5, "Must be at least 5 characters") + .superRefine( + fieldConfig({ + fieldType: "custom", + }) + ), + }); + + const schemaProvider = new ZodProvider(customSchema); + + it("renders and interacts with custom field components", () => { + cy.mount( + + + + ); + + cy.get(".custom-input").should("exist"); + cy.get(".custom-input").type("Hello"); + cy.get(".custom-input").should("have.value", "Hello"); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + customField: "Hello", + }); + }); + + it("shows validation errors for custom fields", () => { + cy.mount( + + + + ); + + cy.get(".custom-input").type("Hi"); + cy.get('button[type="submit"]').click(); + + cy.get(".error").should("contain", "Must be at least 5 characters"); + cy.get("@onSubmit").should("not.have.been.called"); + }); +}); diff --git a/apps/web/cypress/component/autoform/shadcn-zod/subobjects.cy.tsx b/apps/web/cypress/component/autoform/shadcn-zod/subobjects.cy.tsx new file mode 100644 index 0000000..9e3a9e3 --- /dev/null +++ b/apps/web/cypress/component/autoform/shadcn-zod/subobjects.cy.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { AutoForm } from "@autoform/shadcn/components/ui/autoform/AutoForm"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Sub-objects Tests", () => { + const subObjectSchema = z.object({ + user: z.object({ + name: z.string(), + address: z.object({ + street: z.string(), + city: z.string(), + }), + }), + }); + + const schemaProvider = new ZodProvider(subObjectSchema); + + it("renders sub-object fields correctly", () => { + cy.mount( + + + + ); + + cy.get('input[name="user.name"]').should("exist"); + cy.get('input[name="user.address.street"]').should("exist"); + cy.get('input[name="user.address.city"]').should("exist"); + }); + + it("submits sub-object data correctly", () => { + cy.mount( + + + + ); + + cy.get('input[name="user.name"]').type("John Doe"); + cy.get('input[name="user.address.street"]').type("123 Main St"); + cy.get('input[name="user.address.city"]').type("New York"); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + user: { + name: "John Doe", + address: { + street: "123 Main St", + city: "New York", + }, + }, + }); + }); + + it("handles sub-sub-objects correctly", () => { + const subSubObjectSchema = z.object({ + user: z.object({ + name: z.string(), + contact: z.object({ + email: z.string().email(), + phone: z.object({ + country: z.string(), + number: z.string(), + }), + }), + }), + }); + + const subSubSchemaProvider = new ZodProvider(subSubObjectSchema); + + cy.mount( + + + + ); + + cy.get('input[name="user.name"]').type("John Doe"); + cy.get('input[name="user.contact.email"]').type("john@example.com"); + cy.get('input[name="user.contact.phone.country"]').type("US"); + cy.get('input[name="user.contact.phone.number"]').type("1234567890"); + + cy.get('button[type="submit"]').click(); + + cy.get("@onSubmit").should("have.been.calledOnce"); + cy.get("@onSubmit").should("have.been.calledWith", { + user: { + name: "John Doe", + contact: { + email: "john@example.com", + phone: { + country: "US", + number: "1234567890", + }, + }, + }, + }); + }); +}); diff --git a/apps/web/cypress/component/autoform/shadcn-zod/ui-customization.cy.tsx b/apps/web/cypress/component/autoform/shadcn-zod/ui-customization.cy.tsx new file mode 100644 index 0000000..21a4e9f --- /dev/null +++ b/apps/web/cypress/component/autoform/shadcn-zod/ui-customization.cy.tsx @@ -0,0 +1,86 @@ +import React from "react"; +import { AutoForm } from "@autoform/shadcn/components/ui/autoform/AutoForm"; +import { ZodProvider, fieldConfig } from "@autoform/zod"; +import { z } from "zod"; +import { TextField } from "@mui/material"; +import { TestWrapper } from "./utils"; +import { FieldWrapperProps } from "@autoform/react"; + +describe("AutoForm UI Customization Tests", () => { + const customSchema = z.object({ + name: z.string().superRefine( + fieldConfig({ + fieldWrapper: ({ label, children }: FieldWrapperProps) => ( +
+ + {children} +
+ ), + }) + ), + email: z.string().email(), + }); + + const schemaProvider = new ZodProvider(customSchema); + + it("renders custom field wrapper", () => { + cy.mount( + + + + ); + + cy.get(".custom-wrapper").should("exist"); + cy.get(".custom-label").should("contain", "Name"); + }); + + it("overrides UI components", () => { + cy.mount( + + ( +
+ {label} + {children} +
+ ), + }} + /> +
+ ); + + cy.get(".override-wrapper").should("exist"); + cy.get(".override-label").first().should("contain", "Email"); + }); + + it("uses custom form components", () => { + cy.mount( + + ( + + ), + }} + /> + + ); + + cy.get(".custom-text-field").should("exist"); + }); +}); diff --git a/apps/web/cypress/component/autoform/shadcn-zod/utils.tsx b/apps/web/cypress/component/autoform/shadcn-zod/utils.tsx new file mode 100644 index 0000000..025d00c --- /dev/null +++ b/apps/web/cypress/component/autoform/shadcn-zod/utils.tsx @@ -0,0 +1,5 @@ +import "@autoform/shadcn/globals.css"; + +export const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + <>{children} +); diff --git a/apps/web/cypress/component/autoform/shadcn-zod/validation.cy.tsx b/apps/web/cypress/component/autoform/shadcn-zod/validation.cy.tsx new file mode 100644 index 0000000..2e7370e --- /dev/null +++ b/apps/web/cypress/component/autoform/shadcn-zod/validation.cy.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { AutoForm } from "@autoform/shadcn/components/ui/autoform/AutoForm"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Validation Tests", () => { + const validationSchema = z.object({ + username: z.string().min(3, "Username must be at least 3 characters"), + password: z.string().min(8, "Password must be at least 8 characters"), + email: z.string().email("Invalid email address"), + }); + + const schemaProvider = new ZodProvider(validationSchema); + + it("shows validation errors when submitting invalid data", () => { + cy.mount( + + + + ); + + cy.get('input[name="username"]').type("ab"); + cy.get('input[name="password"]').type("1234567"); + cy.get('input[name="email"]').type("invalid"); + + cy.get('button[type="submit"]').click(); + + cy.contains("Username must be at least 3 characters").should("be.visible"); + cy.contains("Password must be at least 8 characters").should("be.visible"); + cy.contains("Invalid email address").should("be.visible"); + + cy.get("@onSubmit").should("not.have.been.called"); + }); + + it("does not show errors for valid data", () => { + cy.mount( + + + + ); + + cy.get('input[name="username"]').type("johndoe"); + cy.get('input[name="password"]').type("password123"); + cy.get('input[name="email"]').type("john@example.com"); + + cy.get('button[type="submit"]').click(); + + cy.contains("Username must be at least 3 characters").should("not.exist"); + cy.contains("Password must be at least 8 characters").should("not.exist"); + cy.contains("Invalid email address").should("not.exist"); + + cy.get("@onSubmit").should("have.been.calledOnce"); + }); +}); diff --git a/apps/web/cypress/fixtures/example.json b/apps/web/cypress/fixtures/example.json new file mode 100644 index 0000000..02e4254 --- /dev/null +++ b/apps/web/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/apps/web/cypress/support/commands.ts b/apps/web/cypress/support/commands.ts new file mode 100644 index 0000000..698b01a --- /dev/null +++ b/apps/web/cypress/support/commands.ts @@ -0,0 +1,37 @@ +/// +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } \ No newline at end of file diff --git a/apps/web/cypress/support/component-index.html b/apps/web/cypress/support/component-index.html new file mode 100644 index 0000000..3e16e9b --- /dev/null +++ b/apps/web/cypress/support/component-index.html @@ -0,0 +1,14 @@ + + + + + + + Components App + +
+ + +
+ + \ No newline at end of file diff --git a/apps/web/cypress/support/component.ts b/apps/web/cypress/support/component.ts new file mode 100644 index 0000000..37f59ed --- /dev/null +++ b/apps/web/cypress/support/component.ts @@ -0,0 +1,39 @@ +// *********************************************************** +// This example support/component.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +import { mount } from 'cypress/react18' + +// Augment the Cypress namespace to include type definitions for +// your custom command. +// Alternatively, can be defined in cypress/support/component.d.ts +// with a at the top of your spec. +declare global { + namespace Cypress { + interface Chainable { + mount: typeof mount + } + } +} + +Cypress.Commands.add('mount', mount) + +// Example use: +// cy.mount() \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index 8c6aba8..a5f32ee 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -6,11 +6,13 @@ "dev": "next dev --turbo", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "cypress": "cypress open" }, "dependencies": { "@autoform/mantine": "*", "@autoform/mui": "*", + "@autoform/shadcn": "*", "@autoform/yup": "*", "@autoform/zod": "*", "@emotion/react": "^11.13.3", @@ -33,6 +35,7 @@ "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10", + "cypress": "^13.15.0", "eslint": "^8", "eslint-config-next": "14.2.6", "postcss": "^8.4.47", diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 608524a..7ca6831 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -5,7 +5,11 @@ { "name": "next" } - ] + ], + "baseUrl": ".", + "paths": { + "@/*": ["../../packages/shadcn/src/*"] + } }, "include": [ "next-env.d.ts", diff --git a/package-lock.json b/package-lock.json index 06d901b..f9a5e67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ } }, "apps/docs": { - "version": "0.0.1", + "version": "0.1.0", "license": "MIT", "dependencies": { "@autoform/core": "*", @@ -354,10 +354,11 @@ } }, "apps/web": { - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "@autoform/mantine": "*", "@autoform/mui": "*", + "@autoform/shadcn": "*", "@autoform/yup": "*", "@autoform/zod": "*", "@emotion/react": "^11.13.3", @@ -380,6 +381,7 @@ "@types/react": "^18", "@types/react-dom": "^18", "autoprefixer": "^10", + "cypress": "^13.15.0", "eslint": "^8", "eslint-config-next": "14.2.6", "postcss": "^8.4.47", @@ -1312,6 +1314,16 @@ "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==" }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "devOptional": true, @@ -1332,6 +1344,54 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@cypress/request": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.5.tgz", + "integrity": "sha512-v+XHd9XmWbufxF1/bTaVm2yhbxY+TB4YtWRqF2zaXBlDNMkls34KiATz0AVDLavL3iB6bQk9/7n3oY1EoLSWGA==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.13.0", + "safe-buffer": "^5.1.2", + "tough-cookie": "^4.1.3", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/@devnomic/marquee": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@devnomic/marquee/-/marquee-1.0.2.tgz", @@ -5152,6 +5212,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true + }, "node_modules/@types/through": { "version": "0.0.33", "dev": true, @@ -5170,6 +5242,16 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "7.18.0", "dev": true, @@ -6108,6 +6190,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ast-types": { "version": "0.13.4", "dev": true, @@ -6124,6 +6224,15 @@ "dev": true, "license": "MIT" }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/astring": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", @@ -6132,6 +6241,27 @@ "astring": "bin/astring" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.20", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", @@ -6183,6 +6313,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true + }, "node_modules/axe-core": { "version": "4.10.0", "dev": true, @@ -6269,6 +6414,15 @@ "node": ">=10.0.0" } }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/better-path-resolve": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/better-path-resolve/-/better-path-resolve-1.0.0.tgz", @@ -6312,6 +6466,18 @@ "readable-stream": "^3.4.0" } }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "node_modules/brace-expansion": { "version": "2.0.1", "license": "MIT", @@ -6383,6 +6549,15 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "dev": true, @@ -6425,6 +6600,15 @@ "node": ">=8" } }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/call-bind": { "version": "1.0.7", "dev": true, @@ -6485,6 +6669,12 @@ ], "license": "CC-BY-4.0" }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -6578,6 +6768,15 @@ "version": "0.7.0", "license": "MIT" }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/chevrotain": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", @@ -6717,6 +6916,37 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-width": { "version": "3.0.0", "dev": true, @@ -6883,6 +7113,24 @@ "version": "1.1.3", "license": "MIT" }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -6900,6 +7148,15 @@ "node": ">=14" } }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/compute-scroll-into-view": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", @@ -6947,6 +7204,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, "node_modules/cose-base": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", @@ -7010,6 +7273,234 @@ "version": "3.1.3", "license": "MIT" }, + "node_modules/cypress": { + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.0.tgz", + "integrity": "sha512-53aO7PwOfi604qzOkCSzNlWquCynLlKE/rmmpSPcziRH6LNfaDUAklQT6WJIsD8ywxlIy+uVZsnTMCCQVd2kTw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.4", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.3", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/cypress/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cypress/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cypress/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cypress/node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/cypress/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cypress/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cypress/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/cypress/node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/cypress/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, "node_modules/cytoscape": { "version": "3.30.2", "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.30.2.tgz", @@ -7491,6 +7982,18 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/data-uri-to-buffer": { "version": "6.0.2", "dev": true, @@ -7771,6 +8274,15 @@ "robust-predicates": "^3.0.2" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -7892,6 +8404,22 @@ "version": "0.2.0", "license": "MIT" }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecc-jsbn/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, "node_modules/electron-to-chromium": { "version": "1.5.36", "dev": true, @@ -7926,6 +8454,15 @@ "version": "9.2.2", "license": "MIT" }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "dev": true, @@ -9411,6 +9948,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true + }, "node_modules/execa": { "version": "5.1.1", "license": "MIT", @@ -9432,6 +9975,18 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -9465,6 +10020,50 @@ "node": ">=4" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "dev": true, @@ -9523,6 +10122,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/figures": { "version": "3.2.0", "dev": true, @@ -9641,6 +10249,29 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", @@ -9834,6 +10465,24 @@ "node": ">=14.14" } }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/git-hooks-list": { "version": "3.1.0", "dev": true, @@ -9877,6 +10526,30 @@ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/globals": { "version": "13.24.0", "dev": true, @@ -10473,6 +11146,20 @@ "node": ">= 14" } }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.5", "dev": true, @@ -10857,6 +11544,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, "node_modules/is-core-module": { "version": "2.15.1", "license": "MIT", @@ -10973,6 +11672,22 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "dev": true, @@ -11169,6 +11884,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "dev": true, @@ -11253,6 +11974,12 @@ "version": "2.0.0", "license": "ISC" }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, "node_modules/iterator.prototype": { "version": "1.1.3", "dev": true, @@ -11344,6 +12071,12 @@ "version": "2.3.1", "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "dev": true, @@ -11354,6 +12087,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/json5": { "version": "2.2.3", "dev": true, @@ -11376,6 +12115,21 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "dev": true, @@ -11475,33 +12229,134 @@ "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==" }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, "node_modules/levn": { "version": "0.4.1", "dev": true, "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "license": "MIT" + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/listr2/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/listr2/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lilconfig": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", - "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, "engines": { - "node": ">=14" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/antonk52" + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "license": "MIT" - }, "node_modules/load-tsconfig": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", @@ -11559,6 +12414,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -11648,6 +12509,74 @@ "node": ">=8" } }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-update/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -13798,6 +14727,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "license": "MIT", @@ -14683,6 +15633,12 @@ "node": ">=0.10.0" } }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true + }, "node_modules/outdent": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/outdent/-/outdent-0.5.0.tgz", @@ -14965,6 +15921,18 @@ "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==" }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, "node_modules/periscopic": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", @@ -15293,6 +16261,27 @@ } } }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/prop-types": { "version": "15.8.1", "license": "MIT", @@ -15344,6 +16333,22 @@ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==" }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "license": "MIT", @@ -15351,6 +16356,27 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "funding": [ @@ -16091,6 +17117,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve": { "version": "2.0.0-next.5", "dev": true, @@ -16199,6 +17240,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, "node_modules/rimraf": { "version": "3.0.2", "dev": true, @@ -16534,6 +17581,53 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/smart-buffer": { "version": "4.2.0", "dev": true, @@ -16779,6 +17873,37 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sshpk/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, "node_modules/state-local": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", @@ -17289,6 +18414,15 @@ "node": ">=0.8" } }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/through": { "version": "2.3.8", "dev": true, @@ -17462,6 +18596,30 @@ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/tr46": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", @@ -17676,6 +18834,18 @@ "dev": true, "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/turbo": { "version": "2.1.3", "dev": true, @@ -17704,6 +18874,12 @@ "darwin" ] }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, "node_modules/twoslash": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/twoslash/-/twoslash-0.2.12.tgz", @@ -18025,6 +19201,15 @@ "node": ">= 10.0.0" } }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "dev": true, @@ -18084,6 +19269,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/use-callback-ref": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", @@ -18166,6 +19361,15 @@ "version": "1.0.2", "license": "MIT" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "devOptional": true, @@ -18188,6 +19392,20 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -18484,6 +19702,16 @@ "node": ">= 14" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yn": { "version": "3.1.1", "devOptional": true, @@ -18555,7 +19783,7 @@ }, "packages/core": { "name": "@autoform/core", - "version": "1.1.2", + "version": "1.2.0", "devDependencies": { "@autoform/eslint-config": "*", "@autoform/typescript-config": "*", @@ -18585,7 +19813,7 @@ }, "packages/mantine": { "name": "@autoform/mantine", - "version": "1.1.2", + "version": "1.2.0", "dependencies": { "@autoform/core": "*", "@autoform/react": "*", @@ -18612,7 +19840,7 @@ }, "packages/mui": { "name": "@autoform/mui", - "version": "1.1.2", + "version": "1.2.0", "dependencies": { "@autoform/core": "*", "@autoform/react": "*", @@ -18826,9 +20054,10 @@ }, "packages/react": { "name": "@autoform/react", - "version": "1.2.0", + "version": "1.3.0", "dependencies": { "@autoform/core": "*", + "@autoform/yup": "*", "@autoform/zod": "*", "@hookform/resolvers": "^3.9.0", "react": "^18.2.0", @@ -18849,7 +20078,7 @@ }, "packages/shadcn": { "name": "@autoform/shadcn", - "version": "1.0.2", + "version": "1.1.0", "dependencies": { "@autoform/core": "*", "@autoform/react": "*", @@ -18940,7 +20169,7 @@ }, "packages/yup": { "name": "@autoform/yup", - "version": "1.0.0", + "version": "1.1.0", "dependencies": { "@autoform/core": "*", "yup": "^1.4.0" @@ -18960,7 +20189,7 @@ }, "packages/zod": { "name": "@autoform/zod", - "version": "1.1.2", + "version": "1.2.0", "dependencies": { "@autoform/core": "*", "zod": "^3.23.8" diff --git a/package.json b/package.json index 59182ea..9caf6e1 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "build": "turbo build", "dev": "turbo dev", "lint": "turbo lint", - "format": "prettier --write \"**/*.{ts,tsx,md}\"" + "format": "prettier --write \"**/*.{ts,tsx,md}\"", + "cypress": "turbo run cypress --filter=web", + "cypress:dev": "turbo run cypress --filter=web --parallel" }, "devDependencies": { "prettier": "^3.2.5", diff --git a/packages/core/.eslintrc.js b/packages/core/.eslintrc.js index 4646413..1c49702 100644 --- a/packages/core/.eslintrc.js +++ b/packages/core/.eslintrc.js @@ -1,7 +1,7 @@ /** @type {import("eslint").Linter.Config} */ module.exports = { root: true, - extends: ["@repo/eslint-config/react-internal.js"], + extends: ["@repo/eslint-config/library.js"], parser: "@typescript-eslint/parser", parserOptions: { project: "./tsconfig.lint.json", diff --git a/packages/core/src/label.ts b/packages/core/src/label.ts index c04b663..f905d1c 100644 --- a/packages/core/src/label.ts +++ b/packages/core/src/label.ts @@ -14,6 +14,10 @@ export function getLabel(field: ParsedField) { } function beautifyLabel(label: string) { + if (!label) { + return ""; + } + let output = label.replace(/([A-Z])/g, " $1"); output = output.charAt(0).toUpperCase() + output.slice(1); diff --git a/packages/core/src/logic.ts b/packages/core/src/logic.ts index fe4ce33..d8ac35b 100644 --- a/packages/core/src/logic.ts +++ b/packages/core/src/logic.ts @@ -1,8 +1,12 @@ import { SchemaProvider } from "./schema-provider"; -import { ParsedSchema } from "./types"; +import { ParsedField, ParsedSchema } from "./types"; export function parseSchema(schemaProvider: SchemaProvider): ParsedSchema { - return schemaProvider.parseSchema(); + const schema = schemaProvider.parseSchema(); + return { + ...schema, + fields: sortFieldsByOrder(schema.fields), + }; } export function validateSchema(schemaProvider: SchemaProvider, values: any) { @@ -10,7 +14,63 @@ export function validateSchema(schemaProvider: SchemaProvider, values: any) { } export function getDefaultValues( - schemaProvider: SchemaProvider, + schemaProvider: SchemaProvider ): Record { return schemaProvider.getDefaultValues(); } + +// Recursively remove empty values from an object (null, undefined, "", [], {}) +export function removeEmptyValues>( + values: T +): Partial { + const result: Partial = {}; + for (const key in values) { + const value = values[key]; + if ([null, undefined, "", [], {}].includes(value)) { + continue; + } + + if (Array.isArray(value)) { + const newArray = value.map((item: any) => { + if (typeof item === "object") { + return removeEmptyValues(item); + } + return item; + }); + result[key] = newArray.filter((item: any) => item !== null); + } else if (typeof value === "object") { + result[key] = removeEmptyValues(value) as any; + } else { + result[key] = value; + } + } + + return result; +} + +/** + * Sort the fields by order. + * If no order is set, the field will be sorted based on the order in the schema. + */ +export function sortFieldsByOrder( + fields: ParsedField[] | undefined +): ParsedField[] { + if (!fields) return []; + const sortedFields = fields + .map((field): ParsedField => { + if (field.schema) { + return { + ...field, + schema: sortFieldsByOrder(field.schema), + }; + } + return field; + }) + .sort((a, b) => { + const fieldA: number = a.fieldConfig?.order ?? 0; + const fieldB = b.fieldConfig?.order ?? 0; + return fieldA - fieldB; + }); + + return sortedFields; +} diff --git a/packages/core/src/schema-provider.ts b/packages/core/src/schema-provider.ts index ca36605..7895119 100644 --- a/packages/core/src/schema-provider.ts +++ b/packages/core/src/schema-provider.ts @@ -19,7 +19,7 @@ export interface SchemaProvider { * * @param values - Form values to validate */ - validateSchema(values: T): SchemaValidation; + validateSchema(_values: T): SchemaValidation; /** * Get the default values for the fields. diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 73a949e..1ffdd7c 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -6,11 +6,19 @@ export type Renderable = | undefined | AdditionalRenderable; -export interface FieldConfig { +export interface FieldConfig< + AdditionalRenderable = null, + FieldTypes = string, + FieldWrapper = any, + CustomData = Record, +> { description?: Renderable; inputProps?: Record; label?: Renderable; fieldType?: FieldTypes; + order?: number; + fieldWrapper?: FieldWrapper; + customData?: CustomData; } export interface ParsedField { @@ -33,21 +41,6 @@ export interface ParsedSchema< fields: ParsedField[]; } -export enum DependencyType { - DISABLES, - REQUIRES, - HIDES, - SETS_OPTIONS, -} - -export interface Dependency { - sourceField: string; - targetField: string; - type: DependencyType; - condition: (sourceValue: any, targetValue: any) => boolean; - options?: string[]; -} - export type SuccessfulSchemaValidation = { success: true; data: any; diff --git a/packages/eslint-config/library.js b/packages/eslint-config/library.js index 9b59cc0..2234bb7 100644 --- a/packages/eslint-config/library.js +++ b/packages/eslint-config/library.js @@ -4,8 +4,14 @@ const project = resolve(process.cwd(), "tsconfig.json"); /** @type {import("eslint").Linter.Config} */ module.exports = { - extends: ["eslint:recommended", "prettier", "turbo"], - plugins: ["only-warn"], + extends: [ + "eslint:recommended", + "prettier", + "turbo", + "plugin:@typescript-eslint/recommended", + ], + parser: "@typescript-eslint/parser", + plugins: ["only-warn", "@typescript-eslint"], globals: { React: true, JSX: true, @@ -26,6 +32,17 @@ module.exports = { "node_modules/", "dist/", ], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/no-explicit-any": "off", + }, overrides: [ { files: ["*.js?(x)", "*.ts?(x)"], diff --git a/packages/eslint-config/react-internal.js b/packages/eslint-config/react-internal.js index bf0a208..f1a6d2b 100644 --- a/packages/eslint-config/react-internal.js +++ b/packages/eslint-config/react-internal.js @@ -10,8 +10,14 @@ const project = resolve(process.cwd(), "tsconfig.json"); /** @type {import("eslint").Linter.Config} */ module.exports = { - extends: ["eslint:recommended", "prettier", "turbo"], - plugins: ["only-warn"], + extends: [ + "eslint:recommended", + "prettier", + "turbo", + "plugin:@typescript-eslint/recommended", + ], + parser: "@typescript-eslint/parser", + plugins: ["only-warn", "@typescript-eslint"], globals: { React: true, JSX: true, @@ -36,4 +42,15 @@ module.exports = { // Force ESLint to detect .tsx files { files: ["*.js?(x)", "*.ts?(x)"] }, ], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/no-explicit-any": "off", + }, }; diff --git a/packages/mantine/src/AutoForm.tsx b/packages/mantine/src/AutoForm.tsx index 14f4eb8..134ae11 100644 --- a/packages/mantine/src/AutoForm.tsx +++ b/packages/mantine/src/AutoForm.tsx @@ -2,7 +2,6 @@ import React from "react"; import { AutoForm as BaseAutoForm, AutoFormUIComponents, - AutoFormFieldComponents, } from "@autoform/react"; import { MantineProvider } from "@mantine/core"; import { AutoFormProps } from "./types"; @@ -40,13 +39,15 @@ export type FieldTypes = keyof typeof MantineAutoFormFieldComponents; export function AutoForm>({ theme, + uiComponents, + formComponents, ...props }: AutoFormProps) { const ThemedForm = () => ( ); diff --git a/packages/mantine/src/components/ArrayElementWrapper.tsx b/packages/mantine/src/components/ArrayElementWrapper.tsx index 061651a..d32fcc4 100644 --- a/packages/mantine/src/components/ArrayElementWrapper.tsx +++ b/packages/mantine/src/components/ArrayElementWrapper.tsx @@ -19,9 +19,9 @@ export const ArrayElementWrapper: React.FC = ({ > diff --git a/packages/mantine/src/components/ArrayWrapper.tsx b/packages/mantine/src/components/ArrayWrapper.tsx index a971bd3..ff778ef 100644 --- a/packages/mantine/src/components/ArrayWrapper.tsx +++ b/packages/mantine/src/components/ArrayWrapper.tsx @@ -12,7 +12,7 @@ export const ArrayWrapper: React.FC = ({ {label} {children} - diff --git a/packages/mantine/src/components/BooleanField.tsx b/packages/mantine/src/components/BooleanField.tsx index ba06713..edab15b 100644 --- a/packages/mantine/src/components/BooleanField.tsx +++ b/packages/mantine/src/components/BooleanField.tsx @@ -3,17 +3,6 @@ import { Checkbox } from "@mantine/core"; import { AutoFormFieldProps } from "@autoform/react"; export const BooleanField: React.FC = ({ - field, - value, - onChange, + inputProps, label, -}) => ( - onChange(e.currentTarget.checked)} - label={label} - required={field.required} - description={field.description} - {...field.fieldConfig?.inputProps} - /> -); +}) => ; diff --git a/packages/mantine/src/components/DateField.tsx b/packages/mantine/src/components/DateField.tsx index bf5ea4f..2debbeb 100644 --- a/packages/mantine/src/components/DateField.tsx +++ b/packages/mantine/src/components/DateField.tsx @@ -4,18 +4,23 @@ import { AutoFormFieldProps } from "@autoform/react"; export const DateField: React.FC = ({ field, - value, - onChange, - error, + inputProps, label, }) => ( onChange(date)} - error={!!error} label={label} - required={field.required} description={field.description} - {...field.fieldConfig?.inputProps} + error={inputProps.error} + onChange={(value) => { + // react-hook-form expects an event object + const event = { + target: { + name: field.key, + value: value?.toISOString(), + }, + }; + inputProps.onChange(event); + }} + value={inputProps.value} /> ); diff --git a/packages/mantine/src/components/NumberField.tsx b/packages/mantine/src/components/NumberField.tsx index f436592..c3cb259 100644 --- a/packages/mantine/src/components/NumberField.tsx +++ b/packages/mantine/src/components/NumberField.tsx @@ -1,21 +1,16 @@ import React from "react"; -import { NumberInput } from "@mantine/core"; +import { TextInput } from "@mantine/core"; import { AutoFormFieldProps } from "@autoform/react"; export const NumberField: React.FC = ({ field, - value, - onChange, - error, + inputProps, label, }) => ( - onChange(val)} - error={!!error} + ); diff --git a/packages/mantine/src/components/SelectField.tsx b/packages/mantine/src/components/SelectField.tsx index 5cdd51e..5f8c5f7 100644 --- a/packages/mantine/src/components/SelectField.tsx +++ b/packages/mantine/src/components/SelectField.tsx @@ -4,19 +4,15 @@ import { AutoFormFieldProps } from "@autoform/react"; export const SelectField: React.FC = ({ field, - value, - onChange, error, + inputProps, label, }) => ( onChange(Number(e.target.value))} - error={!!error} - fullWidth - {...field.fieldConfig?.inputProps} - /> -); + inputProps, +}) => ; diff --git a/packages/mui/src/components/SelectField.tsx b/packages/mui/src/components/SelectField.tsx index e3a00ad..03368b2 100644 --- a/packages/mui/src/components/SelectField.tsx +++ b/packages/mui/src/components/SelectField.tsx @@ -5,17 +5,10 @@ import { AutoFormFieldProps } from "@autoform/react"; export const SelectField: React.FC = ({ field, - value, - onChange, + inputProps, error, }) => ( - {(field.options || []).map(([key, label]) => ( {label} diff --git a/packages/mui/src/components/StringField.tsx b/packages/mui/src/components/StringField.tsx index dd7fc2d..522f83d 100644 --- a/packages/mui/src/components/StringField.tsx +++ b/packages/mui/src/components/StringField.tsx @@ -3,18 +3,7 @@ import Input from "@mui/material/Input"; import { AutoFormFieldProps } from "@autoform/react"; export const StringField: React.FC = ({ - field, - value, - onChange, error, id, -}) => ( - onChange(e.target.value)} - error={!!error} - fullWidth - {...field.fieldConfig?.inputProps} - /> -); + inputProps, +}) => ; diff --git a/packages/mui/src/types.ts b/packages/mui/src/types.ts index 8a1f3c5..4371815 100644 --- a/packages/mui/src/types.ts +++ b/packages/mui/src/types.ts @@ -1,7 +1,8 @@ -import { AutoFormProps as BaseAutoFormProps } from "@autoform/react"; +import { ExtendableAutoFormProps } from "@autoform/react"; import { ThemeProvider } from "@mui/material/styles"; +import { FieldValues } from "react-hook-form"; -export interface AutoFormProps - extends Omit, "uiComponents" | "formComponents"> { +export interface AutoFormProps + extends ExtendableAutoFormProps { theme?: Parameters[0]["theme"]; } diff --git a/packages/react/package.json b/packages/react/package.json index d1d0591..a65f8e4 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -28,6 +28,7 @@ "dependencies": { "@autoform/core": "*", "@autoform/zod": "*", + "@autoform/yup": "*", "@hookform/resolvers": "^3.9.0", "react": "^18.2.0", "react-hook-form": "^7.53.0" diff --git a/packages/react/src/ArrayField.tsx b/packages/react/src/ArrayField.tsx index b5aed97..fafa1f2 100644 --- a/packages/react/src/ArrayField.tsx +++ b/packages/react/src/ArrayField.tsx @@ -1,64 +1,46 @@ import React from "react"; -import { AutoFormFieldProps } from "./types"; +import { useFieldArray, useFormContext } from "react-hook-form"; import { AutoFormField } from "./AutoFormField"; import { useAutoForm } from "./context"; -import { getLabel } from "@autoform/core"; +import { getLabel, ParsedField } from "@autoform/core"; -export const ArrayField: React.FC = ({ field, path }) => { - const { uiComponents, setFieldValue, getError, getFieldValue } = - useAutoForm(); +export const ArrayField: React.FC<{ + field: ParsedField; + path: string[]; +}> = ({ field, path }) => { + const { uiComponents } = useAutoForm(); + const { control } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + control, + name: path.join("."), + }); - const fullPath = path || []; - const fieldPathString = fullPath.join("."); - const value = getFieldValue(fieldPathString) || []; - - const handleAddItem = () => { - const subFieldType = field.schema?.[0]?.type; - let defaultValue: any; - if (subFieldType === "object") { - defaultValue = {}; - } else if (subFieldType === "array") { - defaultValue = []; - } else { - defaultValue = null; - } - - const newValue = [...value, defaultValue]; - setFieldValue(fieldPathString, newValue); - }; - - const handleRemoveItem = (index: number) => { - const newValue = value.filter((_: any, i: number) => i !== index); - setFieldValue(fieldPathString, newValue); - }; + const subFieldType = field.schema?.[0]?.type; + let defaultValue: any; + if (subFieldType === "object") { + defaultValue = {}; + } else if (subFieldType === "array") { + defaultValue = []; + } else { + defaultValue = null; + } return ( append(defaultValue)} > - {value.map((item: any, index: number) => ( + {fields.map((item, index) => ( handleRemoveItem(index)} + key={item.id} + onRemove={() => remove(index)} index={index} > - {field.schema![0] && ( - { - const newArray = [...value]; - newArray[index] = newValue; - setFieldValue(fieldPathString, newArray); - }} - error={getError(`${fieldPathString}.${index}`)} - id={`${fieldPathString}.${index}`} - label={`${getLabel(field)} ${index + 1}`} - path={[...fullPath, index.toString()]} - /> - )} + ))} diff --git a/packages/react/src/AutoForm.tsx b/packages/react/src/AutoForm.tsx index 302c99d..99ca1ac 100644 --- a/packages/react/src/AutoForm.tsx +++ b/packages/react/src/AutoForm.tsx @@ -1,9 +1,9 @@ -import React, { useState } from "react"; +import React, { useEffect } from "react"; +import { useForm, FormProvider, DefaultValues } from "react-hook-form"; import { parseSchema, - validateSchema, getDefaultValues, - getLabel, + removeEmptyValues, } from "@autoform/core"; import { AutoFormProps } from "./types"; import { AutoFormProvider } from "./context"; @@ -13,119 +13,74 @@ export function AutoForm>({ schema, onSubmit = () => {}, defaultValues, + values, children, uiComponents, formComponents, withSubmit = false, - - setValues: externalSetValues, - values: externalValues, + onFormInit = () => {}, }: AutoFormProps) { const parsedSchema = parseSchema(schema); - const [internalValues, internalSetValues] = useState>(() => ({ - ...(getDefaultValues(schema) as T), - ...defaultValues, - })); - - const values = externalValues ?? internalValues; - const setValues = externalSetValues ?? internalSetValues; - - const [errors, setErrors] = useState>({}); - - const getFieldValue = (name: string) => { - const keys = name.split("."); - let current = values as any; - for (const key of keys) { - current = current[key]; + const methods = useForm({ + defaultValues: { + ...(getDefaultValues(schema) as Partial), + ...defaultValues, + } as DefaultValues, + values: values as T, + }); - if (current === undefined) { - return undefined; - } + useEffect(() => { + if (onFormInit) { + onFormInit(methods); } - return current; - }; - - const setFieldValue = (name: string, value: any) => { - setValues((prev) => { - const keys = name.split("."); - const lastKey = keys.pop()!; - let current = { ...prev } as any; - const currentRoot = current; - - for (const key of keys) { - if (current[key] === undefined) { - current[key] = {}; - } - current = current[key]; - } - current[lastKey] = value; - - return currentRoot; - }); - }; + }, [onFormInit, methods]); - const getError = (name: string) => { - return errors[name]; - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setErrors({}); - - const validationResult = validateSchema(schema, values); + const handleSubmit = async (dataRaw: T) => { + const data = removeEmptyValues(dataRaw); + const validationResult = schema.validateSchema(data as T); + console.log("validationResult", { validationResult, dataRaw, data }); if (validationResult.success) { - await onSubmit(validationResult.data, { - setErrors, - clearForm: () => { - setValues(getDefaultValues(schema) as T); - }, - }); + await onSubmit(validationResult.data, methods); } else { - const newErrors: Record = {}; + methods.clearErrors(); validationResult.errors?.forEach((error) => { const path = error.path.join("."); - newErrors[path] = error.message; + methods.setError(path as any, { + type: "custom", + message: error.message, + }); // For some custom errors, zod adds the final element twice for some reason - const correctedPath = error.path.slice(0, -1); - if (correctedPath.length > 0) { - newErrors[correctedPath.join(".")] = error.message; + const correctedPath = error.path?.slice?.(0, -1); + if (correctedPath?.length > 0) { + methods.setError(correctedPath.join(".") as any, { + type: "custom", + message: error.message, + }); } }); - setErrors(newErrors); } }; return ( - - - {parsedSchema.fields.map((field) => ( - setFieldValue(field.key, value)} - id={field.key} - label={getLabel(field)} - path={[field.key]} - /> - ))} - {withSubmit && ( - Submit - )} - {children} - - + + + + {parsedSchema.fields.map((field) => ( + + ))} + {withSubmit && ( + Submit + )} + {children} + + + ); } diff --git a/packages/react/src/AutoFormField.tsx b/packages/react/src/AutoFormField.tsx index 890591b..ab09bc1 100644 --- a/packages/react/src/AutoFormField.tsx +++ b/packages/react/src/AutoFormField.tsx @@ -1,27 +1,29 @@ import React from "react"; +import { useFormContext } from "react-hook-form"; import { useAutoForm } from "./context"; -import { AutoFormFieldProps } from "./types"; -import { getLabel } from "@autoform/core"; +import { getLabel, ParsedField } from "@autoform/core"; import { ObjectField } from "./ObjectField"; import { ArrayField } from "./ArrayField"; +import { AutoFormFieldProps } from "./types"; +import { getPathInObject } from "./utils"; -export function AutoFormField({ field, path }: AutoFormFieldProps) { +export const AutoFormField: React.FC<{ + field: ParsedField; + path: string[]; +}> = ({ field, path }) => { + const { formComponents, uiComponents } = useAutoForm(); const { - getFieldValue, - setFieldValue, - getError, - formComponents, - uiComponents, - } = useAutoForm(); - - const fieldPathString = path.join("."); + register, + formState: { errors }, + getValues, + } = useFormContext(); - const value = getFieldValue(fieldPathString); - const error = getError(fieldPathString); + const fullPath = path.join("."); + const error = getPathInObject(errors, path)?.message as string | undefined; + const value = getValues(fullPath); - const onChange = (newValue: any) => { - setFieldValue(fieldPathString, newValue); - }; + const FieldWrapper = + field.fieldConfig?.fieldWrapper || uiComponents.FieldWrapper; let FieldComponent: React.ComponentType = () => ( - {error && } - + ); -} +}; diff --git a/packages/react/src/ObjectField.tsx b/packages/react/src/ObjectField.tsx index bfd1690..dbb312f 100644 --- a/packages/react/src/ObjectField.tsx +++ b/packages/react/src/ObjectField.tsx @@ -1,36 +1,23 @@ import React from "react"; -import { AutoFormFieldProps } from "./types"; import { AutoFormField } from "./AutoFormField"; import { useAutoForm } from "./context"; -import { getLabel } from "@autoform/core"; +import { getLabel, ParsedField } from "@autoform/core"; -export const ObjectField: React.FC = ({ field, path }) => { - const { uiComponents, setFieldValue, getError, getFieldValue } = - useAutoForm(); - - const fullPath = path || []; +export const ObjectField: React.FC<{ + field: ParsedField; + path: string[]; +}> = ({ field, path }) => { + const { uiComponents } = useAutoForm(); return ( - {Object.entries(field.schema!).map(([key, subField]) => { - const fullKey = [...fullPath, subField.key]; - const fullKeyString = fullKey.join("."); - - return ( - { - setFieldValue(fullKeyString, newValue); - }} - error={getError(fullKeyString)} - id={fullKeyString} - path={fullKey} - /> - ); - })} + {Object.entries(field.schema!).map(([_key, subField]) => ( + + ))} ); }; diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 608136a..a7e9885 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -4,27 +4,35 @@ import { ParsedSchema, Renderable, SchemaProvider, + FieldConfig as BaseFieldConfig, } from "@autoform/core"; +import { FieldValues, UseFormReturn } from "react-hook-form"; -export interface AutoFormProps { +export interface AutoFormProps { schema: SchemaProvider; onSubmit?: ( values: T, - extra: { - setErrors: React.Dispatch>>; - clearForm: () => void; - } + form: UseFormReturn ) => void | Promise; + defaultValues?: Partial; + values?: Partial; + children?: ReactNode; uiComponents: AutoFormUIComponents; formComponents: AutoFormFieldComponents; withSubmit?: boolean; - - setValues?: React.Dispatch>>; - values?: Partial; + onFormInit?: (form: UseFormReturn) => void; } +export type ExtendableAutoFormProps = Omit< + AutoFormProps, + "uiComponents" | "formComponents" +> & { + uiComponents?: Partial; + formComponents?: Partial; +}; + export interface AutoFormUIComponents { Form: React.ComponentType<{ onSubmit: (e: React.FormEvent) => void; @@ -73,19 +81,25 @@ export interface AutoFormFieldProps { label: Renderable; field: ParsedField; value: any; - onChange: (value: any) => void; error?: string; id: string; path: string[]; + + inputProps: any; } export interface AutoFormContextType { schema: ParsedSchema; - values: Record; - errors: Record; - getFieldValue: (name: string) => any; - setFieldValue: (name: string, value: any) => void; - getError: (name: string) => string | undefined; uiComponents: AutoFormUIComponents; formComponents: AutoFormFieldComponents; } + +export type FieldConfig< + FieldTypes = string, + CustomData = Record, +> = BaseFieldConfig< + ReactNode, + FieldTypes, + React.ComponentType, + CustomData +>; diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts index 2921a49..26368ac 100644 --- a/packages/react/src/utils.ts +++ b/packages/react/src/utils.ts @@ -1,15 +1,82 @@ import { FieldConfig } from "@autoform/core"; import { SuperRefineFunction, - fieldConfig as baseFieldConfig, + fieldConfig as zodBaseFieldConfig, } from "@autoform/zod"; -import { ReactNode } from "react"; +import { fieldConfig as yupBaseFieldConfig } from "@autoform/yup"; +import React, { ReactNode } from "react"; +import { FieldWrapperProps } from "./types"; /** - * @deprecated Use `fieldConfig` from `@autoform/zod` or `@autoform/yup` with "React.ReactNode" for Renderables instead. + * @deprecated Use `buildZodFieldConfig` instead. */ -export function fieldConfig( - config: FieldConfig +export function fieldConfig< + FieldTypes = string, + CustomData = Record, +>( + config: FieldConfig< + ReactNode, + FieldTypes, + React.ComponentType, + CustomData + > ): SuperRefineFunction { - return baseFieldConfig(config); + return zodBaseFieldConfig< + ReactNode, + FieldTypes, + React.ComponentType, + CustomData + >(config); +} + +export function buildZodFieldConfig< + FieldTypes = string, + CustomData = Record, +>(): ( + config: FieldConfig< + ReactNode, + FieldTypes, + React.ComponentType, + CustomData + > +) => SuperRefineFunction { + return (config) => + zodBaseFieldConfig< + ReactNode, + FieldTypes, + React.ComponentType, + CustomData + >(config); +} + +export function buildYupFieldConfig< + FieldTypes = string, + CustomData = Record, +>(): ( + config: FieldConfig< + ReactNode, + FieldTypes, + React.ComponentType, + CustomData + > +) => ReturnType { + return (config) => + yupBaseFieldConfig< + ReactNode, + FieldTypes, + React.ComponentType, + CustomData + >(config); +} + +export function getPathInObject(obj: any, path: string[]): any { + let current = obj; + for (const key of path) { + current = current[key]; + + if (current === undefined) { + return undefined; + } + } + return current; } diff --git a/packages/shadcn/.eslintrc.js b/packages/shadcn/.eslintrc.cjs similarity index 70% rename from packages/shadcn/.eslintrc.js rename to packages/shadcn/.eslintrc.cjs index 4646413..406eead 100644 --- a/packages/shadcn/.eslintrc.js +++ b/packages/shadcn/.eslintrc.cjs @@ -7,4 +7,10 @@ module.exports = { project: "./tsconfig.lint.json", tsconfigRootDir: __dirname, }, + ignorePatterns: [ + "node_modules", + "dist", + "tailwind.config.ts", + "src/components/ui/*.tsx", + ], }; diff --git a/packages/shadcn/package.json b/packages/shadcn/package.json index 64f8344..3084546 100644 --- a/packages/shadcn/package.json +++ b/packages/shadcn/package.json @@ -8,8 +8,7 @@ "types": "./dist/index.d.ts", "scripts": { "lint": "eslint . --max-warnings 0", - "build": "npx tsx src/scripts/build.ts", - "dev": "npx tsx watch src/scripts/build.ts" + "build": "npx tsx src/scripts/build.ts" }, "repository": { "type": "git", diff --git a/packages/shadcn/registry/autoform.json b/packages/shadcn/registry/autoform.json index 10add6e..717560d 100644 --- a/packages/shadcn/registry/autoform.json +++ b/packages/shadcn/registry/autoform.json @@ -29,13 +29,13 @@ { "path": "autoform/utils.ts", "target": "components/ui/autoform/utils.ts", - "content": "import { FieldConfig } from \"@autoform/core\";\nimport {\n SuperRefineFunction,\n fieldConfig as baseFieldConfig,\n} from \"@autoform/zod\";\nimport { ReactNode } from \"react\";\nimport { FieldTypes } from \"./AutoForm\";\n\nexport function fieldConfig(\n config: FieldConfig\n): SuperRefineFunction {\n return baseFieldConfig(config);\n}\n", + "content": "import { buildZodFieldConfig } from \"@autoform/react\";\nimport { FieldTypes } from \"./AutoForm\";\n\nexport const fieldConfig = buildZodFieldConfig<\n FieldTypes,\n {\n // Add types for `customData` here.\n }\n>();\n", "type": "registry:ui" }, { "path": "autoform/types.ts", "target": "components/ui/autoform/types.ts", - "content": "import { AutoFormProps as BaseAutoFormProps } from \"@autoform/react\";\n\nexport interface AutoFormProps\n extends Omit, \"uiComponents\" | \"formComponents\"> {}\n", + "content": "import {\n AutoFormProps as BaseAutoFormProps,\n ExtendableAutoFormProps,\n} from \"@autoform/react\";\nimport { FieldValues } from \"react-hook-form\";\n\nexport interface AutoFormProps\n extends ExtendableAutoFormProps {}\n", "type": "registry:ui" }, { @@ -47,7 +47,7 @@ { "path": "autoform/AutoForm.tsx", "target": "components/ui/autoform/AutoForm.tsx", - "content": "import React from \"react\";\nimport {\n AutoForm as BaseAutoForm,\n AutoFormUIComponents,\n AutoFormFieldComponents,\n} from \"@autoform/react\";\nimport { AutoFormProps } from \"./types\";\nimport { Form } from \"./components/Form\";\nimport { FieldWrapper } from \"./components/FieldWrapper\";\nimport { ErrorMessage } from \"./components/ErrorMessage\";\nimport { SubmitButton } from \"./components/SubmitButton\";\nimport { StringField } from \"./components/StringField\";\nimport { NumberField } from \"./components/NumberField\";\nimport { BooleanField } from \"./components/BooleanField\";\nimport { DateField } from \"./components/DateField\";\nimport { SelectField } from \"./components/SelectField\";\nimport { ObjectWrapper } from \"./components/ObjectWrapper\";\nimport { ArrayWrapper } from \"./components/ArrayWrapper\";\nimport { ArrayElementWrapper } from \"./components/ArrayElementWrapper\";\n\nconst ShadcnUIComponents: AutoFormUIComponents = {\n Form,\n FieldWrapper,\n ErrorMessage,\n SubmitButton,\n ObjectWrapper,\n ArrayWrapper,\n ArrayElementWrapper,\n};\n\nexport const ShadcnAutoFormFieldComponents = {\n string: StringField,\n number: NumberField,\n boolean: BooleanField,\n date: DateField,\n select: SelectField,\n} as const;\nexport type FieldTypes = keyof typeof ShadcnAutoFormFieldComponents;\n\nexport function AutoForm>({\n ...props\n}: AutoFormProps) {\n return (\n \n );\n}\n", + "content": "import React from \"react\";\nimport {\n AutoForm as BaseAutoForm,\n AutoFormUIComponents,\n AutoFormFieldComponents,\n} from \"@autoform/react\";\nimport { AutoFormProps } from \"./types\";\nimport { Form } from \"./components/Form\";\nimport { FieldWrapper } from \"./components/FieldWrapper\";\nimport { ErrorMessage } from \"./components/ErrorMessage\";\nimport { SubmitButton } from \"./components/SubmitButton\";\nimport { StringField } from \"./components/StringField\";\nimport { NumberField } from \"./components/NumberField\";\nimport { BooleanField } from \"./components/BooleanField\";\nimport { DateField } from \"./components/DateField\";\nimport { SelectField } from \"./components/SelectField\";\nimport { ObjectWrapper } from \"./components/ObjectWrapper\";\nimport { ArrayWrapper } from \"./components/ArrayWrapper\";\nimport { ArrayElementWrapper } from \"./components/ArrayElementWrapper\";\n\nconst ShadcnUIComponents: AutoFormUIComponents = {\n Form,\n FieldWrapper,\n ErrorMessage,\n SubmitButton,\n ObjectWrapper,\n ArrayWrapper,\n ArrayElementWrapper,\n};\n\nexport const ShadcnAutoFormFieldComponents = {\n string: StringField,\n number: NumberField,\n boolean: BooleanField,\n date: DateField,\n select: SelectField,\n} as const;\nexport type FieldTypes = keyof typeof ShadcnAutoFormFieldComponents;\n\nexport function AutoForm>({\n uiComponents,\n formComponents,\n ...props\n}: AutoFormProps) {\n return (\n \n );\n}\n", "type": "registry:ui" }, { @@ -59,13 +59,13 @@ { "path": "autoform/components/StringField.tsx", "target": "components/ui/autoform/components/StringField.tsx", - "content": "import React from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { AutoFormFieldProps } from \"@autoform/react\";\n\nexport const StringField: React.FC = ({\n field,\n value,\n onChange,\n error,\n id,\n}) => (\n onChange(e.target.value)}\n className={error ? \"border-destructive\" : \"\"}\n {...field.fieldConfig?.inputProps}\n />\n);\n", + "content": "import React from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { AutoFormFieldProps } from \"@autoform/react\";\n\nexport const StringField: React.FC = ({\n inputProps,\n error,\n id,\n}) => (\n \n);\n", "type": "registry:ui" }, { "path": "autoform/components/SelectField.tsx", "target": "components/ui/autoform/components/SelectField.tsx", - "content": "import React from \"react\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { AutoFormFieldProps } from \"@autoform/react\";\n\nexport const SelectField: React.FC = ({\n field,\n value,\n onChange,\n error,\n id,\n}) => (\n \n);\n", + "content": "import React from \"react\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@/components/ui/select\";\nimport { AutoFormFieldProps } from \"@autoform/react\";\n\nexport const SelectField: React.FC = ({\n field,\n inputProps,\n error,\n id,\n}) => (\n \n);\n", "type": "registry:ui" }, { @@ -77,7 +77,7 @@ { "path": "autoform/components/NumberField.tsx", "target": "components/ui/autoform/components/NumberField.tsx", - "content": "import React from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { AutoFormFieldProps } from \"@autoform/react\";\n\nexport const NumberField: React.FC = ({\n field,\n value,\n onChange,\n error,\n id,\n}) => (\n onChange(Number(e.target.value))}\n className={error ? \"border-destructive\" : \"\"}\n {...field.fieldConfig?.inputProps}\n />\n);\n", + "content": "import React from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { AutoFormFieldProps } from \"@autoform/react\";\n\nexport const NumberField: React.FC = ({\n inputProps,\n error,\n id,\n}) => (\n \n);\n", "type": "registry:ui" }, { @@ -89,7 +89,7 @@ { "path": "autoform/components/FieldWrapper.tsx", "target": "components/ui/autoform/components/FieldWrapper.tsx", - "content": "import React from \"react\";\nimport { Label } from \"@/components/ui/label\";\nimport { FieldWrapperProps } from \"@autoform/react\";\n\nconst DISABLED_LABELS = [\"boolean\", \"date\", \"object\", \"array\"];\n\nexport const FieldWrapper: React.FC = ({\n label,\n error,\n children,\n id,\n field,\n}) => {\n const isDisabled = DISABLED_LABELS.includes(field.type);\n\n return (\n
\n {!isDisabled && (\n \n )}\n {children}\n {field.fieldConfig?.description && (\n

\n {field.fieldConfig.description}\n

\n )}\n
\n );\n};\n", + "content": "import React from \"react\";\nimport { Label } from \"@/components/ui/label\";\nimport { FieldWrapperProps } from \"@autoform/react\";\n\nconst DISABLED_LABELS = [\"boolean\", \"object\", \"array\"];\n\nexport const FieldWrapper: React.FC = ({\n label,\n children,\n id,\n field,\n error,\n}) => {\n const isDisabled = DISABLED_LABELS.includes(field.type);\n\n return (\n
\n {!isDisabled && (\n \n )}\n {children}\n {field.fieldConfig?.description && (\n

\n {field.fieldConfig.description}\n

\n )}\n {error &&

{error}

}\n
\n );\n};\n", "type": "registry:ui" }, { @@ -101,13 +101,13 @@ { "path": "autoform/components/DateField.tsx", "target": "components/ui/autoform/components/DateField.tsx", - "content": "import React from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { AutoFormFieldProps } from \"@autoform/react\";\n\nexport const DateField: React.FC = ({\n field,\n value,\n onChange,\n error,\n id,\n}) => (\n onChange(new Date(e.target.value))}\n className={error ? \"border-destructive\" : \"\"}\n {...field.fieldConfig?.inputProps}\n />\n);\n", + "content": "import React from \"react\";\nimport { Input } from \"@/components/ui/input\";\nimport { AutoFormFieldProps } from \"@autoform/react\";\n\nexport const DateField: React.FC = ({\n inputProps,\n error,\n id,\n}) => (\n \n);\n", "type": "registry:ui" }, { "path": "autoform/components/BooleanField.tsx", "target": "components/ui/autoform/components/BooleanField.tsx", - "content": "import React from \"react\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { AutoFormFieldProps } from \"@autoform/react\";\nimport { Label } from \"../../label\";\n\nexport const BooleanField: React.FC = ({\n field,\n value,\n onChange,\n label,\n id,\n}) => (\n
\n onChange(checked)}\n {...field.fieldConfig?.inputProps}\n />\n \n
\n);\n", + "content": "import React from \"react\";\nimport { Checkbox } from \"@/components/ui/checkbox\";\nimport { AutoFormFieldProps } from \"@autoform/react\";\nimport { Label } from \"../../label\";\n\nexport const BooleanField: React.FC = ({\n field,\n label,\n id,\n inputProps,\n}) => (\n
\n {\n // react-hook-form expects an event object\n const event = {\n target: {\n name: field.key,\n value: checked,\n },\n };\n inputProps.onChange(event);\n }}\n checked={inputProps.value}\n />\n \n
\n);\n", "type": "registry:ui" }, { @@ -119,7 +119,7 @@ { "path": "autoform/components/ArrayElementWrapper.tsx", "target": "components/ui/autoform/components/ArrayElementWrapper.tsx", - "content": "import React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { TrashIcon } from \"lucide-react\";\nimport { ArrayElementWrapperProps } from \"@autoform/react\";\n\nexport const ArrayElementWrapper: React.FC = ({\n children,\n onRemove,\n index,\n}) => {\n return (\n
\n \n \n \n {children}\n
\n );\n};\n", + "content": "import React from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { TrashIcon } from \"lucide-react\";\nimport { ArrayElementWrapperProps } from \"@autoform/react\";\n\nexport const ArrayElementWrapper: React.FC = ({\n children,\n onRemove,\n}) => {\n return (\n
\n \n \n \n {children}\n
\n );\n};\n", "type": "registry:ui" } ] diff --git a/packages/shadcn/src/components/ui/autoform/AutoForm.tsx b/packages/shadcn/src/components/ui/autoform/AutoForm.tsx index 69d9c9a..c9aef63 100644 --- a/packages/shadcn/src/components/ui/autoform/AutoForm.tsx +++ b/packages/shadcn/src/components/ui/autoform/AutoForm.tsx @@ -2,7 +2,6 @@ import React from "react"; import { AutoForm as BaseAutoForm, AutoFormUIComponents, - AutoFormFieldComponents, } from "@autoform/react"; import { AutoFormProps } from "./types"; import { Form } from "./components/Form"; @@ -38,13 +37,15 @@ export const ShadcnAutoFormFieldComponents = { export type FieldTypes = keyof typeof ShadcnAutoFormFieldComponents; export function AutoForm>({ + uiComponents, + formComponents, ...props }: AutoFormProps) { return ( ); } diff --git a/packages/shadcn/src/components/ui/autoform/components/ArrayElementWrapper.tsx b/packages/shadcn/src/components/ui/autoform/components/ArrayElementWrapper.tsx index 784856e..7174174 100644 --- a/packages/shadcn/src/components/ui/autoform/components/ArrayElementWrapper.tsx +++ b/packages/shadcn/src/components/ui/autoform/components/ArrayElementWrapper.tsx @@ -6,7 +6,6 @@ import { ArrayElementWrapperProps } from "@autoform/react"; export const ArrayElementWrapper: React.FC = ({ children, onRemove, - index, }) => { return (
@@ -15,6 +14,7 @@ export const ArrayElementWrapper: React.FC = ({ variant="ghost" size="sm" className="absolute top-2 right-2" + type="button" > diff --git a/packages/shadcn/src/components/ui/autoform/components/BooleanField.tsx b/packages/shadcn/src/components/ui/autoform/components/BooleanField.tsx index 5fe8dfb..a39d7eb 100644 --- a/packages/shadcn/src/components/ui/autoform/components/BooleanField.tsx +++ b/packages/shadcn/src/components/ui/autoform/components/BooleanField.tsx @@ -5,17 +5,24 @@ import { Label } from "../../label"; export const BooleanField: React.FC = ({ field, - value, - onChange, label, id, + inputProps, }) => (
onChange(checked)} - {...field.fieldConfig?.inputProps} + onCheckedChange={(checked) => { + // react-hook-form expects an event object + const event = { + target: { + name: field.key, + value: checked, + }, + }; + inputProps.onChange(event); + }} + checked={inputProps.value} />
); }; diff --git a/packages/shadcn/src/components/ui/autoform/components/NumberField.tsx b/packages/shadcn/src/components/ui/autoform/components/NumberField.tsx index 67d9b25..aeb17f2 100644 --- a/packages/shadcn/src/components/ui/autoform/components/NumberField.tsx +++ b/packages/shadcn/src/components/ui/autoform/components/NumberField.tsx @@ -3,18 +3,14 @@ import { Input } from "@/components/ui/input"; import { AutoFormFieldProps } from "@autoform/react"; export const NumberField: React.FC = ({ - field, - value, - onChange, + inputProps, error, id, }) => ( onChange(Number(e.target.value))} className={error ? "border-destructive" : ""} - {...field.fieldConfig?.inputProps} + {...inputProps} /> ); diff --git a/packages/shadcn/src/components/ui/autoform/components/SelectField.tsx b/packages/shadcn/src/components/ui/autoform/components/SelectField.tsx index 58a36d0..8d9077a 100644 --- a/packages/shadcn/src/components/ui/autoform/components/SelectField.tsx +++ b/packages/shadcn/src/components/ui/autoform/components/SelectField.tsx @@ -10,12 +10,11 @@ import { AutoFormFieldProps } from "@autoform/react"; export const SelectField: React.FC = ({ field, - value, - onChange, + inputProps, error, id, }) => ( - diff --git a/packages/shadcn/src/components/ui/autoform/components/StringField.tsx b/packages/shadcn/src/components/ui/autoform/components/StringField.tsx index 6f1d84e..0143e3b 100644 --- a/packages/shadcn/src/components/ui/autoform/components/StringField.tsx +++ b/packages/shadcn/src/components/ui/autoform/components/StringField.tsx @@ -3,17 +3,13 @@ import { Input } from "@/components/ui/input"; import { AutoFormFieldProps } from "@autoform/react"; export const StringField: React.FC = ({ - field, - value, - onChange, + inputProps, error, id, }) => ( onChange(e.target.value)} className={error ? "border-destructive" : ""} - {...field.fieldConfig?.inputProps} + {...inputProps} /> ); diff --git a/packages/shadcn/src/components/ui/autoform/types.ts b/packages/shadcn/src/components/ui/autoform/types.ts index 793bfd1..dd6eef0 100644 --- a/packages/shadcn/src/components/ui/autoform/types.ts +++ b/packages/shadcn/src/components/ui/autoform/types.ts @@ -1,4 +1,5 @@ -import { AutoFormProps as BaseAutoFormProps } from "@autoform/react"; +import { ExtendableAutoFormProps } from "@autoform/react"; +import { FieldValues } from "react-hook-form"; -export interface AutoFormProps - extends Omit, "uiComponents" | "formComponents"> {} +export interface AutoFormProps + extends ExtendableAutoFormProps {} diff --git a/packages/shadcn/src/components/ui/autoform/utils.ts b/packages/shadcn/src/components/ui/autoform/utils.ts index f8a519a..f1360d3 100644 --- a/packages/shadcn/src/components/ui/autoform/utils.ts +++ b/packages/shadcn/src/components/ui/autoform/utils.ts @@ -1,13 +1,9 @@ -import { FieldConfig } from "@autoform/core"; -import { - SuperRefineFunction, - fieldConfig as baseFieldConfig, -} from "@autoform/zod"; -import { ReactNode } from "react"; +import { buildZodFieldConfig } from "@autoform/react"; import { FieldTypes } from "./AutoForm"; -export function fieldConfig( - config: FieldConfig -): SuperRefineFunction { - return baseFieldConfig(config); -} +export const fieldConfig = buildZodFieldConfig< + FieldTypes, + { + // Add types for `customData` here. + } +>(); diff --git a/packages/shadcn/tsconfig.lint.json b/packages/shadcn/tsconfig.lint.json new file mode 100644 index 0000000..d9c88fa --- /dev/null +++ b/packages/shadcn/tsconfig.lint.json @@ -0,0 +1,13 @@ +{ + "extends": "@autoform/typescript-config/react-library.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src", "turbo"], + "exclude": [ + "node_modules", + "dist", + "tailwind.config.ts", + "src/components/ui/*.tsx" + ] +} diff --git a/packages/yup/src/default-values.ts b/packages/yup/src/default-values.ts index 21d7b3d..4916398 100644 --- a/packages/yup/src/default-values.ts +++ b/packages/yup/src/default-values.ts @@ -1,4 +1,3 @@ -import * as yup from "yup"; import { YupField, YupObjectOrWrapped } from "./types"; export function getYupFieldDefaultValue(schema: YupField): any { diff --git a/packages/yup/src/field-config.ts b/packages/yup/src/field-config.ts index 9b8685b..09009e8 100644 --- a/packages/yup/src/field-config.ts +++ b/packages/yup/src/field-config.ts @@ -1,23 +1,33 @@ import { FieldConfig } from "@autoform/core"; import { YupField } from "./types"; -export const FIELD_CONFIG_SYMBOL = Symbol("GetFieldConfig"); +export const YUP_FIELD_CONFIG_SYMBOL = Symbol("GetFieldConfig"); -export function fieldConfig( - config: FieldConfig +export function fieldConfig< + AdditionalRenderable = null, + FieldTypes = string, + FieldWrapper = any, + CustomData = Record, +>( + config: FieldConfig< + AdditionalRenderable, + FieldTypes, + FieldWrapper, + CustomData + > ) { const transformFunction = function (value: any) { return value; // Always pass, we're just using this for metadata }; - transformFunction[FIELD_CONFIG_SYMBOL] = config; + transformFunction[YUP_FIELD_CONFIG_SYMBOL] = config; return transformFunction; } export function getYupFieldConfig(schema: YupField): FieldConfig | undefined { for (const transform of schema.transforms) { - if (FIELD_CONFIG_SYMBOL in transform) { - return (transform as any)[FIELD_CONFIG_SYMBOL]; + if (YUP_FIELD_CONFIG_SYMBOL in transform) { + return (transform as any)[YUP_FIELD_CONFIG_SYMBOL]; } } diff --git a/packages/yup/src/field-type-inference.ts b/packages/yup/src/field-type-inference.ts index 93b2da6..3185043 100644 --- a/packages/yup/src/field-type-inference.ts +++ b/packages/yup/src/field-type-inference.ts @@ -1,5 +1,4 @@ import { FieldConfig } from "@autoform/core"; -import * as yup from "yup"; import { YupEnumSchema, YupField } from "./types"; export function inferFieldType( diff --git a/packages/yup/src/provider.ts b/packages/yup/src/provider.ts index 4be75a6..f093352 100644 --- a/packages/yup/src/provider.ts +++ b/packages/yup/src/provider.ts @@ -3,7 +3,6 @@ import { SchemaProvider } from "@autoform/core"; import { parseSchema } from "./schema-parser"; import { validateSchema } from "./validator"; import { getDefaultValues } from "./default-values"; -import { YupObjectOrWrapped } from "./types"; export class YupProvider implements SchemaProvider diff --git a/packages/yup/src/validator.ts b/packages/yup/src/validator.ts index 654bb47..518dae6 100644 --- a/packages/yup/src/validator.ts +++ b/packages/yup/src/validator.ts @@ -3,8 +3,8 @@ import { YupObjectOrWrapped } from "./types"; export function validateSchema(schema: YupObjectOrWrapped, values: any) { try { - schema.validateSync(values, { abortEarly: false }); - return { success: true, data: values } as const; + const data = schema.validateSync(values, { abortEarly: false }); + return { success: true, data } as const; } catch (error) { if (error instanceof yup.ValidationError) { return { diff --git a/packages/zod/src/field-config.ts b/packages/zod/src/field-config.ts index 818f96d..2463f96 100644 --- a/packages/zod/src/field-config.ts +++ b/packages/zod/src/field-config.ts @@ -1,23 +1,33 @@ import { RefinementEffect, z } from "zod"; -import { FieldConfig } from "../../core/dist"; -export const FIELD_CONFIG_SYMBOL = Symbol("GetFieldConfig"); +import { FieldConfig } from "@autoform/core"; +export const ZOD_FIELD_CONFIG_SYMBOL = Symbol("GetFieldConfig"); export type SuperRefineFunction = () => unknown; -export function fieldConfig( - config: FieldConfig, +export function fieldConfig< + AdditionalRenderable = null, + FieldTypes = string, + FieldWrapper = any, + CustomData = Record, +>( + config: FieldConfig< + AdditionalRenderable, + FieldTypes, + FieldWrapper, + CustomData + > ): SuperRefineFunction { const refinementFunction: SuperRefineFunction = () => { // Do nothing. }; // @ts-expect-error This is a symbol and not a real value. - refinementFunction[FIELD_CONFIG_SYMBOL] = config; + refinementFunction[ZOD_FIELD_CONFIG_SYMBOL] = config; return refinementFunction; } export function getFieldConfigInZodStack( - schema: z.ZodTypeAny, + schema: z.ZodTypeAny ): FieldConfig | undefined { const typedSchema = schema as unknown as z.ZodEffects< z.ZodNumber | z.ZodString @@ -27,19 +37,19 @@ export function getFieldConfigInZodStack( const effect = typedSchema._def.effect as RefinementEffect; const refinementFunction = effect.refinement; - if (FIELD_CONFIG_SYMBOL in refinementFunction) { - return refinementFunction[FIELD_CONFIG_SYMBOL] as FieldConfig; + if (ZOD_FIELD_CONFIG_SYMBOL in refinementFunction) { + return refinementFunction[ZOD_FIELD_CONFIG_SYMBOL] as FieldConfig; } } if ("innerType" in typedSchema._def) { return getFieldConfigInZodStack( - typedSchema._def.innerType as unknown as z.ZodAny, + typedSchema._def.innerType as unknown as z.ZodAny ); } if ("schema" in typedSchema._def) { return getFieldConfigInZodStack( - (typedSchema._def as any).schema as z.ZodAny, + (typedSchema._def as any).schema as z.ZodAny ); } diff --git a/packages/zod/src/provider.ts b/packages/zod/src/provider.ts index ddb2c4a..f291fe1 100644 --- a/packages/zod/src/provider.ts +++ b/packages/zod/src/provider.ts @@ -2,8 +2,9 @@ import { z } from "zod"; import { SchemaProvider, ParsedSchema, SchemaValidation } from "@autoform/core"; import { getDefaultValues } from "./default-values"; import { parseSchema } from "./schema-parser"; +import { ZodObjectOrWrapped } from "./types"; -export class ZodProvider> +export class ZodProvider implements SchemaProvider> { /** @@ -23,8 +24,8 @@ export class ZodProvider> validateSchema(values: z.infer): SchemaValidation { try { - this.schema.parse(values); - return { success: true, data: values } as const; + const data = this.schema.parse(values); + return { success: true, data } as const; } catch (error) { if (error instanceof z.ZodError) { return { diff --git a/turbo.json b/turbo.json index af76cf6..e79cc86 100644 --- a/turbo.json +++ b/turbo.json @@ -13,6 +13,14 @@ "dev": { "cache": false, "persistent": true + }, + "cypress": { + "dependsOn": ["^build"], + "cache": false + }, + "cypress:dev": { + "dependsOn": ["^dev"], + "cache": false } } }