Skip to content

Commit 2644a67

Browse files
authored
Merge pull request #11201 from marmelab/fix-simpleformiterator-nested-defaults
[Fix] Prevent stale nested field values when re-adding items in SimpleFormIterator
2 parents 581775b + ea32863 commit 2644a67

File tree

2 files changed

+198
-3
lines changed

2 files changed

+198
-3
lines changed

packages/ra-core/src/controller/input/useGetArrayInputNewItemDefaults.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Children, isValidElement, useRef, type ReactNode } from 'react';
2+
import set from 'lodash/set.js';
23
import { FormDataConsumer } from '../../form/FormDataConsumer';
34
import type { ArrayInputContextValue } from './ArrayInputContext';
45
import { useEvent } from '../../util';
@@ -31,15 +32,18 @@ export const useGetArrayInputNewItemDefaults = (
3132

3233
// ArrayInput used for an array of objects
3334
// (e.g. authors: [{ firstName: 'John', lastName: 'Doe' }, { firstName: 'Jane', lastName: 'Doe' }])
34-
const defaultValue = initialDefaultValue.current;
35+
const defaultValue = { ...initialDefaultValue.current };
3536
Children.forEach(inputs, input => {
3637
if (
3738
isValidElement(input) &&
3839
input.type !== FormDataConsumer &&
3940
input.props.source
4041
) {
41-
defaultValue[input.props.source] =
42-
input.props.defaultValue ?? null;
42+
set(
43+
defaultValue,
44+
input.props.source,
45+
input.props.defaultValue ?? null
46+
);
4347
}
4448
});
4549
return defaultValue;

packages/ra-ui-materialui/src/input/ArrayInput/SimpleFormIterator.spec.tsx

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,197 @@ describe('<SimpleFormIterator />', () => {
505505
expect(screen.queryAllByLabelText('ra.action.remove').length).toBe(1);
506506
});
507507

508+
it('should not reuse removed values for nested sources when adding a new item', async () => {
509+
render(
510+
<Wrapper>
511+
<SimpleForm
512+
record={{
513+
id: 1,
514+
venueList: [
515+
{
516+
venue: 'Madison Square Garden, New York',
517+
details: {
518+
stageManagerId: '101',
519+
ticketTier: null,
520+
language: null,
521+
},
522+
},
523+
{
524+
venue: 'Wembley Stadium, London',
525+
details: {
526+
stageManagerId: '102',
527+
ticketTier: 'premium',
528+
language: 'en',
529+
},
530+
},
531+
{
532+
venue: 'Tokyo Dome, Tokyo',
533+
details: {
534+
stageManagerId: '103',
535+
ticketTier: 'vip',
536+
language: 'ja',
537+
},
538+
},
539+
],
540+
}}
541+
>
542+
<ArrayInput source="venueList">
543+
<SimpleFormIterator>
544+
<TextInput source="venue" label="Venue" />
545+
<TextInput
546+
source="details.stageManagerId"
547+
label="Stage Manager ID"
548+
/>
549+
<TextInput
550+
source="details.ticketTier"
551+
label="Ticket Tier"
552+
/>
553+
<TextInput
554+
source="details.language"
555+
label="Language"
556+
/>
557+
</SimpleFormIterator>
558+
</ArrayInput>
559+
</SimpleForm>
560+
</Wrapper>
561+
);
562+
563+
await waitFor(() => {
564+
expect(
565+
screen
566+
.queryAllByLabelText('Stage Manager ID')
567+
.map(
568+
inputElement => (inputElement as HTMLInputElement).value
569+
)
570+
).toEqual(['101', '102', '103']);
571+
});
572+
573+
const lastItem = screen
574+
.queryAllByLabelText('Venue')[2]
575+
.closest('li') as HTMLElement;
576+
const removeLastButton = getByLabelText(
577+
lastItem,
578+
'ra.action.remove'
579+
).closest('button') as HTMLButtonElement;
580+
581+
fireEvent.click(removeLastButton);
582+
await waitFor(() => {
583+
expect(screen.queryAllByLabelText('Venue').length).toEqual(2);
584+
});
585+
586+
fireEvent.click(
587+
screen
588+
.getByLabelText('ra.action.add')
589+
.closest('button') as HTMLButtonElement
590+
);
591+
592+
await waitFor(() => {
593+
expect(screen.queryAllByLabelText('Venue').length).toEqual(3);
594+
});
595+
596+
expect(
597+
screen
598+
.queryAllByLabelText('Stage Manager ID')
599+
.map(inputElement => (inputElement as HTMLInputElement).value)
600+
).toEqual(['101', '102', '']);
601+
expect(
602+
screen
603+
.queryAllByLabelText('Ticket Tier')
604+
.map(inputElement => (inputElement as HTMLInputElement).value)
605+
).toEqual(['', 'premium', '']);
606+
expect(
607+
screen
608+
.queryAllByLabelText('Language')
609+
.map(inputElement => (inputElement as HTMLInputElement).value)
610+
).toEqual(['', 'en', '']);
611+
});
612+
613+
it('should create nested null defaults for nested sources when adding a new item', async () => {
614+
const save = jest.fn();
615+
616+
render(
617+
<Wrapper>
618+
<SimpleForm
619+
onSubmit={save}
620+
record={{
621+
id: 1,
622+
venueList: [
623+
{
624+
venue: 'Tokyo Dome, Tokyo',
625+
details: {
626+
stageManagerId: '103',
627+
ticketTier: 'vip',
628+
language: 'ja',
629+
},
630+
},
631+
],
632+
}}
633+
>
634+
<ArrayInput source="venueList">
635+
<SimpleFormIterator>
636+
<TextInput source="venue" label="Venue" />
637+
<TextInput
638+
source="details.stageManagerId"
639+
label="Stage Manager ID"
640+
/>
641+
<TextInput
642+
source="details.ticketTier"
643+
label="Ticket Tier"
644+
/>
645+
<TextInput
646+
source="details.language"
647+
label="Language"
648+
/>
649+
</SimpleFormIterator>
650+
</ArrayInput>
651+
</SimpleForm>
652+
</Wrapper>
653+
);
654+
655+
const firstItem = screen
656+
.queryAllByLabelText('Venue')[0]
657+
.closest('li') as HTMLElement;
658+
const removeFirstButton = getByLabelText(
659+
firstItem,
660+
'ra.action.remove'
661+
).closest('button') as HTMLButtonElement;
662+
663+
fireEvent.click(removeFirstButton);
664+
await waitFor(() => {
665+
expect(screen.queryAllByLabelText('Venue').length).toEqual(0);
666+
});
667+
668+
fireEvent.click(
669+
screen
670+
.getByLabelText('ra.action.add')
671+
.closest('button') as HTMLButtonElement
672+
);
673+
674+
await waitFor(() => {
675+
expect(screen.queryAllByLabelText('Venue').length).toEqual(1);
676+
});
677+
678+
fireEvent.click(screen.getByText('ra.action.save'));
679+
680+
await waitFor(() => {
681+
expect(save).toHaveBeenCalled();
682+
});
683+
684+
expect(save.mock.calls[0][0]).toEqual({
685+
id: 1,
686+
venueList: [
687+
{
688+
venue: null,
689+
details: {
690+
stageManagerId: null,
691+
ticketTier: null,
692+
language: null,
693+
},
694+
},
695+
],
696+
});
697+
});
698+
508699
it('should remove children row on remove button click', async () => {
509700
const emails = [{ email: 'foo@bar.com' }, { email: 'bar@foo.com' }];
510701

0 commit comments

Comments
 (0)