Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce SimpleList rowClick #10385

Merged
merged 6 commits into from
Dec 4, 2024
Merged
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
22 changes: 12 additions & 10 deletions docs/SimpleList.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const PostList = () => (
primaryText={record => record.title}
secondaryText={record => `${record.views} views`}
tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
linkType={record => record.canEdit ? "edit" : "show"}
rowClick={(id, resource, record) => record.canEdit ? "edit" : "show"}
rowSx={record => ({ backgroundColor: record.nb_views >= 500 ? '#efe' : 'white' })}
/>
</List>
Expand All @@ -44,7 +44,7 @@ export const PostList = () => (
| `primaryText` | Optional | mixed | record representation | The primary text to display. |
| `secondaryText` | Optional | mixed | | The secondary text to display. |
| `tertiaryText` | Optional | mixed | | The tertiary text to display. |
| `linkType` | Optional |mixed | `"edit"` | The target of each item click. |
| `rowClick` | Optional |mixed | `"edit"` | The action to trigger when the user clicks on a row. |
| `leftAvatar` | Optional | function | | A function returning an `<Avatar>` component to display before the primary text. |
| `leftIcon` | Optional | function | | A function returning an `<Icon>` component to display before the primary text. |
| `rightAvatar` | Optional | function | | A function returning an `<Avatar>` component to display after the primary text. |
Expand Down Expand Up @@ -80,9 +80,9 @@ This prop should be a function returning an `<Avatar>` component. When present,

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

## `linkType`
## `rowClick`

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.
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.

```jsx
import { List, SimpleList } from 'react-admin';
Expand All @@ -93,17 +93,19 @@ export const PostList = () => (
primaryText={record => record.title}
secondaryText={record => `${record.views} views`}
tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
linkType="show"
rowClick="show"
/>
</List>
);
```

`linkType` accepts the following values:
`rowClick` accepts the following values:

* `linkType="edit"`: links to the edit page. This is the default behavior.
* `linkType="show"`: links to the show page.
* `linkType={false}`: does not create any link.
* `rowClick="edit"`: links to the edit page. This is the default behavior.
* `rowClick="show"`: links to the show page.
* `rowClick={false}`: does not link to anything.
* `rowClick="/custom"`: links to a custom path.
* `rowClick={(id, resource, record) => path}`: path can be any of the above values
slax57 marked this conversation as resolved.
Show resolved Hide resolved

## `primaryText`

Expand Down Expand Up @@ -254,7 +256,7 @@ export const PostList = () => {
primaryText={record => record.title}
secondaryText={record => `${record.views} views`}
tertiaryText={record => new Date(record.published_at).toLocaleDateString()}
linkType={record => record.canEdit ? "edit" : "show"}
rowClick={(id, resource, record) => record.canEdit ? "edit" : "show"}
/>
) : (
<Datagrid>
Expand Down
5 changes: 5 additions & 0 deletions packages/ra-core/src/routing/useGetPathForRecord.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ export const useGetPathForRecord = <RecordType extends RaRecord = RaRecord>(
useEffect(() => {
if (!record) return;

if (link === false) {
setPath(false);
return;
}

// Handle the inferred link type case
if (link == null) {
// We must check whether the resource has an edit view because if there is no
Expand Down
157 changes: 109 additions & 48 deletions packages/ra-ui-materialui/src/list/SimpleList/SimpleList.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,41 @@ import {
waitFor,
within,
} from '@testing-library/react';
import { ListContext, ResourceContextProvider } from 'ra-core';
import {
ListContext,
ResourceContextProvider,
ResourceDefinitionContextProvider,
} from 'ra-core';
import { Location } from 'react-router';

import { AdminContext } from '../../AdminContext';
import { SimpleList } from './SimpleList';
import { TextField } from '../../field/TextField';
import {
LinkType,
NoPrimaryText,
RowClick,
Standalone,
StandaloneEmpty,
} from './SimpleList.stories';
import { Basic } from '../filter/FilterButton.stories';

const Wrapper = ({ children }: any) => (
<AdminContext>
<ResourceContextProvider value="posts">
{children}
</ResourceContextProvider>
<ResourceDefinitionContextProvider
definitions={{
posts: {
name: 'posts',
hasList: true,
hasEdit: true,
hasShow: true,
},
}}
>
<ResourceContextProvider value="posts">
{children}
</ResourceContextProvider>
</ResourceDefinitionContextProvider>
</AdminContext>
);

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

it.each([
['edit', 'edit', '/books/1'],
['show', 'show', '/books/1/show'],
[
'a function that returns a custom path',
(record, id) =>
`/books/${id}/${record.title.toLowerCase().replaceAll(' ', '-')}`,
'/books/1/war-and-peace',
],
['a function that returns edit', () => 'edit', '/books/1'],
['a function that returns show', () => 'show', '/books/1/show'],
])(
'Providing %s as linkType should render a link for each item',
async (_, linkType, expectedUrl) => {
let location: Location;
render(
<LinkType
linkType={linkType}
locationCallback={l => {
location = l;
}}
/>
);
fireEvent.click(await screen.findByText('War and Peace'));
await waitFor(() => {
expect(location?.pathname).toEqual(expectedUrl);
});
}
);

it('should not render links if linkType is false', async () => {
render(
<ListContext.Provider
value={{
isLoading: false,
data: [
{ id: 1, title: 'foo' },
{ id: 2, title: 'bar' },
],
total: 2,
resource: 'posts',
}}
>
<SimpleList
linkType={false}
primaryText={record => record.id.toString()}
secondaryText={<TextField source="title" />}
/>
</ListContext.Provider>,
{ wrapper: Wrapper }
);

await waitFor(() => {
expect(screen.getByText('1').closest('a')).toBeNull();
expect(screen.getByText('2').closest('a')).toBeNull();
});
});

it.each([
['edit', 'edit', '/books/1'],
['show', 'show', '/books/1/show'],
[
'edit',
'edit',
['http://localhost/#/posts/1', 'http://localhost/#/posts/2'],
'a function that returns a custom path',
(id, resource, record) =>
`/${resource}/${id}/${record.title.toLowerCase().replaceAll(' ', '-')}`,
'/books/1/war-and-peace',
],
['a function that returns edit', () => 'edit', '/books/1'],
['a function that returns show', () => 'show', '/books/1/show'],
['a function that resolves to edit', async () => 'edit', '/books/1'],
[
'show',
'show',
[
'http://localhost/#/posts/1/show',
'http://localhost/#/posts/2/show',
],
'a function that resolves to show',
async () => 'show',
'/books/1/show',
],
[
'custom',
(record, id) => `/posts/${id}/custom`,
[
'http://localhost/#/posts/1/custom',
'http://localhost/#/posts/2/custom',
],
'a function that resolves to a custom path',
async (id, resource, record) =>
`/${resource}/${id}/${record.title.toLowerCase().replaceAll(' ', '-')}`,
'/books/1/war-and-peace',
],
])(
'should render %s links for each item',
async (_, link, expectedUrls) => {
'Providing %s as rowClick should render a link for each item',
async (_, rowClick, expectedUrls) => {
let location: Location;
render(
<ListContext.Provider
value={{
isLoading: false,
data: [
{ id: 1, title: 'foo' },
{ id: 2, title: 'bar' },
],
total: 2,
resource: 'posts',
<RowClick
rowClick={rowClick}
locationCallback={l => {
location = l;
}}
>
<SimpleList
linkType={link}
primaryText={record => record.id.toString()}
secondaryText={<TextField source="title" />}
/>
</ListContext.Provider>,
{ wrapper: Wrapper }
/>
);

fireEvent.click(await screen.findByText('War and Peace'));
await waitFor(() => {
expect(screen.getByText('1').closest('a').href).toEqual(
expectedUrls[0]
);
expect(screen.getByText('2').closest('a').href).toEqual(
expectedUrls[1]
);
expect(location?.pathname).toEqual(expectedUrls);
});
}
);

it('should not render links if linkType is false', async () => {
it('should not render links if rowClick is false', async () => {
render(
<ListContext.Provider
value={{
Expand All @@ -129,7 +190,7 @@ describe('<SimpleList />', () => {
}}
>
<SimpleList
linkType={false}
rowClick={false}
primaryText={record => record.id.toString()}
secondaryText={<TextField source="title" />}
/>
Expand Down Expand Up @@ -205,7 +266,7 @@ describe('<SimpleList />', () => {
});
it('should display a message when there is no result', async () => {
render(<StandaloneEmpty />);
await screen.findByText('No results found.');
await screen.findByText('ra.navigation.no_results');
});
});
});
Loading
Loading