diff --git a/docs/List.md b/docs/List.md index 887942956d1..3e657699c95 100644 --- a/docs/List.md +++ b/docs/List.md @@ -486,7 +486,7 @@ const Dashboard = () => ( ) ``` -Please note that the selection state is not synced in the URL but in a global store using the resource as key. Thus, all lists in the page using the same resource will share the same synced selection state. This is a design choice because if row selection is not tied to a resource, then when a user deletes a record it may remain selected without any ability to unselect it. If you want to allow custom `storeKey`'s for managing selection state, you will have to implement your own `useListController` hook and pass a custom key to the `useRecordSelection` hook. You will then need to implement your own `DeleteButton` and `BulkDeleteButton` to manually unselect rows when deleting records. You can still opt out of all store interactions including selection if you set it to `false`. +Please note that the selection state is not synced in the URL but in a global store using the resource and, if provided, `storeKey` as part of the key. Thus, all lists in the page using the same resource and `storeKey` will share the same synced selection state. This is a design choice because if row selection is not tied to a resource, then when a user deletes a record it may remain selected without any ability to unselect it. You can still opt out of all store interactions for list state if you set it to `false`. ## `empty` @@ -1097,7 +1097,9 @@ const Admin = () => { **Tip:** The `storeKey` is actually passed to the underlying `useListController` hook, which you can use directly for more complex scenarios. See the [`useListController` doc](./useListController.md#storekey) for more info. -**Note:** *Selection state* will remain linked to a resource-based key regardless of the specified `storeKey` string. This is a design choice because if row selection is not tied to a resource, then when a user deletes a record it may remain selected without any ability to unselect it. If you want to allow custom `storeKey`'s for managing selection state, you will have to implement your own `useListController` hook and pass a custom key to the `useRecordSelection` hook. You will then need to implement your own `DeleteButton` and `BulkDeleteButton` to manually unselect rows when deleting records. You can still opt out of all store interactions including selection if you set it to `false`. +**Tip:** The `storeKey` is also passed to the underlying `useRecordSelection` hook, so that lists with different storeKeys for same resource will have independent selection states. + +**Tip:** Setting `storeKey` to `false` will opt out of all store interactions including selection. ## `title` diff --git a/packages/ra-core/src/controller/button/useBulkDeleteController.ts b/packages/ra-core/src/controller/button/useBulkDeleteController.ts index b21465114a5..cb742b57157 100644 --- a/packages/ra-core/src/controller/button/useBulkDeleteController.ts +++ b/packages/ra-core/src/controller/button/useBulkDeleteController.ts @@ -47,7 +47,7 @@ export const useBulkDeleteController = < undoable: mutationMode === 'undoable', } ); - onUnselectItems(); + onUnselectItems(true); }, onError: (error: any) => { notify( diff --git a/packages/ra-core/src/controller/button/useDeleteController.tsx b/packages/ra-core/src/controller/button/useDeleteController.tsx index 11e6c8f4841..b7723ce85c3 100644 --- a/packages/ra-core/src/controller/button/useDeleteController.tsx +++ b/packages/ra-core/src/controller/button/useDeleteController.tsx @@ -94,7 +94,7 @@ export const useDeleteController = < undoable: mutationMode === 'undoable', } ); - record && unselect([record.id]); + record && unselect([record.id], true); redirect(redirectTo, resource); }, onError: (error: any) => { diff --git a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.spec.tsx b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.spec.tsx index be5e46d2423..4a98e4303e0 100644 --- a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.spec.tsx +++ b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.spec.tsx @@ -11,6 +11,7 @@ import useDeleteWithConfirmController, { import { TestMemoryRouter } from '../../routing'; import { useNotificationContext } from '../../notification'; +import { memoryStore, StoreSetter } from '../../store'; describe('useDeleteWithConfirmController', () => { it('should call the dataProvider.delete() function with the meta param', async () => { @@ -101,4 +102,51 @@ describe('useDeleteWithConfirmController', () => { ]); }); }); + + it('should unselect records from all storeKeys in useRecordSelection', async () => { + const dataProvider = testDataProvider({ + delete: jest.fn((resource, params) => { + return Promise.resolve({ data: params.previousData }); + }), + }); + + const MockComponent = () => { + const { handleDelete } = useDeleteWithConfirmController({ + record: { id: 456 }, + resource: 'posts', + mutationMode: 'pessimistic', + } as UseDeleteWithConfirmControllerParams); + return ; + }; + + const store = memoryStore(); + + render( + + + + + } /> + + + + + ); + + const button = await screen.findByText('Delete'); + fireEvent.click(button); + await waitFor( + () => + expect(store.getItem('posts.selectedIds')).toEqual({ + ['']: [123], + ['bar']: [], + }), + { + timeout: 1000, + } + ); + }); }); diff --git a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx index c45b70818ec..dc54fa46a2f 100644 --- a/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx +++ b/packages/ra-core/src/controller/button/useDeleteWithConfirmController.tsx @@ -109,7 +109,7 @@ const useDeleteWithConfirmController = < undoable: mutationMode === 'undoable', } ); - record && unselect([record.id]); + record && unselect([record.id], true); redirect(redirectTo, resource); }, onError: error => { diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx b/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx index 3537e2307b2..f9f05e6f538 100644 --- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.spec.tsx @@ -332,6 +332,42 @@ describe('useReferenceManyFieldController', () => { }); }); + it('should store selection state linked to referencing record', async () => { + const store = memoryStore(); + const setStore = jest.spyOn(store, 'setItem'); + + render( + + + {({ onToggleItem }) => { + return ( + + ); + }} + + + ); + + fireEvent.click(await screen.findByText('Toggle')); + await waitFor(() => { + expect(setStore).toHaveBeenCalledWith('books.selectedIds', { + ['authors.123']: [456], + }); + }); + }); + it('should support custom storeKey', async () => { const store = memoryStore(); const setStore = jest.spyOn(store, 'setItem'); @@ -352,7 +388,7 @@ describe('useReferenceManyFieldController', () => { > {({ onToggleItem }) => { return ( - ); @@ -363,9 +399,9 @@ describe('useReferenceManyFieldController', () => { fireEvent.click(await screen.findByText('Toggle')); await waitFor(() => { - expect(setStore).toHaveBeenCalledWith('customKey.selectedIds', [ - 123, - ]); + expect(setStore).toHaveBeenCalledWith('books.selectedIds', { + ['customKey']: [456], + }); }); }); diff --git a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts index 66495665d4b..07b32823fcd 100644 --- a/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts +++ b/packages/ra-core/src/controller/field/useReferenceManyFieldController.ts @@ -72,7 +72,6 @@ export const useReferenceManyFieldController = < const resource = useResourceContext(props); const dataProvider = useDataProvider(); const queryClient = useQueryClient(); - const storeKey = props.storeKey ?? `${resource}.${record?.id}.${reference}`; const { meta, ...otherQueryOptions } = queryOptions; // pagination logic @@ -93,9 +92,17 @@ export const useReferenceManyFieldController = < // selection logic const [selectedIds, selectionModifiers] = useRecordSelection({ - resource: storeKey, + resource: reference, + storeKey: props.storeKey ?? `${resource}.${record?.id}`, }); + const onUnselectItems = useCallback( + (fromAllStoreKeys?: boolean) => { + return selectionModifiers.unselect(selectedIds, fromAllStoreKeys); + }, + [selectedIds, selectionModifiers] + ); + // filter logic const filterRef = useRef(filter); const [displayedFilters, setDisplayedFilters] = useState<{ @@ -280,7 +287,7 @@ export const useReferenceManyFieldController = < onSelect: selectionModifiers.select, onSelectAll, onToggleItem: selectionModifiers.toggle, - onUnselectItems: selectionModifiers.clearSelection, + onUnselectItems, page, perPage, refetch, diff --git a/packages/ra-core/src/controller/list/useInfiniteListController.ts b/packages/ra-core/src/controller/list/useInfiniteListController.ts index 23662d68042..0847eed2f41 100644 --- a/packages/ra-core/src/controller/list/useInfiniteListController.ts +++ b/packages/ra-core/src/controller/list/useInfiniteListController.ts @@ -1,4 +1,4 @@ -import { isValidElement, useEffect, useMemo } from 'react'; +import { isValidElement, useCallback, useEffect, useMemo } from 'react'; import type { InfiniteQueryObserverBaseResult, InfiniteData, @@ -97,6 +97,13 @@ export const useInfiniteListController = < const [selectedIds, selectionModifiers] = useRecordSelection({ resource }); + const onUnselectItems = useCallback( + (fromAllStoreKeys?: boolean) => { + return selectionModifiers.unselect(selectedIds, fromAllStoreKeys); + }, + [selectedIds, selectionModifiers] + ); + const { data, total, @@ -212,7 +219,7 @@ export const useInfiniteListController = < onSelect: selectionModifiers.select, onSelectAll, onToggleItem: selectionModifiers.toggle, - onUnselectItems: selectionModifiers.clearSelection, + onUnselectItems, page: query.page, perPage: query.perPage, refetch, diff --git a/packages/ra-core/src/controller/list/useList.ts b/packages/ra-core/src/controller/list/useList.ts index 072ade60b0a..cadbcbeedbb 100644 --- a/packages/ra-core/src/controller/list/useList.ts +++ b/packages/ra-core/src/controller/list/useList.ts @@ -104,6 +104,13 @@ export const useList = ( : { disableSyncWithStore: true } ); + const onUnselectItems = useCallback( + (fromAllStoreKeys?: boolean) => { + return selectionModifiers.unselect(selectedIds, fromAllStoreKeys); + }, + [selectedIds, selectionModifiers] + ); + // filter logic const filterRef = useRef(filter); const [displayedFilters, setDisplayedFilters] = useState<{ @@ -263,7 +270,7 @@ export const useList = ( onSelect: selectionModifiers.select, onSelectAll, onToggleItem: selectionModifiers.toggle, - onUnselectItems: selectionModifiers.clearSelection, + onUnselectItems, page, perPage, resource: '', diff --git a/packages/ra-core/src/controller/list/useListController.ts b/packages/ra-core/src/controller/list/useListController.ts index 87ddaca534e..ab137783feb 100644 --- a/packages/ra-core/src/controller/list/useListController.ts +++ b/packages/ra-core/src/controller/list/useListController.ts @@ -1,4 +1,4 @@ -import { isValidElement, useEffect, useMemo } from 'react'; +import { isValidElement, useCallback, useEffect, useMemo } from 'react'; import { useAuthenticated, useRequireAccess } from '../../auth'; import { useTranslate } from '../../i18n'; @@ -104,8 +104,16 @@ export const useListController = < const [selectedIds, selectionModifiers] = useRecordSelection({ resource, disableSyncWithStore: storeKey === false, + storeKey: storeKey === false ? undefined : storeKey, }); + const onUnselectItems = useCallback( + (fromAllStoreKeys?: boolean) => { + return selectionModifiers.unselect(selectedIds, fromAllStoreKeys); + }, + [selectedIds, selectionModifiers] + ); + const { data, pageInfo, @@ -212,7 +220,7 @@ export const useListController = < onSelect: selectionModifiers.select, onSelectAll, onToggleItem: selectionModifiers.toggle, - onUnselectItems: selectionModifiers.clearSelection, + onUnselectItems, page: query.page, perPage: query.perPage, refetch, @@ -515,7 +523,7 @@ export interface ListControllerBaseResult { | UseReferenceManyFieldControllerParams['queryOptions']; }) => void; onToggleItem: (id: RecordType['id']) => void; - onUnselectItems: () => void; + onUnselectItems: (fromAllStoreKeys?: boolean) => void; page: number; perPage: number; refetch: (() => void) | UseGetListHookValue['refetch']; diff --git a/packages/ra-core/src/controller/list/useRecordSelection.spec.tsx b/packages/ra-core/src/controller/list/useRecordSelection.spec.tsx index aaa8529b464..ba7ea1bb3da 100644 --- a/packages/ra-core/src/controller/list/useRecordSelection.spec.tsx +++ b/packages/ra-core/src/controller/list/useRecordSelection.spec.tsx @@ -23,6 +23,26 @@ describe('useRecordSelection', () => { }); it('should use the stored value', () => { + const { result } = renderHook( + () => useRecordSelection({ resource: 'foo' }), + { + wrapper: ({ children }) => ( + + + {children} + + + ), + } + ); + const [selected] = result.current; + expect(selected).toEqual([123, 456]); + }); + + it('should use the stored value in previous format', () => { const { result } = renderHook( () => useRecordSelection({ resource: 'foo' }), { @@ -39,6 +59,31 @@ describe('useRecordSelection', () => { expect(selected).toEqual([123, 456]); }); + it('should store in a new format after any operation', async () => { + const store = memoryStore(); + const { result } = renderHook( + () => useRecordSelection({ resource: 'foo' }), + { + wrapper: ({ children }) => ( + + + {children} + + + ), + } + ); + + const [, { select }] = result.current; + select([123, 456, 7]); + await waitFor(() => { + const stored = store.getItem('foo.selectedIds'); + expect(stored).toEqual({ + ['']: [123, 456, 7], + }); + }); + }); + describe('select', () => { it('should allow to select a record', async () => { const { result } = renderHook( @@ -378,4 +423,115 @@ describe('useRecordSelection', () => { }); }); }); + describe('using storeKey', () => { + it('should return empty array by default', () => { + const { result } = renderHook( + () => + useRecordSelection({ + resource: 'foo', + storeKey: 'bar', + }), + { wrapper } + ); + const [selected] = result.current; + expect(selected).toEqual([]); + }); + + it('should use the stored value', () => { + const { result } = renderHook( + () => useRecordSelection({ resource: 'foo', storeKey: 'bar' }), + { + wrapper: ({ children }) => ( + + + {children} + + + ), + } + ); + const [selected] = result.current; + expect(selected).toEqual([123, 456]); + }); + + it('should allow to unselect from all storeKeys', async () => { + const { result } = renderHook( + () => [ + useRecordSelection({ resource: 'foo', storeKey: 'bar1' }), + useRecordSelection({ resource: 'foo', storeKey: 'bar2' }), + ], + { + wrapper, + } + ); + + const [, { toggle: toggle1 }] = result.current[0]; + const [, { toggle: toggle2 }] = result.current[1]; + toggle1(123); + await waitFor(() => {}); + toggle2(123); + await waitFor(() => {}); + toggle2(456); + await waitFor(() => { + const [selected1] = result.current[0]; + expect(selected1).toEqual([123]); + const [selected2] = result.current[1]; + expect(selected2).toEqual([123, 456]); + }); + + const [, { unselect }] = result.current[0]; + unselect([123], true); + + await waitFor(() => { + const [selected1] = result.current[0]; + expect(selected1).toEqual([]); + const [selected2] = result.current[1]; + expect(selected2).toEqual([456]); + }); + }); + + it('should allow to clear the selection from all storeKeys', async () => { + const { result } = renderHook( + () => [ + useRecordSelection({ + resource: 'foo', + storeKey: 'bar1', + }), + useRecordSelection({ + resource: 'foo', + storeKey: 'bar2', + }), + ], + { + wrapper, + } + ); + + const [, { toggle: toggle1 }] = result.current[0]; + const [, { toggle: toggle2 }] = result.current[1]; + toggle1(123); + // `set` in useStore doesn't chain set calls happened in one render cycle... + await waitFor(() => {}); + toggle2(456); + await waitFor(() => { + const [selected1] = result.current[0]; + expect(selected1).toEqual([123]); + const [selected2] = result.current[1]; + expect(selected2).toEqual([456]); + }); + + const [, { clearSelection }] = result.current[0]; + clearSelection(true); + + await waitFor(() => { + const [selected1] = result.current[0]; + expect(selected1).toEqual([]); + const [selected2] = result.current[1]; + expect(selected2).toEqual([]); + }); + }); + }); }); diff --git a/packages/ra-core/src/controller/list/useRecordSelection.ts b/packages/ra-core/src/controller/list/useRecordSelection.ts index 831c68e7cbd..ddf2d21be53 100644 --- a/packages/ra-core/src/controller/list/useRecordSelection.ts +++ b/packages/ra-core/src/controller/list/useRecordSelection.ts @@ -1,14 +1,16 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useMemo, useState } from 'react'; -import { useStore, useRemoveFromStore } from '../../store'; +import { useStore } from '../../store'; import { RaRecord } from '../../types'; type UseRecordSelectionWithResourceArgs = { resource: string; + storeKey?: string; disableSyncWithStore?: false; }; type UseRecordSelectionWithNoStoreArgs = { resource?: string; + storeKey?: string; disableSyncWithStore: true; }; @@ -20,79 +22,166 @@ export type UseRecordSelectionResult = [ RecordType['id'][], { select: (ids: RecordType['id'][]) => void; - unselect: (ids: RecordType['id'][]) => void; + unselect: (ids: RecordType['id'][], fromAllStoreKeys?: boolean) => void; toggle: (id: RecordType['id']) => void; - clearSelection: () => void; + clearSelection: (fromAllStoreKeys?: boolean) => void; }, ]; +type SelectionStore = Record< + string, + RecordType['id'][] +>; + /** * Get the list of selected items for a resource, and callbacks to change the selection * * @param args.resource The resource name, e.g. 'posts' - * @param args.disableSyncWithStore Controls the selection syncronization with the store + * @param args.storeKey The key to use to store selected items. Pass false to disable synchronization with the store. + * @param args.disableSyncWithStore Controls the selection synchronization with the store * - * @returns {Object} Destructure as [selectedIds, { select, toggle, clearSelection }]. + * @returns {Object} Destructure as [selectedIds, { select, unselect, toggle, clearSelection }]. */ export const useRecordSelection = ( args: UseRecordSelectionArgs ): UseRecordSelectionResult => { - const { resource = '', disableSyncWithStore = false } = args; + const { resource = '', storeKey, disableSyncWithStore } = args; - const storeKey = `${resource}.selectedIds`; + const namespace = storeKey ?? defaultNamespace; - const [localIds, setLocalIds] = - useState(defaultSelection); - // As we can't conditionally call a hook, if the storeKey is false, - // we'll ignore the params variable later on and won't call setParams either. - const [storeIds, setStoreIds] = useStore( - storeKey, - defaultSelection - ); - const resetStore = useRemoveFromStore(storeKey); + const finalStoreKey = `${resource}.selectedIds`; + + const [localSelectionStore, setLocalSelectionStore] = useState< + SelectionStore + >(defaultSelectionStore); + // As we can't conditionally call a hook, if the disableSyncWithStore is true, + // we'll ignore the store value later on and won't call setSelectionStore either. + const [selectionStoreUnknownVersion, setSelectionStore] = useStore< + SelectionStore + >(finalStoreKey, defaultSelectionStore); - const ids = disableSyncWithStore ? localIds : storeIds; - const setIds = disableSyncWithStore ? setLocalIds : setStoreIds; + const store = disableSyncWithStore + ? localSelectionStore + : migrateSelectionStoreToNewVersion(selectionStoreUnknownVersion); + const ids = store[namespace] ?? defaultEmptyIds; - const reset = useCallback(() => { - if (disableSyncWithStore) { - setLocalIds(defaultSelection); - } else { - resetStore(); - } - }, [disableSyncWithStore, resetStore]); + const setStore = useMemo( + () => + disableSyncWithStore + ? setLocalSelectionStore + : (function migrateAndSetSelectionStore(valueOrSetter) { + if (typeof valueOrSetter === 'function') { + setSelectionStore(prevValue => + valueOrSetter( + migrateSelectionStoreToNewVersion(prevValue) + ) + ); + } else { + setSelectionStore(valueOrSetter); + } + } satisfies typeof setSelectionStore), + [disableSyncWithStore, setSelectionStore] + ); const selectionModifiers = useMemo( () => ({ - select: (idsToAdd: RecordType['id'][]) => { - if (!idsToAdd) return; - setIds([...idsToAdd]); + select: (idsToSelect: RecordType['id'][]) => { + if (!idsToSelect) return; + + setStore(store => ({ + ...store, + [namespace]: [...idsToSelect], + })); }, - unselect(idsToRemove: RecordType['id'][]) { + unselect( + idsToRemove: RecordType['id'][], + fromAllStoreKeys?: boolean + ) { if (!idsToRemove || idsToRemove.length === 0) return; - setIds(ids => { - if (!Array.isArray(ids)) return []; - return ids.filter(id => !idsToRemove.includes(id)); + setStore(store => { + if (!fromAllStoreKeys) { + return { + ...store, + [namespace]: store[namespace]?.filter( + id => !idsToRemove.includes(id) + ), + }; + } else { + return Object.fromEntries( + Object.entries(store).map(([namespace, ids]) => { + return [ + namespace, + ids?.filter( + id => !idsToRemove.includes(id) + ), + ]; + }) + ); + } }); }, toggle: (id: RecordType['id']) => { if (typeof id === 'undefined') return; - setIds(ids => { - if (!Array.isArray(ids)) return [...ids]; + + setStore(store => { + const ids = store[namespace] ?? defaultEmptyIds; + + if (!Array.isArray(ids)) + return { ...store, [namespace]: [...ids] }; + const index = ids.indexOf(id); - return index > -1 - ? [...ids.slice(0, index), ...ids.slice(index + 1)] - : [...ids, id]; + const hasId = index > -1; + + return { + ...store, + [namespace]: hasId + ? [...ids.slice(0, index), ...ids.slice(index + 1)] + : [...ids, id], + }; }); }, - clearSelection: () => { - reset(); + clearSelection: (fromAllStoreKeys?: boolean) => { + setStore(store => { + if (fromAllStoreKeys) { + console.log( + store, + Object.fromEntries( + Object.keys(store).map(namespace => [ + namespace, + [], + ]) + ) + ); + + return Object.fromEntries( + Object.keys(store).map(namespace => [namespace, []]) + ); + } else { + return { + ...store, + [namespace]: [], + }; + } + }); }, }), - [setIds, reset] + [setStore, namespace] ); return [ids, selectionModifiers]; }; -const defaultSelection = []; +const defaultNamespace = ''; +const defaultSelectionStore = {}; +const defaultEmptyIds = []; + +function migrateSelectionStoreToNewVersion( + selectionStoreUnknownVersion: SelectionStore +) { + return Array.isArray(selectionStoreUnknownVersion) + ? { + ...defaultSelectionStore, + [defaultNamespace]: selectionStoreUnknownVersion, + } + : selectionStoreUnknownVersion; +} diff --git a/packages/ra-core/src/controller/list/useUnselect.ts b/packages/ra-core/src/controller/list/useUnselect.ts index ce5742eaeba..edfb3d35dea 100644 --- a/packages/ra-core/src/controller/list/useUnselect.ts +++ b/packages/ra-core/src/controller/list/useUnselect.ts @@ -16,8 +16,8 @@ export const useUnselect = (resource?: string) => { resource ? { resource } : { disableSyncWithStore: true } ); return useCallback( - (ids: Identifier[]) => { - unselect(ids); + (ids: Identifier[], fromAllStoreKeys: boolean = false) => { + unselect(ids, fromAllStoreKeys); }, [unselect] ); diff --git a/packages/ra-core/src/controller/list/useUnselectAll.ts b/packages/ra-core/src/controller/list/useUnselectAll.ts index d7dae4d76e8..121a878368b 100644 --- a/packages/ra-core/src/controller/list/useUnselectAll.ts +++ b/packages/ra-core/src/controller/list/useUnselectAll.ts @@ -10,11 +10,22 @@ import { useRecordSelection } from './useRecordSelection'; * const unselectAll = useUnselectAll('posts'); * unselectAll(); */ -export const useUnselectAll = (resource?: string) => { +export const useUnselectAll = ({ + resource, + storeKey, +}: { + resource?: string; + storeKey?: string; +}) => { const [, { clearSelection }] = useRecordSelection( - resource ? { resource } : { disableSyncWithStore: true } + resource + ? { resource, storeKey } + : { disableSyncWithStore: true, storeKey } + ); + return useCallback( + (fromAllStoreKeys?: boolean) => { + clearSelection(fromAllStoreKeys); + }, + [clearSelection] ); - return useCallback(() => { - clearSelection(); - }, [clearSelection]); }; diff --git a/packages/ra-ui-materialui/src/list/List.stories.tsx b/packages/ra-ui-materialui/src/list/List.stories.tsx index 3f7a5d6c845..f77547522db 100644 --- a/packages/ra-ui-materialui/src/list/List.stories.tsx +++ b/packages/ra-ui-materialui/src/list/List.stories.tsx @@ -771,6 +771,95 @@ export const LocationNotSyncWithStore = () => { ); }; +const BooksWithStoreKeyA = () => ( + + + + + + + + +); + +const BooksWithStoreKeyB = () => ( + + + + + + + + +); + +const BooksWithoutStoreKey = () => ( + + + + + + + + +); + +const RecordSelectionDashboard = () => ( + <> + + + + + + +); + +export const RecordSelection = () => ( + + + + } + /> + } + /> + } + /> + + + + +); + export const ErrorInFetch = () => (