-
-
Notifications
You must be signed in to change notification settings - Fork 5.3k
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
feat: support Component and Element for List props #10539
base: next
Are you sure you want to change the base?
Changes from all commits
63ddf92
abf98d3
940a711
00ddb18
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -52,29 +52,29 @@ You can find more advanced examples of `<List>` usage in the [demos](./Demos.md) | |||||
|
||||||
## Props | ||||||
|
||||||
| Prop | Required | Type | Default | Description | | ||||||
|---------------------------|----------|----------------|----------------|----------------------------------------------------------------------------------------------| | ||||||
| `children` | Required | `ReactNode` | - | The components rendering the list of records. | | ||||||
| `actions` | Optional | `ReactElement` | - | The actions to display in the toolbar. | | ||||||
| `aside` | Optional | `ReactElement` | - | The component to display on the side of the list. | | ||||||
| `component` | Optional | `Component` | `Card` | The component to render as the root element. | | ||||||
| `debounce` | Optional | `number` | `500` | The debounce delay in milliseconds to apply when users change the sort or filter parameters. | | ||||||
| `disable Authentication` | Optional | `boolean` | `false` | Set to `true` to disable the authentication check. | | ||||||
| `disable SyncWithLocation`| Optional | `boolean` | `false` | Set to `true` to disable the synchronization of the list parameters with the URL. | | ||||||
| `empty` | Optional | `ReactElement` | - | The component to display when the list is empty. | | ||||||
| `empty WhileLoading` | Optional | `boolean` | `false` | Set to `true` to return `null` while the list is loading. | | ||||||
| `exporter` | Optional | `function` | - | The function to call to export the list. | | ||||||
| `filters` | Optional | `ReactElement` | - | The filters to display in the toolbar. | | ||||||
| `filter` | Optional | `object` | - | The permanent filter values. | | ||||||
| `filter DefaultValues` | Optional | `object` | - | The default filter values. | | ||||||
| `pagination` | Optional | `ReactElement` | `<Pagination>` | The pagination component to use. | | ||||||
| `perPage` | Optional | `number` | `10` | The number of records to fetch per page. | | ||||||
| `queryOptions` | Optional | `object` | - | The options to pass to the `useQuery` hook. | | ||||||
| `resource` | Optional | `string` | - | The resource name, e.g. `posts`. | | ||||||
| `sort` | Optional | `object` | - | The initial sort parameters. | | ||||||
| `storeKey` | Optional | `string | false` | - | The key to use to store the current filter & sort. Pass `false` to disable store synchronization | | ||||||
| `title` | Optional | `string | ReactElement | false` | - | The title to display in the App Bar. | | ||||||
| `sx` | Optional | `object` | - | The CSS styles to apply to the component. | | ||||||
| Prop | Required | Type | Default | Description | | ||||||
|---------------------------|----------|----------------------------------------|----------------|-------------------------------------------------------------------------------------------------| | ||||||
| `children` | Required | `ReactNode` | - | The components rendering the list of records. | | ||||||
| `actions` | Optional | `ReactElement | ComponentType | false` | - | The actions to display in the toolbar. | | ||||||
| `aside` | Optional | `ReactElement | ComponentType` | - | The component to display on the side of the list. | | ||||||
| `component` | Optional | `Component` | `Card` | The component to render as the root element. | | ||||||
| `debounce` | Optional | `number` | `500` | The debounce delay in milliseconds to apply when users change the sort or filter parameters. | | ||||||
| `disable Authentication` | Optional | `boolean` | `false` | Set to `true` to disable the authentication check. | | ||||||
| `disable SyncWithLocation`| Optional | `boolean` | `false` | Set to `true` to disable the synchronization of the list parameters with the URL. | | ||||||
| `empty` | Optional | `ReactElement | ComponentType | false` | - | The component to display when the list is empty. | | ||||||
| `empty WhileLoading` | Optional | `boolean` | `false` | Set to `true` to return `null` while the list is loading. | | ||||||
| `exporter` | Optional | `function` | - | The function to call to export the list. | | ||||||
| `filters` | Optional | `ReactElement` | - | The filters to display in the toolbar. | | ||||||
| `filter` | Optional | `object` | - | The permanent filter values. | | ||||||
| `filter DefaultValues` | Optional | `object` | - | The default filter values. | | ||||||
| `pagination` | Optional | `ReactElement | ComponentType | false` | `<Pagination>` | The pagination component to use. | | ||||||
| `perPage` | Optional | `number` | `10` | The number of records to fetch per page. | | ||||||
| `queryOptions` | Optional | `object` | - | The options to pass to the `useQuery` hook. | | ||||||
| `resource` | Optional | `string` | - | The resource name, e.g. `posts`. | | ||||||
| `sort` | Optional | `object` | - | The initial sort parameters. | | ||||||
| `storeKey` | Optional | `string | false` | - | The key to use to store the current filter & sort. Pass `false` to disable store synchronization | | ||||||
| `title` | Optional | `string | ReactElement | ComponentType | false` | - | The title to display in the App Bar. | | ||||||
| `sx` | Optional | `object` | - | The CSS styles to apply to the component. | | ||||||
|
||||||
Additional props are passed down to the root component (a MUI `<Card>` by default). | ||||||
|
||||||
|
@@ -139,6 +139,7 @@ export const PostList = () => ( | |||||
</List> | ||||||
); | ||||||
``` | ||||||
You can use a `ReactElement` (e.g. `actions={<ListActions/>}`), a `ComponentType` (e.g. `actions={ListActions}`) or false to disable it. | ||||||
|
||||||
**Tip**: If you are looking for an `<ImportButton>`, check out this third-party package: [benwinding/react-admin-import-csv](https://github.com/benwinding/react-admin-import-csv). | ||||||
|
||||||
|
@@ -175,7 +176,7 @@ The default `<List>` layout lets you render the component of your choice on the | |||||
|
||||||
 | ||||||
|
||||||
Pass a React element as the `aside` prop for that purpose: | ||||||
Pass a React element or ComponentType as the `aside` prop for that purpose: | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: I believe
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But it IS expected a ComponentType (as defined in the |
||||||
|
||||||
{% raw %} | ||||||
```jsx | ||||||
|
@@ -483,7 +484,7 @@ When there is no result, and there is no active filter, and the resource has a c | |||||
|
||||||
 | ||||||
