From 1aa6b00ca81cdb1464682a95dfc82aac29e80eeb Mon Sep 17 00:00:00 2001 From: Eti Ijeoma Date: Mon, 17 Mar 2025 00:49:15 +0100 Subject: [PATCH 1/4] Isolate selection in multiple ArrayFields using storeKey --- .../src/field/ArrayField.spec.tsx | 43 +++++++++++- .../src/field/ArrayField.stories.tsx | 67 +++++++++++++++++++ .../ra-ui-materialui/src/field/ArrayField.tsx | 8 ++- 3 files changed, 115 insertions(+), 3 deletions(-) diff --git a/packages/ra-ui-materialui/src/field/ArrayField.spec.tsx b/packages/ra-ui-materialui/src/field/ArrayField.spec.tsx index e4b83574012..18d1a8d8f16 100644 --- a/packages/ra-ui-materialui/src/field/ArrayField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/ArrayField.spec.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { render, screen, waitFor } from '@testing-library/react'; +import { render, screen, waitFor, fireEvent } from '@testing-library/react'; import { CoreAdminContext, ResourceContextProvider, @@ -12,7 +12,7 @@ import { NumberField } from './NumberField'; import { TextField } from './TextField'; import { Datagrid } from '../list'; import { SimpleList } from '../list'; -import { ListContext } from './ArrayField.stories'; +import { ListContext, TwoArrayFieldsSelection } from './ArrayField.stories'; describe('', () => { const sort = { field: 'id', order: 'ASC' }; @@ -158,4 +158,43 @@ describe('', () => { ).toBeTruthy(); }); }); + + it('should not select the same id in both ArrayFields when selected in one', async () => { + render(); + + await waitFor(() => { + expect(screen.queryAllByRole('checkbox').length).toBeGreaterThan(2); + }); + + const checkboxes = screen.queryAllByRole('checkbox'); + + expect(checkboxes.length).toBeGreaterThan(3); + + // Select an item in the memberships list + fireEvent.click(checkboxes[1]); // Membership row 1 + render(); + + await waitFor(() => { + expect(checkboxes[1]).toBeChecked(); + }); + + //Ensure the same id in portfolios is NOT selected + expect(checkboxes[3]).not.toBeChecked(); + + fireEvent.click(checkboxes[3]); + render(); + + await waitFor(() => { + expect(checkboxes[3]).toBeChecked(); + expect(checkboxes[1]).toBeChecked(); + }); + + fireEvent.click(checkboxes[1]); + render(); + + await waitFor(() => { + expect(checkboxes[1]).not.toBeChecked(); + expect(checkboxes[3]).toBeChecked(); + }); + }); }); diff --git a/packages/ra-ui-materialui/src/field/ArrayField.stories.tsx b/packages/ra-ui-materialui/src/field/ArrayField.stories.tsx index 98f21adab32..ce4b4318d90 100644 --- a/packages/ra-ui-materialui/src/field/ArrayField.stories.tsx +++ b/packages/ra-ui-materialui/src/field/ArrayField.stories.tsx @@ -182,3 +182,70 @@ export const InShowLayout = () => ( ); + +export const TwoArrayFieldsSelection = () => ( + + + + + + + + + {/* Memberships ArrayField */} + + true} + > + + + + + + {/* Portfolios ArrayField */} + + true} + > + + + + + + + + + + +); diff --git a/packages/ra-ui-materialui/src/field/ArrayField.tsx b/packages/ra-ui-materialui/src/field/ArrayField.tsx index 59b8adf9474..1314bb37fa5 100644 --- a/packages/ra-ui-materialui/src/field/ArrayField.tsx +++ b/packages/ra-ui-materialui/src/field/ArrayField.tsx @@ -81,7 +81,13 @@ const ArrayFieldImpl = < ) => { const { children, resource, perPage, sort, filter } = props; const data = useFieldValue(props) || emptyArray; - const listContext = useList({ data, resource, perPage, sort, filter }); + const listContext = useList({ + data, + resource: storeKey || resource, // Prioritize storeKey if provided + perPage, + sort, + filter, + }); return ( {children} From 21dbb8828b9a2fe7ba713501a6ac023c2c5fe42c Mon Sep 17 00:00:00 2001 From: Eti Ijeoma Date: Mon, 17 Mar 2025 00:58:00 +0100 Subject: [PATCH 2/4] add instruction on the usage of storeKey in ArrayFIeld docs --- docs/ArrayField.md | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/docs/ArrayField.md b/docs/ArrayField.md index 94eff1283a1..346734c9012 100644 --- a/docs/ArrayField.md +++ b/docs/ArrayField.md @@ -71,12 +71,13 @@ const PostShow = () => ( ## Props -| Prop | Required | Type | Default | Description | -|------------|----------|-------------------|---------|------------------------------------------| -| `children` | Required | `ReactNode` | | The component to render the list. | -| `filter` | Optional | `object` | | The filter to apply to the list. | -| `perPage` | Optional | `number` | 1000 | The number of items to display per page. | -| `sort` | Optional | `{ field, order}` | | The sort to apply to the list. | +| Prop | Required | Type | Default | Description | +|------------|----------|-------------------|---------|----------------------------------------------------| +| `children` | Required | `ReactNode` | | The component to render the list. | +| `filter` | Optional | `object` | | The filter to apply to the list. | +| `perPage` | Optional | `number` | 1000 | The number of items to display per page. | +| `sort` | Optional | `{ field, order}` | | The sort to apply to the list. | +| `storeKey` | Optional | `string` | | The key to use to store the records selection state| `` accepts the [common field props](./Fields.md#common-field-props), except `emptyText` (use the child `empty` prop instead). @@ -217,6 +218,35 @@ By default, `` displays the items in the order they are stored in th ``` {% endraw %} +## `storeKey` + +By default, `ArrayField` stores the selection state in localStorage so users can revisit the page and find the selection preserved. The key for storing this state is based on the resource name, formatted as `${resource}.selectedIds`. + +When displaying multiple lists with the same data source, you may need to distinguish their selection states. To achieve this, assign a unique `storeKey` to each `ArrayField`. This allows each list to maintain its own selection state independently. + +In the example below, two `ArrayField` components display the same data source (`books`), but each stores its selection state under a different key (`customOne.selectedIds` and `customTwo.selectedIds`). This ensures that both components can coexist on the same page without interfering with each other's state. + +```jsx + + + + + + + + + + + + +``` + ## Using The List Context `` creates a [`ListContext`](./useListContext.md) with the field value, so you can use any of the list context values in its children. This includes callbacks to sort, filter, and select items. From 91327b6c6a1bd67b6805fec05d71c38d04cbfdfc Mon Sep 17 00:00:00 2001 From: Eti Ijeoma Date: Mon, 17 Mar 2025 01:11:27 +0100 Subject: [PATCH 3/4] fix lint --- packages/ra-ui-materialui/src/field/ArrayField.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ra-ui-materialui/src/field/ArrayField.tsx b/packages/ra-ui-materialui/src/field/ArrayField.tsx index 1314bb37fa5..7265a80f592 100644 --- a/packages/ra-ui-materialui/src/field/ArrayField.tsx +++ b/packages/ra-ui-materialui/src/field/ArrayField.tsx @@ -79,7 +79,7 @@ const ArrayFieldImpl = < >( props: ArrayFieldProps ) => { - const { children, resource, perPage, sort, filter } = props; + const { children, resource, perPage, sort, filter, storeKey } = props; const data = useFieldValue(props) || emptyArray; const listContext = useList({ data, @@ -105,6 +105,7 @@ export interface ArrayFieldProps< perPage?: number; sort?: SortPayload; filter?: FilterPayload; + storeKey?: string | false; } const emptyArray = []; From 5949c2304da5d365125c25354875470b1bb09dc4 Mon Sep 17 00:00:00 2001 From: Eti Ijeoma Date: Mon, 17 Mar 2025 02:05:22 +0100 Subject: [PATCH 4/4] add test for array field with storekey --- .../src/field/ArrayField.spec.tsx | 26 ++++++++++++++----- .../src/field/ArrayField.stories.tsx | 4 +-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/ra-ui-materialui/src/field/ArrayField.spec.tsx b/packages/ra-ui-materialui/src/field/ArrayField.spec.tsx index 18d1a8d8f16..0b96a4fc05c 100644 --- a/packages/ra-ui-materialui/src/field/ArrayField.spec.tsx +++ b/packages/ra-ui-materialui/src/field/ArrayField.spec.tsx @@ -14,6 +14,22 @@ import { Datagrid } from '../list'; import { SimpleList } from '../list'; import { ListContext, TwoArrayFieldsSelection } from './ArrayField.stories'; +beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation((message, ...args) => { + if ( + typeof message === 'string' && + message.includes('React will try recreating this component tree') + ) { + return; + } + console.warn(message, ...args); // Still log other errors + }); +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); + describe('', () => { const sort = { field: 'id', order: 'ASC' }; @@ -161,7 +177,6 @@ describe('', () => { it('should not select the same id in both ArrayFields when selected in one', async () => { render(); - await waitFor(() => { expect(screen.queryAllByRole('checkbox').length).toBeGreaterThan(2); }); @@ -171,22 +186,21 @@ describe('', () => { expect(checkboxes.length).toBeGreaterThan(3); // Select an item in the memberships list - fireEvent.click(checkboxes[1]); // Membership row 1 + fireEvent.click(checkboxes[1]); render(); await waitFor(() => { expect(checkboxes[1]).toBeChecked(); }); - //Ensure the same id in portfolios is NOT selected expect(checkboxes[3]).not.toBeChecked(); - fireEvent.click(checkboxes[3]); + fireEvent.click(checkboxes[3]); // Portfolios row 1 render(); await waitFor(() => { expect(checkboxes[3]).toBeChecked(); - expect(checkboxes[1]).toBeChecked(); + expect(checkboxes[1]).toBeChecked(); // Membership remains checked }); fireEvent.click(checkboxes[1]); @@ -194,7 +208,7 @@ describe('', () => { await waitFor(() => { expect(checkboxes[1]).not.toBeChecked(); - expect(checkboxes[3]).toBeChecked(); + expect(checkboxes[3]).toBeChecked(); // Portfolios remain selected }); }); }); diff --git a/packages/ra-ui-materialui/src/field/ArrayField.stories.tsx b/packages/ra-ui-materialui/src/field/ArrayField.stories.tsx index ce4b4318d90..fe0e76a7650 100644 --- a/packages/ra-ui-materialui/src/field/ArrayField.stories.tsx +++ b/packages/ra-ui-materialui/src/field/ArrayField.stories.tsx @@ -219,8 +219,8 @@ export const TwoArrayFieldsSelection = () => ( storeKey="organization_memberships" > true} > @@ -234,8 +234,8 @@ export const TwoArrayFieldsSelection = () => ( storeKey="organization_portfolios" > true} >