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

Add support for response metadata in dataProvider.getList() #10179

Merged
merged 5 commits into from
Sep 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
14 changes: 11 additions & 3 deletions docs/DataProviderWriting.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ interface GetListParams {
pagination: { page: number, perPage: number };
sort: { field: string, order: 'ASC' | 'DESC' };
filter: any;
meta?: any;
meta?: any; // request metadata
signal?: AbortSignal;
}
interface GetListResult {
Expand All @@ -66,6 +66,7 @@ interface GetListResult {
hasNextPage?: boolean;
hasPreviousPage?: boolean;
};
meta?: any; // response metadata
}
function getList(resource: string, params: GetListParams): Promise<GetListResult>
```
Expand All @@ -88,7 +89,13 @@ dataProvider.getList('posts', {
// { id: 123, title: "hello, world", author_id: 12 },
// { id: 125, title: "howdy partner", author_id: 12 },
// ],
// total: 27
// total: 27,
// meta: {
// facets: [
// { name: "published", count: 12 },
// { name: "draft", count: 15 },
// ],
// },
// }
```

Expand Down Expand Up @@ -167,7 +174,7 @@ interface GetManyReferenceParams {
pagination: { page: number, perPage: number };
sort: { field: string, order: 'ASC' | 'DESC' };
filter: any;
meta?: any;
meta?: any; // request metadata
signal?: AbortSignal;
}
interface GetManyReferenceResult {
Expand All @@ -178,6 +185,7 @@ interface GetManyReferenceResult {
hasNextPage?: boolean;
hasPreviousPage?: boolean;
};
meta?: any; // response metadata
}
function getManyReference(resource: string, params: GetManyReferenceParams): Promise<GetManyReferenceResult>
```
Expand Down
39 changes: 39 additions & 0 deletions docs/List.md
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,45 @@ const ProductList = () => (
)
```

## Accessing Extra Response Data

If `dataProvider.getList()` returns additional metadata in the response under the `meta` key, you can access it in the list view using the `meta` property of the `ListContext`.

This is often used by APIs to return statistics or other metadata about the list of records.

```tsx
// dataProvider.getLists('posts') returns response like
// {
// data: [ ... ],
// total: 293,
// meta: {
// facets: [
// { value: 'Novels', count: 245 },
// { value: 'Essays', count: 23 },
// { value: 'Short stories', count: 25 },
// ],
// },
// }
const Facets = () => {
const { isLoading, error, meta } = useListContext();
if (isLoading || error) return null;
const facets = meta.facets;
return (
<Stack direction="row" gap={3} mt={2} ml={1}>
{facets.map(facet => (
<Badge
key={facet.value}
badgeContent={facet.count}
color="primary"
>
<Chip label={facet.value} size="small" />
</Badge>
))}
</Stack>
);
};
```

## Controlled Mode

