All files / src/form/wizard WizardAction.tsx

35.93% Statements 46/128
18.75% Branches 12/64
26.08% Functions 6/23
36.8% Lines 46/125

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305                                                                4x 4x 4x   4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x 4x   4x 1x     4x 1x   1x 1x 1x 1x 1x 1x 1x               4x 1x     4x           4x                     4x                                                 4x                 4x                                                                                                                                                                   4x                                   4x   4x   4x                                                                                                                                 4x    
import React, { ReactElement, useCallback, useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import { Button, Modal, Result } from 'antd';
import { useTranslation } from 'react-i18next';
import { PlusCircleOutlined } from "@ant-design/icons";
import { ExecuteStatus, MapOfEnumMetaProps, Store, TableMetaProps, WizardMetaProps } from "@props/RecordProps";
import { fetchCanCreate, getWizardMeta, postWizardStep } from "@utils/FetchUtils";
import WizardComponent from "./WizardComponent";
import { modalPropsBuilder, WizardLoadingButtonType } from "../../form";
import { DynamicFormDomainName } from "@config/domain";
import { RedirectComponent } from "../../components/redirect";
import { getFormFieldsValue, useCustomHookForm } from "@utils/FormUtils";
 
interface WizardAppProps {
  stepId: number;
  wizardId: number;
  zIndex: number;
  visibleSeed?: number | false;
  onCancelCallback?: () => void;
  queryParameters?: Record<string, string | number>;
  refererUrl?: string;
}
 
// 每个步骤传递到后台处理的数据都是之前所有步骤的字段数据的全集
// 所有全量字段数据保存在 allStepFormFieldValues 中
// visibleSeed 用于调用本组件的父组件每次使用 Math.random() 来传递一个随机值
// 来确保每次初始调用本组件的时候,可以将 visible 状态设置为 false
// 不然每次点击 close 按钮关闭本组件后,无法再将 visible 设置为 true
export default function WizardAction(props: WizardAppProps): ReactElement {
  const {
    stepId, wizardId, zIndex, visibleSeed, onCancelCallback,
    queryParameters, refererUrl
  } = props;
  console.log("WizardAction: ", props);
  const { t } = useTranslation();
 
  const [wizardMeta, setWizardMeta] = useState<WizardMetaProps>();
  const [current, setCurrent] = useState<number>(0);
  const [open, setOpen] = useState<boolean>(true);
  const [loadingButton, setLoadingButton] = useState<WizardLoadingButtonType>(undefined);
  const [infoMessage, setInfoMessage] = useState<string>();
  const [warningMessage, setWarningMessage] = useState<string>();
  const [errorMessage, setErrorMessage] = useState<string>();
  const [disabledSteps, setDisabledSteps] = useState<Array<string>>([]);
  const [finalSubmitted, setFinalSubmitted] = useState<boolean>(false);
  const [redirect, setRedirect] = useState<string>();
  const [allStepFormFieldValues, setAllStepFormFieldValues] = useState<Store>(queryParameters as Store);
  const [savedLastStepValues, setSavedLastStepValues] = useState<Store>();
  const [hasStep, setHasStep] = useState<boolean>();
  const [canCreateWizardStep, setCanCreateWizardStep] = useState<boolean>();
  const [showAddStepModal, setShowAddStepModal] = useState<boolean>(false);
  const metasRef = useRef<Record<number, Array<TableMetaProps> | undefined>>({});
  const optionsRef = useRef<Record<number, Array<MapOfEnumMetaProps> | undefined>>({});
  const form = useCustomHookForm();
  const location = useLocation();
  const currentUrl = refererUrl ?? location.pathname;
 
  useEffect(() => {
    setOpen(visibleSeed !== false);
  }, [visibleSeed]);
 
  const refreshWizardMeta = useCallback(() => {
    getWizardMeta(wizardId)
      .then((wizardMeta: WizardMetaProps) => {
        const cHasStep = (wizardMeta?.steps?.length > 0);
        setWizardMeta(wizardMeta);
        setHasStep(cHasStep);
        setCurrent(0);
        Eif (cHasStep === false) {
          fetchCanCreate("DynamicFormWizardStep")
            .then(json => setCanCreateWizardStep(json.create))
            .catch(e => {
              console.error(`Failed to get canCreate of domain DynamicFormWizardStep: ${e}`);
            });
        }
      });
  }, [wizardId]);
 
  useEffect(() => {
    refreshWizardMeta();
  }, [refreshWizardMeta]);
 
  const clearMessages = (): void => {
    setInfoMessage(undefined);
    setWarningMessage(undefined);
    setErrorMessage(undefined);
  };
 
  const setMessage = (msg: string, status: ExecuteStatus): void => {
    clearMessages();
    if (status === 'FAILED') {
      setErrorMessage(msg);
    } else if (status === 'SUCCESS') {
      setInfoMessage(msg);
    } else if (status === 'SUCCESS_WITH_WARNING') {
      setWarningMessage(msg);
    }
  };
 
  const onWizardPreviousStep = (): void => {
    clearMessages();
    setFinalSubmitted(false);
    // 将当前没有保存到 allStepFormFieldValues 的 form 数据保存起来
    // 解决 checkbox 多选控件上点击了 previous step 之后,
    // 用户输入的值丢失的问题
    setLoadingButton("previous");
    const currentFormValues = getFormFieldsValue(form);
    setAllStepFormFieldValues({
      ...allStepFormFieldValues, ...currentFormValues
    });
    if (current > 0) {
      // 用户点击 preview step 时,跳过 disable 的 step
      for (let i = current - 1; i >= 0; i--) {
        const stepName = wizardMeta?.steps[i]?.name;
        // 设置 current 为当前 step 之前最近的,非 disable 的 step
        if (stepName != null && !disabledSteps.includes(stepName)) {
          setCurrent(i);
          break;
        }
      }
    }
    setLoadingButton(undefined);
  };
 
  const onWizardRerun = (): void => {
    clearMessages();
    setCurrent(0);
    setAllStepFormFieldValues({});
    setDisabledSteps([]);
    setFinalSubmitted(false);
    setRedirect(undefined);
  };
 
  const onWizardNextStep = (): void => {
    form.validateFields().then(() => {
      const isLastStep = (current === (wizardMeta?.steps?.length ?? 0) - 1);
      const lastStepResubmit = (isLastStep && finalSubmitted);
      const valuesFromLastStepForm = getFormFieldsValue(form);
      if (isLastStep && !lastStepResubmit) {
        setSavedLastStepValues(valuesFromLastStepForm);
      }
      const values = lastStepResubmit ? savedLastStepValues : valuesFromLastStepForm;
      const formValues = { ...allStepFormFieldValues, ...values };
      setLoadingButton("next");
      postWizardStep({
        stepId: wizardMeta?.steps?.[current]?.id ?? stepId,
        formValues,
        isLastStep
      }).then((json) => {
        const {
          status, formValues, message, nextStepName,
          redirect: redirectUrl, options, metas,
        } = json;
        //ATTENTION: setRedirect should comes before other state update
        //Otherwise the page will not be display correctly
        if (redirectUrl != null && redirectUrl !== '') {
          setRedirect(redirectUrl);
        } else {
          setRedirect(undefined);
        }
        if (message != null && message !== '') {
          setMessage(message, status);
        }
        if (isLastStep) {
          setFinalSubmitted(true);
        }
        if (['SUCCESS', 'SUCCESS_WITH_WARNING'].includes(status)) {
          const newFormFields = { ...allStepFormFieldValues, ...formValues };
          setAllStepFormFieldValues(newFormFields);
          if (wizardMeta?.steps != null &&
            current < wizardMeta?.steps?.length - 1) {
            //如果后台返回了下一个步骤的 id
            if (nextStepName != null) {
              // 使用 every 来实现找到了 next step 之后, 返回 false, 即跳出循环,
              // https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/every
              wizardMeta.steps.every((step, idx) => {
                if (step.name === nextStepName) {
                  // 如果其中跳过了某些步骤,
                  // 则在显示时将这些步骤设置为 disabled
                  // 将当前显示步骤与 nextStepName 中的所有步骤都设置为 disabled
                  const startDisable = current + 1;
                  const endDisable = idx;
 
                  // 将当前的 nextStepName 从 disabledSteps 中去掉
                  // 处理这样的场景:某个 step 之前是 disabled 的,
                  // 但是用户返回该 step 再前面的步骤,重新输入了一些字段的值,
                  // 按照新的逻辑,这个 step 现在又 enable 了,
                  // 此时,需要将其从 disableSteps 中删掉
                  const newDisabledSteps = disabledSteps.filter(
                    e => e !== nextStepName);
                  for (let i = startDisable; i < endDisable; i++) {
                    newDisabledSteps.push(wizardMeta?.steps[i]?.name);
                  }
                  setDisabledSteps(newDisabledSteps);
                  metasRef.current[idx] = metas;
                  optionsRef.current[idx] = options;
                  setCurrent(idx);
                  //return false to break the loop
                  return false;
                }
                // 否则返回 true, 则会继续循环查找
                return true;
              });
            } else {
              const newCurrent = current + 1;
              metasRef.current[newCurrent] = metas;
              optionsRef.current[newCurrent] = options;
              setCurrent(newCurrent);
            }
          }
        }
      }).finally(() => setLoadingButton(undefined));
    });
  };
 
  const addStepModal = <>{showAddStepModal &&
    <RedirectComponent
      forMultiple={false}
      fetchDataCallback={() => {
        setShowAddStepModal(false);
        refreshWizardMeta();
      }}
      redirect={`/DynamicFormWizardStep/create`}
      //Use 7 to make sure it appears on top of dashboard widgets
      zIndex={7}
      showText={false}
      ownerClass={DynamicFormDomainName}
      ownerId={wizardId}
      columnNameInOwnerClass="formWizardSteps"
      hasRelateObjectField={true}
    />
  }</>;
 
  const wizardHasNextStep = (wizardMeta?.steps != null
    && current < wizardMeta?.steps?.length - 1);
  const wizardHasPreviousStep = (current > 0 && finalSubmitted === false);
  const modal = (
    <Modal
      {...modalPropsBuilder({
        mode: "wizard",
        open, t,
        domainName: "",
        wizardLoadingButton: loadingButton,
        wizardTitle: wizardMeta?.label,
        onCancel: () => {
          setOpen(false);
          onCancelCallback?.();
        },
        zIndex: zIndex + 2,
        isValid: (!!hasStep),
        onWizardNextStep,
        onWizardPreviousStep,
        onWizardSave: () => {
          setLoadingButton("save");
          setTimeout(() => setLoadingButton(undefined), 1000);
          // Save the wizard data to backend and user can go back
          // and work with it again in future
        },
        wizardHasNextStep,
        wizardHasPreviousStep,
        wizardDescription: wizardMeta?.description,
        // 只有在没有提交最后一步,或者提交了最后一步,
        // 但是最后一步有错误的情况下 Submit 按钮才可以点击
        wizardCanSubmit: !finalSubmitted ||
          (finalSubmitted && errorMessage != null && errorMessage !== ''),
        refererUrl: currentUrl,
      })}
      destroyOnClose={true}
    >
      {addStepModal}
      {(hasStep === true) && <WizardComponent
        stepId={stepId}
        wizardId={wizardId}
        current={current}
        wizardMeta={wizardMeta}
        finalSubmitted={finalSubmitted}
        allStepFormFieldValues={allStepFormFieldValues}
        disabledSteps={disabledSteps}
        errorMessage={errorMessage}
        infoMessage={infoMessage}
        warningMessage={warningMessage}
        redirect={redirect}
        visibleSeed={visibleSeed}
        zIndex={zIndex}
        form={form}
        onClose={() => setOpen(false)}
        onWizardRerun={onWizardRerun}
        options={optionsRef.current[current]}
        dynamicMetas={metasRef.current[current]}
      />}
      {(hasStep === false) && <Result
        extra={canCreateWizardStep &&
          <Button
            type="primary"
            onClick={() => setShowAddStepModal(true)}
          >
            <PlusCircleOutlined /> {t('Add new wizard step')}
          </Button>
        }
        title={t("No wizard step defined")}
      />}
    </Modal>);
  return modal;
}