diff --git a/docs/TabbedForm.md b/docs/TabbedForm.md index 2e199a124e5..fc0ab6947eb 100644 --- a/docs/TabbedForm.md +++ b/docs/TabbedForm.md @@ -471,10 +471,11 @@ export const TagEdit = () => ( ## `` -`` expect `` elements as children. `` elements accept four props: +`` expect `` elements as children. `` elements accept five props: - `label`: the label of the tab - `path`: the path of the tab in the URL (ignored when `syncWithLocation={false}`) +- `count`: the number of items in the tab (dislayed close to the label) - `sx`: custom styles to apply to the tab - `children`: the content of the tab (usually a list of inputs) diff --git a/docs/TabbedShowLayout.md b/docs/TabbedShowLayout.md index 0922456cfe0..1b8b0a1410b 100644 --- a/docs/TabbedShowLayout.md +++ b/docs/TabbedShowLayout.md @@ -76,6 +76,7 @@ It accepts the following props: - `label`: The string displayed for each tab - `icon`: The icon to show before the label (optional). Must be a component. - `path`: The string used for custom urls (optional) +- `count`: the number of items in the tab (dislayed close to the label) ```jsx // in src/posts.js diff --git a/examples/demo/src/orders/OrderList.tsx b/examples/demo/src/orders/OrderList.tsx index 1c8aced907a..3ead7ce59e8 100644 --- a/examples/demo/src/orders/OrderList.tsx +++ b/examples/demo/src/orders/OrderList.tsx @@ -3,23 +3,23 @@ import { Fragment, useCallback } from 'react'; import { AutocompleteInput, BooleanField, + Count, DatagridConfigurable, DateField, DateInput, + ExportButton, + FilterButton, List, NullableBooleanInput, NumberField, - ReferenceInput, ReferenceField, + ReferenceInput, SearchInput, + SelectColumnsButton, TextField, TextInput, - useGetList, - useListContext, TopToolbar, - SelectColumnsButton, - FilterButton, - ExportButton, + useListContext, } from 'react-admin'; import { useMediaQuery, Divider, Tabs, Tab, Theme } from '@mui/material'; @@ -72,37 +72,12 @@ const tabs = [ { id: 'cancelled', name: 'cancelled' }, ]; -const useGetTotals = (filterValues: any) => { - const { total: totalOrdered } = useGetList('commands', { - pagination: { perPage: 1, page: 1 }, - sort: { field: 'id', order: 'ASC' }, - filter: { ...filterValues, status: 'ordered' }, - }); - const { total: totalDelivered } = useGetList('commands', { - pagination: { perPage: 1, page: 1 }, - sort: { field: 'id', order: 'ASC' }, - filter: { ...filterValues, status: 'delivered' }, - }); - const { total: totalCancelled } = useGetList('commands', { - pagination: { perPage: 1, page: 1 }, - sort: { field: 'id', order: 'ASC' }, - filter: { ...filterValues, status: 'cancelled' }, - }); - - return { - ordered: totalOrdered, - delivered: totalDelivered, - cancelled: totalCancelled, - }; -}; - const TabbedDatagrid = () => { const listContext = useListContext(); const { filterValues, setFilters, displayedFilters } = listContext; const isXSmall = useMediaQuery(theme => theme.breakpoints.down('sm') ); - const totals = useGetTotals(filterValues) as any; const handleChange = useCallback( (event: React.ChangeEvent<{}>, value: any) => { @@ -129,9 +104,17 @@ const TabbedDatagrid = () => { + {choice.name} ( + + ) + } value={choice.id} /> diff --git a/examples/demo/src/products/ProductEdit.tsx b/examples/demo/src/products/ProductEdit.tsx index fa161df6790..c35da562fa7 100644 --- a/examples/demo/src/products/ProductEdit.tsx +++ b/examples/demo/src/products/ProductEdit.tsx @@ -6,13 +6,12 @@ import { EditButton, Pagination, ReferenceManyField, + ReferenceManyCount, required, TabbedForm, TextField, TextInput, useRecordContext, - useGetManyReference, - useTranslate, } from 'react-admin'; import { RichTextInput } from 'ra-input-rich-text'; @@ -52,7 +51,17 @@ const ProductEdit = () => ( > - + + } + path="reviews" + > ( - + ); const req = [required()]; -const ReviewsFormTab = (props: any) => { - const record = useRecordContext(); - const { isLoading, total } = useGetManyReference( - 'reviews', - { - target: 'product_id', - id: record.id, - pagination: { page: 1, perPage: 25 }, - sort: { field: 'id', order: 'DESC' }, - }, - { - enabled: !!record, - } - ); - const translate = useTranslate(); - let label = translate('resources.products.tabs.reviews'); - if (!isLoading) { - label += ` (${total})`; - } - return ; -}; - export default ProductEdit; diff --git a/examples/simple/src/posts/PostEdit.tsx b/examples/simple/src/posts/PostEdit.tsx index 3f920215c3f..16af0bffb17 100644 --- a/examples/simple/src/posts/PostEdit.tsx +++ b/examples/simple/src/posts/PostEdit.tsx @@ -18,6 +18,7 @@ import { ImageInput, NumberInput, ReferenceManyField, + ReferenceManyCount, ReferenceInput, SelectInput, SimpleFormIterator, @@ -239,7 +240,16 @@ const PostEdit = () => { - + + } + > { - + + } + > ( - - ); + const renderHeader = () => { + let tabLabel = + typeof label === 'string' ? translate(label, { _: label }) : label; + if (count !== undefined) { + tabLabel = ( + + {tabLabel} ({count}) + + ); + } + + return ( + + ); + }; const renderContent = () => ( @@ -115,10 +124,11 @@ export const Tab = ({ }; Tab.propTypes = { + children: PropTypes.node, className: PropTypes.string, contentClassName: PropTypes.string, - children: PropTypes.node, context: PropTypes.oneOf(['header', 'content']), + count: PropTypes.node, icon: PropTypes.element, label: PropTypes.oneOfType([PropTypes.string, PropTypes.element]) .isRequired, @@ -131,6 +141,7 @@ export interface TabProps extends Omit { children: ReactNode; contentClassName?: string; context?: 'header' | 'content'; + count?: ReactNode; className?: string; divider?: ReactNode; icon?: ReactElement; diff --git a/packages/ra-ui-materialui/src/detail/TabbedShowLayout.stories.tsx b/packages/ra-ui-materialui/src/detail/TabbedShowLayout.stories.tsx index 518f54eaddf..bcef7257a6c 100644 --- a/packages/ra-ui-materialui/src/detail/TabbedShowLayout.stories.tsx +++ b/packages/ra-ui-materialui/src/detail/TabbedShowLayout.stories.tsx @@ -42,6 +42,29 @@ export const Basic = () => ( ); +export const Count = () => ( + + + + + + + + + + + + + + + + + + + + +); + const BookTitle = () => { const record = useRecordContext(); return record ? {record.title} : null; diff --git a/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx b/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx index 4eb16c91114..f05a2c78198 100644 --- a/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx +++ b/packages/ra-ui-materialui/src/field/ReferenceManyCount.tsx @@ -76,6 +76,7 @@ export const ReferenceManyCount = (props: ReferenceManyCountProps) => { })}`, }} variant="body2" + component="span" onClick={e => e.stopPropagation()} {...rest} > diff --git a/packages/ra-ui-materialui/src/form/FormTab.tsx b/packages/ra-ui-materialui/src/form/FormTab.tsx index 68d0c811a18..5c9e2c7f80d 100644 --- a/packages/ra-ui-materialui/src/form/FormTab.tsx +++ b/packages/ra-ui-materialui/src/form/FormTab.tsx @@ -8,9 +8,10 @@ import { FormTabHeader } from './FormTabHeader'; export const FormTab = (props: FormTabProps) => { const { + children, className, contentClassName, - children, + count, hidden, icon, intent, @@ -26,6 +27,7 @@ export const FormTab = (props: FormTabProps) => { const renderHeader = () => ( + {tabLabel} ({count}) + + ); + } return ( { className?: string; + count?: ReactNode; hidden?: boolean; icon?: ReactElement; intent?: 'header' | 'content'; @@ -68,6 +78,7 @@ interface FormTabHeaderProps extends Omit { FormTabHeader.propTypes = { className: PropTypes.string, contentClassName: PropTypes.string, + count: PropTypes.node, children: PropTypes.node, intent: PropTypes.oneOf(['header', 'content']), hidden: PropTypes.bool, diff --git a/packages/ra-ui-materialui/src/form/TabbedForm.stories.tsx b/packages/ra-ui-materialui/src/form/TabbedForm.stories.tsx index 389b3fac798..95085cb09c9 100644 --- a/packages/ra-ui-materialui/src/form/TabbedForm.stories.tsx +++ b/packages/ra-ui-materialui/src/form/TabbedForm.stories.tsx @@ -89,3 +89,18 @@ export const NoToolbar = () => ( ); + +export const Count = () => ( + + + + + + + + + + + + +);