Search code examples
javascriptreactjstypescriptreact-admin

How to add a form input on a Dialog from Show view with React Admin?


Summary

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.

Tries (and failures)

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>
);

Question

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.


Solution

  • 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!