Source: app/enhanced-redux-form/actions/wizardFormActions.js

/* global WP_DEFINE_DEVELOPMENT, WP_DEFINE_IS_NODE */
import debugLib from 'debug';
import { destroy } from 'redux-form';
import isSubmissionError from '../utils/isSubmissionError';
import { HANDLE_SUBMIT_ERRORS } from './enhancedFormActions';

/**
 * All functions in this module are action creators and the return value
 * should be passed to the redux store dispatch() function
 *
 * @module enhanced-redux-form/actions/wizardFormActions
 * @category forms
 */

const debug = debugLib('SlimmingWorld:wizardFormActions');

export const REGISTER_WIZARD_FORM = 'wizardFormActions/REGISTER_WIZARD_FORM';
export const DESTROY_WIZARD_FORM = 'wizardFormActions/DESTROY_WIZARD_FORM';
export const WIZARD_STEP_SUBMIT = 'wizardFormActions/WIZARD_STEP_SUBMIT';
export const WIZARD_STEP_MOUNT = 'wizardFormActions/WIZARD_STEP_MOUNT';
export const SUBMIT_WIZARD_FORM = 'wizardFormActions/SUBMIT_WIZARD_FORM';
export const TRIGGER_WIZARD_FORM_SUBMIT = 'wizardFormActions/TRIGGER_WIZARD_FORM_SUBMIT';

/* eslint-disable max-len */
/**
 * __Note: this action is dispatched by the enhancedFormWizard() HOC. You probable won't need to
 * call it yourself__
 *
 * Calls the given submit handler with the form values of the given wizard form. If submission
 * errors occur during submit, it will persist these errors on the relevant form step and redirect
 * to the first form step that contained any error.
 *
 * @function submitWizardForm
 * @param {string} wizardName The wizard name as passed to
 * the {@link module:enhanced-redux-form/enhancedFormWizard~enhancedFormWizard|enhancedFormWizard function}
 * @param {function} submitHandler The handler that should be called to perform the submit. The
 * handler will receive the following parameters:
 *  - **dispatch** The redux dispatch function
 *  - **values** An object containing all the values that have been submitted to individual form steps
 * @param {function} [transformApiFieldNames] A function that maps the field names in the validation
 * errors returned by the API to field names in the actual form.
 * @param {string} generalErrorMessage When the API call comes back with an error that doesn't
 * have the default error response shape, this message will be shown instead.
 * @param {function} historyPush A function that performs a history.push. If the wizard form
 * is mounted in QueryRouting, this can be a different history instance than the main
 * browserHistory
 */
export const submitWizardForm = (
  wizardName,
  submitHandler = () => {},
  transformApiFieldNames = name => name,
  generalErrorMessage,
  routingAdapter,
) => (dispatch, getState) => {
  const state = getState();
  const wizard = state.enhancedForm.wizard[wizardName];

  if (!wizard) {
    throw new ReferenceError(
      `Trying to submit wizard with name "${wizardName}" but it is not found in the Redux state.`,
    );
  }

  return dispatch({
    type: SUBMIT_WIZARD_FORM,
    payload: Promise.resolve(submitHandler(dispatch, wizard.submittedValues)).catch(
      gatewayError => {
        if (isSubmissionError(gatewayError) && gatewayError.response.parsed.error.fields) {
          const errors = gatewayError.response.parsed.error.fields;
          const errorsPerStep = wizard.steps.map(() => []);

          errors.forEach(error => {
            const localFieldName = transformApiFieldNames(error.field);
            const stepWithErrorIndex = wizard.steps.findIndex(step =>
              step.submittedKeys.includes(localFieldName),
            );

            if (stepWithErrorIndex >= 0) {
              errorsPerStep[stepWithErrorIndex].push(error);
            } else {
              debug(
                `Submission errors contain a field with name '${error.field}' (transformed to '${localFieldName}') but the field is not found on any form step`,
              );
            }
          });

          errorsPerStep.forEach((stepErrors, stepIndex) => {
            if (stepErrors.length) {
              dispatch({
                type: HANDLE_SUBMIT_ERRORS,
                payload: {
                  errors: stepErrors.reduce((errorObject, error) => {
                    // eslint-disable-next-line no-param-reassign
                    errorObject[transformApiFieldNames(error.field)] = {
                      message: error.message,
                      code: error.code,
                    };

                    return errorObject;
                  }, {}),
                },
                meta: { form: wizard.steps[stepIndex].form },
              });
            }
          });

          const stepsWithError = wizard.steps.filter((step, index) => errorsPerStep[index].length);
          if (stepsWithError.length) {
            dispatch(routingAdapter.gotoStep(stepsWithError[0].stepIndex));
          }
        } else if (generalErrorMessage) {
          // the wizardReducer will respond to the error format below
          // eslint-disable-next-line no-param-reassign
          gatewayError.error = { message: generalErrorMessage };
        }

        // don't silence the error. This will reject the SUBMIT_WIZARD_FORM async action
        throw gatewayError;
      },
    ),
    meta: { wizardName },
  });
};

