diff --git a/packages/ra-core/src/controller/input/useGetArrayInputNewItemDefaults.ts b/packages/ra-core/src/controller/input/useGetArrayInputNewItemDefaults.ts index adf9fbc3e30..402834f082b 100644 --- a/packages/ra-core/src/controller/input/useGetArrayInputNewItemDefaults.ts +++ b/packages/ra-core/src/controller/input/useGetArrayInputNewItemDefaults.ts @@ -1,4 +1,5 @@ import { Children, isValidElement, useRef, type ReactNode } from 'react'; +import set from 'lodash/set.js'; import { FormDataConsumer } from '../../form/FormDataConsumer'; import type { ArrayInputContextValue } from './ArrayInputContext'; import { useEvent } from '../../util'; @@ -31,15 +32,18 @@ export const useGetArrayInputNewItemDefaults = ( // ArrayInput used for an array of objects // (e.g. authors: [{ firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Doe' }]) - const defaultValue = initialDefaultValue.current; + const defaultValue = { ...initialDefaultValue.current }; Children.forEach(inputs, input => { if ( isValidElement(input) && input.type !== FormDataConsumer && input.props.source ) { - defaultValue[input.props.source] = - input.props.defaultValue ?? null; + set( + defaultValue, + input.props.source, + input.props.defaultValue ?? null + ); } }); return defaultValue; diff --git a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx index 70c46da832a..b934b93826f 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx +++ b/packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx @@ -505,6 +505,197 @@ describe('', () => { expect(screen.queryAllByLabelText('ra.action.remove').length).toBe(1); }); + it('should not reuse removed values for nested sources when adding a new item', async () => { + render( + + + + + + + + + + + + + ); + + await waitFor(() => { + expect( + screen + .queryAllByLabelText('Stage Manager ID') + .map( + inputElement => (inputElement as HTMLInputElement).value + ) + ).toEqual(['101', '102', '103']); + }); + + const lastItem = screen + .queryAllByLabelText('Venue')[2] + .closest('li') as HTMLElement; + const removeLastButton = getByLabelText( + lastItem, + 'ra.action.remove' + ).closest('button') as HTMLButtonElement; + + fireEvent.click(removeLastButton); + await waitFor(() => { + expect(screen.queryAllByLabelText('Venue').length).toEqual(2); + }); + + fireEvent.click( + screen + .getByLabelText('ra.action.add') + .closest('button') as HTMLButtonElement + ); + + await waitFor(() => { + expect(screen.queryAllByLabelText('Venue').length).toEqual(3); + }); + + expect( + screen + .queryAllByLabelText('Stage Manager ID') + .map(inputElement => (inputElement as HTMLInputElement).value) + ).toEqual(['101', '102', '']); + expect( + screen + .queryAllByLabelText('Ticket Tier') + .map(inputElement => (inputElement as HTMLInputElement).value) + ).toEqual(['', 'premium', '']); + expect( + screen + .queryAllByLabelText('Language') + .map(inputElement => (inputElement as HTMLInputElement).value) + ).toEqual(['', 'en', '']); + }); + + it('should create nested null defaults for nested sources when adding a new item', async () => { + const save = jest.fn(); + + render( + + + + + + + + + + + + + ); + + const firstItem = screen + .queryAllByLabelText('Venue')[0] + .closest('li') as HTMLElement; + const removeFirstButton = getByLabelText( + firstItem, + 'ra.action.remove' + ).closest('button') as HTMLButtonElement; + + fireEvent.click(removeFirstButton); + await waitFor(() => { + expect(screen.queryAllByLabelText('Venue').length).toEqual(0); + }); + + fireEvent.click( + screen + .getByLabelText('ra.action.add') + .closest('button') as HTMLButtonElement + ); + + await waitFor(() => { + expect(screen.queryAllByLabelText('Venue').length).toEqual(1); + }); + + fireEvent.click(screen.getByText('ra.action.save')); + + await waitFor(() => { + expect(save).toHaveBeenCalled(); + }); + + expect(save.mock.calls[0][0]).toEqual({ + id: 1, + venueList: [ + { + venue: null, + details: { + stageManagerId: null, + ticketTier: null, + language: null, + }, + }, + ], + }); + }); + it('should remove children row on remove button click', async () => { const emails = [{ email: 'foo@bar.com' }, { email: 'bar@foo.com' }];