diff --git a/cypress/integration/edit.js b/cypress/integration/edit.js index d23df5aefa9..496e27277e4 100644 --- a/cypress/integration/edit.js +++ b/cypress/integration/edit.js @@ -69,6 +69,17 @@ describe('Edit Page', () => { ); }); + it('should validate inputs inside ArrayInput', () => { + EditPostPage.gotoTab(3); + + cy.get(EditPostPage.elements.addBacklinkButton).click(); + + EditPostPage.clickInput('backlinks[0].url'); + cy.get(EditPostPage.elements.input('backlinks[0].url')).blur(); + + cy.contains('Required'); + }); + it('should change reference list correctly when changing filter', () => { const EditPostTagsPage = editPageFactory('/#/posts/13'); EditPostTagsPage.navigate(); diff --git a/cypress/support/EditPage.js b/cypress/support/EditPage.js index 4a9314028fc..b9c80aa473e 100644 --- a/cypress/support/EditPage.js +++ b/cypress/support/EditPage.js @@ -2,6 +2,7 @@ export default url => ({ elements: { body: 'body', deleteButton: '.ra-delete-button', + addBacklinkButton: '.button-add-backlinks', input: (name, type = 'input') => { if (type === 'rich-text-input') { return `.ra-input-${name} .ql-editor`; diff --git a/examples/simple/src/posts/PostEdit.js b/examples/simple/src/posts/PostEdit.js index 371b9155382..de8a6a5e0dc 100644 --- a/examples/simple/src/posts/PostEdit.js +++ b/examples/simple/src/posts/PostEdit.js @@ -144,7 +144,7 @@ const PostEdit = ({ permissions, ...props }) => ( - + diff --git a/packages/ra-core/src/form/useFormGroup.ts b/packages/ra-core/src/form/useFormGroup.ts index 3936912f772..f5a6dbe2005 100644 --- a/packages/ra-core/src/form/useFormGroup.ts +++ b/packages/ra-core/src/form/useFormGroup.ts @@ -5,11 +5,12 @@ import { useFormContext } from './useFormContext'; import { FieldState } from 'final-form'; type FormGroupState = { + dirty: boolean; errors: object; - valid: boolean; invalid: boolean; pristine: boolean; - dirty: boolean; + touched: boolean; + valid: boolean; }; /** @@ -58,18 +59,23 @@ export const useFormGroup = (name: string): FormGroupState => { const form = useForm(); const formContext = useFormContext(); const [state, setState] = useState({ + dirty: false, errors: undefined, - valid: true, invalid: false, pristine: true, - dirty: false, + touched: false, + valid: true, }); useEffect(() => { const unsubscribe = form.subscribe( () => { const fields = formContext.getGroupFields(name); - const fieldStates = fields.map(form.getFieldState); + const fieldStates = fields + .map(field => { + return form.getFieldState(field); + }) + .filter(fieldState => fieldState != undefined); // eslint-disable-line const newState = getFormGroupState(fieldStates); setState(oldState => { @@ -86,6 +92,7 @@ export const useFormGroup = (name: string): FormGroupState => { dirty: true, pristine: true, valid: true, + touched: true, } ); return unsubscribe; @@ -102,8 +109,8 @@ export const useFormGroup = (name: string): FormGroupState => { */ export const getFormGroupState = ( fieldStates: FieldState[] -): FormGroupState => - fieldStates.reduce( +): FormGroupState => { + return fieldStates.reduce( (acc, fieldState) => { let errors = acc.errors || {}; @@ -112,20 +119,23 @@ export const getFormGroupState = ( } const newState = { + dirty: acc.dirty || fieldState.dirty, errors, - valid: acc.valid && fieldState.valid, invalid: acc.invalid || fieldState.invalid, pristine: acc.pristine && fieldState.pristine, - dirty: acc.dirty || fieldState.dirty, + touched: acc.touched || fieldState.touched, + valid: acc.valid && fieldState.valid, }; return newState; }, { + dirty: false, errors: undefined, - valid: true, invalid: false, pristine: true, - dirty: false, + valid: true, + touched: false, } ); +}; diff --git a/packages/ra-ui-materialui/src/form/FormTab.tsx b/packages/ra-ui-materialui/src/form/FormTab.tsx index cf369b97945..3cfd3271370 100644 --- a/packages/ra-ui-materialui/src/form/FormTab.tsx +++ b/packages/ra-ui-materialui/src/form/FormTab.tsx @@ -93,7 +93,7 @@ export const FormTabHeader = ({ className={classnames('form-tab', className, { [classes.errorTabButton]: formGroup.invalid && - formGroup.dirty && + formGroup.touched && location.pathname !== value, })} component={Link} diff --git a/packages/ra-ui-materialui/src/input/TranslatableInputsTab.tsx b/packages/ra-ui-materialui/src/input/TranslatableInputsTab.tsx index ab23914f491..9a48783ffb1 100644 --- a/packages/ra-ui-materialui/src/input/TranslatableInputsTab.tsx +++ b/packages/ra-ui-materialui/src/input/TranslatableInputsTab.tsx @@ -13,7 +13,8 @@ export const TranslatableInputsTab = ( props: TranslatableInputsTabProps & TabProps ) => { const { groupKey = '', locale, classes: classesOverride, ...rest } = props; - const { invalid } = useFormGroup(`${groupKey}${locale}`); + const { invalid, touched } = useFormGroup(`${groupKey}${locale}`); + const classes = useStyles(props); const translate = useTranslate(); @@ -23,7 +24,7 @@ export const TranslatableInputsTab = ( label={translate(`ra.locales.${locale}`, { _: capitalize(locale), })} - className={invalid ? classes.error : undefined} + className={invalid && touched ? classes.error : undefined} {...rest} /> );