I want to add a quick action button on a record show view that will display a dialog with a form field.
When the form is submitted, I want the data provided to dispatch an update action with the form field data plus a few option.
I know how to create action button that will dispatch update action on data provider, but I am stuck about how to handle a form on a dialog.
So I started by creating a button displaying a dialog with a TextInput
(from react-admin
) on it:
const ValidateButton: FC<ButtonProps> = ({ record }) => {
const [open, setOpen] = useState<boolean>(false);
const handleInit = () => {
setOpen(true)
}
const handleCancel = () => {
setOpen(false);
}
const handleSubmit = () => {
setOpen(false);
}
return (
<>
<Button
label="Validate"
onClick={handleInit}
>
<ValidateIcon />
</Button>
<Dialog
open={open}
>
<DialogTitle>Validate</DialogTitle>
<DialogContent>
<TextInput
source="comment"
label="Validation comment"
/>
</DialogContent>
<DialogActions>
<Button
label="Cancel"
onClick={handleCancel}
/>
<Button
label="Validate"
onClick={handleSubmit}
/>
</DialogActions>
</Dialog>
</>
);
}
This gave me the following error:
Error: useField must be used inside of a <Form> component
Well. Pretty clear. So I tried wrapping it on a SimpleForm
component:
<DialogContent>
<SimpleForm
form={createForm({ onSubmit: handleSubmit })}
resource="rides"
record={record}
>
<TextInput
source="comment"
label="Validation comment"
/>
</SimpleForm>
</DialogContent>
Then I got:
TypeError: can't access property "save", context is undefined
useSaveContext
node_modules/ra-core/esm/controller/details/SaveContext.js:23
20 | */
21 | export var useSaveContext = function (props) {
22 | var context = useContext(SaveContext);
> 23 | if (!context.save || !context.setOnFailure) {
24 | /**
25 | * The element isn't inside a <SaveContextProvider>
26 | * To avoid breakage in that case, fallback to props
The ValidateButton
is implemented on the Show view toolbar:
const RideShowActions: FC<ShowActionsProps> = ({ basePath, data }) => {
if (!data) {
return null;
}
return (
<TopToolbar>
<ValidateButton />
{[
'created',
'validated',
'confirmed',
].includes(data?.status) && <CancelButton basePath={basePath} record={data} />}
<EditButton basePath={basePath} record={data} />
</TopToolbar>
);
};
export const RideShow: FC<ShowProps> = (props) => (
<Show
title={<RideTitle />}
actions={<RideShowActions />}
{...props}
>
<SimpleShowLayout>
// Show classic stuff.
</SimpleShowLayout>
</Show>
);
I suppose the last error is because the SimpleForm
needs to be placed on <Edit>
or similar.
My goal is just to have an input with the same functionnality and UX/UI design than the edit form on a dialog and do custom actions.
What I am missing? How can I achieve that?
Note: I tried to directly use the TextField
component of @material-ui/core
. It works, but I loose all the react-admin functionnalities that will be useful to me like for ReferenceInput
.
I finally found the a solution by creating a custom DialogForm
component, combining the common logic like the form itself and the action buttons:
import { FC, MouseEventHandler } from 'react';
import { Button } from 'react-admin';
import { Dialog, DialogProps, DialogTitle, DialogContent, DialogActions } from '@material-ui/core'
import { Form } from 'react-final-form';
import { Config } from 'final-form';
export interface DialogFormProps {
open: DialogProps['open'],
loading?: boolean;
onSubmit: Config['onSubmit'],
onCancel: MouseEventHandler,
title?: string,
submitLabel?: string;
cancelLabel?: string;
}
export const DialogForm: FC<DialogFormProps> = ({
open,
loading,
onSubmit,
onCancel,
title,
cancelLabel,
submitLabel,
children,
}) => {
return (
<Dialog
open={open}
>
<Form
onSubmit={onSubmit}
render={({handleSubmit}) => (
<form onSubmit={handleSubmit}>
{title && (
<DialogTitle>
{title}
</DialogTitle>
)}
<DialogContent>
{children}
</DialogContent>
<DialogActions>
<Button
label={cancelLabel || 'Cancel'}
onClick={onCancel}
disabled={loading}
/>
<Button
label={submitLabel || 'Validate'}
type="submit"
disabled={loading}
/>
</DialogActions>
</form>
)}
/>
</Dialog>
)
};
export default null;
You can copy/paste the example above to have it working out of the box.
Here is an implementation example with a button:
const ValidateButton: FC<ButtonProps> = ({ record }) => {
const [open, setOpen] = useState<boolean>(false);
(undefined);
const [mutate, { loading }] = useMutation();
const notify = useNotify();
const refresh = useRefresh();
const handleInit = () => {
setOpen(true)
}
const handleCancel = () => {
setOpen(false);
}
const handleSubmit: DialogFormProps['onSubmit'] = (values) => {
console.log(values);
mutate(
// Your mutation logic.
);
}
return (
<>
<Button
label="Validate"
onClick={handleInit}
>
<ValidateIcon />
</Button>
<DialogForm
open={open}
loading={loading}
onSubmit={handleSubmit}
onCancel={handleCancel}
title="Validate that thing"
submitLabel="Let's go!"
>
<DialogContentText>
Some text.
</DialogContentText>
<ReferenceInput
source="referenceId"
label="Reference"
reference="service"
filter={{ userId: record?.customerId }}
>
<SelectInput
optionText="name"
resettable
fullWidth
/>
</ReferenceInput>
</DialogForm>
</>
);
}
Hope it will help somehow!