import * as React from 'react';
import {
useController,
UseControllerProps,
useFormState,
} from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import polyglotI18nProvider from 'ra-i18n-polyglot';
import englishMessages from 'ra-language-english';
import fakeRestDataProvider from 'ra-data-fakerest';
import {
Route,
Routes,
useNavigate,
Link,
HashRouter,
useLocation,
} from 'react-router-dom';
import { CoreAdminContext } from '../core';
import {
EditBase,
RecordContextProvider,
SaveContextProvider,
} from '../controller';
import { DeleteButton } from '../test-ui/DeleteButton';
import { Form, FormProps } from './Form';
import { useInput } from './useInput';
import { required, ValidationError } from './validation';
import { mergeTranslations } from '../i18n';
import { I18nProvider, RaRecord } from '../types';
import { TestMemoryRouter } from '../routing';
import { useNotificationContext } from '../notification';
export default {
title: 'ra-core/form/Form',
};
const Input = props => {
const { field, fieldState } = useInput(props);
return (
{fieldState.error && fieldState.error.message ? (
) : null}
);
};
const SubmitButton = () => {
const { dirtyFields } = useFormState();
// useFormState().isDirty might differ from useFormState().dirtyFields (https://github.com/react-hook-form/react-hook-form/issues/4740)
const isDirty = Object.keys(dirtyFields).length > 0;
return (
);
};
export const Basic = () => {
const [submittedData, setSubmittedData] = React.useState();
return (
{JSON.stringify(submittedData, null, 2)}
);
};
const CustomInput = (props: UseControllerProps) => {
const { field, fieldState } = useController(props);
return (
);
};
export const SanitizeEmptyValues = () => {
const [submittedData, setSubmittedData] = React.useState();
const field11 = { name: 'field11' };
const field12 = {
name: 'field12',
defaultValue: 'bar',
};
const field13 = {
name: 'field13',
defaultValue: '',
};
const field14 = { name: 'field14' };
const field16 = { name: 'field16' };
return (
{JSON.stringify(submittedData, null, 2)}
);
};
export const NullValue = () => {
const [result, setResult] = React.useState();
return (
{JSON.stringify(result, null, 2)}
);
};
export const UndefinedValue = () => {
const [result, setResult] = React.useState();
return (
{JSON.stringify(result, null, 2)}
);
};
const defaultI18nProvider = polyglotI18nProvider(() =>
mergeTranslations(englishMessages, {
app: {
validation: {
required: 'This field must be provided',
},
},
})
);
export const FormLevelValidation = ({
i18nProvider = defaultI18nProvider,
}: {
i18nProvider?: I18nProvider;
}) => {
const [submittedData, setSubmittedData] = React.useState();
return (
{JSON.stringify(submittedData, null, 2)}
);
};
export const InputLevelValidation = ({
i18nProvider = defaultI18nProvider,
}: {
i18nProvider?: I18nProvider;
}) => {
const [submittedData, setSubmittedData] = React.useState();
return (
{JSON.stringify(submittedData, null, 2)}
);
};
const zodSchema = z.object({
defaultMessage: z.string(), //.min(1),
customMessage: z.string({ error: 'This field is required' }),
customMessageTranslationKey: z.string({ error: 'app.validation.required' }),
missingCustomMessageTranslationKey: z.string({
error: 'app.validation.missing',
}),
});
export const ZodResolver = ({
i18nProvider = defaultI18nProvider,
}: {
i18nProvider?: I18nProvider;
}) => {
const [result, setResult] = React.useState();
return (
{JSON.stringify(result, null, 2)}
);
};
type Schema = z.infer;
export const ZodResolverWithSchema = ({
i18nProvider = defaultI18nProvider,
}: {
i18nProvider?: I18nProvider;
}) => {
const [result, setResult] = React.useState();
return (
{JSON.stringify(result, null, 2)}
);
};
const yupSchema = yup.object({
defaultMessage: yup.string().required(),
customMessage: yup.string().required('This field is required'),
customMessageTranslationKey: yup
.string()
.required('app.validation.required'),
missingCustomMessageTranslationKey: yup
.string()
.required('app.validation.missing'),
});
export const YupResolver = ({
i18nProvider = defaultI18nProvider,
}: {
i18nProvider?: I18nProvider;
}) => {
const [result, setResult] = React.useState();
return (
{JSON.stringify(result, null, 2)}
);
};
const FormUnderTest = () => {
const navigate = useNavigate();
return (
<>
Leave the form
>
);
};
export const WarnWhenUnsavedChanges = ({
i18nProvider = defaultI18nProvider,
}: {
i18nProvider?: I18nProvider;
}) => (
Go to form} />
} />
);
export const InNonDataRouter = ({
i18nProvider = defaultI18nProvider,
}: {
i18nProvider?: I18nProvider;
}) => (
Go to form} />
} />
);
const PostEditWithDelete = ({
mutationMode,
}: {
mutationMode: 'undoable' | 'optimistic' | 'pessimistic';
}) => (
);
/**
* Edit a field (making the form dirty), then click Delete. The record is
* deleted and the app redirects to the list, but the unsaved-changes blocker
* pops a spurious "unsaved changes" confirm dialog even though the record is
* already gone.
*/
export const WarnWhenUnsavedChangesWithDelete = ({
i18nProvider = defaultI18nProvider,
mutationMode = 'undoable',
}: {
i18nProvider?: I18nProvider;
mutationMode?: 'undoable' | 'optimistic' | 'pessimistic';
}) => (
}
/>
Post list} />
);
WarnWhenUnsavedChangesWithDelete.argTypes = {
mutationMode: {
options: ['undoable', 'optimistic', 'pessimistic'],
control: { type: 'select' },
},
};
const Notifications = () => {
const { notifications } = useNotificationContext();
return (
{notifications.map(({ message }, id) => (
- {message}
))}
);
};
export const ServerSideValidation = () => {
const save = React.useCallback(values => {
const errors: any = {};
if (!values.defaultMessage) {
errors.defaultMessage = 'ra.validation.required';
}
if (!values.customMessage) {
errors.customMessage = 'This field is required';
}
if (!values.customMessageTranslationKey) {
errors.customMessageTranslationKey = 'app.validation.required';
}
if (!values.missingCustomMessageTranslationKey) {
errors.missingCustomMessageTranslationKey =
'app.validation.missing';
}
if (!values.customGlobalMessage) {
errors.customGlobalMessage = 'ra.validation.required';
errors.root = {
serverError: 'There are validation errors. Please fix them.',
};
}
return Object.keys(errors).length > 0 ? errors : undefined;
}, []);
return (
);
};
export const MultiRoutesForm = ({
url,
initialRecord,
defaultValues,
}: {
url?: any;
initialRecord?: Partial;
defaultValues?: Partial;
}) => (
}
/>
);
MultiRoutesForm.args = {
url: 'unmodified',
initialRecord: 'none',
};
MultiRoutesForm.argTypes = {
url: {
options: [
'unmodified',
'modified with location state',
'modified with location search',
],
mapping: {
unmodified: '/form/general',
'modified with location state': {
pathname: '/form/general',
state: { record: { body: 'from-state' } },
},
'modified with location search': `/form/general?source=${encodeURIComponent(JSON.stringify({ body: 'from-search' }))}`,
},
control: { type: 'select' },
},
defaultValues: {
options: ['none', 'provided'],
mapping: {
none: undefined,
provided: {
category: 'default category',
},
},
control: { type: 'select' },
},
initialRecord: {
options: ['none', 'provided'],
mapping: {
none: undefined,
provided: { title: 'lorem', body: 'unmodified' },
},
control: { type: 'select' },
},
};
const FormWithSubRoutes = (props: Partial) => {
return (
);
};
const TabbedForm = () => {
const location = useLocation();
return (
<>
General
Settings
>
);
};
const Tab = ({
children,
name,
}: {
children: React.ReactNode;
name: string;
}) => {
const location = useLocation();
return (
{children}
);
};