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
13 changes: 13 additions & 0 deletions docs/ReferenceManyCount.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const PostList = () => (
| ----------- | -------- | ------------------------------------------ | --------------------------------- | ------------------------------------------------------------------------- |
| `reference` | Required | string | - | Name of the related resource to fetch (e.g. `comments`) |
| `target` | Required | string | - | Name of the field in the related resource that points to the current one. |
| `children` | Optional | `ReactNode` | - | The component used to render the count. |
| `filter` | Optional | Object | - | Filter to apply to the query. |
| `link` | Optional | bool | `false` | If true, the count is wrapped in a `<Link>` to the filtered list view. |
| `offline` | Optional | `ReactNode` | | The component to render when there is no connectivity and the record isn't in the cache
Expand All @@ -77,6 +78,18 @@ export const PostList = () => (

Additional props are passed to [the underlying Material UI `<Typography>` element](https://mui.com/material-ui/api/typography/).

## `children`

By default, `<ReferenceManyCount>` renders the total as a raw number. You can pass a child component to customize the rendering. `<ReferenceManyCount>` creates a `RecordContext` with a `total` field, so any Field component can read it:

```jsx
import { NumberField, ReferenceManyCount } from 'react-admin';

<ReferenceManyCount reference="comments" target="post_id" link>
<NumberField source="total" locales="en-US" />
</ReferenceManyCount>
```

## `filter`

If you want to count the number of records matching a given filter, pass it as the `filter` prop. For example, to count the number of comments already published:
Expand Down
19 changes: 19 additions & 0 deletions docs_headless/src/content/docs/ReferenceManyCountBase.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,31 @@ export const PostList = () => (
| ----------- | -------- | ------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------- |
| `reference` | Required | string | - | Name of the related resource to fetch (e.g. `comments`) |
| `target` | Required | string | - | Name of the field in the related resource that points to the current one. |
| `children` | Optional | `ReactNode` | - | The component used to render the count. |
| `filter` | Optional | Object | - | Filter to apply to the query. |
| `resource` | Optional | string | - | Resource to count. Default to the current `ResourceContext` |
| `sort` | Optional | `{ field: string, order: 'ASC' or 'DESC' }` | `{ field: 'id', order: 'DESC' }` | The sort option sent to `getManyReference` |
| `timeout` | Optional | number | 1000 | Number of milliseconds to wait before displaying the loading indicator. |


## `children`

By default, `<ReferenceManyCountBase>` renders the total as a raw number. You can pass a child component to customize the rendering. `<ReferenceManyCountBase>` creates a `RecordContext` with a `total` field, so descendants can read it with `useRecordContext`:

```jsx
import { ReferenceManyCountBase, useRecordContext } from 'ra-core';

const Count = () => {
const record = useRecordContext();

return <span>{record?.total} comments</span>;
};

<ReferenceManyCountBase reference="comments" target="post_id">
<Count />
</ReferenceManyCountBase>
```

## `filter`

If you want to count the number of records matching a given filter, pass it as the `filter` prop. For example, to count the number of comments already published:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
ErrorState,
LoadingState,
Offline,
WithChildren,
WithRenderFunction,
} from './ReferenceManyCountBase.stories';
import { onlineManager } from '@tanstack/react-query';

Expand All @@ -31,6 +33,16 @@ describe('ReferenceManyCountBase', () => {
await screen.findByText('3');
});

it('should render children in a record context containing the total', async () => {
render(<WithChildren />);
await screen.findByText('3 comments');
});

it('should accept a render function as children', async () => {
render(<WithRenderFunction />);
await screen.findByText('3 comments');
});

it('should render the offline prop node when offline', async () => {
render(<Offline />);
fireEvent.click(await screen.findByText('Simulate offline'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
QueryClient,
onlineManager,
} from '@tanstack/react-query';
import { RecordContextProvider } from '../record';
import { RecordContextProvider, useRecordContext } from '../record';
import { DataProviderContext } from '../../dataProvider';
import { ResourceContextProvider, useIsOffline } from '../../core';
import { TestMemoryRouter } from '../../routing';
Expand Down Expand Up @@ -65,6 +65,38 @@ export const Basic = () => (
</Wrapper>
);

export const WithChildren = () => (
<Wrapper
dataProvider={{
getManyReference: () =>
Promise.resolve({
data: [comments.filter(c => c.post_id === 1)[0]],
total: comments.filter(c => c.post_id === 1).length,
}),
}}
>
<ReferenceManyCountBase reference="comments" target="post_id">
<Count />
</ReferenceManyCountBase>
</Wrapper>
);

export const WithRenderFunction = () => (
<Wrapper
dataProvider={{
getManyReference: () =>
Promise.resolve({
data: [comments.filter(c => c.post_id === 1)[0]],
total: comments.filter(c => c.post_id === 1).length,
}),
}}
>
<ReferenceManyCountBase reference="comments" target="post_id">
{({ total }) => <span>{total} comments</span>}
</ReferenceManyCountBase>
</Wrapper>
);

export const LoadingState = () => (
<Wrapper dataProvider={{ getManyReference: () => new Promise(() => {}) }}>
<ReferenceManyCountBase
Expand Down Expand Up @@ -199,3 +231,9 @@ const RenderChildOnDemand = ({ children }) => {
</>
);
};

const Count = () => {
const record = useRecordContext();

return <span>{record?.total} comments</span>;
};
45 changes: 35 additions & 10 deletions packages/ra-core/src/controller/field/ReferenceManyCountBase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type UseReferenceManyFieldControllerParams,
} from './useReferenceManyFieldController';
import { useTimeout } from '../../util/hooks';
import { RecordContextProvider } from '../record';

/**
* Fetch and render the number of records related to the current one
Expand All @@ -17,7 +18,14 @@ import { useTimeout } from '../../util/hooks';
* <ReferenceManyCountBase reference="comments" target="post_id" filter={{ is_published: true }} />
*/
export const ReferenceManyCountBase = (props: ReferenceManyCountBaseProps) => {
const { loading, error, offline, timeout = 1000, ...rest } = props;
const {
children,
loading,
error,
offline,
timeout = 1000,
...rest
} = props;
const oneSecondHasPassed = useTimeout(timeout);

const {
Expand All @@ -37,24 +45,41 @@ export const ReferenceManyCountBase = (props: ReferenceManyCountBaseProps) => {
isPending && isPaused && offline !== undefined && offline !== false;
const shouldRenderError =
!isPending && fetchError && error !== undefined && error !== false;
const totalRecord = React.useMemo(() => ({ id: 'count', total }), [total]);
const renderChildren =
typeof children === 'function' ? children(totalRecord) : children;

return (
<>
{shouldRenderLoading
? oneSecondHasPassed
? loading
: null
: shouldRenderOffline
? offline
: shouldRenderError
? error
: total}
{shouldRenderLoading ? (
oneSecondHasPassed ? (
loading
) : null
) : shouldRenderOffline ? (
offline
) : shouldRenderError ? (
error
) : children != null ? (
<RecordContextProvider value={totalRecord}>
{renderChildren}
</RecordContextProvider>
) : (
total
)}
</>
);
};

export interface ReferenceManyCountRecord {
id: string;
total: number | undefined;
}

export interface ReferenceManyCountBaseProps
extends UseReferenceManyFieldControllerParams {
children?:
| React.ReactNode
| ((record: ReferenceManyCountRecord) => React.ReactNode);
timeout?: number;
loading?: React.ReactNode;
error?: React.ReactNode;
Expand Down
15 changes: 15 additions & 0 deletions packages/ra-ui-materialui/src/field/ReferenceManyCount.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { fireEvent, render, screen } from '@testing-library/react';
import {
Basic,
ErrorState,
LinkWithChildren,
Offline,
Themed,
WithChildren,
WithFilter,
Wrapper,
} from './ReferenceManyCount.stories';
Expand All @@ -20,6 +22,19 @@ describe('<ReferenceManyCount />', () => {
render(<Basic />);
await screen.findByText('3');
});

it('should render children in a record context containing the total', async () => {
render(<WithChildren />);
await screen.findByText('30,060');
});

it('should wrap children in a link when link is true', async () => {
render(<LinkWithChildren />);
expect(
await screen.findByRole('link', { name: '30,060' })
).toHaveAttribute('href', '/comments?filter={"post_id":1}');
});

it('should render an error icon when the request fails', async () => {
jest.spyOn(console, 'error').mockImplementation(() => {});
render(<ErrorState />);
Expand Down
33 changes: 33 additions & 0 deletions packages/ra-ui-materialui/src/field/ReferenceManyCount.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { createTheme, ThemeOptions } from '@mui/material';

import { ReferenceManyCount } from './ReferenceManyCount';
import { defaultLightTheme, ThemeProvider, ThemesContext } from '../theme';
import { NumberField } from './NumberField';

export default {
title: 'ra-ui-materialui/fields/ReferenceManyCount',
Expand Down Expand Up @@ -84,6 +85,22 @@ export const Basic = () => (
</Wrapper>
);

export const WithChildren = () => (
<Wrapper
dataProvider={{
getManyReference: () =>
Promise.resolve({
data: [comments.filter(c => c.post_id === 1)[0]],
total: 30060,
}),
}}
>
<ReferenceManyCount reference="comments" target="post_id">
<NumberField source="total" locales="en-US" />
</ReferenceManyCount>
</Wrapper>
);

export const LoadingState = () => (
<Wrapper dataProvider={{ getManyReference: () => new Promise(() => {}) }}>
<ReferenceManyCount reference="comments" target="post_id" />
Expand Down Expand Up @@ -144,6 +161,22 @@ export const Link = () => (
</Wrapper>
);

export const LinkWithChildren = () => (
<Wrapper
dataProvider={{
getManyReference: () =>
Promise.resolve({
data: [comments.filter(c => c.post_id === 1)[0]],
total: 30060,
}),
}}
>
<ReferenceManyCount reference="comments" target="post_id" link>
<NumberField source="total" locales="en-US" />
</ReferenceManyCount>
</Wrapper>
);

export const LinkWithFilter = () => (
<Wrapper
dataProvider={{
Expand Down
7 changes: 5 additions & 2 deletions packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const ReferenceManyCount = <RecordType extends RaRecord = RaRecord>(
resource,
source = 'id',
offline = defaultOffline,
children,
...rest
} = props;
const record = useRecordContext(props);
Expand All @@ -66,7 +67,9 @@ export const ReferenceManyCount = <RecordType extends RaRecord = RaRecord>(
<ErrorIcon color="error" fontSize="small" titleAccess="error" />
}
offline={offline}
/>
>
{children}
</ReferenceManyCountBase>
);
return (
<StyledTypography
Expand Down Expand Up @@ -106,7 +109,7 @@ const defaultOffline = <Offline variant="inline" />;
export interface ReferenceManyCountProps<RecordType extends RaRecord = RaRecord>
extends Omit<FieldProps<RecordType>, 'source'>,
Omit<ReferenceManyCountBaseProps, 'record'>,
Omit<TypographyProps, 'textAlign'> {
Omit<TypographyProps, 'children' | 'textAlign'> {
link?: boolean;
}

Expand Down
Loading