/**
 * __Note:  this action is used internally by enhanced-redux-form. You will probably not
 * need this for general usage__
 *
 * Registers a new wizard form
 * @function registerWizardForm
 * @param {string} wizardName The name of the wizard
 * @param {Array<Object>} steps An array of steps in the wizard
 * @param {string} steps[].path The path of the route for this step
 * @returns {object} The action
 */
export const registerWizardForm = (wizardName, steps) => ({
  type: REGISTER_WIZARD_FORM,
  payload: { steps },
  meta: { wizardName },
});

/**
 * __Note:  this action is used internally by enhanced-redux-form. You will probably not
 * need this for general usage__
 *
 * Registers a mount of a form that is part of a wizard.
 * @function wizardStepMount
 * @param {string} form The name of the form that is being mounted.
 * @param {string} wizardName The name of the wizard this form is part of.
 * todo: updated param docs
 */
export const wizardStepMount = (
  form,
  wizardName,
  routingAdapter,
  // note: this argument may be undefined when routingAdapter is not ReactRouterWizardRoutingAdapter
  router,
) => (dispatch, getState) => {
  const { activeStepIndex, steps, allowStepSkipping } = dispatch(
    routingAdapter.getStepInfo(wizardName, router),
  );

  if (!allowStepSkipping) {
    for (let i = 0; i < activeStepIndex; i++) {
      const precedingStep = steps[i];
      const { isDisabledCheck, submitted } = precedingStep;

      // redirect to the step if it has not been submitted and it is not disabled either
      if (!submitted && !(isDisabledCheck && isDisabledCheck(getState))) {
        // temporary workaround until we have a better way of redirecting during page render
        dispatch(routingAdapter.gotoStep(i, true));
        return null;
      }
    }
  }

  // Check if the current step is disabled. if not, return early
  const { isDisabledCheck } = steps[activeStepIndex];
  if (!isDisabledCheck || !isDisabledCheck(getState)) {
    return dispatch({
      type: WIZARD_STEP_MOUNT,
      payload: { stepIndex: activeStepIndex },
      meta: { form, wizardName },
    });
  }

  // the current step is disabled. find the first enabled step
  for (let i = activeStepIndex + 1; i < steps.length; i++) {
    const nextStep = steps[i];

    if (!nextStep.isDisabledCheck || !nextStep.isDisabledCheck(getState)) {
      dispatch(routingAdapter.gotoStep(i, true));
      return null;
    }
  }

  // we can't access any step. This should never be the case
  throw new Error(
    `All wizard form steps are disabled: WizardName: ${wizardName} activeStepIndex: ${activeStepIndex} isDisabledCheck: ${isDisabledCheck}`,
  );
};

export const destroyWizardForm = wizardName => (dispatch, getState) => {
  const state = getState();
  const wizard = state.enhancedForm.wizard[wizardName];
  if (!wizard || !wizard.steps) {
    debug(`Could not destroy wizard form "${wizardName}": wizard not found`);
    return;
  }
  const steps = wizard.steps;

  steps.forEach(step => {
    if (step.form) {
      dispatch(destroy(step.form));
    }
  });

  dispatch({
    type: DESTROY_WIZARD_FORM,
    meta: { wizardName },
  });
};

/**
 * __Note:  this action is used internally by enhanced-redux-form. You will probably not
 * need this for general usage__
 *
 * Handles when a form step submission has been completed
 * @function wizardStepSubmit
 * @param {string} wizardName The name of the wizard
 * @param {number} stepIndex Index of the step that has been submitted in the steps array
 * @param {Object} wizardRoutingAdapter The routingAdapter passed to enhancedWizardForm config
 * @param {Object} formValues The values submitted to the form
 */
export const wizardStepSubmit = (wizardName, stepIndex, wizardRoutingAdapter, formValues) => (
  dispatch,
  getState,
) => {
  if (!wizardRoutingAdapter) {
    throw new ReferenceError(
      `No wizardRoutingAdapter provided to submitForm in ${wizardName}[${stepIndex}]`,
    );
  }

  const state = getState();
  const wizard = state.enhancedForm.wizard[wizardName];
  if (!wizard || !wizard.steps) {
    throw new ReferenceError(`Could not find wizard "${wizardName}" in wizard reducer`);
  }
  const steps = wizard.steps;

  dispatch({
    type: WIZARD_STEP_SUBMIT,
    payload: { stepIndex, formValues },
    meta: { wizardName, form: steps[stepIndex].form },
  });

  const nextStep = steps[stepIndex + 1];
  if (nextStep) {
    dispatch(wizardRoutingAdapter.gotoStep(nextStep.stepIndex));
    return null;
  }

  // this action will set 'submitting' to 'true' in the Redux state. It will signal the wizard
  // component to call the 'submitWizardForm' action with the submit handler
  return dispatch({
    type: TRIGGER_WIZARD_FORM_SUBMIT,
    meta: { wizardName },
  });
};