Skip to content

Commit 87ca409

Browse files
committed
Add ReferenceManyCountBase
The current ReferenceManyCount component is tied to MUI. To reuse the same (simple) logic in other UI kits, I moved that logic to `ra-core`. ```jsx <ReferenceManyCountBase reference="comments" target="post_id" /> ```
1 parent f233324 commit 87ca409

File tree

7 files changed

+211
-34
lines changed

7 files changed

+211
-34
lines changed

packages/ra-core/src/controller/field/ReferenceFieldBase.stories.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { useReferenceFieldContext } from './ReferenceFieldContext';
1111
import { DataProvider } from '../../types';
1212

1313
export default {
14-
title: 'ra-core/fields/ReferenceFieldBase',
14+
title: 'ra-core/controller/field/ReferenceFieldBase',
1515
excludeStories: ['dataProviderWithAuthors'],
1616
};
1717

@@ -359,7 +359,7 @@ const MyReferenceField = (props: { children: React.ReactNode }) => {
359359
}
360360

361361
if (context.error) {
362-
return <p style={{ color: 'red' }}>{context.error}</p>;
362+
return <p style={{ color: 'red' }}>{context.error.toString()}</p>;
363363
}
364364
return props.children;
365365
};
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import * as React from 'react';
2+
import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
3+
import { RecordContextProvider } from '../record';
4+
import { DataProviderContext } from '../../dataProvider';
5+
import { ResourceContextProvider } from '../../core';
6+
import { TestMemoryRouter } from '../../routing';
7+
import { ReferenceManyCountBase } from './ReferenceManyCountBase';
8+
9+
export default {
10+
title: 'ra-core/controller/field/ReferenceManyCountBase',
11+
excludeStories: ['Wrapper'],
12+
};
13+
14+
const post = {
15+
id: 1,
16+
title: 'Lorem Ipsum',
17+
};
18+
const comments = [
19+
{ id: 1, post_id: 1, is_published: true },
20+
{ id: 2, post_id: 1, is_published: true },
21+
{ id: 3, post_id: 1, is_published: false },
22+
{ id: 4, post_id: 2, is_published: true },
23+
{ id: 5, post_id: 2, is_published: false },
24+
];
25+
26+
export const Wrapper = ({ dataProvider, children }) => (
27+
<TestMemoryRouter>
28+
<DataProviderContext.Provider value={dataProvider}>
29+
<QueryClientProvider
30+
client={
31+
new QueryClient({
32+
defaultOptions: {
33+
queries: {
34+
retry: false,
35+
},
36+
},
37+
})
38+
}
39+
>
40+
<ResourceContextProvider value="posts">
41+
<RecordContextProvider value={post}>
42+
{children}
43+
</RecordContextProvider>
44+
</ResourceContextProvider>
45+
</QueryClientProvider>
46+
</DataProviderContext.Provider>
47+
</TestMemoryRouter>
48+
);
49+
50+
export const Basic = () => (
51+
<Wrapper
52+
dataProvider={{
53+
getManyReference: () =>
54+
Promise.resolve({
55+
data: [comments.filter(c => c.post_id === 1)[0]],
56+
total: comments.filter(c => c.post_id === 1).length,
57+
}),
58+
}}
59+
>
60+
<ReferenceManyCountBase reference="comments" target="post_id" />
61+
</Wrapper>
62+
);
63+
64+
export const LoadingState = () => (
65+
<Wrapper dataProvider={{ getManyReference: () => new Promise(() => {}) }}>
66+
<ReferenceManyCountBase
67+
reference="comments"
68+
target="post_id"
69+
loading="loading..."
70+
/>
71+
</Wrapper>
72+
);
73+
74+
export const ErrorState = () => (
75+
<Wrapper
76+
dataProvider={{
77+
getManyReference: () => Promise.reject(new Error('problem')),
78+
}}
79+
>
80+
<ReferenceManyCountBase
81+
reference="comments"
82+
target="post_id"
83+
error="Error!"
84+
/>
85+
</Wrapper>
86+
);
87+
88+
export const Filter = () => (
89+
<Wrapper
90+
dataProvider={{
91+
getManyReference: (resource, params) =>
92+
Promise.resolve({
93+
data: comments
94+
.filter(c => c.post_id === 1)
95+
.filter(post =>
96+
Object.keys(params.filter).every(
97+
key => post[key] === params.filter[key]
98+
)
99+
),
100+
total: comments
101+
.filter(c => c.post_id === 1)
102+
.filter(post =>
103+
Object.keys(params.filter).every(
104+
key => post[key] === params.filter[key]
105+
)
106+
).length,
107+
}),
108+
}}
109+
>
110+
<ReferenceManyCountBase
111+
reference="comments"
112+
target="post_id"
113+
filter={{ is_published: true }}
114+
/>
115+
</Wrapper>
116+
);
117+
118+
export const Slow = () => (
119+
<Wrapper
120+
dataProvider={{
121+
getManyReference: () =>
122+
new Promise(resolve =>
123+
setTimeout(
124+
() =>
125+
resolve({
126+
data: [
127+
comments.filter(c => c.post_id === 1)[0],
128+
],
129+
total: comments.filter(c => c.post_id === 1)
130+
.length,
131+
}),
132+
2000
133+
)
134+
),
135+
}}
136+
>
137+
<ReferenceManyCountBase
138+
reference="comments"
139+
target="post_id"
140+
loading="Loading..."
141+
/>
142+
</Wrapper>
143+
);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import React from 'react';
2+
import {
3+
useReferenceManyFieldController,
4+
type UseReferenceManyFieldControllerParams,
5+
} from './useReferenceManyFieldController';
6+
import { useTimeout } from '../../util/hooks';
7+
8+
/**
9+
* Fetch and render the number of records related to the current one
10+
*
11+
* Relies on dataProvider.getManyReference() returning a total property
12+
*
13+
* @example // Display the number of comments for the current post
14+
* <ReferenceManyCountBase reference="comments" target="post_id" />
15+
*
16+
* @example // Display the number of published comments for the current post
17+
* <ReferenceManyCountBase reference="comments" target="post_id" filter={{ is_published: true }} />
18+
*/
19+
export const ReferenceManyCountBase = (props: ReferenceManyCountBaseProps) => {
20+
const { loading = null, error = null, timeout = 1000, ...rest } = props;
21+
const oneSecondHasPassed = useTimeout(timeout);
22+
23+
const {
24+
isPending,
25+
error: fetchError,
26+
total,
27+
} = useReferenceManyFieldController<any, any>({
28+
...rest,
29+
page: 1,
30+
perPage: 1,
31+
});
32+
33+
return (
34+
<>
35+
{isPending
36+
? oneSecondHasPassed
37+
? loading
38+
: null
39+
: fetchError
40+
? error
41+
: total}
42+
</>
43+
);
44+
};
45+
46+
export interface ReferenceManyCountBaseProps
47+
extends UseReferenceManyFieldControllerParams {
48+
timeout?: number;
49+
loading?: React.ReactNode;
50+
error?: React.ReactNode;
51+
}