|
||||||
You can use the `empty` prop to replace that page by a custom component: | ||||||
You can use the `empty` prop to replace that page by a custom component, passing a ReactElement or ComponentType : | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
{% raw %} | ||||||
```jsx | ||||||
|
@@ -783,7 +784,7 @@ By default, the `<List>` view displays a set of pagination controls at the botto | |||||
|
||||||
 | ||||||
|
||||||
The `pagination` prop allows to replace the default pagination controls by your own. | ||||||
The `pagination` prop allows to replace the default pagination controls by your own, either with a ReactElement or a ComponentType. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
```jsx | ||||||
// in src/MyPagination.js | ||||||
|
@@ -990,7 +991,7 @@ export const PostList = () => ( | |||||
); | ||||||
``` | ||||||
|
||||||
The title can be a string, a React element, or `false` to disable the title. | ||||||
The title can be a string, a React element, a ComponentType or `false` to disable the title. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
## `sx`: CSS API | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -22,6 +22,9 @@ import { | |||||||||||||
PartialPagination, | ||||||||||||||
Default, | ||||||||||||||
SelectAllLimit, | ||||||||||||||
TitleComponent, | ||||||||||||||
ActionsElement, | ||||||||||||||
ActionsComponent, | ||||||||||||||
} from './List.stories'; | ||||||||||||||
|
||||||||||||||
const theme = createTheme(defaultTheme); | ||||||||||||||
|
@@ -112,6 +115,25 @@ describe('<List />', () => { | |||||||||||||
expect(screen.queryAllByText('Hello')).toHaveLength(1); | ||||||||||||||
}); | ||||||||||||||
|
||||||||||||||
it('should display aside component with ComponentType', () => { | ||||||||||||||
const Dummy = () => <div />; | ||||||||||||||
const Aside = () => <div id="aside">Hello</div>; | ||||||||||||||
render( | ||||||||||||||
<CoreAdminContext | ||||||||||||||
dataProvider={testDataProvider({ | ||||||||||||||
getList: () => Promise.resolve({ data: [], total: 0 }), | ||||||||||||||
})} | ||||||||||||||
> | ||||||||||||||
<ThemeProvider theme={theme}> | ||||||||||||||
<List resource="posts" aside={Aside}> | ||||||||||||||
<Dummy /> | ||||||||||||||
</List> | ||||||||||||||
</ThemeProvider> | ||||||||||||||
</CoreAdminContext> | ||||||||||||||
); | ||||||||||||||
expect(screen.queryAllByText('Hello')).toHaveLength(1); | ||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
}); | ||||||||||||||
|
||||||||||||||
describe('empty', () => { | ||||||||||||||
it('should render an invite when the list is empty', async () => { | ||||||||||||||
const Dummy = () => { | ||||||||||||||
|
@@ -197,6 +219,36 @@ describe('<List />', () => { | |||||||||||||
}); | ||||||||||||||
}); | ||||||||||||||
|
||||||||||||||
it('should render custom empty component when data is empty with ComponentType', async () => { | ||||||||||||||
const Dummy = () => null; | ||||||||||||||
const CustomEmpty = () => <div>Custom Empty</div>; | ||||||||||||||
|
||||||||||||||
const dataProvider = { | ||||||||||||||
getList: jest.fn(() => | ||||||||||||||
Promise.resolve({ | ||||||||||||||
data: [], | ||||||||||||||
pageInfo: { | ||||||||||||||
hasNextPage: false, | ||||||||||||||
hasPreviousPage: false, | ||||||||||||||
}, | ||||||||||||||
}) | ||||||||||||||
), | ||||||||||||||
} as any; | ||||||||||||||
render( | ||||||||||||||
<CoreAdminContext dataProvider={dataProvider}> | ||||||||||||||
<ThemeProvider theme={theme}> | ||||||||||||||
<List resource="posts" empty={CustomEmpty}> | ||||||||||||||
<Dummy /> | ||||||||||||||
</List> | ||||||||||||||
</ThemeProvider> | ||||||||||||||
</CoreAdminContext> | ||||||||||||||
); | ||||||||||||||
await waitFor(() => { | ||||||||||||||
expect(screen.queryByText('resources.posts.empty')).toBeNull(); | ||||||||||||||
screen.getByText('Custom Empty'); | ||||||||||||||
}); | ||||||||||||||
Comment on lines
+246
to
+249
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
}); | ||||||||||||||
|
||||||||||||||
it('should not render an invite when a filter is active', async () => { | ||||||||||||||
const Dummy = () => { | ||||||||||||||
const { isPending } = useListContext(); | ||||||||||||||
|
@@ -336,6 +388,12 @@ describe('<List />', () => { | |||||||||||||
screen.getByText('Custom list title'); | ||||||||||||||
}); | ||||||||||||||
|
||||||||||||||
it('should render custom title component when defined', async () => { | ||||||||||||||
render(<TitleComponent />); | ||||||||||||||
await screen.findByText('War and Peace (1869)'); | ||||||||||||||
screen.getByText('Custom list title'); | ||||||||||||||
}); | ||||||||||||||
|
||||||||||||||
it('should not render default title when false', async () => { | ||||||||||||||
render(<TitleFalse />); | ||||||||||||||
await screen.findByText('War and Peace (1869)'); | ||||||||||||||
|
@@ -486,4 +544,16 @@ describe('<List />', () => { | |||||||||||||
); | ||||||||||||||
}); | ||||||||||||||
}); | ||||||||||||||
describe('Custom actions', () => { | ||||||||||||||
it('should render custom actions with ReactElement', async () => { | ||||||||||||||
render(<ActionsElement />); | ||||||||||||||
await screen.findByText('War and Peace (1869)'); | ||||||||||||||
screen.getByText('Actions'); | ||||||||||||||
}); | ||||||||||||||
it('should render custom actions with ComponentType', async () => { | ||||||||||||||
render(<ActionsComponent />); | ||||||||||||||
await screen.findByText('War and Peace (1869)'); | ||||||||||||||
screen.getByText('Actions'); | ||||||||||||||
}); | ||||||||||||||
}); | ||||||||||||||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import * as React from 'react'; | ||
import { Admin, AutocompleteInput } from 'react-admin'; | ||
import { Admin, AutocompleteInput, Pagination } from 'react-admin'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. linter warning at this line |
||
import { | ||
CustomRoutes, | ||
Resource, | ||
|
@@ -10,6 +10,8 @@ | |
import fakeRestDataProvider from 'ra-data-fakerest'; | ||
import { Box, Card, Typography, Button, Link as MuiLink } from '@mui/material'; | ||
|
||
import { Empty as DefaultEmpty } from './Empty'; | ||
import { Pagination as DefaultPagination } from './pagination'; | ||
import { List } from './List'; | ||
import { SimpleList } from './SimpleList'; | ||
import { ListActions } from './ListActions'; | ||
|
@@ -169,7 +171,7 @@ | |
</TestMemoryRouter> | ||
); | ||
|
||
export const Actions = () => ( | ||
export const ActionsElement = () => ( | ||
<TestMemoryRouter initialEntries={['/books']}> | ||
<Admin dataProvider={defaultDataProvider}> | ||
<Resource | ||
|
@@ -190,6 +192,23 @@ | |
</TestMemoryRouter> | ||
); | ||
|
||
const Actions = () => <Box sx={{ backgroundColor: 'info.main' }}>Actions</Box>; | ||
|
||
export const ActionsComponent = () => ( | ||
<TestMemoryRouter initialEntries={['/books']}> | ||
<Admin dataProvider={defaultDataProvider}> | ||
<Resource | ||
name="books" | ||
list={() => ( | ||
<List actions={Actions}> | ||
<BookList /> | ||
</List> | ||
)} | ||
/> | ||
</Admin> | ||
</TestMemoryRouter> | ||
); | ||
|
||
export const Filters = () => ( | ||
<TestMemoryRouter initialEntries={['/books']}> | ||
<Admin dataProvider={defaultDataProvider}> | ||
|
@@ -267,6 +286,23 @@ | |
</TestMemoryRouter> | ||
); | ||
|
||
const TitleSpan = () => <span>Custom list title</span>; | ||
|
||
export const TitleComponent = () => ( | ||
<TestMemoryRouter initialEntries={['/books']}> | ||
<Admin dataProvider={defaultDataProvider}> | ||
<Resource | ||
name="books" | ||
list={() => ( | ||
<List title={TitleSpan}> | ||
<BookList /> | ||
</List> | ||
)} | ||
/> | ||
</Admin> | ||
</TestMemoryRouter> | ||
); | ||
|
||
export const TitleFalse = () => ( | ||
<TestMemoryRouter initialEntries={['/books']}> | ||
<Admin dataProvider={defaultDataProvider}> | ||
|
@@ -297,15 +333,30 @@ | |
</TestMemoryRouter> | ||
); | ||
|
||
const AsideComponent = () => <Card sx={{ padding: 2 }}>Aside</Card>; | ||
const AsideBlock = () => <Card sx={{ padding: 2 }}>Aside</Card>; | ||
|
||
export const AsideElement = () => ( | ||
<TestMemoryRouter initialEntries={['/books']}> | ||
<Admin dataProvider={defaultDataProvider}> | ||
<Resource | ||
name="books" | ||
list={() => ( | ||
<List aside={<AsideBlock />}> | ||
<BookList /> | ||
</List> | ||
)} | ||
/> | ||
</Admin> | ||
</TestMemoryRouter> | ||
); | ||
|
||
export const Aside = () => ( | ||
export const AsideComponent = () => ( | ||
<TestMemoryRouter initialEntries={['/books']}> | ||
<Admin dataProvider={defaultDataProvider}> | ||
<Resource | ||
name="books" | ||
list={() => ( | ||
<List aside={<AsideComponent />}> | ||
<List aside={AsideBlock}> | ||
<BookList /> | ||
</List> | ||
)} | ||
|
@@ -387,6 +438,22 @@ | |
</TestMemoryRouter> | ||
); | ||
|
||
export const EmptyComponent = () => ( | ||
<TestMemoryRouter initialEntries={['/authors']}> | ||
<Admin dataProvider={defaultDataProvider}> | ||
<Resource | ||
name="authors" | ||
list={() => ( | ||
<List empty={DefaultEmpty}> | ||
<span /> | ||
</List> | ||
)} | ||
create={() => <span />} | ||
/> | ||
</Admin> | ||
</TestMemoryRouter> | ||
); | ||
|
||
export const EmptyPartialPagination = () => ( | ||
<TestMemoryRouter initialEntries={['/authors']}> | ||
<Admin | ||
|
@@ -415,6 +482,21 @@ | |
</TestMemoryRouter> | ||
); | ||
|
||
export const PaginationComponent = () => ( | ||
<TestMemoryRouter initialEntries={['/books']}> | ||
<Admin dataProvider={defaultDataProvider}> | ||
<Resource | ||
name="books" | ||
list={() => ( | ||
<List pagination={DefaultPagination}> | ||
<BookList /> | ||
</List> | ||
)} | ||
/> | ||
</Admin> | ||
</TestMemoryRouter> | ||
); | ||
|
||
export const SX = () => ( | ||
<TestMemoryRouter initialEntries={['/books']}> | ||
<Admin dataProvider={defaultDataProvider}> | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: I'd be tempted to use the component syntax in the example above (since it's shorter and offers better performances), and simply mention that
action
also accepts the other typesBut on the other hand, it may be odd to encourage this syntax only for the
List
component (since it's the only one we support for now...)So I'm hesitating.
Same remark applies to other props below.
@fzaninotto @djhi wdyt?