diff --git a/examples/demo/src/invoices/InvoiceShow.js b/examples/demo/src/invoices/InvoiceShow.js index 62cfea98c72..905d23b700d 100644 --- a/examples/demo/src/invoices/InvoiceShow.js +++ b/examples/demo/src/invoices/InvoiceShow.js @@ -3,7 +3,7 @@ import Card from '@material-ui/core/Card'; import CardContent from '@material-ui/core/CardContent'; import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; -import { ShowController, ReferenceField, TextField } from 'react-admin'; +import { useShowController, ReferenceField, TextField } from 'react-admin'; import Basket from '../orders/Basket'; @@ -17,80 +17,54 @@ const CustomerField = ({ record }) => ( ); -const InvoiceShow = props => ( - - {({ record }) => - record && ( - - - - - - Posters Galore - - - - - Invoice {record.id} - - - - - - - - - - -
 
- - - - Date{' '} - - - {new Date(record.date).toLocaleDateString()} - - +const InvoiceShow = props => { + const { record } = useShowController(props); + if (!record) return null; + return ( + + + + + + Posters Galore + + + + + Invoice {record.id} + + + + + + + + + + +
 
+ + + + Date{' '} + + + {new Date(record.date).toLocaleDateString()} + + - - - Order - - - - - - - - -
+ + + Order + + ( record={record} linkType={false} > - + -
-
-
- ) - } -
-); + + + +
+ + + +
+ + + ); +}; export default InvoiceShow; diff --git a/examples/simple/src/comments/index.js b/examples/simple/src/comments/index.js index 4b5cc430734..cd4bbeeed41 100644 --- a/examples/simple/src/comments/index.js +++ b/examples/simple/src/comments/index.js @@ -3,11 +3,12 @@ import CommentCreate from './CommentCreate'; import CommentEdit from './CommentEdit'; import CommentList from './CommentList'; import CommentShow from './CommentShow'; +import { ShowGuesser } from 'react-admin'; export default { list: CommentList, create: CommentCreate, edit: CommentEdit, - show: CommentShow, + show: ShowGuesser, icon: ChatBubbleIcon, }; diff --git a/examples/simple/src/posts/PostShow.js b/examples/simple/src/posts/PostShow.js index 91df8df1e62..e7724d85bf6 100644 --- a/examples/simple/src/posts/PostShow.js +++ b/examples/simple/src/posts/PostShow.js @@ -1,4 +1,4 @@ -import { ShowController } from 'ra-core'; +import { useShowController } from 'ra-core'; import React from 'react'; import { ArrayField, @@ -36,73 +36,72 @@ const CreateRelatedComment = ({ record }) => ( ); -const PostShow = props => ( - - {controllerProps => ( - }> - - - - - {controllerProps.record && - controllerProps.record.title === - 'Fusce massa lorem, pulvinar a posuere ut, accumsan ac nisi' && ( - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - )} - -); +const PostShow = props => { + const controllerProps = useShowController(props); + return ( + }> + + + + + {controllerProps.record && + controllerProps.record.title === + 'Fusce massa lorem, pulvinar a posuere ut, accumsan ac nisi' && ( + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; export default PostShow; diff --git a/packages/ra-core/src/controller/ShowController.tsx b/packages/ra-core/src/controller/ShowController.tsx index 270f36aa141..ffcd0c462a6 100644 --- a/packages/ra-core/src/controller/ShowController.tsx +++ b/packages/ra-core/src/controller/ShowController.tsx @@ -1,115 +1,35 @@ -import { ReactNode } from 'react'; -import inflection from 'inflection'; -import { useCheckMinimumRequiredProps } from './checkMinimumRequiredProps'; -import { Translate, Record, Identifier } from '../types'; -import useGetOne from '../fetch/useGetOne'; +import useShowController, { + ShowProps, + ShowControllerProps, +} from './useShowController'; +import { Translate } from '../types'; import { useTranslate } from '../i18n'; -import useVersion from './useVersion'; -interface ChildrenFuncParams { - isLoading: boolean; - defaultTitle: string; - resource: string; - basePath: string; - record?: Record; +interface ShowControllerComponentProps extends ShowControllerProps { translate: Translate; - version: number; } -interface Props { - basePath: string; - children: (params: ChildrenFuncParams) => ReactNode; - hasCreate?: boolean; - hasEdit?: boolean; - hasShow?: boolean; - hasList?: boolean; - id: Identifier; - isLoading: boolean; - resource: string; +interface Props extends ShowProps { + children: (params: ShowControllerComponentProps) => JSX.Element; } /** - * Page component for the Show view - * - * The `` component renders the page title and actions, - * fetches the record from the data provider. - * It is not responsible for rendering the actual form - - * that's the job of its child component (usually ``), - * to which it passes pass the `record` as prop. - * - * The `` component accepts the following props: - * - * - title - * - actions - * - * Both expect an element for value. + * Render prop version of the useShowController hook * + * @see useShowController * @example - * // in src/posts.js - * import React from 'react'; - * import { Show, SimpleShowLayout, TextField } from 'react-admin'; * - * export const PostShow = (props) => ( - * - * - * - * - * - * ); - * - * // in src/App.js - * import React from 'react'; - * import { Admin, Resource } from 'react-admin'; - * - * import { PostShow } from './posts'; - * - * const App = () => ( - * - * - * - * ); - * export default App; + * const ShowView = () =>
...
+ * const MyShow = props => ( + * + * {controllerProps => } + * + * ); */ -const ShowController = (props: Props) => { - useCheckMinimumRequiredProps( - 'Show', - ['basePath', 'resource', 'children'], - props - ); - const { basePath, children, id, resource } = props; - const translate = useTranslate(); - const version = useVersion(); - const { data: record, loading } = useGetOne(resource, id, { - basePath, - version, // used to force reload - onFailure: { - notification: { - body: 'ra.notification.item_doesnt_exist', - level: 'warning', - }, - redirectTo: 'list', - refresh: true, - }, - }); - const resourceName = translate(`resources.${resource}.name`, { - smart_count: 1, - _: inflection.humanize(inflection.singularize(resource)), - }); - const defaultTitle = translate('ra.page.show', { - name: `${resourceName}`, - id, - record, - }); - - return children({ - isLoading: loading, - defaultTitle, - resource, - basePath, - record, - translate, - version, - }); +const ShowController = ({ children, ...props }: Props) => { + const controllerProps = useShowController(props); + const translate = useTranslate(); // injected for backwards compatibility + return children({ translate, ...controllerProps }); }; export default ShowController; diff --git a/packages/ra-core/src/controller/index.ts b/packages/ra-core/src/controller/index.ts index 78b0be00bce..4c0f990973c 100644 --- a/packages/ra-core/src/controller/index.ts +++ b/packages/ra-core/src/controller/index.ts @@ -12,6 +12,7 @@ import useSortState from './useSortState'; import usePaginationState from './usePaginationState'; import useListController from './useListController'; import useEditController from './useEditController'; +import useShowController from './useShowController'; import { useCheckMinimumRequiredProps } from './checkMinimumRequiredProps'; export { getListControllerProps, @@ -23,6 +24,7 @@ export { useCheckMinimumRequiredProps, useListController, useEditController, + useShowController, useRecordSelection, useVersion, useSortState, diff --git a/packages/ra-core/src/controller/useShowController.ts b/packages/ra-core/src/controller/useShowController.ts new file mode 100644 index 00000000000..704cae86e92 --- /dev/null +++ b/packages/ra-core/src/controller/useShowController.ts @@ -0,0 +1,85 @@ +import inflection from 'inflection'; + +import useVersion from './useVersion'; +import { useCheckMinimumRequiredProps } from './checkMinimumRequiredProps'; +import { Record, Identifier } from '../types'; +import { useGetOne } from '../fetch'; +import { useTranslate } from '../i18n'; + +export interface ShowProps { + basePath: string; + hasCreate?: boolean; + hasedit?: boolean; + hasShow?: boolean; + hasList?: boolean; + id: Identifier; + resource: string; + undoable?: boolean; + [key: string]: any; +} + +export interface ShowControllerProps { + isLoading: boolean; + defaultTitle: string; + resource: string; + basePath: string; + record?: Record; + version: number; +} + +/** + * Prepare data for the Show view + * + * @param {Object} props The props passed to the Show component. + * + * @return {Object} controllerProps Fetched data and callbacks for the Show view + * + * @example + * + * import { useShowController } from 'react-admin'; + * import ShowView from './ShowView'; + * + * const MyShow = props => { + * const controllerProps = useShowController(props); + * return ; + * } + */ +const useShowController = (props: ShowProps): ShowControllerProps => { + useCheckMinimumRequiredProps('Show', ['basePath', 'resource'], props); + const { basePath, id, resource, undoable = true } = props; + const translate = useTranslate(); + const version = useVersion(); + const { data: record, loading } = useGetOne(resource, id, { + basePath, + version, // used to force reload + onFailure: { + notification: { + body: 'ra.notification.item_doesnt_exist', + level: 'warning', + }, + redirectTo: 'list', + refresh: true, + }, + }); + + const resourceName = translate(`resources.${resource}.name`, { + smart_count: 1, + _: inflection.humanize(inflection.singularize(resource)), + }); + const defaultTitle = translate('ra.page.show', { + name: `${resourceName}`, + id, + record, + }); + + return { + isLoading: loading, + defaultTitle, + resource, + basePath, + record, + version, + }; +}; + +export default useShowController; diff --git a/packages/ra-ui-materialui/src/detail/Show.js b/packages/ra-ui-materialui/src/detail/Show.js index 46c600b6b60..00dcf403b20 100644 --- a/packages/ra-ui-materialui/src/detail/Show.js +++ b/packages/ra-ui-materialui/src/detail/Show.js @@ -1,14 +1,73 @@ import React, { cloneElement, Children } from 'react'; import PropTypes from 'prop-types'; import Card from '@material-ui/core/Card'; -import { withStyles, createStyles } from '@material-ui/core/styles'; +import { makeStyles } from '@material-ui/core/styles'; import classnames from 'classnames'; -import { ShowController } from 'ra-core'; +import { useShowController } from 'ra-core'; import DefaultActions from './ShowActions'; import TitleForRecord from '../layout/TitleForRecord'; -export const styles = createStyles({ +/** + * Page component for the Show view + * + * The `` component renders the page title and actions, + * fetches the record from the data provider. + * It is not responsible for rendering the actual form - + * that's the job of its child component (usually ``), + * to which it passes pass the `record` as prop. + * + * The `` component accepts the following props: + * + * - title + * - actions + * + * Both expect an element for value. + * + * @example + * // in src/posts.js + * import React from 'react'; + * import { Show, SimpleShowLayout, TextField } from 'react-admin'; + * + * export const PostShow = (props) => ( + * + * + * + * + * + * ); + * + * // in src/App.js + * import React from 'react'; + * import { Admin, Resource } from 'react-admin'; + * + * import { PostShow } from './posts'; + * + * const App = () => ( + * + * + * + * ); + * export default App; + */ +const Show = props => ; + +Show.propTypes = { + actions: PropTypes.element, + aside: PropTypes.element, + children: PropTypes.element, + classes: PropTypes.object, + className: PropTypes.string, + hasCreate: PropTypes.bool, + hasEdit: PropTypes.bool, + hasList: PropTypes.bool, + hasShow: PropTypes.bool, + id: PropTypes.any.isRequired, + resource: PropTypes.string.isRequired, + title: PropTypes.node, +}; + +export const useStyles = makeStyles({ root: {}, main: { display: 'flex', @@ -27,7 +86,6 @@ const sanitizeRestProps = ({ title, children, className, - crudGetOne, id, data, isLoading, @@ -36,7 +94,6 @@ const sanitizeRestProps = ({ hasEdit, hasList, hasShow, - translate, version, match, location, @@ -47,76 +104,75 @@ const sanitizeRestProps = ({ ...rest }) => rest; -export const ShowView = withStyles(styles)( - ({ - actions, - aside, - basePath, - children, - classes, - className, - defaultTitle, - hasEdit, - hasList, - isLoading, - record, - resource, - title, - version, - ...rest - }) => { - if (typeof actions === 'undefined' && hasEdit) { - actions = ; - } - if (!children) { - return null; - } - return ( +export const ShowView = ({ + actions, + aside, + basePath, + children, + classes: classesOverride, + className, + defaultTitle, + hasEdit, + hasList, + isLoading, + record, + resource, + title, + version, + ...rest +}) => { + const classes = useStyles({ classes: classesOverride }); + if (typeof actions === 'undefined' && hasEdit) { + actions = ; + } + if (!children) { + return null; + } + return ( +
+ + {actions && + cloneElement(actions, { + basePath, + data: record, + hasList, + hasEdit, + resource, + // Ensure we don't override any user provided props + ...actions.props, + })}
- - {actions && - cloneElement(actions, { - basePath, - data: record, - hasList, - hasEdit, - resource, - // Ensure we don't override any user provided props - ...actions.props, - })} -
- - {record && - cloneElement(Children.only(children), { - resource, - basePath, - record, - version, - })} - - {aside && - cloneElement(aside, { + + {record && + cloneElement(Children.only(children), { resource, basePath, record, version, })} -
+ + {aside && + cloneElement(aside, { + resource, + basePath, + record, + version, + })}
- ); - } -); +
+ ); +}; ShowView.propTypes = { actions: PropTypes.element, @@ -139,67 +195,4 @@ ShowView.defaultProps = { classes: {}, }; -/** - * Page component for the Show view - * - * The `` component renders the page title and actions, - * fetches the record from the data provider. - * It is not responsible for rendering the actual form - - * that's the job of its child component (usually ``), - * to which it passes pass the `record` as prop. - * - * The `` component accepts the following props: - * - * - title - * - actions - * - * Both expect an element for value. - * - * @example - * // in src/posts.js - * import React from 'react'; - * import { Show, SimpleShowLayout, TextField } from 'react-admin'; - * - * export const PostShow = (props) => ( - * - * - * - * - * - * ); - * - * // in src/App.js - * import React from 'react'; - * import { Admin, Resource } from 'react-admin'; - * - * import { PostShow } from './posts'; - * - * const App = () => ( - * - * - * - * ); - * export default App; - */ -const Show = props => ( - - {controllerProps => } - -); - -Show.propTypes = { - actions: PropTypes.element, - aside: PropTypes.element, - children: PropTypes.element, - classes: PropTypes.object, - className: PropTypes.string, - hasCreate: PropTypes.bool, - hasEdit: PropTypes.bool, - hasList: PropTypes.bool, - hasShow: PropTypes.bool, - id: PropTypes.any.isRequired, - resource: PropTypes.string.isRequired, - title: PropTypes.node, -}; - export default Show; diff --git a/packages/ra-ui-materialui/src/detail/ShowGuesser.js b/packages/ra-ui-materialui/src/detail/ShowGuesser.js index 7d2981e9a31..948f39ca515 100644 --- a/packages/ra-ui-materialui/src/detail/ShowGuesser.js +++ b/packages/ra-ui-materialui/src/detail/ShowGuesser.js @@ -1,21 +1,19 @@ -import React, { Component } from 'react'; +import React, { useEffect, useState } from 'react'; import inflection from 'inflection'; -import { withStyles } from '@material-ui/core/styles'; import { - ShowController, + useShowController, InferredElement, getElementsFromRecords, } from 'ra-core'; -import { ShowView, styles } from './Show'; + +import { ShowView } from './Show'; import showFieldTypes from './showFieldTypes'; -export class ShowViewGuesser extends Component { - state = { - inferredChild: null, - }; - componentDidUpdate() { - const { record, resource } = this.props; - if (record && !this.state.inferredChild) { +const ShowViewGuesser = props => { + const { record, resource } = props; + const [inferredChild, setInferredChild] = useState(null); + useEffect(() => { + if (record && !inferredChild) { const inferredElements = getElementsFromRecords( [record], showFieldTypes @@ -39,21 +37,17 @@ ${inferredChild.getRepresentation()} );` ); - this.setState({ inferredChild: inferredChild.getElement() }); + setInferredChild(inferredChild.getElement()); } - } + }, [record, inferredChild, resource]); - render() { - return {this.state.inferredChild}; - } -} + return {inferredChild}; +}; ShowViewGuesser.propTypes = ShowView.propTypes; const ShowGuesser = props => ( - - {controllerProps => } - + ); -export default withStyles(styles)(ShowGuesser); +export default ShowGuesser;