diff --git a/docs/Create.md b/docs/Create.md index a5da1680914..c49ed6665a1 100644 --- a/docs/Create.md +++ b/docs/Create.md @@ -424,31 +424,24 @@ You can do the same for error notifications, by passing a custom `onError` call ## Prefilling the Form -You sometimes need to pre-populate a record based on a *related* record. For instance, to create a comment related to an existing post. +You sometimes need to pre-populate a record based on a *related* record. For instance, to create a comment related to an existing post. By default, the `` view starts with an empty `record`. However, if the `location` object (injected by [react-router-dom](https://reacttraining.com/react-router/web/api/location)) contains a `record` in its `state`, the `` view uses that `record` instead of the empty object. That's how the `` works under the hood. -That means that if you want to create a link to a creation form, presetting *some* values, all you have to do is to set the location `state`. `react-router-dom` provides the `` component for that: +That means that if you want to create a link to a creation form, presetting *some* values, all you have to do is to set the `state` prop of the ``: {% raw %} ```jsx import * as React from 'react'; -import { Datagrid, useRecordContext } from 'react-admin'; -import { Button } from '@mui/material'; -import { Link } from 'react-router-dom'; +import { CreateButton, Datagrid, List, useRecordContext } from 'react-admin'; const CreateRelatedCommentButton = () => { const record = useRecordContext(); return ( - + /> ); }; @@ -463,29 +456,22 @@ export default PostList = () => ( ``` {% endraw %} -**Tip**: To style the button with the main color from the Material UI theme, use the `Link` component from the `react-admin` package rather than the one from `react-router-dom`. - **Tip**: The `` component also watches the "source" parameter of `location.search` (the query string in the URL) in addition to `location.state` (a cross-page message hidden in the router memory). So the `CreateRelatedCommentButton` could also be written as: {% raw %} ```jsx import * as React from 'react'; -import { useRecordContext } from 'react-admin'; -import Button from '@mui/material/Button'; -import { Link } from 'react-router-dom'; +import { CreateButton, useRecordContext } from 'react-admin'; const CreateRelatedCommentButton = () => { const record = useRecordContext(); return ( - + /> ); }; ``` diff --git a/docs/CreateInDialogButton.md b/docs/CreateInDialogButton.md index 136bcf771f0..965dd13b541 100644 --- a/docs/CreateInDialogButton.md +++ b/docs/CreateInDialogButton.md @@ -41,15 +41,15 @@ const CompanyShow = () => ( - ( - - - - - - - )} /> + ( + + + + + + + )} /> @@ -188,6 +188,3 @@ const EmployerEdit = () => ( ); ``` {% endraw %} - - - diff --git a/examples/demo/src/products/CreateRelatedReviewButton.tsx b/examples/demo/src/products/CreateRelatedReviewButton.tsx new file mode 100644 index 00000000000..1a2d00c98f4 --- /dev/null +++ b/examples/demo/src/products/CreateRelatedReviewButton.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { CreateButton, useRecordContext } from 'react-admin'; + +const CreateRelatedReviewButton = () => { + const record = useRecordContext(); + + return ( + + ); +}; + +export default CreateRelatedReviewButton; diff --git a/examples/demo/src/products/ProductEdit.tsx b/examples/demo/src/products/ProductEdit.tsx index c35da562fa7..56579b1fa74 100644 --- a/examples/demo/src/products/ProductEdit.tsx +++ b/examples/demo/src/products/ProductEdit.tsx @@ -20,6 +20,7 @@ import CustomerReferenceField from '../visitors/CustomerReferenceField'; import StarRatingField from '../reviews/StarRatingField'; import Poster from './Poster'; import { Product } from '../types'; +import CreateRelatedReviewButton from './CreateRelatedReviewButton'; const ProductTitle = () => { const record = useRecordContext(); @@ -85,6 +86,7 @@ const ProductEdit = () => ( + diff --git a/examples/demo/src/reviews/ReviewCreate.tsx b/examples/demo/src/reviews/ReviewCreate.tsx new file mode 100644 index 00000000000..7458fc38d8e --- /dev/null +++ b/examples/demo/src/reviews/ReviewCreate.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { + SimpleForm, + Create, + ReferenceInput, + TextInput, + DateInput, + AutocompleteInput, + required, + useNotify, + useRedirect, + getRecordFromLocation, +} from 'react-admin'; +import { useLocation } from 'react-router'; + +import StarRatingInput from './StarRatingInput'; + +const ReviewCreate = () => { + const notify = useNotify(); + const redirect = useRedirect(); + const location = useLocation(); + + const onSuccess = (_: any) => { + const record = getRecordFromLocation(location); + notify('ra.notification.created'); + if (record && record.product_id) { + redirect(`/products/${record.product_id}/reviews`); + } else { + redirect(`/reviews`); + } + }; + + return ( + + + + + + + + + + + + + + ); +}; + +export default ReviewCreate; diff --git a/examples/demo/src/reviews/StarRatingInput.tsx b/examples/demo/src/reviews/StarRatingInput.tsx new file mode 100644 index 00000000000..1dfbd34c4bc --- /dev/null +++ b/examples/demo/src/reviews/StarRatingInput.tsx @@ -0,0 +1,31 @@ +import { Box, Rating, Typography, styled } from '@mui/material'; +import Icon from '@mui/icons-material/Stars'; +import { InputProps, useInput, useTranslate } from 'react-admin'; + +const StarRatingInput = (props: InputProps) => { + const { name = 'resources.reviews.fields.rating' } = props; + const { field } = useInput(props); + const translate = useTranslate(); + + return ( + + {translate(name)} + } + onChange={(event, value) => field.onChange(value)} + /> + + ); +}; + +const StyledRating = styled(Rating)({ + '& .MuiRating-iconFilled': { + color: '#000', + }, + '& .MuiRating-iconHover': { + color: '#000', + }, +}); + +export default StarRatingInput; diff --git a/examples/demo/src/reviews/index.ts b/examples/demo/src/reviews/index.ts index 6f6abed029c..810cb14977f 100644 --- a/examples/demo/src/reviews/index.ts +++ b/examples/demo/src/reviews/index.ts @@ -1,7 +1,9 @@ import ReviewIcon from '@mui/icons-material/Comment'; import ReviewList from './ReviewList'; +import ReviewCreate from './ReviewCreate'; export default { icon: ReviewIcon, list: ReviewList, + create: ReviewCreate, }; diff --git a/examples/demo/src/visitors/index.ts b/examples/demo/src/visitors/index.ts index 510d038769a..5345f0ee500 100644 --- a/examples/demo/src/visitors/index.ts +++ b/examples/demo/src/visitors/index.ts @@ -9,6 +9,8 @@ const resource = { create: VisitorCreate, edit: VisitorEdit, icon: VisitorIcon, + recordRepresentation: (record: any) => + `${record.first_name} ${record.last_name}`, }; export default resource; diff --git a/packages/ra-ui-materialui/src/auth/AuthError.tsx b/packages/ra-ui-materialui/src/auth/AuthError.tsx index fcc9839bdd9..f3a19ff2c28 100644 --- a/packages/ra-ui-materialui/src/auth/AuthError.tsx +++ b/packages/ra-ui-materialui/src/auth/AuthError.tsx @@ -4,6 +4,7 @@ import LockIcon from '@mui/icons-material/Lock'; import PropTypes from 'prop-types'; import { useTranslate } from 'ra-core'; import { Button } from '../button'; +import { Link } from 'react-router-dom'; export const AuthError = (props: AuthErrorProps) => { const { @@ -19,7 +20,7 @@ export const AuthError = (props: AuthErrorProps) => {

