mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 01:12:56 +03:00
console: create new form component without using forwardRef
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5755 GitOrigin-RevId: 9078553499377484b15d53bfbe0c19eaee9a376a
This commit is contained in:
parent
bd071ddc7f
commit
35873b1897
@ -82,3 +82,58 @@ export const Form = React.forwardRef(
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type TFormValues = Record<string, unknown>;
|
||||
|
||||
type Schema = ZodType<TFormValues, ZodTypeDef, TFormValues>;
|
||||
|
||||
export const UpdatedForm = <FormSchema extends Schema>(
|
||||
props: FormProps<zodInfer<FormSchema>, FormSchema> & {
|
||||
autoFocus?: Path<zodInfer<FormSchema>>;
|
||||
trigger?: boolean;
|
||||
}
|
||||
) => {
|
||||
const {
|
||||
id,
|
||||
options,
|
||||
schema,
|
||||
onSubmit,
|
||||
className,
|
||||
children,
|
||||
autoFocus,
|
||||
trigger,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const methods = useForm<zodInfer<FormSchema>>({
|
||||
...options,
|
||||
resolver: schema && zodResolver(schema),
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (autoFocus) {
|
||||
methods.setFocus(autoFocus);
|
||||
}
|
||||
if (trigger) {
|
||||
methods.trigger();
|
||||
}
|
||||
}, [trigger, autoFocus, methods]);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
id={id}
|
||||
className={`space-y-md bg-legacybg p-4 ${className || ''}`}
|
||||
onSubmit={methods.handleSubmit(onSubmit)}
|
||||
{...rest}
|
||||
>
|
||||
{children(methods)}
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
UpdatedForm.defaultProps = {
|
||||
autoFocus: undefined,
|
||||
trigger: false,
|
||||
};
|
||||
|
@ -1,49 +1,55 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import get from 'lodash.get';
|
||||
import { FieldError, useFormContext } from 'react-hook-form';
|
||||
import { FieldError, FieldPath, useFormContext } from 'react-hook-form';
|
||||
import { z, ZodTypeDef, ZodType } from 'zod';
|
||||
import { FieldWrapper, FieldWrapperPassThroughProps } from './FieldWrapper';
|
||||
|
||||
export type InputFieldProps = FieldWrapperPassThroughProps & {
|
||||
/**
|
||||
* The input field name
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The input field type
|
||||
*/
|
||||
type?: 'text' | 'email' | 'password' | 'number';
|
||||
/**
|
||||
* The input field classes
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The input field icon
|
||||
*/
|
||||
icon?: ReactElement;
|
||||
/**
|
||||
* The input field icon position
|
||||
*/
|
||||
iconPosition?: 'start' | 'end';
|
||||
/**
|
||||
* The input field placeholder
|
||||
*/
|
||||
placeholder?: string;
|
||||
/**
|
||||
* Flag to indicate if the field is disabled
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* The input field prepend label
|
||||
*/
|
||||
prependLabel?: string;
|
||||
/**
|
||||
* The input field append label
|
||||
*/
|
||||
appendLabel?: string;
|
||||
};
|
||||
type TFormValues = Record<string, unknown>;
|
||||
|
||||
export const InputField: React.FC<InputFieldProps> = ({
|
||||
type Schema = ZodType<TFormValues, ZodTypeDef, TFormValues>;
|
||||
|
||||
export type InputFieldProps<T extends z.infer<Schema>> =
|
||||
FieldWrapperPassThroughProps & {
|
||||
/**
|
||||
* The input field name
|
||||
*/
|
||||
name: FieldPath<T>;
|
||||
/**
|
||||
* The input field type
|
||||
*/
|
||||
type?: 'text' | 'email' | 'password' | 'number';
|
||||
/**
|
||||
* The input field classes
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* The input field icon
|
||||
*/
|
||||
icon?: ReactElement;
|
||||
/**
|
||||
* The input field icon position
|
||||
*/
|
||||
iconPosition?: 'start' | 'end';
|
||||
/**
|
||||
* The input field placeholder
|
||||
*/
|
||||
placeholder?: string;
|
||||
/**
|
||||
* Flag to indicate if the field is disabled
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* The input field prepend label
|
||||
*/
|
||||
prependLabel?: string;
|
||||
/**
|
||||
* The input field append label
|
||||
*/
|
||||
appendLabel?: string;
|
||||
};
|
||||
|
||||
export const InputField = <T extends z.infer<Schema>>({
|
||||
type = 'text',
|
||||
name,
|
||||
icon,
|
||||
@ -54,11 +60,11 @@ export const InputField: React.FC<InputFieldProps> = ({
|
||||
appendLabel = '',
|
||||
dataTest,
|
||||
...wrapperProps
|
||||
}: InputFieldProps) => {
|
||||
}: InputFieldProps<T>) => {
|
||||
const {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
} = useFormContext<T>();
|
||||
|
||||
const maybeError = get(errors, name) as FieldError | undefined;
|
||||
return (
|
||||
|
445
console/src/new-components/Form/UpdatedForm.stories.tsx
Normal file
445
console/src/new-components/Form/UpdatedForm.stories.tsx
Normal file
@ -0,0 +1,445 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { DevTool } from '@hookform/devtools';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
UpdatedForm as Form,
|
||||
InputField,
|
||||
Textarea,
|
||||
Select,
|
||||
Checkbox,
|
||||
Radio,
|
||||
CodeEditorField,
|
||||
} from '@/new-components/Form';
|
||||
import { Button } from '@/new-components/Button';
|
||||
|
||||
export default {
|
||||
title: 'components/Forms 📁/Updated Form🦠',
|
||||
component: Form,
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
component: ``,
|
||||
},
|
||||
source: { type: 'code', state: 'open' },
|
||||
},
|
||||
},
|
||||
} as ComponentMeta<typeof Form>;
|
||||
|
||||
export const Primary = () => {
|
||||
const validationSchema = z.object({
|
||||
inputFieldName: z.string().min(1, { message: 'Mandatory field' }),
|
||||
});
|
||||
const onSubmit = (output: z.infer<typeof validationSchema>) => {
|
||||
action('onSubmit')(output);
|
||||
};
|
||||
|
||||
type Schema = z.infer<typeof validationSchema>;
|
||||
|
||||
return (
|
||||
<Form
|
||||
// Apply validation schema to the form
|
||||
schema={validationSchema}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{/* 💡 The `control` prop is provided by [**React Hook Form**](https://react-hook-form.com/api/useform)
|
||||
and is used to access the form state. */}
|
||||
{({ control }) => (
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-xl font-semibold mb-xs">Basic form</h1>
|
||||
<InputField<Schema>
|
||||
name="inputFieldName"
|
||||
label="The input field label"
|
||||
placeholder="Input field placeholder"
|
||||
/>
|
||||
<Button type="submit" mode="primary">
|
||||
Submit
|
||||
</Button>
|
||||
{/* Debug form state */}
|
||||
<DevTool control={control} />
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const FormInputDefaultValue: ComponentStory<typeof Form> = () => {
|
||||
const validationSchema = z.object({
|
||||
inputFieldName: z.string().min(1, { message: 'Mandatory field' }),
|
||||
});
|
||||
|
||||
const onSubmit = (output: z.infer<typeof validationSchema>) => {
|
||||
action('onSubmit')(output);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
schema={validationSchema}
|
||||
options={{
|
||||
defaultValues: {
|
||||
inputFieldName: 'Hello world !',
|
||||
},
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ control }) => (
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-xl font-semibold mb-xs">Default value</h1>
|
||||
<InputField
|
||||
name="inputFieldName"
|
||||
label="The input field label"
|
||||
placeholder="Input field placeholder"
|
||||
/>
|
||||
<Button type="submit" mode="primary">
|
||||
Submit
|
||||
</Button>
|
||||
<DevTool control={control} />
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
FormInputDefaultValue.storyName = '💠 Form input default value';
|
||||
FormInputDefaultValue.parameters = {
|
||||
docs: {
|
||||
description: {
|
||||
story: `In this example, the form is automatically filled with the \`Hello world !\`
|
||||
value for the \`inputFieldName\` input.`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ManuallyTriggerFormValidation: ComponentStory<typeof Form> =
|
||||
() => {
|
||||
const validationSchema = z.object({
|
||||
inputFieldName: z.string().min(1, { message: 'Mandatory field' }),
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
id="formId"
|
||||
schema={validationSchema}
|
||||
trigger
|
||||
onSubmit={action('onSubmit')}
|
||||
>
|
||||
{options => {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-xl font-semibold mb-xs">
|
||||
Manually trigger form validation
|
||||
</h1>
|
||||
<InputField
|
||||
name="inputFieldName"
|
||||
label="The input field label"
|
||||
placeholder="Input field placeholder"
|
||||
/>
|
||||
<Button type="submit" mode="primary">
|
||||
Submit
|
||||
</Button>
|
||||
{/* Debug form state */}
|
||||
<DevTool control={options.control} />
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
ManuallyTriggerFormValidation.storyName = '💠 Manually trigger form validation';
|
||||
ManuallyTriggerFormValidation.parameters = {
|
||||
docs: {
|
||||
description: {
|
||||
story: `In this example, the form is automatically validated thanks to a \`useImperativeHandle\` hook.`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ManuallyFocusField: ComponentStory<typeof Form> = () => {
|
||||
const validationSchema = z.object({
|
||||
codeEditorFieldName: z.string().min(1, { message: 'Mandatory field' }),
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
id="formId"
|
||||
schema={validationSchema}
|
||||
autoFocus="codeEditorFieldName"
|
||||
onSubmit={action('onSubmit')}
|
||||
>
|
||||
{({ control }) => (
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-xl font-semibold mb-xs">Manually focus field</h1>
|
||||
<CodeEditorField
|
||||
name="codeEditorFieldName"
|
||||
label="The code editor field label"
|
||||
/>
|
||||
<Button type="submit" mode="primary">
|
||||
Submit
|
||||
</Button>
|
||||
{/* Debug form state */}
|
||||
<DevTool control={control} />
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
ManuallyFocusField.storyName = '💠 Manually focus a field';
|
||||
ManuallyFocusField.parameters = {
|
||||
docs: {
|
||||
description: {
|
||||
story: `In this example, the form \`codeEditorFieldName\` field is automatically focused thanks to a \`useImperativeHandle\` hook.`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const AllInputs: ComponentStory<typeof Form> = () => {
|
||||
const validationSchema = z.object({
|
||||
inputFieldName: z.string().min(1, { message: 'Mandatory field' }),
|
||||
textareaName: z.string().min(1, { message: 'Mandatory field' }),
|
||||
selectName: z.string().min(1, { message: 'Mandatory field' }),
|
||||
checkboxNames: z
|
||||
// When nothing is selected, the value is a false boolean
|
||||
.union([z.string().array(), z.boolean()])
|
||||
.refine(
|
||||
value => Array.isArray(value) && value.length > 0,
|
||||
'Choose at least one option'
|
||||
),
|
||||
radioName: z
|
||||
// When nothing is selected, the value is null
|
||||
.union([z.string(), z.null()])
|
||||
.refine(
|
||||
value => typeof value === 'string' && value.length > 0,
|
||||
'Choose one option'
|
||||
),
|
||||
codeEditorFieldName: z.string().min(1, { message: 'Mandatory field' }),
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
id="formId"
|
||||
options={{
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onChange',
|
||||
}}
|
||||
schema={validationSchema}
|
||||
onSubmit={action('onSubmit')}
|
||||
>
|
||||
{({ control, reset }) => (
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-xl font-semibold mb-xs">Form title</h1>
|
||||
<InputField
|
||||
name="inputFieldName"
|
||||
label="The input field label"
|
||||
description="The input field description"
|
||||
tooltip="The input field tooltip"
|
||||
placeholder="Input field placeholder"
|
||||
/>
|
||||
<Textarea
|
||||
name="textareaName"
|
||||
label="The textarea label"
|
||||
description="The textarea description"
|
||||
tooltip="The textarea tooltip"
|
||||
placeholder="Textarea field placeholder"
|
||||
/>
|
||||
<Select
|
||||
name="selectName"
|
||||
options={[
|
||||
{ value: 'selectValue0', label: 'Select value 0' },
|
||||
{
|
||||
value: 'selectValue1',
|
||||
label: 'Select value 1',
|
||||
disabled: true,
|
||||
},
|
||||
{ value: 'selectValue2', label: 'Select value 2' },
|
||||
]}
|
||||
label="The select label *"
|
||||
description="The select description"
|
||||
tooltip="The select tooltip"
|
||||
placeholder="--Select placeholder--"
|
||||
/>
|
||||
<Checkbox
|
||||
name="checkboxNames"
|
||||
label="The checkbox label *"
|
||||
description="The checkbox description"
|
||||
tooltip="The checkbox tooltip"
|
||||
options={[
|
||||
{ value: 'checkboxValue0', label: 'Checkbox value 0' },
|
||||
{
|
||||
value: 'checkboxValue1',
|
||||
label: 'Checkbox value 1',
|
||||
disabled: true,
|
||||
},
|
||||
{ value: 'checkboxValue2', label: 'Checkbox value 2' },
|
||||
]}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
<Radio
|
||||
name="radioName"
|
||||
label="The radio label *"
|
||||
description="The radio description"
|
||||
tooltip="The radio tooltip"
|
||||
options={[
|
||||
{ value: 'radioValue0', label: 'Radio value 0' },
|
||||
{
|
||||
value: 'radioValue1',
|
||||
label: 'Radio value 1',
|
||||
disabled: true,
|
||||
},
|
||||
{ value: 'radioValue2', label: 'Radio value 2' },
|
||||
]}
|
||||
orientation="horizontal"
|
||||
/>
|
||||
<CodeEditorField
|
||||
name="codeEditorFieldName"
|
||||
label="The code editor label *"
|
||||
description="The code editor description"
|
||||
tooltip="The code editor tooltip"
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<Button type="button" onClick={() => reset({})}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" mode="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
<DevTool control={control} />
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
AllInputs.storyName = '💠 Demo with all inputs and form reset';
|
||||
AllInputs.parameters = {
|
||||
docs: {
|
||||
description: {
|
||||
story: `Validation schema with all inputs mandatory.`,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const AllInputsHorizontal: ComponentStory<typeof Form> = () => {
|
||||
const validationSchema = z.object({
|
||||
inputFieldName: z.string().min(1, { message: 'Mandatory field' }),
|
||||
textareaName: z.string().min(1, { message: 'Mandatory field' }),
|
||||
selectName: z.string().min(1, { message: 'Mandatory field' }),
|
||||
checkboxNames: z
|
||||
// When nothing is selected, the value is a false boolean
|
||||
.union([z.string().array(), z.boolean()])
|
||||
.refine(
|
||||
value => Array.isArray(value) && value.length > 0,
|
||||
'Choose at least one option'
|
||||
),
|
||||
radioName: z
|
||||
// When nothing is selected, the value is null
|
||||
.union([z.string(), z.null()])
|
||||
.refine(
|
||||
value => typeof value === 'string' && value.length > 0,
|
||||
'Choose one option'
|
||||
),
|
||||
codeEditorFieldName: z.string().min(1, { message: 'Mandatory field' }),
|
||||
});
|
||||
|
||||
return (
|
||||
<Form
|
||||
id="formId"
|
||||
options={{
|
||||
mode: 'onSubmit',
|
||||
reValidateMode: 'onChange',
|
||||
}}
|
||||
schema={validationSchema}
|
||||
onSubmit={action('onSubmit')}
|
||||
>
|
||||
{({ control, reset }) => (
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-xl font-semibold mb-xs">Form title</h1>
|
||||
<InputField
|
||||
name="inputFieldName"
|
||||
label="The input field label"
|
||||
description="The input field description"
|
||||
tooltip="The input field tooltip"
|
||||
placeholder="Input field placeholder"
|
||||
horizontal
|
||||
/>
|
||||
<Textarea
|
||||
name="textareaName"
|
||||
label="The textarea label"
|
||||
description="The textarea description"
|
||||
tooltip="The textarea tooltip"
|
||||
placeholder="Textarea field placeholder"
|
||||
horizontal
|
||||
/>
|
||||
<Select
|
||||
name="selectName"
|
||||
options={[
|
||||
{ value: 'selectValue0', label: 'Select value 0' },
|
||||
{
|
||||
value: 'selectValue1',
|
||||
label: 'Select value 1',
|
||||
disabled: true,
|
||||
},
|
||||
{ value: 'selectValue2', label: 'Select value 2' },
|
||||
]}
|
||||
label="The select label *"
|
||||
description="The select description"
|
||||
tooltip="The select tooltip"
|
||||
placeholder="--Select placeholder--"
|
||||
horizontal
|
||||
/>
|
||||
<Checkbox
|
||||
name="checkboxNames"
|
||||
label="The checkbox label *"
|
||||
description="The checkbox description"
|
||||
tooltip="The checkbox tooltip"
|
||||
options={[
|
||||
{ value: 'checkboxValue0', label: 'Checkbox value 0' },
|
||||
{
|
||||
value: 'checkboxValue1',
|
||||
label: 'Checkbox value 1',
|
||||
disabled: true,
|
||||
},
|
||||
{ value: 'checkboxValue2', label: 'Checkbox value 2' },
|
||||
]}
|
||||
orientation="vertical"
|
||||
horizontal
|
||||
/>
|
||||
<Radio
|
||||
name="radioName"
|
||||
label="The radio label *"
|
||||
description="The radio description"
|
||||
tooltip="The radio tooltip"
|
||||
options={[
|
||||
{ value: 'radioValue0', label: 'Radio value 0' },
|
||||
{
|
||||
value: 'radioValue1',
|
||||
label: 'Radio value 1',
|
||||
disabled: true,
|
||||
},
|
||||
{ value: 'radioValue2', label: 'Radio value 2' },
|
||||
]}
|
||||
orientation="vertical"
|
||||
horizontal
|
||||
/>
|
||||
<CodeEditorField
|
||||
name="codeEditorFieldName"
|
||||
label="The code editor label *"
|
||||
description="The code editor description"
|
||||
tooltip="The code editor tooltip"
|
||||
horizontal
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<Button type="button" onClick={() => reset({})}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" mode="primary">
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
<DevTool control={control} />
|
||||
</div>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
AllInputsHorizontal.storyName = ' 💠 Demo with horizontal fields';
|
@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { z } from 'zod';
|
||||
import { Form, InputField, InputFieldProps } from '@/new-components/Form';
|
||||
import { Button } from '@/new-components/Button';
|
||||
import { Form, InputField, InputFieldProps } from '..';
|
||||
import { Button } from '../../Button';
|
||||
|
||||
const renderInputField = (
|
||||
props: Omit<InputFieldProps, 'name'>,
|
||||
props: Omit<InputFieldProps<any>, 'name'>,
|
||||
schema: any = z.object({ title: z.string() })
|
||||
) => {
|
||||
const onSubmit = jest.fn();
|
||||
|
Loading…
Reference in New Issue
Block a user