`<List>` deduces the resource and the list parameters from the URL. This is fine for a page showing a single list of records, but if you need to display more than one list in a page, you probably want to define the list parameters yourself.
Expand Down
3 changes: 2 additions & 1 deletion docs/WithListContext.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ As a reminder, the [`ListContext`](./useListContext.md) is an object with the fo
<WithListContext render={({
// fetched data
data, // an array of the list records, e.g. [{ id: 123, title: 'hello world' }, { ... }]
total, // the total number of results for the current filters, excluding pagination. Useful to build the pagination controls, e.g. 23
total, // the total number of results for the current filters, excluding pagination. Useful to build the pagination controls, e.g. 23
meta, // Additional information about the list, like facets & statistics
isPending, // boolean that is true until the data is available for the first time
isLoading, // boolean that is true until the data is fetched for the first time
isFetching, // boolean that is true while the data is being fetched, and false once the data is fetched
Expand Down
41 changes: 39 additions & 2 deletions docs/useGetList.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This hook calls `dataProvider.getList()` when the component mounts. It's ideal f
## Syntax

```jsx
const { data, total, isPending, error, refetch } = useGetList(
const { data, total, isPending, error, refetch, meta } = useGetList(
resource,
{
pagination: { page, perPage },
Expand All @@ -23,7 +23,7 @@ const { data, total, isPending, error, refetch } = useGetList(
);
```

The `meta` argument is optional. It can be anything you want to pass to the data provider, e.g. a list of fields to show in the result.
The `meta` argument is optional. It can be anything you want to pass to the data provider, e.g. a list of fields to show in the result. It is distinct from the `meta` property of the response, which may contain additional metadata returned by the data provider.

The `options` parameter is optional, and is passed to [react-query's `useQuery` hook](https://tanstack.com/query/v5/docs/react/reference/useQuery). It may contain the following options:

Expand Down Expand Up @@ -134,6 +134,43 @@ const LatestNews = () => {

In this example, the `useGetList` hook fetches all the posts, and displays a list of the 10 most recent posts in a `<Datagrid>`. The `<Pagination>` component allows the user to navigate through the list. Users can also sort the list by clicking on the column headers.

## Passing Additional Arguments

If you need to pass additional arguments to the data provider, you can pass them in the `meta` argument.

For example, if you want to embed related records in the response, and your data provider supports the `embed` meta parameter, you can pass it like this:

```jsx
const { data, total, isPending, error } = useGetList(
'posts',
{
pagination: { page: 1, perPage: 10 },
sort: { field: 'published_at', order: 'DESC' },
// Pass extra parameters using the meta argument
meta: { embed: ['author', 'category'] }
}
);
```

**Tip**: Don't mix the `meta` parameter with the `meta` property of the response (see below). Although they share the same name, they are not related.

## Accessing Response Metadata

If your backend returns additional metadata along with the records, you can access it in the `meta` property of the result.

```jsx
const {
data,
total,
isPending,
error,
// access the extra response details in the meta property
meta
} = useGetList('posts', { pagination: { page: 1, perPage: 10 }});
```

**Tip**: Don't mix the `meta` property of the response with the `meta` parameter (see above). Although they share the same name, they are not related.

## Partial Pagination

If your data provider doesn't return the `total` number of records (see [Partial Pagination](./DataProviderWriting.md#partial-pagination)), you can use the `pageInfo` field to determine if there are more records to fetch.
Expand Down
2 changes: 1 addition & 1 deletion docs/useGetManyReference.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This hook calls `dataProvider.getManyReference()` when the component mounts. It
## Syntax

```jsx
const { data, total, isPending, error, refetch } = useGetManyReference(
const { data, total, isPending, error, refetch, meta } = useGetManyReference(
resource,
{ target, id, pagination, sort, filter, meta },
options
Expand Down
1 change: 1 addition & 0 deletions docs/useInfiniteGetList.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ It is based on react-query's [`useInfiniteQuery`](https://tanstack.com/query/v5/
const {
data: { pages, pageParams },
total,
meta,
pageInfo,
isPending,
error,
Expand Down
1 change: 1 addition & 0 deletions docs/useListContext.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const {
// Data
data, // Array of the list records, e.g. [{ id: 123, title: 'hello world' }, { ... }
total, // Total number of results for the current filters, excluding pagination. Useful to build the pagination controls, e.g. 23
meta, // Additional information about the list, like facets & statistics
isPending, // Boolean, true until the data is available
isFetching, // Boolean, true while the data is being fetched, false once the data is fetched
isLoading, // Boolean, true until the data is fetched for the first time
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,4 +364,52 @@ describe('useReferenceManyFieldController', () => {
]);
});
});

describe('response metadata', () => {
it('should return response metadata as meta', async () => {
const dataProvider = testDataProvider({
getManyReference: () =>
Promise.resolve({
data: [
{ id: 1, title: 'hello' },
{ id: 2, title: 'world' },
],
total: 2,
meta: {
facets: [
{ foo: 'bar', count: 1 },
{ foo: 'baz', count: 2 },
],
},
}) as any,
});

const ListMetadataInspector = () => {
const listContext = useReferenceManyFieldController({
resource: 'authors',
source: 'id',
record: { id: 123, name: 'James Joyce' },
reference: 'books',
target: 'author_id',
});

return (
<>
Response metadata:{' '}
<pre>{JSON.stringify(listContext.meta, null)}</pre>
</>
);
};

render(
<CoreAdminContext dataProvider={dataProvider}>
<ListMetadataInspector />
</CoreAdminContext>
);

await screen.findByText(
'{"facets":[{"foo":"bar","count":1},{"foo":"baz","count":2}]}'
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ export const useReferenceManyFieldController = <
const {
data,
total,
meta: responseMeta,
pageInfo,
error,
isFetching,
Expand Down Expand Up @@ -202,6 +203,7 @@ export const useReferenceManyFieldController = <
return {
sort,
data,
meta: responseMeta,
defaultTitle: undefined,
displayedFilters,
error,
Expand Down
43 changes: 41 additions & 2 deletions packages/ra-core/src/controller/list/ListBase.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ const dataProvider = fakeRestProvider(data, true, 300);
const BookListView = () => {
const {
data,
isLoading,
error,
isPending,
sort,
filterValues,
page,
Expand All @@ -61,9 +62,12 @@ const BookListView = () => {
sort,
filterValues,
});
if (isLoading) {
if (isPending) {
return <div>Loading...</div>;
}
if (error) {
return <div>Error...</div>;
}

const handleClick = () => {
const value = JSON.parse(inputRef.current!.value);
Expand Down Expand Up @@ -114,3 +118,38 @@ export const SetParams = () => (
</ListBase>
</CoreAdminContext>
);

const ListMetadataInspector = () => {
const listContext = useListContext();
return (
<>
Response metadata:{' '}
<pre>{JSON.stringify(listContext.meta, null, 2)}</pre>
</>
);
};

export const WithResponseMetadata = () => (
<CoreAdminContext
dataProvider={{
...dataProvider,
getList: async (resource, params) => {
const result = await dataProvider.getList(resource, params);
return {
...result,
meta: {
facets: [
{ value: 'bar', count: 2 },
{ value: 'baz', count: 1 },
],
},
};
},
}}
>
<ListBase resource="books" perPage={5}>
<BookListView />
<ListMetadataInspector />
</ListBase>
</CoreAdminContext>
);
6 changes: 6 additions & 0 deletions packages/ra-core/src/controller/list/useListController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const useListController = <RecordType extends RaRecord = any>(
data,
pageInfo,
total,
meta: responseMeta,
error,
isLoading,
isFetching,
Expand Down Expand Up @@ -157,6 +158,7 @@ export const useListController = <RecordType extends RaRecord = any>(
return {
sort: currentSort,
data,
meta: responseMeta,
defaultTitle,
displayedFilters: query.displayedFilters,
error,
Expand Down Expand Up @@ -486,6 +488,7 @@ export interface ListControllerLoadingResult<RecordType extends RaRecord = any>
extends ListControllerBaseResult<RecordType> {
data: undefined;
total: undefined;
meta: undefined;
error: null;
isPending: true;
}
Expand All @@ -495,6 +498,7 @@ export interface ListControllerErrorResult<
> extends ListControllerBaseResult<RecordType> {
data: undefined;
total: undefined;
meta: undefined;
error: TError;
isPending: false;
}
Expand All @@ -504,13 +508,15 @@ export interface ListControllerRefetchErrorResult<
> extends ListControllerBaseResult<RecordType> {
data: RecordType[];
total: number;
meta?: any;
error: TError;
isPending: false;
}
export interface ListControllerSuccessResult<RecordType extends RaRecord = any>
extends ListControllerBaseResult<RecordType> {
data: RecordType[];
total: number;
meta?: any;
error: null;
isPending: false;
}
Expand Down
Loading
Loading