{translate(title, { _: title })}

{translate(message, { _: message })}
-
diff --git a/packages/ra-ui-materialui/src/button/Button.tsx b/packages/ra-ui-materialui/src/button/Button.tsx index 4f60a5d8c71..eb88caa560f 100644 --- a/packages/ra-ui-materialui/src/button/Button.tsx +++ b/packages/ra-ui-materialui/src/button/Button.tsx @@ -1,6 +1,4 @@ import * as React from 'react'; -import { ReactElement, ElementType } from 'react'; -import PropTypes from 'prop-types'; import { Button as MuiButton, ButtonProps as MuiButtonProps, @@ -10,6 +8,8 @@ import { Theme, } from '@mui/material'; import { styled } from '@mui/material/styles'; +import { To } from 'history'; +import PropTypes from 'prop-types'; import { useTranslate } from 'ra-core'; import { Path } from 'react-router'; @@ -26,7 +26,9 @@ import { Path } from 'react-router'; * * */ -export const Button = (props: ButtonProps) => { +export const Button = ( + props: ButtonProps +) => { const { alignIcon = 'left', children, @@ -54,8 +56,8 @@ export const Button = (props: ButtonProps) => { className={className} color={color} size="large" - {...rest} {...linkParams} + {...rest} > {children} @@ -66,8 +68,8 @@ export const Button = (props: ButtonProps) => { color={color} disabled={disabled} size="large" - {...rest} {...linkParams} + {...rest} > {children} @@ -81,27 +83,29 @@ export const Button = (props: ButtonProps) => { disabled={disabled} startIcon={alignIcon === 'left' && children ? children : undefined} endIcon={alignIcon === 'right' && children ? children : undefined} - {...rest} {...linkParams} + {...rest} > {translatedLabel} ); }; -interface Props { +interface Props { alignIcon?: 'left' | 'right'; - children?: ReactElement; + children?: React.ReactElement; className?: string; - component?: ElementType; - to?: string | LocationDescriptor; + component?: RootComponent; + to?: LocationDescriptor | To; disabled?: boolean; label?: string; size?: 'small' | 'medium' | 'large'; variant?: string; } -export type ButtonProps = Props & MuiButtonProps; +export type ButtonProps< + RootComponent extends React.ElementType = 'button' +> = Props & MuiButtonProps; Button.propTypes = { alignIcon: PropTypes.oneOf(['left', 'right']), diff --git a/packages/ra-ui-materialui/src/button/CloneButton.tsx b/packages/ra-ui-materialui/src/button/CloneButton.tsx index fd68c75ade9..d2078dbf70e 100644 --- a/packages/ra-ui-materialui/src/button/CloneButton.tsx +++ b/packages/ra-ui-materialui/src/button/CloneButton.tsx @@ -29,10 +29,10 @@ export const CloneButton = (props: CloneButtonProps) => { search: stringify({ source: JSON.stringify(omitId(record)), }), - state: { _scrollToTop: scrollToTop }, } : pathname } + state={{ _scrollToTop: scrollToTop }} label={label} onClick={stopPropagation} {...sanitizeRestProps(rest)} @@ -61,7 +61,7 @@ interface Props { scrollToTop?: boolean; } -export type CloneButtonProps = Props & ButtonProps; +export type CloneButtonProps = Props & Omit, 'to'>; CloneButton.propTypes = { icon: PropTypes.element, diff --git a/packages/ra-ui-materialui/src/button/CreateButton.tsx b/packages/ra-ui-materialui/src/button/CreateButton.tsx index 518fa7df7cc..cfbe21c0d90 100644 --- a/packages/ra-ui-materialui/src/button/CreateButton.tsx +++ b/packages/ra-ui-materialui/src/button/CreateButton.tsx @@ -1,15 +1,14 @@ import * as React from 'react'; -import { styled } from '@mui/material/styles'; -import { ReactElement, memo } from 'react'; -import PropTypes from 'prop-types'; -import { Fab, useMediaQuery, Theme } from '@mui/material'; import ContentAdd from '@mui/icons-material/Add'; +import { Fab, useMediaQuery, Theme } from '@mui/material'; +import { styled } from '@mui/material/styles'; import clsx from 'clsx'; -import { Link } from 'react-router-dom'; +import { isEqual, merge } from 'lodash'; +import PropTypes from 'prop-types'; import { useTranslate, useResourceContext, useCreatePath } from 'ra-core'; -import isEqual from 'lodash/isEqual'; +import { Link, To } from 'react-router-dom'; -import { Button, ButtonProps } from './Button'; +import { Button, ButtonProps, LocationDescriptor } from './Button'; /** * Opens the Create view of a given resource @@ -32,6 +31,8 @@ const CreateButton = (props: CreateButtonProps) => { resource: resourceProp, scrollToTop = true, variant, + to: locationDescriptor, + state: initialState = {}, ...rest } = props; @@ -41,17 +42,21 @@ const CreateButton = (props: CreateButtonProps) => { const isSmall = useMediaQuery((theme: Theme) => theme.breakpoints.down('md') ); + const state = merge(scrollStates.get(String(scrollToTop)), initialState); + // Duplicated behaviour of Button component (legacy use) which will be removed in v5. + const linkParams = getLinkParams(locationDescriptor); return isSmall ? ( {icon} @@ -59,11 +64,12 @@ const CreateButton = (props: CreateButtonProps) => { {icon} @@ -71,20 +77,21 @@ const CreateButton = (props: CreateButtonProps) => { }; // avoids using useMemo to get a constant value for the link state -const scrollStates = { - true: { _scrollToTop: true }, - false: {}, -}; +const scrollStates = new Map([ + ['true', { _scrollToTop: true }], + ['false', {}], +]); const defaultIcon = ; interface Props { resource?: string; - icon?: ReactElement; + icon?: React.ReactElement; scrollToTop?: boolean; + to?: LocationDescriptor | To; } -export type CreateButtonProps = Props & ButtonProps; +export type CreateButtonProps = Props & Omit, 'to'>; CreateButton.propTypes = { resource: PropTypes.string, @@ -121,7 +128,7 @@ const StyledButton = styled(Button, { overridesResolver: (_props, styles) => styles.root, })({}); -export default memo(CreateButton, (prevProps, nextProps) => { +export default React.memo(CreateButton, (prevProps, nextProps) => { return ( prevProps.resource === nextProps.resource && prevProps.label === nextProps.label && @@ -130,3 +137,22 @@ export default memo(CreateButton, (prevProps, nextProps) => { isEqual(prevProps.to, nextProps.to) ); }); + +const getLinkParams = (locationDescriptor?: LocationDescriptor | string) => { + // eslint-disable-next-line eqeqeq + if (locationDescriptor == undefined) { + return undefined; + } + + if (typeof locationDescriptor === 'string') { + return { to: locationDescriptor }; + } + + const { redirect, replace, state, ...to } = locationDescriptor; + return { + to, + redirect, + replace, + state, + }; +}; diff --git a/packages/ra-ui-materialui/src/button/ShowButton.tsx b/packages/ra-ui-materialui/src/button/ShowButton.tsx index 6f85a1dc2aa..02b87265426 100644 --- a/packages/ra-ui-materialui/src/button/ShowButton.tsx +++ b/packages/ra-ui-materialui/src/button/ShowButton.tsx @@ -9,7 +9,6 @@ import { useRecordContext, useCreatePath, } from 'ra-core'; -import isEqual from 'lodash/isEqual'; import { Button, ButtonProps } from './Button'; @@ -77,7 +76,7 @@ interface Props { export type ShowButtonProps = Props< RecordType > & - ButtonProps; + Omit, 'to'>; ShowButton.propTypes = { icon: PropTypes.element, @@ -94,8 +93,7 @@ const PureShowButton = memo( ? prevProps.record.id === nextProps.record.id : prevProps.record == nextProps.record) && // eslint-disable-line eqeqeq prevProps.label === nextProps.label && - prevProps.disabled === nextProps.disabled && - isEqual(prevProps.to, nextProps.to) + prevProps.disabled === nextProps.disabled ); export default PureShowButton;