All files / src/form/action PopoverActionButton.tsx

37.2% Statements 32/86
29.62% Branches 16/54
10.52% Functions 2/19
37.64% Lines 32/85

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                                        66x   66x   66x           3x   3x   3x   3x 3x 3x 3x 3x   3x   3x   3x   3x   3x     3x   3x   3x     3x               3x           3x           3x                                                                                                                                                   3x           3x                                 3x   3x 3x                                                                                                                                                     3x                                   3x                                                 3x           3x        
import React, { ReactElement, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { executeDynamicAction } from '@utils/FetchUtils';
import {
  ActionProps, ExecResultProps, MultipleExecResults, ActionExecResult, RecordProps,
  ActionConfirmType,  ActionExtInfoProp
} from "@props/RecordProps";
import { displaySingleResult } from '@utils/ComponentUtils';
import { Popover, Space, Tabs, Tooltip, Alert } 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 { SpecActionButtonProps } from './ActionButton';
 
const { TabPane } = Tabs;
 
const DefaultActionExtInfo : ActionExtInfoProp = { displayLabel: true, refreshPage: true };
 
const PopoverActionButton = (props: SpecActionButtonProps): ReactElement => {
  const {
    action, fetchDataCallback, selectedData, zIndex, domainName,
    mode, setVisiblePopoverCallback, ownerClass, ownerId, columnNameInOwnerClass,
    parameters, loadingParameters, actionElem, open,
    closeCallback, 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);
 
  // 标识是否为 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 shouldDisplaySingleResult = displaySingleResult(actionMode);
 
  function showPanel(action: ActionProps): void {
    const { id } = action;
    //Hide all other action execute result panel
    setVisiblePopoverCallback(id.toString());
  }
 
  const callbackWhenRefreshPage = (): void => {
    if (refreshPage) {
      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;
    executeDynamicAction({
      domainName, actionId, formValues, ids, ownerClass, ownerId, columnNameInOwnerClass
    })
      .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);
        } 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);
        }
        setErrorCode(undefined);
        setExecutionError("");
        callbackWhenRefreshPage();
        if (shouldDisplaySingleResult && isSimpleAction) {
          const r = result[ids[0]];
          const { redirect } = 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 }));
            }
          }
        } 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 = (): void => {
    // 默认显示当前 action 面板,隐藏所有其他面板
    setVisiblePopoverCallback(id.toString());
    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 wrap = (child: ReactElement): ReactElement => {
    return (
      <Popover
        trigger={["click"]}
        open={open}
        overlayClassName="action-button-popover-container"
        overlayStyle={{ zIndex: zIndex + 1, width: "613px" }}
        title={undefined}
        content={
          <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
                    className="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={zIndex + 1}
                  />
                )}
              </div>
            </TabPane>
          </Tabs>
        }
      >
        {actionElem}
      </Popover>
    );
  };
 
const actionComponent = (<span key={id}>{wrap(
    <ActionComponent
      setVisiblePopoverCallback={setVisiblePopoverCallback}
      action={action}
      element={actionElem ?? (<></>)}
      executeCallback={executeCallback}
      setResults={setResults}
      zIndex={zIndex + 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={zIndex + 1}
      showText={false}
    />)}
  </>);
 
  const confirmTypeToElemMapping: {
    [key in ActionConfirmType] : ReactElement
  } = {
    "NO_POPUP_NO_CONFIRM": simpleActionComponent,
    "NO_CONFIRM": actionComponent,
    "DISPLAY_CONFIRM": actionComponent,
  };
 
  return confirmTypeToElemMapping[confirmType];
};
 
export default PopoverActionButton;