Skip to content

Commit

Permalink
Merge pull request #9319 from marmelab/tip-create-relatde-resource
Browse files Browse the repository at this point in the history
Add ability to pass url state in `<CreateButton>`
  • Loading branch information
djhi authored Oct 5, 2023
2 parents 1fd126a + c2956f4 commit dcd86cd
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 69 deletions.
34 changes: 10 additions & 24 deletions docs/Create.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<Create>` 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 `<Create>` view uses that `record` instead of the empty object. That's how the `<CloneButton>` 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 `<Link>` 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 `<CreateButton>`:

{% 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 (
<Button
component={Link}
to={{
pathname: '/comments/create',
}}
<CreateButton
resource="comments"
state={{ record: { post_id: record.id } }}
>
Write a comment for that post
</Button>
/>
);
};

Expand All @@ -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 `<Create>` 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 (
<Button
component={Link}
<CreateButton
resource="comments"
to={{
pathname: '/comments/create',
search: `?source=${JSON.stringify({ post_id: record.id })}`,
}}
>
Write a comment for that post
</Button>
/>
);
};
```
Expand Down
19 changes: 8 additions & 11 deletions docs/CreateInDialogButton.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ const CompanyShow = () => (
<TextField source="name" />
<TextField source="address" />
<TextField source="city" />
<WithRecord render={record => (
<CreateInDialogButton record={{ company_id: record.id }}>
<SimpleForm>
<TextInput source="first_name" />
<TextInput source="last_name" />
</SimpleForm>
</CreateInDialogButton>
)} />
<ReferenceManyField target="company_id" reference="employees">
<WithRecord render={record => (
<CreateInDialogButton record={{ company_id: record.id }}>
<SimpleForm>
<TextInput source="first_name" />
<TextInput source="last_name" />
</SimpleForm>
</CreateInDialogButton>
)} />
<Datagrid>
<TextField source="first_name" />
<TextField source="last_name" />
Expand Down Expand Up @@ -188,6 +188,3 @@ const EmployerEdit = () => (
);
```
{% endraw %}
15 changes: 15 additions & 0 deletions examples/demo/src/products/CreateRelatedReviewButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as React from 'react';
import { CreateButton, useRecordContext } from 'react-admin';

const CreateRelatedReviewButton = () => {
const record = useRecordContext();

return (
<CreateButton
resource="reviews"
state={{ record: { product_id: record.id } }}
/>
);
};

export default CreateRelatedReviewButton;
2 changes: 2 additions & 0 deletions examples/demo/src/products/ProductEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Product>();
Expand Down Expand Up @@ -85,6 +86,7 @@ const ProductEdit = () => (
<TextField source="status" />
<EditButton />
</Datagrid>
<CreateRelatedReviewButton />
</ReferenceManyField>
</TabbedForm.Tab>
</TabbedForm>
Expand Down
63 changes: 63 additions & 0 deletions examples/demo/src/reviews/ReviewCreate.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Create mutationOptions={{ onSuccess }}>
<SimpleForm defaultValues={{ status: 'pending' }}>
<ReferenceInput source="customer_id" reference="customers">
<AutocompleteInput validate={required()} />
</ReferenceInput>
<ReferenceInput source="product_id" reference="products">
<AutocompleteInput
optionText="reference"
validate={required()}
/>
</ReferenceInput>
<DateInput
source="date"
defaultValue={new Date()}
validate={required()}
/>
<StarRatingInput source="rating" defaultValue={2} />
<TextInput
source="comment"
multiline
fullWidth
resettable
validate={required()}
/>
</SimpleForm>
</Create>
);
};

export default ReviewCreate;
31 changes: 31 additions & 0 deletions examples/demo/src/reviews/StarRatingInput.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Box display="flex" flexDirection="column" marginBottom="1rem">
<Typography>{translate(name)}</Typography>
<StyledRating
{...field}
icon={<Icon />}
onChange={(event, value) => field.onChange(value)}
/>
</Box>
);
};

const StyledRating = styled(Rating)({
'& .MuiRating-iconFilled': {
color: '#000',
},
'& .MuiRating-iconHover': {
color: '#000',
},
});

export default StarRatingInput;
2 changes: 2 additions & 0 deletions examples/demo/src/reviews/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};
2 changes: 2 additions & 0 deletions examples/demo/src/visitors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const resource = {
create: VisitorCreate,
edit: VisitorEdit,
icon: VisitorIcon,
recordRepresentation: (record: any) =>
`${record.first_name} ${record.last_name}`,
};

export default resource;
3 changes: 2 additions & 1 deletion packages/ra-ui-materialui/src/auth/AuthError.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -19,7 +20,7 @@ export const AuthError = (props: AuthErrorProps) => {
<div className={AuthErrorClasses.message}>
<h1>{translate(title, { _: title })}</h1>
<div>{translate(message, { _: message })}</div>
<Button to="/login" label="ra.auth.sign_in">
<Button component={Link} to="/login" label="ra.auth.sign_in">
<LockIcon />
</Button>
</div>
Expand Down
26 changes: 15 additions & 11 deletions packages/ra-ui-materialui/src/button/Button.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';

Expand All @@ -26,7 +26,9 @@ import { Path } from 'react-router';
* </Button>
*
*/
export const Button = (props: ButtonProps) => {
export const Button = <RootComponent extends React.ElementType = 'button'>(
props: ButtonProps<RootComponent>
) => {
const {
alignIcon = 'left',
children,
Expand Down Expand Up @@ -54,8 +56,8 @@ export const Button = (props: ButtonProps) => {
className={className}
color={color}
size="large"
{...rest}
{...linkParams}
{...rest}
>
{children}
</IconButton>
Expand All @@ -66,8 +68,8 @@ export const Button = (props: ButtonProps) => {
color={color}
disabled={disabled}
size="large"
{...rest}
{...linkParams}
{...rest}
>
{children}
</IconButton>
Expand All @@ -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}
</StyledButton>
);
};

interface Props {
interface Props<RootComponent extends React.ElementType> {
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<RootComponent> & MuiButtonProps<RootComponent>;

Button.propTypes = {
alignIcon: PropTypes.oneOf(['left', 'right']),
Expand Down
4 changes: 2 additions & 2 deletions packages/ra-ui-materialui/src/button/CloneButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down Expand Up @@ -61,7 +61,7 @@ interface Props {
scrollToTop?: boolean;
}

export type CloneButtonProps = Props & ButtonProps;
export type CloneButtonProps = Props & Omit<ButtonProps<typeof Link>, 'to'>;

CloneButton.propTypes = {
icon: PropTypes.element,
Expand Down
Loading

0 comments on commit dcd86cd

Please sign in to comment.