packages/ra-core/src/controller/field/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export * from './ReferenceFieldBase';
22
export * from './ReferenceFieldContext';
3+
export * from './ReferenceManyCountBase';
34
export * from './useReferenceArrayFieldController';
45
export * from './useReferenceFieldController';
56
export * from './useReferenceManyFieldController';

packages/ra-core/src/controller/field/useReferenceManyFieldController.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import usePaginationState from '../usePaginationState';
1313
import { useRecordSelection } from '../list/useRecordSelection';
1414
import useSortState from '../useSortState';
1515
import { useResourceContext } from '../../core';
16+
import { useRecordContext } from '../record';
1617

1718
/**
1819
* Fetch reference records, and return them when available
@@ -55,7 +56,6 @@ export const useReferenceManyFieldController = <
5556
const {
5657
debounce = 500,
5758
reference,
58-
record,
5959
target,
6060
filter = defaultFilter,
6161
source = 'id',
@@ -68,6 +68,7 @@ export const useReferenceManyFieldController = <
6868
>,
6969
} = props;
7070
const notify = useNotify();
71+
const record = useRecordContext(props);
7172
const resource = useResourceContext(props);
7273
const dataProvider = useDataProvider();
7374
const queryClient = useQueryClient();

packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx

Lines changed: 11 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import React from 'react';
22
import {
3-
useReferenceManyFieldController,
43
useRecordContext,
5-
useTimeout,
64
useCreatePath,
5+
ReferenceManyCountBase,
76
SortPayload,
87
RaRecord,
98
} from 'ra-core';
@@ -15,6 +14,7 @@ import {
1514
useThemeProps,
1615
} from '@mui/material/styles';
1716
import ErrorIcon from '@mui/icons-material/Error';
17+
import get from 'lodash/get';
1818

1919
import { FieldProps } from './types';
2020
import { sanitizeFieldRestProps } from './sanitizeFieldRestProps';
@@ -51,39 +51,20 @@ export const ReferenceManyCount = <RecordType extends RaRecord = RaRecord>(
5151
link,
5252
resource,
5353
source = 'id',
54-
timeout = 1000,
5554
...rest
5655
} = props;
5756
const record = useRecordContext(props);
58-
const oneSecondHasPassed = useTimeout(timeout);
5957
const createPath = useCreatePath();
6058

61-
const { isPending, error, total } =
62-
useReferenceManyFieldController<RecordType>({
63-
filter,
64-
sort,
65-
page: 1,
66-
perPage: 1,
67-
record,
68-
reference,
69-
// @ts-ignore remove when #8491 is released
70-
resource,
71-
source,
72-
target,
73-
});
74-
75-
const body = isPending ? (
76-
oneSecondHasPassed ? (
77-
<CircularProgress size={14} />
78-
) : (
79-
''
80-
)
81-
) : error ? (
82-
<ErrorIcon color="error" fontSize="small" titleAccess="error" />
83-
) : (
84-
total
59+
const body = (
60+
<ReferenceManyCountBase
61+
{...props}
62+
loading={<CircularProgress size={14} />}
63+
error={
64+
<ErrorIcon color="error" fontSize="small" titleAccess="error" />
65+
}
66+
/>
8567
);
86-
8768
return (
8869
<StyledTypography
8970
className={clsx(className, ReferenceManyCountClasses.root)}
@@ -101,7 +82,7 @@ export const ReferenceManyCount = <RecordType extends RaRecord = RaRecord>(
10182
}),
10283
search: `filter=${JSON.stringify({
10384
...(filter || {}),
104-
[target]: record[source],
85+
[target]: get(record, source),
10586
})}`,
10687
}}
10788
onClick={e => e.stopPropagation()}

packages/ra-ui-materialui/src/field/ReferenceManyField.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const ReferenceManyField = <
6969
page = 1,
7070
pagination = null,
7171
perPage = 25,
72+
record,
7273
reference,
7374
resource,
7475
sort = defaultSort,
@@ -77,7 +78,6 @@ export const ReferenceManyField = <
7778
target,
7879
queryOptions,
7980
} = props;
80-
const record = useRecordContext(props);
8181

8282
const controllerProps = useReferenceManyFieldController<
8383
RecordType,

0 commit comments

Comments
 (0)