Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/List.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const useBulkDeleteController = <
undoable: mutationMode === 'undoable',
}
);
onUnselectItems();
onUnselectItems(true);
},
onError: (error: any) => {
notify(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -101,4 +102,60 @@ 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 <button onClick={handleDelete}>Delete</button>;
};

const store = memoryStore();

render(
<TestMemoryRouter>
<CoreAdminContext store={store} dataProvider={dataProvider}>
<StoreSetter
name="posts.selectedIds.storeKeys"
value={['bar.selectedIds']}
>
<StoreSetter
name="posts.selectedIds"
value={[123, 456]}
>
<StoreSetter name="bar.selectedIds" value={[456]}>
<Routes>
<Route
path="/"
element={<MockComponent />}
/>
</Routes>
</StoreSetter>
</StoreSetter>
</StoreSetter>
</CoreAdminContext>
</TestMemoryRouter>
);

const button = await screen.findByText('Delete');
fireEvent.click(button);
await waitFor(
() => {
expect(store.getItem('posts.selectedIds')).toEqual([123]);
expect(store.getItem('bar.selectedIds')).toEqual([]);
},
{
timeout: 1000,
}
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const useDeleteWithConfirmController = <
undoable: mutationMode === 'undoable',
}
);
record && unselect([record.id]);
record && unselect([record.id], true);
redirect(redirectTo, resource);
},
onError: error => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,43 @@ describe('useReferenceManyFieldController', () => {
});
});

it('should store selection state linked to referencing record', async () => {
const store = memoryStore();
const setStore = jest.spyOn(store, 'setItem');

render(
<CoreAdminContext store={store}>
<ReferenceManyFieldController
resource="authors"
source="uniqueName"
record={{
id: 123,
uniqueName: 'jamesjoyce256',
name: 'James Joyce',
}}
reference="books"
target="author_id"
>
{({ onToggleItem }) => {
return (
<button onClick={() => onToggleItem(456)}>
Toggle
</button>
);
}}
</ReferenceManyFieldController>
</CoreAdminContext>
);

fireEvent.click(await screen.findByText('Toggle'));
await waitFor(() => {
expect(setStore).toHaveBeenCalledWith(
'authors.123.books.selectedIds',
[456]
);
});
});

it('should support custom storeKey', async () => {
const store = memoryStore();
const setStore = jest.spyOn(store, 'setItem');
Expand All @@ -352,7 +389,7 @@ describe('useReferenceManyFieldController', () => {
>
{({ onToggleItem }) => {
return (
<button onClick={() => onToggleItem(123)}>
<button onClick={() => onToggleItem(456)}>
Toggle
</button>
);
Expand All @@ -364,7 +401,7 @@ describe('useReferenceManyFieldController', () => {
fireEvent.click(await screen.findByText('Toggle'));
await waitFor(() => {
expect(setStore).toHaveBeenCalledWith('customKey.selectedIds', [
123,
456,
]);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -93,9 +92,17 @@ export const useReferenceManyFieldController = <

// selection logic
const [selectedIds, selectionModifiers] = useRecordSelection({
resource: storeKey,
resource: reference,
storeKey: props.storeKey ?? `${resource}.${record?.id}.${reference}`,
});

const onUnselectItems = useCallback(
(fromAllStoreKeys?: boolean) => {
return selectionModifiers.unselect(selectedIds, fromAllStoreKeys);
},
[selectedIds, selectionModifiers]
);

// filter logic
const filterRef = useRef(filter);
const [displayedFilters, setDisplayedFilters] = useState<{
Expand Down Expand Up @@ -280,7 +287,7 @@ export const useReferenceManyFieldController = <
onSelect: selectionModifiers.select,
onSelectAll,
onToggleItem: selectionModifiers.toggle,
onUnselectItems: selectionModifiers.clearSelection,
onUnselectItems,
page,
perPage,
refetch,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isValidElement, useEffect, useMemo } from 'react';
import { isValidElement, useCallback, useEffect, useMemo } from 'react';
import type {
InfiniteQueryObserverBaseResult,
InfiniteData,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 8 additions & 1 deletion packages/ra-core/src/controller/list/useList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,13 @@ export const useList = <RecordType extends RaRecord = any, ErrorType = Error>(
: { disableSyncWithStore: true }
);

const onUnselectItems = useCallback(
(fromAllStoreKeys?: boolean) => {
return selectionModifiers.unselect(selectedIds, fromAllStoreKeys);
},
[selectedIds, selectionModifiers]
);

// filter logic
const filterRef = useRef(filter);
const [displayedFilters, setDisplayedFilters] = useState<{
Expand Down Expand Up @@ -263,7 +270,7 @@ export const useList = <RecordType extends RaRecord = any, ErrorType = Error>(
onSelect: selectionModifiers.select,
onSelectAll,
onToggleItem: selectionModifiers.toggle,
onUnselectItems: selectionModifiers.clearSelection,
onUnselectItems,
page,
perPage,
resource: '',
Expand Down
14 changes: 11 additions & 3 deletions packages/ra-core/src/controller/list/useListController.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -515,7 +523,7 @@ export interface ListControllerBaseResult<RecordType extends RaRecord = any> {
| UseReferenceManyFieldControllerParams<RecordType>['queryOptions'];
}) => void;
onToggleItem: (id: RecordType['id']) => void;
onUnselectItems: () => void;
onUnselectItems: (fromAllStoreKeys?: boolean) => void;
page: number;
perPage: number;
refetch: (() => void) | UseGetListHookValue<RecordType>['refetch'];
Expand Down
Loading