Skip to content

Commit e60a58e

Browse files
authoredJan 5, 2023
Merge pull request #8543 from marmelab/tab-count
Add ability to pass a tab count in `<TabbedForm.Tab>` and `<TabbedShowLayout.Tab>`
2 parents cc7645e + fe32f3c commit e60a58e

File tree

12 files changed

+141
-84
lines changed

12 files changed

+141
-84
lines changed
 

‎docs/TabbedForm.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -471,10 +471,11 @@ export const TagEdit = () => (
471471

472472
## `<TabbedForm.Tab>`
473473

474-
`<TabbedForm>` expect `<TabbedForm.Tab>` elements as children. `<TabbedForm.Tab>` elements accept four props:
474+
`<TabbedForm>` expect `<TabbedForm.Tab>` elements as children. `<TabbedForm.Tab>` elements accept five props:
475475

476476
- `label`: the label of the tab
477477
- `path`: the path of the tab in the URL (ignored when `syncWithLocation={false}`)
478+
- `count`: the number of items in the tab (dislayed close to the label)
478479
- `sx`: custom styles to apply to the tab
479480
- `children`: the content of the tab (usually a list of inputs)
480481

‎docs/TabbedShowLayout.md

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ It accepts the following props:
7676
- `label`: The string displayed for each tab
7777
- `icon`: The icon to show before the label (optional). Must be a component.
7878
- `path`: The string used for custom urls (optional)
79+
- `count`: the number of items in the tab (dislayed close to the label)
7980

8081
```jsx
8182
// in src/posts.js

‎examples/demo/src/orders/OrderList.tsx

+17-34
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,23 @@ import { Fragment, useCallback } from 'react';
33
import {
44
AutocompleteInput,
55
BooleanField,
6+
Count,
67
DatagridConfigurable,
78
DateField,
89
DateInput,
10+
ExportButton,
11+
FilterButton,
912
List,
1013
NullableBooleanInput,
1114
NumberField,
12-
ReferenceInput,
1315
ReferenceField,
16+
ReferenceInput,
1417
SearchInput,
18+
SelectColumnsButton,
1519
TextField,
1620
TextInput,
17-
useGetList,
18-
useListContext,
1921
TopToolbar,
20-
SelectColumnsButton,
21-
FilterButton,
22-
ExportButton,
22+
useListContext,
2323
} from 'react-admin';
2424
import { useMediaQuery, Divider, Tabs, Tab, Theme } from '@mui/material';
2525

@@ -72,37 +72,12 @@ const tabs = [
7272
{ id: 'cancelled', name: 'cancelled' },
7373
];
7474

75-
const useGetTotals = (filterValues: any) => {
76-
const { total: totalOrdered } = useGetList('commands', {
77-
pagination: { perPage: 1, page: 1 },
78-
sort: { field: 'id', order: 'ASC' },
79-
filter: { ...filterValues, status: 'ordered' },
80-
});
81-
const { total: totalDelivered } = useGetList('commands', {
82-
pagination: { perPage: 1, page: 1 },
83-
sort: { field: 'id', order: 'ASC' },
84-
filter: { ...filterValues, status: 'delivered' },
85-
});
86-
const { total: totalCancelled } = useGetList('commands', {
87-
pagination: { perPage: 1, page: 1 },
88-
sort: { field: 'id', order: 'ASC' },
89-
filter: { ...filterValues, status: 'cancelled' },
90-
});
91-
92-
return {
93-
ordered: totalOrdered,
94-
delivered: totalDelivered,
95-
cancelled: totalCancelled,
96-
};
97-
};
98-
9975
const TabbedDatagrid = () => {
10076
const listContext = useListContext();
10177
const { filterValues, setFilters, displayedFilters } = listContext;
10278
const isXSmall = useMediaQuery<Theme>(theme =>
10379
theme.breakpoints.down('sm')
10480
);
105-
const totals = useGetTotals(filterValues) as any;
10681

10782
const handleChange = useCallback(
10883
(event: React.ChangeEvent<{}>, value: any) => {
@@ -129,9 +104,17 @@ const TabbedDatagrid = () => {
129104
<Tab
130105
key={choice.id}
131106
label={
132-
totals[choice.name]
133-
? `${choice.name} (${totals[choice.name]})`
134-
: choice.name
107+
<span>
108+
{choice.name} (
109+
<Count
110+
filter={{
111+
...filterValues,
112+
status: choice.name,
113+
}}
114+
sx={{ lineHeight: 'inherit' }}
115+
/>
116+
)
117+
</span>
135118
}
136119
value={choice.id}
137120
/>

‎examples/demo/src/products/ProductEdit.tsx

+13-26
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ import {
66
EditButton,
77
Pagination,
88
ReferenceManyField,
9+
ReferenceManyCount,
910
required,
1011
TabbedForm,
1112
TextField,
1213
TextInput,
1314
useRecordContext,
14-
useGetManyReference,
15-
useTranslate,
1615
} from 'react-admin';
1716
import { RichTextInput } from 'ra-input-rich-text';
1817

@@ -52,7 +51,17 @@ const ProductEdit = () => (
5251
>
5352
<RichTextInput source="description" label="" validate={req} />
5453
</TabbedForm.Tab>
55-
<ReviewsFormTab path="reviews">
54+
<TabbedForm.Tab
55+
label="resources.products.tabs.reviews"
56+
count={
57+
<ReferenceManyCount
58+
reference="reviews"
59+
target="product_id"
60+
sx={{ lineHeight: 'inherit' }}
61+
/>
62+
}
63+
path="reviews"
64+
>
5665
<ReferenceManyField
5766
reference="reviews"
5867
target="product_id"
@@ -77,33 +86,11 @@ const ProductEdit = () => (
7786
<EditButton />
7887
</Datagrid>
7988
</ReferenceManyField>
80-
</ReviewsFormTab>
89+
</TabbedForm.Tab>
8190
</TabbedForm>
8291
</Edit>
8392
);
8493

8594
const req = [required()];
8695

87-
const ReviewsFormTab = (props: any) => {
88-
const record = useRecordContext();
89-
const { isLoading, total } = useGetManyReference(
90-
'reviews',
91-
{
92-
target: 'product_id',
93-
id: record.id,
94-
pagination: { page: 1, perPage: 25 },
95-
sort: { field: 'id', order: 'DESC' },
96-
},
97-
{
98-
enabled: !!record,
99-
}
100-
);
101-
const translate = useTranslate();
102-
let label = translate('resources.products.tabs.reviews');
103-
if (!isLoading) {
104-
label += ` (${total})`;
105-
}
106-
return <TabbedForm.Tab label={label} {...props} />;
107-
};
108-
10996
export default ProductEdit;

‎examples/simple/src/posts/PostEdit.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
ImageInput,
1919
NumberInput,
2020
ReferenceManyField,
21+
ReferenceManyCount,
2122
ReferenceInput,
2223
SelectInput,
2324
SimpleFormIterator,
@@ -239,7 +240,16 @@ const PostEdit = () => {
239240
</SimpleFormIterator>
240241
</ArrayInput>
241242
</TabbedForm.Tab>
242-
<TabbedForm.Tab label="post.form.comments">
243+
<TabbedForm.Tab
244+
label="post.form.comments"
245+
count={
246+
<ReferenceManyCount
247+
reference="comments"
248+
target="post_id"
249+
sx={{ lineHeight: 'inherit' }}
250+
/>
251+
}
252+
>
243253
<ReferenceManyField
244254
reference="comments"
245255
target="post_id"

‎examples/simple/src/posts/PostShow.tsx

+11-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
NumberField,
1111
ReferenceArrayField,
1212
ReferenceManyField,
13+
ReferenceManyCount,
1314
RichTextField,
1415
SelectField,
1516
ShowContextProvider,
@@ -87,7 +88,16 @@ const PostShow = () => {
8788
<TextField source="views" />
8889
<CloneButton />
8990
</TabbedShowLayout.Tab>
90-
<TabbedShowLayout.Tab label="post.form.comments">
91+
<TabbedShowLayout.Tab
92+
label="post.form.comments"
93+
count={
94+
<ReferenceManyCount
95+
reference="comments"
96+
target="post_id"
97+
sx={{ lineHeight: 'inherit' }}
98+
/>
99+
}
100+
>
91101
<ReferenceManyField
92102
reference="comments"
93103
target="post_id"

‎packages/ra-ui-materialui/src/detail/Tab.tsx

+27-16
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export const Tab = ({
5858
children,
5959
contentClassName,
6060
context,
61+
count,
6162
className,
6263
divider,
6364
icon,
@@ -75,21 +76,29 @@ export const Tab = ({
7576
to: { ...location, pathname: value },
7677
};
7778

78-
const renderHeader = () => (
79-
<MuiTab
80-
key={`tab-header-${value}`}
81-
label={
82-
typeof label === 'string'
83-
? translate(label, { _: label })
84-
: label
85-
}
86-
value={value}
87-
icon={icon}
88-
className={clsx('show-tab', className)}
89-
{...(syncWithLocation ? propsForLink : {})} // to avoid TypeScript screams, see https://github.com/mui-org/material-ui/issues/9106#issuecomment-451270521
90-
{...rest}
91-
/>
92-
);
79+
const renderHeader = () => {
80+
let tabLabel =
81+
typeof label === 'string' ? translate(label, { _: label }) : label;
82+
if (count !== undefined) {
83+
tabLabel = (
84+
<span>
85+
{tabLabel} ({count})
86+
</span>
87+
);
88+
}
89+
90+
return (
91+
<MuiTab
92+
key={`tab-header-${value}`}
93+
label={tabLabel}
94+
value={value}
95+
icon={icon}
96+
className={clsx('show-tab', className)}
97+
{...(syncWithLocation ? propsForLink : {})} // to avoid TypeScript screams, see https://github.com/mui-org/material-ui/issues/9106#issuecomment-451270521
98+
{...rest}
99+
/>
100+
);
101+
};
93102

94103
const renderContent = () => (
95104
<Stack className={contentClassName} spacing={spacing} divider={divider}>
@@ -115,10 +124,11 @@ export const Tab = ({
115124
};
116125

117126
Tab.propTypes = {
127+
children: PropTypes.node,
118128
className: PropTypes.string,
119129
contentClassName: PropTypes.string,
120-
children: PropTypes.node,
121130
context: PropTypes.oneOf(['header', 'content']),
131+
count: PropTypes.node,
122132
icon: PropTypes.element,
123133
label: PropTypes.oneOfType([PropTypes.string, PropTypes.element])
124134
.isRequired,
@@ -131,6 +141,7 @@ export interface TabProps extends Omit<MuiTabProps, 'children'> {
131141
children: ReactNode;
132142
contentClassName?: string;
133143
context?: 'header' | 'content';
144+
count?: ReactNode;
134145
className?: string;
135146
divider?: ReactNode;
136147
icon?: ReactElement;

‎packages/ra-ui-materialui/src/detail/TabbedShowLayout.stories.tsx

+23
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,29 @@ export const Basic = () => (
4242
</MemoryRouter>
4343
);
4444

45+
export const Count = () => (
46+
<MemoryRouter>
47+
<ResourceContext.Provider value="books">
48+
<RecordContextProvider value={record}>
49+
<TabbedShowLayout>
50+
<TabbedShowLayout.Tab label="Main">
51+
<TextField source="id" />
52+
<TextField source="title" />
53+
</TabbedShowLayout.Tab>
54+
<TabbedShowLayout.Tab label="Details">
55+
<TextField source="author" />
56+
<TextField source="summary" />
57+
<NumberField source="year" />
58+
</TabbedShowLayout.Tab>
59+
<TabbedShowLayout.Tab label="Reviews" count={27}>
60+
<TextField source="reviews" />
61+
</TabbedShowLayout.Tab>
62+
</TabbedShowLayout>
63+
</RecordContextProvider>
64+
</ResourceContext.Provider>
65+
</MemoryRouter>
66+
);
67+
4568
const BookTitle = () => {
4669
const record = useRecordContext();
4770
return record ? <span>{record.title}</span> : null;

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

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export const ReferenceManyCount = (props: ReferenceManyCountProps) => {
7676
})}`,
7777
}}
7878
variant="body2"
79+
component="span"
7980
onClick={e => e.stopPropagation()}
8081
{...rest}
8182
>

