Skip to content

Commit aaba7ef

Browse files
authored
Merge pull request #10385 from marmelab/simple-list-rowclick
Introduce SimpleList rowClick
2 parents dcabae1 + 52f3008 commit aaba7ef

File tree

12 files changed

+579
-287
lines changed

12 files changed

+579
-287
lines changed

docs/SimpleList.md

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export const PostList = () => (
2828
primaryText={record => record.title}
2929
secondaryText={record => `${record.views} views`}
3030
tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
31-
linkType={record => record.canEdit ? "edit" : "show"}
31+
rowClick={(id, resource, record) => record.canEdit ? "edit" : "show"}
3232
rowSx={record => ({ backgroundColor: record.nb_views >= 500 ? '#efe' : 'white' })}
3333
/>
3434
</List>
@@ -44,7 +44,7 @@ export const PostList = () => (
4444
| `primaryText` | Optional | mixed | record representation | The primary text to display. |
4545
| `secondaryText` | Optional | mixed | | The secondary text to display. |
4646
| `tertiaryText` | Optional | mixed | | The tertiary text to display. |
47-
| `linkType` | Optional |mixed | `"edit"` | The target of each item click. |
47+
| `rowClick` | Optional |mixed | `"edit"` | The action to trigger when the user clicks on a row. |
4848
| `leftAvatar` | Optional | function | | A function returning an `<Avatar>` component to display before the primary text. |
4949
| `leftIcon` | Optional | function | | A function returning an `<Icon>` component to display before the primary text. |
5050
| `rightAvatar` | Optional | function | | A function returning an `<Avatar>` component to display after the primary text. |
@@ -80,9 +80,9 @@ This prop should be a function returning an `<Avatar>` component. When present,
8080

8181
This prop should be a function returning an `<Icon>` component. When present, the `<ListItem>` renders a `<ListIcon>` before the `<ListItemText>`
8282

83-
## `linkType`
83+
## `rowClick`
8484

85-
The `<SimpleList>` items link to the edition page by default. You can also set the `linkType` prop to `show` directly to link to the `<Show>` page instead.
85+
The `<SimpleList>` items link to the edition page by default. You can also set the `rowClick` prop to `show` directly to link to the `<Show>` page instead.
8686

8787
```jsx
8888
import { List, SimpleList } from 'react-admin';
@@ -93,17 +93,19 @@ export const PostList = () => (
9393
primaryText={record => record.title}
9494
secondaryText={record => `${record.views} views`}
9595
tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
96-
linkType="show"
96+
rowClick="show"
9797
/>
9898
</List>
9999
);
100100
```
101101

102-
`linkType` accepts the following values:
102+
`rowClick` accepts the following values:
103103

104-
* `linkType="edit"`: links to the edit page. This is the default behavior.
105-
* `linkType="show"`: links to the show page.
106-
* `linkType={false}`: does not create any link.
104+
* `rowClick="edit"`: links to the edit page. This is the default behavior.
105+
* `rowClick="show"`: links to the show page.
106+
* `rowClick={false}`: does not link to anything.
107+
* `rowClick="/custom"`: links to a custom path.
108+
* `rowClick={(id, resource, record) => path}`: path can be any of the above values
107109

108110
## `primaryText`
109111

@@ -254,7 +256,7 @@ export const PostList = () => {
254256
primaryText={record => record.title}
255257
secondaryText={record => `${record.views} views`}
256258
tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
257-
linkType={record => record.canEdit ? "edit" : "show"}
259+
rowClick={(id, resource, record) => record.canEdit ? "edit" : "show"}
258260
/>
259261
) : (
260262
<Datagrid>

packages/ra-core/src/routing/useGetPathForRecord.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ export const useGetPathForRecord = <RecordType extends RaRecord = RaRecord>(
8181
useEffect(() => {
8282
if (!record) return;
8383

84+
if (link === false) {
85+
setPath(false);
86+
return;
87+
}
88+
8489
// Handle the inferred link type case
8590
if (link == null) {
8691
// We must check whether the resource has an edit view because if there is no

packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx

Lines changed: 109 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,41 @@ import {
66
waitFor,
77
within,
88
} from '@testing-library/react';
9-
import { ListContext, ResourceContextProvider } from 'ra-core';
9+
import {
10+
ListContext,
11+
ResourceContextProvider,
12+
ResourceDefinitionContextProvider,
13+
} from 'ra-core';
14+
import { Location } from 'react-router';
1015

1116
import { AdminContext } from '../../AdminContext';
1217
import { SimpleList } from './SimpleList';
1318
import { TextField } from '../../field/TextField';
1419
import {
20+
LinkType,
1521
NoPrimaryText,
22+
RowClick,
1623
Standalone,
1724
StandaloneEmpty,
1825
} from './SimpleList.stories';
1926
import { Basic } from '../filter/FilterButton.stories';
2027

2128
const Wrapper = ({ children }: any) => (
2229
<AdminContext>
23-
<ResourceContextProvider value="posts">
24-
{children}
25-
</ResourceContextProvider>
30+
<ResourceDefinitionContextProvider
31+
definitions={{
32+
posts: {
33+
name: 'posts',
34+
hasList: true,
35+
hasEdit: true,
36+
hasShow: true,
37+
},
38+
}}
39+
>
40+
<ResourceContextProvider value="posts">
41+
{children}
42+
</ResourceContextProvider>
43+
</ResourceDefinitionContextProvider>
2644
</AdminContext>
2745
);
2846

@@ -59,63 +77,106 @@ describe('<SimpleList />', () => {
5977
});
6078

6179
it.each([
80+
['edit', 'edit', '/books/1'],
81+
['show', 'show', '/books/1/show'],
82+
[
83+
'a function that returns a custom path',
84+
(record, id) =>
85+
`/books/${id}/${record.title.toLowerCase().replaceAll(' ', '-')}`,
86+
'/books/1/war-and-peace',
87+
],
88+
['a function that returns edit', () => 'edit', '/books/1'],
89+
['a function that returns show', () => 'show', '/books/1/show'],
90+
])(
91+
'Providing %s as linkType should render a link for each item',
92+
async (_, linkType, expectedUrl) => {
93+
let location: Location;
94+
render(
95+
<LinkType
96+
linkType={linkType}
97+
locationCallback={l => {
98+
location = l;
99+
}}
100+
/>
101+
);
102+
fireEvent.click(await screen.findByText('War and Peace'));
103+
await waitFor(() => {
104+
expect(location?.pathname).toEqual(expectedUrl);
105+
});
106+
}
107+
);
108+
109+
it('should not render links if linkType is false', async () => {
110+
render(
111+
<ListContext.Provider
112+
value={{
113+
isLoading: false,
114+
data: [
115+
{ id: 1, title: 'foo' },
116+
{ id: 2, title: 'bar' },
117+
],
118+
total: 2,
119+
resource: 'posts',
120+
}}
121+
>
122+
<SimpleList
123+
linkType={false}
124+
primaryText={record => record.id.toString()}
125+
secondaryText={<TextField source="title" />}
126+
/>
127+
</ListContext.Provider>,
128+
{ wrapper: Wrapper }
129+
);
130+
131+
await waitFor(() => {
132+
expect(screen.getByText('1').closest('a')).toBeNull();
133+
expect(screen.getByText('2').closest('a')).toBeNull();
134+
});
135+
});
136+
137+
it.each([
138+
['edit', 'edit', '/books/1'],
139+
['show', 'show', '/books/1/show'],
62140
[
63-
'edit',
64-
'edit',
65-
['http://localhost/#/posts/1', 'http://localhost/#/posts/2'],
141+
'a function that returns a custom path',
142+
(id, resource, record) =>
143+
`/${resource}/${id}/${record.title.toLowerCase().replaceAll(' ', '-')}`,
144+
'/books/1/war-and-peace',
66145
],
146+
['a function that returns edit', () => 'edit', '/books/1'],
147+
['a function that returns show', () => 'show', '/books/1/show'],
148+
['a function that resolves to edit', async () => 'edit', '/books/1'],
67149
[
68-
'show',
69-
'show',
70-
[
71-
'http://localhost/#/posts/1/show',
72-
'http://localhost/#/posts/2/show',
73-
],
150+
'a function that resolves to show',
151+
async () => 'show',
152+
'/books/1/show',
74153
],
75154
[
76-
'custom',
77-
(record, id) => `/posts/${id}/custom`,
78-
[
79-
'http://localhost/#/posts/1/custom',
80-
'http://localhost/#/posts/2/custom',
81-
],
155+
'a function that resolves to a custom path',
156+
async (id, resource, record) =>
157+
`/${resource}/${id}/${record.title.toLowerCase().replaceAll(' ', '-')}`,
158+
'/books/1/war-and-peace',
82159
],
83160
])(
84-
'should render %s links for each item',
85-
async (_, link, expectedUrls) => {
161+
'Providing %s as rowClick should render a link for each item',
162+
async (_, rowClick, expectedUrls) => {
163+
let location: Location;
86164
render(
87-
<ListContext.Provider
88-
value={{
89-
isLoading: false,
90-
data: [
91-
{ id: 1, title: 'foo' },
92-
{ id: 2, title: 'bar' },
93-
],
94-
total: 2,
95-
resource: 'posts',
165+
<RowClick
166+
rowClick={rowClick}
167+
locationCallback={l => {
168+
location = l;
96169
}}
97-
>
98-
<SimpleList
99-
linkType={link}
100-
primaryText={record => record.id.toString()}
101-
secondaryText={<TextField source="title" />}
102-
/>
103-
</ListContext.Provider>,
104-
{ wrapper: Wrapper }
170+
/>
105171
);
106-
172+
fireEvent.click(await screen.findByText('War and Peace'));
107173
await waitFor(() => {
108-
expect(screen.getByText('1').closest('a').href).toEqual(
109-
expectedUrls[0]
110-
);
111-
expect(screen.getByText('2').closest('a').href).toEqual(
112-
expectedUrls[1]
113-
);
174+
expect(location?.pathname).toEqual(expectedUrls);
114175
});
115176
}
116177
);
117178

118-
it('should not render links if linkType is false', async () => {
179+
it('should not render links if rowClick is false', async () => {
119180
render(
120181
<ListContext.Provider
121182
value={{
@@ -129,7 +190,7 @@ describe('<SimpleList />', () => {
129190
}}
130191
>
131192
<SimpleList
132-
linkType={false}
193+
rowClick={false}
133194
primaryText={record => record.id.toString()}
134195
secondaryText={<TextField source="title" />}
135196
/>
@@ -205,7 +266,7 @@ describe('<SimpleList />', () => {
205266
});
206267
it('should display a message when there is no result', async () => {
207268
render(<StandaloneEmpty />);
208-
await screen.findByText('No results found.');
269+
await screen.findByText('ra.navigation.no_results');
209270
});
210271
});
211272
});

0 commit comments

Comments
 (0)