All files / src/form/action DrawerActionButton.tsx

2.63% Statements 3/114
0% Branches 0/82
0% Functions 0/24
2.65% Lines 3/113

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 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395                                            66x   66x   66x                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                
import React, { ReactElement, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { executeDynamicAction } from '@utils/FetchUtils';
import {
  ActionProps, ExecResultProps, MultipleExecResults, ActionExecResult, RecordProps,
  ActionConfirmType, ActionExtInfoProp, MiddleStepExecContextProps, StorageFileValue
} from "@props/RecordProps";
import { displaySingleResult } from '@utils/ComponentUtils';
import { Space, Tabs, Tooltip, Alert, Drawer } from 'antd';
import ActionResultDisplay from './ActionResultDisplay';
import {
  CaretRightOutlined, ExclamationCircleOutlined, SettingOutlined, FileDoneOutlined
} from '@ant-design/icons';
import { LargeSpin, CloseIcon } from '../../components';
import ActionComponent from './ActionComponent';
import { openErrorNotification, openSuccessMessage } from "@utils/NotificationUtils";
import { stopPropagationAndPreventDefault } from "@utils/ObjectUtils";
import { RedirectComponent } from "../../components/redirect";
import MiddleStepExecInstruction from './MiddleStepExecInstruction';
import { SpecActionButtonProps } from './ActionButton';
import PreviewAction from './PreviewAction';
 
const { TabPane } = Tabs;
 
const DefaultActionExtInfo: ActionExtInfoProp = { displayLabel: true, refreshPage: true };
 
const DrawerActionButton = (props: SpecActionButtonProps): ReactElement => {
  const {
    action, fetchDataCallback, selectedData, zIndex, domainName,
    mode, setVisiblePopoverCallback, ownerClass, ownerId,
    columnNameInOwnerClass, parameters, open, actionElem,
    closeCallback, loadingParameters, labelField
  } = props;
 
  const { id, label, name, helpText, mode: actionMode, confirmType, extInfo } = action;
 
  const { refreshPage } = extInfo ?? DefaultActionExtInfo;
 
  const selectedIds = selectedData?.map(d => d.id) ?? [];
  const { t } = useTranslation();
  const initExpandResultPanel = (displaySingleResult(actionMode) || selectedData?.length === 1);
  const [running, setRunning] = useState<boolean>(false);
  const [results, setResults] = useState<MultipleExecResults | ActionExecResult>({});
  //运行参数的值
  const [formValues, setFormValues] = useState<RecordProps>({} as RecordProps);
 
  // 执行错误信息
  const [executionError, setExecutionError] = useState<string>("");
  // 执行错误代码
  const [errorCode, setErrorCode] = useState<number>();
 
  //当前显示的 tab, ptab: 参数输入, rtab: 结果显示
  const [currentTab, setCurrentTab] = useState<string>("ptab");
  // SimpleAction 模式下,转向到别的页面弹出的地址
  const [redirect, setRedirect] = useState<string | undefined>(undefined);
 
  const [download, setDownload] = useState<StorageFileValue>();
 
  // 标识是否为 OBJECT_SINGLE 的 action (只允许在单个对象上执行的 action)
  const isSingleMode = (actionMode === 'OBJECT_SINGLE');
  // 标识是否为可执行 single 和 multiple 的 action (允许在单个或者多个对象上执行的 action)
  const isSingleMultipleMode = (actionMode === 'OBJECT_SINGLE_MULTIPLE');
  // 标识是否是只显示图标,不显示 Popup 的简易模式
  const isSimpleAction = (confirmType === 'NO_POPUP_NO_CONFIRM');
 
  const [middleStepExecContext, setMiddleStepExecContext] = useState<MiddleStepExecContextProps>();
 
  // 传递到后台的,用户微调之后的运行结果参数, action 可以拿到该参数并使用该参
  // 数而不是 action 运行的结果作为下一步的输入
  const [fineTuningResult, setFineTuningResult] = useState<string>();
 
  // 是否应该显示单条的结果
  const shouldDisplaySingleResult = displaySingleResult(actionMode);
 
  // 是否是 final round 运行
  const isFinalRoundExecute = useRef<boolean>();
 
  useEffect(() => {
    // 如果不是 multiple round 的 action, 则每次执行都是 final round
    if (action.supportFineTuning === false) {
      isFinalRoundExecute.current = true;
    }
  }, [action.supportFineTuning]);
 
  // 应该大于 1050, 1050 是 DropdownMenu 的 z-index
  const zzIndex = 1050 + zIndex + 1;
 
  function showPanel(action: ActionProps): void {
    const { id } = action;
    //Hide all other action execute result panel
    setVisiblePopoverCallback(id.toString());
  }
 
  const callbackWhenRefreshPage = (): void => {
    if (refreshPage !== false) {
      fetchDataCallback();
    }
  };
 
  const addResults = (actionId: number, result: ExecResultProps): void => {
    const newResults = {} as MultipleExecResults | ActionExecResult;
    newResults[actionId] = result;
    setResults(newResults);
  };
 
  const executeAction = (actionId: number, objectIds?: Array<number>): void => {
    showPanel(action);
    setRunning(true);
    const actionMode = action?.mode;
    const ids = objectIds ?? selectedIds;
    const fineTuningResultParam = (isFinalRoundExecute.current && fineTuningResult == null) ?
      middleStepExecContext?.result?.execResult : fineTuningResult;
    executeDynamicAction({
      domainName, actionId, formValues, ids, ownerClass, ownerId,
      columnNameInOwnerClass,
      fineTuningResult: isFinalRoundExecute.current ? fineTuningResultParam : undefined,
      isFinalRound: isFinalRoundExecute.current,
    }).then(json => {
      const { result } = json;
      // 如果是需要默认打开执行结果面板的 action 类型:class 和 multiple 和 single Mode
      // single mode 后台传递过来的 result 数组的 key 是 object id,
      // 其他模式后台传递过来的 result 数组的 key 是 actionId
      // FIXME: 优化上述数组 key 的逻辑不一致的地方
      // (针对多条选中记录,将多条记录的 id 作为一个数组传递给 action, 且只进行一个 action 执行) 类型
      if (actionMode != null && shouldDisplaySingleResult) {
        const thisResult = isSingleMode ? result[ids[0]] : result[actionId];
        // This is to open result panel directly after run an action
        // 如果是最终轮次的执行,直接更新并显示结果
        addResults(actionId, thisResult);
        if (!isFinalRoundExecute.current) {
          setMiddleStepExecContext({
            parameters,
            result: thisResult,
            formValues,
          });
        }
      } else if (isSingleMultipleMode) {
        //如果是针对选中的每条记录都进行一个 action 执行
        const newResults = (objectIds == null) ?
          ({} as MultipleExecResults | ActionExecResult) : { ...results };
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        //@ts-ignore
        const resultForThisAction: ActionExecResult = (newResults[actionId] == null) ?
          {} : Object.assign({}, newResults[actionId]);
        (objectIds ?? selectedIds)?.forEach(objId => {
          resultForThisAction[objId] = result[objId];
        });
        newResults[actionId] = resultForThisAction;
        setResults(newResults);
        if (!isFinalRoundExecute.current) {
          // 多步骤 Action 当前仅支持单个对象的执行
          if (isSingleMode) {
            setMiddleStepExecContext({
              parameters,
              result: resultForThisAction as unknown as ExecResultProps,
              formValues
            });
          }
        }
      }
      if (isFinalRoundExecute.current) {
        setErrorCode(undefined);
        setExecutionError("");
        callbackWhenRefreshPage();
 
        if (shouldDisplaySingleResult && isSimpleAction) {
          const r = result[ids[0]];
          const { redirect } = r;
          // 后端执行返回的附件结果
          const { download: attachment } = r;
          // 如果 action 执行的结果需要弹出其他页面,那么在这里弹出
          // 这里的逻辑只适用于 simpleAction 的情况,
          // 其他情况弹出的逻辑在 ActionResultDisplay 组件中处理
          if (redirect != null) {
            setRedirect(redirect);
          } else {
            if (r.status === 'FAILED') {
              openErrorNotification(t('Action execution failed', {
                actionName: label,
                msg: r.execResult,
              }));
            } else {
              openSuccessMessage(t('Action execution success', { actionName: label }));
            }
          }
          if (attachment != null) {
            setDownload(attachment);
          }
        } else {
          if (json.status === 'FAILED') {
            openErrorNotification(t('Action execution failed', {
              actionName: label,
            }));
          } else {
            openSuccessMessage(t('Action execution success', { actionName: label }));
          }
        }
      }
    }).catch((error) => {
      const msg = error?.body?.message;
      const errCode = error?.body?.error;
      setExecutionError(msg);
      setErrorCode(parseInt(errCode));
    }).finally(() => {
      setRunning(false);
      setCurrentTab('rtab');
    });
  };
 
  const executeCallback = (isFinalRound: boolean): void => {
    // 默认显示当前 action 面板,隐藏所有其他面板
    setVisiblePopoverCallback(id.toString());
    isFinalRoundExecute.current = isFinalRound;
    if (!isFinalRound) {
      setMiddleStepExecContext(undefined);
    }
    executeAction(id);
  };
 
  const reRunButton = (objectIds?: Array<number>): ReactElement => (
    <Space size="middle" direction="horizontal">
      <span
        style={{ cursor: "pointer" }}
        onClick={() => {
          executeAction(id, objectIds);
        }}>
        <Tooltip
          title={t("Rerun the action")}
          className="small-clickable-icon"
        >
          <CaretRightOutlined />
        </Tooltip>
      </span>
    </Space>
  );
 
  const hasError = !!errorCode;
 
  const supportFineTuningClassName = action.supportFineTuning ? "multiple-round-action-drawer-container" : "";
  const drawerElem = (child: ReactElement): ReactElement => (
    <Drawer
      open={open}
      title={undefined}
      size="large"
      rootStyle={{ zIndex: zzIndex }}
      rootClassName={`action-button-popover-container ${supportFineTuningClassName}`}
      headerStyle={{ display: "none" }}
    >
      <Tabs
        activeKey={currentTab}
        defaultActiveKey="ptab"
        size={"small"}
        tabBarExtraContent={<CloseIcon onClick={closeCallback} />}
      >
        <TabPane tab={
          <>
            <SettingOutlined />
            <span onClick={() => setCurrentTab('ptab')}>{t("Run parameters")}</span>
          </>
        }
          key="ptab"
        >
          {running && <LargeSpin message={t("Running")} />}
          {!running && child}
        </TabPane>
        <TabPane tab={
          <>
            <FileDoneOutlined />
            <span onClick={() => setCurrentTab('rtab')}>{t("Execution results")}</span>
          </>
        }
          key="rtab"
        >
          <div className="action-result-popover">
            {
              !running && hasError &&
              (<div className="error-msg">
                <Space direction="horizontal">
                  <ExclamationCircleOutlined />
                  <span>{errorCode} {executionError}</span>
                </Space>
              </div>)
            }
            {(results[id] == null) &&
              <Alert
                rootClassName="action-message-info"
                message={t('Please run action first and result will be shown here')}
                showIcon
                type="info"
              />
            }
            {(results[id] != null) && !running && (
              <ActionResultDisplay
                mode={mode}
                key={name}
                action={action}
                result={results[id]}
                initDisplay={initExpandResultPanel}
                // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
                toggleDisplayCallback={(visible: boolean) => { }}
                titleRightExtraRenderFunc={reRunButton}
                fetchDataCallback={callbackWhenRefreshPage}
                zIndex={zzIndex + 1}
                isMiddleStepResult={!(isFinalRoundExecute.current)}
                middleStepExecContext={middleStepExecContext}
                fineTuningResult={fineTuningResult}
              />
            )}
            {!isFinalRoundExecute.current && middleStepExecContext && (results[id] != null) && (
              <MiddleStepExecInstruction
                setCurrentTab={setCurrentTab}
                executeCallback={(isFinalRound: boolean) => {
                  setCurrentTab('ptab');
                  executeCallback(isFinalRound);
                }}
                middleStepExecContext={middleStepExecContext}
                setFineTuningResult={(fineTuning: string) => setFineTuningResult(fineTuning)}
                zIndex={zzIndex}
              />
            )}
          </div>
        </TabPane>
      </Tabs>
    </Drawer>
  );
 
  const wrap = (child: ReactElement): ReactElement => (
    <span
      onClick={(e: React.MouseEvent<HTMLElement>) => stopPropagationAndPreventDefault(e)}
    >
      {actionElem}
      {drawerElem(child)}
    </span>
  );
 
  const actionComponent = (<span key={id}>{wrap(
    <ActionComponent
      setVisiblePopoverCallback={setVisiblePopoverCallback}
      action={action}
      element={actionElem ?? (<></>)}
      executeCallback={executeCallback}
      setResults={setResults}
      zIndex={zzIndex + 1}
      parameters={parameters}
      formValues={formValues}
      setFormValues={setFormValues}
      selectedData={selectedData}
      domainName={domainName}
      loadingParameters={loadingParameters}
      labelField={labelField}
    />
  )}</span>);
 
  const simpleActionComponent = (<>
    <span
      title={`${label ?? name} ${helpText ?? ""} `}
      key={id}
      className={"simple-action-icon"}
      onClick={(e) => {
        stopPropagationAndPreventDefault(e);
        executeAction(id);
      }}
    >{actionElem}</span>
    {redirect && (<RedirectComponent
      forMultiple={false}
      fetchDataCallback={() => {
        callbackWhenRefreshPage();
        // 将状态归位,支持可以再次点击
        setRedirect(undefined);
      }}
      redirect={redirect}
      zIndex={zzIndex + 1}
      showText={false}
    />)}
    {// 如果是报表,那么直接预览
      !!download && (<PreviewAction
        file={download}
        zIndex={zzIndex + 1}
        displayTextAndIcon={false}
      />)}
  </>);
 
  const confirmTypeToElemMapping: {
    [key in ActionConfirmType]: ReactElement
  } = {
    "NO_POPUP_NO_CONFIRM": simpleActionComponent,
    "NO_CONFIRM": actionComponent,
    "DISPLAY_CONFIRM": actionComponent,
  };
 
  return confirmTypeToElemMapping[confirmType];
};
 
export default DrawerActionButton;