‎packages/ra-ui-materialui/src/form/FormTab.tsx

+5-1
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import { FormTabHeader } from './FormTabHeader';
88

99
export const FormTab = (props: FormTabProps) => {
1010
const {
11+
children,
1112
className,
1213
contentClassName,
13-
children,
14+
count,
1415
hidden,
1516
icon,
1617
intent,
@@ -26,6 +27,7 @@ export const FormTab = (props: FormTabProps) => {
2627
const renderHeader = () => (
2728
<FormTabHeader
2829
label={label}
30+
count={count}
2931
value={value}
3032
icon={icon}
3133
className={className}
@@ -60,6 +62,7 @@ FormTab.propTypes = {
6062
className: PropTypes.string,
6163
contentClassName: PropTypes.string,
6264
children: PropTypes.node,
65+
count: PropTypes.node,
6366
intent: PropTypes.oneOf(['header', 'content']),
6467
hidden: PropTypes.bool,
6568
icon: PropTypes.element,
@@ -77,6 +80,7 @@ export interface FormTabProps
7780
className?: string;
7881
children?: ReactNode;
7982
contentClassName?: string;
83+
count?: ReactNode;
8084
hidden?: boolean;
8185
icon?: ReactElement;
8286
intent?: 'header' | 'content';

‎packages/ra-ui-materialui/src/form/FormTabHeader.tsx

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as React from 'react';
2-
import { isValidElement, ReactElement } from 'react';
2+
import { isValidElement, ReactElement, ReactNode } from 'react';
33
import PropTypes from 'prop-types';
44
import { Link, useLocation } from 'react-router-dom';
55
import { Tab as MuiTab, TabProps as MuiTabProps } from '@mui/material';
@@ -10,6 +10,7 @@ import { useFormState } from 'react-hook-form';
1010
import { TabbedFormClasses } from './TabbedFormView';
1111

1212
export const FormTabHeader = ({
13+
count,
1314
label,
1415
value,
1516
icon,
@@ -28,11 +29,19 @@ export const FormTabHeader = ({
2829
to: { ...location, pathname: value },
2930
};
3031

32+
let tabLabel = isValidElement(label)
33+
? label
34+
: translate(label, { _: label });
35+
if (count !== undefined) {
36+
tabLabel = (
37+
<span>
38+
{tabLabel} ({count})
39+
</span>
40+
);
41+
}
3142
return (
3243
<MuiTab
33-
label={
34-
isValidElement(label) ? label : translate(label, { _: label })
35-
}
44+
label={tabLabel}
3645
value={value}
3746
icon={icon}
3847
className={clsx('form-tab', className, {
@@ -52,6 +61,7 @@ export const FormTabHeader = ({
5261

5362
interface FormTabHeaderProps extends Omit<MuiTabProps, 'children'> {
5463
className?: string;
64+
count?: ReactNode;
5565
hidden?: boolean;
5666
icon?: ReactElement;
5767
intent?: 'header' | 'content';
@@ -68,6 +78,7 @@ interface FormTabHeaderProps extends Omit<MuiTabProps, 'children'> {
6878
FormTabHeader.propTypes = {
6979
className: PropTypes.string,
7080
contentClassName: PropTypes.string,
81+
count: PropTypes.node,
7182
children: PropTypes.node,
7283
intent: PropTypes.oneOf(['header', 'content']),
7384
hidden: PropTypes.bool,

‎packages/ra-ui-materialui/src/form/TabbedForm.stories.tsx

+15
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,18 @@ export const NoToolbar = () => (
8989
</TabbedForm>
9090
</Wrapper>
9191
);
92+
93+
export const Count = () => (
94+
<Wrapper>
95+
<TabbedForm>
96+
<TabbedForm.Tab label="main">
97+
<TextInput source="title" fullWidth />
98+
<TextInput source="author" />
99+
<NumberInput source="year" />
100+
</TabbedForm.Tab>
101+
<TabbedForm.Tab label="comments" count={3}>
102+
<TextInput multiline source="bio" fullWidth />
103+
</TabbedForm.Tab>
104+
</TabbedForm>
105+
</Wrapper>
106+
);

0 commit comments

Comments
 (0)
Please sign in to comment.