Skip to content
Snippets Groups Projects
  • Anton Kulyk's avatar
    2e8e313a
    Cherry-pick action creator component (#27673) · 2e8e313a
    Anton Kulyk authored
    
    * Cherry-pick `ModelPicker`
    
    * Cherry-pick actions editor and form components
    
    * Migrate from entity forms
    
    * Remove `ModelPicker`
    
    * Temporarily add "New > Action" flow
    
    * Disable save button if query is empty
    
    * Fix utilities moved
    
    * Fix type errors
    
    * Address comments
    
    * Simplify `ActionForm` tests
    
    * Break down `ActionCreator` props
    
    * Add basic `ActionCreator` tests
    
    * update action creator header gap styling
    
    * Extract `convertActionToQuestionCard`
    
    * Fix `CreateActionForm` ignores action name
    
    * Fix `FormModelPicker` crash
    
    * Address comments
    
    * Remove "New > Action" flow
    
    Co-authored-by: default avatarRyan Laurie <iethree@gmail.com>
    Cherry-pick action creator component (#27673)
    Anton Kulyk authored
    
    * Cherry-pick `ModelPicker`
    
    * Cherry-pick actions editor and form components
    
    * Migrate from entity forms
    
    * Remove `ModelPicker`
    
    * Temporarily add "New > Action" flow
    
    * Disable save button if query is empty
    
    * Fix utilities moved
    
    * Fix type errors
    
    * Address comments
    
    * Simplify `ActionForm` tests
    
    * Break down `ActionCreator` props
    
    * Add basic `ActionCreator` tests
    
    * update action creator header gap styling
    
    * Extract `convertActionToQuestionCard`
    
    * Fix `CreateActionForm` ignores action name
    
    * Fix `FormModelPicker` crash
    
    * Address comments
    
    * Remove "New > Action" flow
    
    Co-authored-by: default avatarRyan Laurie <iethree@gmail.com>
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
ActionForm.tsx 5.94 KiB
import React, { useMemo } from "react";
import { t } from "ttag";
import _ from "underscore";
import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd";

import type {
  DraggableProvided,
  OnDragEndResponder,
  DroppableProvided,
} from "react-beautiful-dnd";
import type { FormikHelpers } from "formik";

import Button from "metabase/core/components/Button";
import Form from "metabase/core/components/Form";
import FormProvider from "metabase/core/components/FormProvider";
import FormSubmitButton from "metabase/core/components/FormSubmitButton";
import FormErrorMessage from "metabase/core/components/FormErrorMessage";
import Icon from "metabase/components/Icon";

import type {
  ActionFormInitialValues,
  ActionFormSettings,
  FieldSettings,
  WritebackParameter,
  Parameter,
  ParametersForActionExecution,
} from "metabase-types/api";

import { reorderFields } from "metabase/actions/containers/ActionCreator/FormCreator";
import { FieldSettingsButtons } from "../../containers/ActionCreator/FormCreator/FieldSettingsButtons";
import { FormFieldWidget } from "./ActionFormFieldWidget";
import {
  ActionFormButtonContainer,
  FormFieldContainer,
  SettingsContainer,
  InputContainer,
} from "./ActionForm.styled";

import { getForm, getFormValidationSchema } from "./utils";

export interface ActionFormComponentProps {
  parameters: WritebackParameter[] | Parameter[];
  initialValues?: ActionFormInitialValues;
  onClose?: () => void;
  onSubmit?: (
    params: ParametersForActionExecution,
    actions: FormikHelpers<ParametersForActionExecution>,
  ) => void;
  submitTitle?: string;
  submitButtonColor?: string;
  formSettings?: ActionFormSettings;
  setFormSettings?: (formSettings: ActionFormSettings) => void;
}

export const ActionForm = ({
  parameters,
  initialValues = {},
  onClose,
  onSubmit,
  submitTitle,
  submitButtonColor = "primary",
  formSettings,
  setFormSettings,
}: ActionFormComponentProps): JSX.Element => {
  // allow us to change the color of the submit button
  const submitButtonVariant = { [submitButtonColor]: true };

  const isSettings = !!(formSettings && setFormSettings);

  const form = useMemo(
    () => getForm(parameters, formSettings?.fields),
    [parameters, formSettings?.fields],
  );

  const formValidationSchema = useMemo(
    () => getFormValidationSchema(parameters, formSettings?.fields),
    [parameters, formSettings?.fields],
  );

  const handleDragEnd: OnDragEndResponder = ({ source, destination }) => {
    if (!isSettings) {
      return;
    }

    const oldOrder = source.index;
    const newOrder = destination?.index ?? source.index;

    const reorderedFields = reorderFields(
      formSettings.fields,
      oldOrder,
      newOrder,
    );
    setFormSettings({
      ...formSettings,
      fields: reorderedFields,
    });
  };

  const handleChangeFieldSettings = (newFieldSettings: FieldSettings) => {
    if (!isSettings || !newFieldSettings?.id) {
      return;
    }

    setFormSettings({
      ...formSettings,
      fields: {
        ...formSettings.fields,
        [newFieldSettings.id]: newFieldSettings,
      },
    });
  };

  const handleSubmit = (
    values: ParametersForActionExecution,
    actions: FormikHelpers<ParametersForActionExecution>,
  ) => onSubmit?.(formValidationSchema.cast(values), actions);

  if (isSettings) {
    return (
      <FormProvider
        initialValues={initialValues}
        validationSchema={formValidationSchema}
        onSubmit={handleSubmit}
      >
        <Form role="form" data-testid="action-form-editor">
          <DragDropContext onDragEnd={handleDragEnd}>
            <Droppable droppableId="action-form-droppable">
              {(provided: DroppableProvided) => (
                <div {...provided.droppableProps} ref={provided.innerRef}>
                  {form.fields.map((field, index) => (
                    <Draggable
                      key={`draggable-${field.name}`}
                      draggableId={field.name}
                      index={index}
                    >
                      {(provided: DraggableProvided) => (
                        <FormFieldContainer
                          ref={provided.innerRef}
                          {...provided.draggableProps}
                          {...provided.dragHandleProps}
                          isSettings={isSettings}
                        >
                          <SettingsContainer>
                            <Icon name="grabber2" size={14} />
                          </SettingsContainer>

                          <InputContainer>
                            <FormFieldWidget
                              key={field.name}
                              formField={field}
                            />
                          </InputContainer>
                          <FieldSettingsButtons
                            fieldSettings={formSettings.fields[field.name]}
                            onChange={handleChangeFieldSettings}
                          />
                        </FormFieldContainer>
                      )}
                    </Draggable>
                  ))}
                </div>
              )}
            </Droppable>
          </DragDropContext>
        </Form>
      </FormProvider>
    );
  }

  return (
    <FormProvider
      initialValues={initialValues}
      validationSchema={formValidationSchema}
      onSubmit={handleSubmit}
      enableReinitialize
    >
      {({ dirty }) => (
        <Form disabled={!dirty} role="form" data-testid="action-form">
          {form.fields.map(field => (
            <FormFieldWidget key={field.name} formField={field} />
          ))}

          <ActionFormButtonContainer>
            {onClose && <Button onClick={onClose}>{t`Cancel`}</Button>}
            <FormSubmitButton
              disabled={!dirty}
              title={submitTitle ?? t`Save`}
              {...submitButtonVariant}
            />
          </ActionFormButtonContainer>

          <FormErrorMessage />
        </Form>
      )}
    </FormProvider>
  );
};