All files / src/kernel ConstraintMapping.tsx

57.74% Statements 41/71
50.79% Branches 32/63
72.72% Functions 8/11
57.97% Lines 40/69

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                                            381x   381x 381x   381x 381x 381x 381x 381x                 38x   8x 8x 8x           8x 2x 2x 2x     2x                     8x 8x     2x 2x                       2x 2x 2x           2x             2x       6x             6x 6x         315x           3x 3x 3x         3x 3x 3x   3x 3x     3x         28x                                                  
import React from "react";
import { ErrorMsg } from "../components";
import { validConstrain } from "@utils/FetchUtils";
import {ConstraintsToRulesMapParams} from "@props/RecordProps";
import { humanReadableTitle, removePackagePart } from "@utils/StringUtils";
import { RuleObject, StoreValue } from "rc-field-form/lib/interface";
import { TRIGGER_BACKGROUND_VALIDATION_MINIMAL_TIME_MS } from '@config/base';
import { Rule } from "antd/lib/form";
import i18n from "@config/i18n";
import { getColumnTransKey, shouldUseFrontendTrans } from "@utils/ColumnsUtils";
 
/**
 * 将后端的数据库元数据转换为前端 antd 的表单校验逻辑,并调用错误显示的控件显示错误信息,
 * unique 校验采用后台定期发送 ajax 请求的方式,
 * 发送的时间间隔在常量 TRIGGER_BACKGROUND_VALIDATION_MINIMAL_TIME_MS 中定义
 * 当前支持 nullable 和 unique 两种元数据校验
 * @param props
 */
export function constraintsToRulesMap(props: ConstraintsToRulesMapParams): Rule | undefined {
  const {
    constraintKey, constraintValue, domainName, column, record,
    callback, nullableCheckBy, form
  } = props;
 
  const displayDomainName = humanReadableTitle(removePackagePart(domainName) ?? "");
  const columnTitle = shouldUseFrontendTrans(column) ?
        i18n.t(getColumnTransKey(domainName, column.key)) : column.title;
  let latestUniqueValue = "";
  let uniqueTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
  let latestValidateValue = "";
  let validateTimeout: ReturnType<typeof setTimeout> | undefined = undefined;
  switch (constraintKey) {
    case "email":
      return {
        required: (nullableCheckBy === constraintKey),
        type: "email",
        message: i18n.t("Please input a valid email address")
      };
    case "unique":
    case "validate":
      return {
        validator: (rule: RuleObject, value: StoreValue) => {
          const isUniqueConstraint = (constraintKey === "unique");
          if (isUniqueConstraint) {
            latestUniqueValue = value;
          } else E{
            latestValidateValue = value;
          }
          //下面的这一段是用于控制在 1 秒(TRIGGER_BACKGROUND_VALIDATION_MINIMAL_TIME_MS)内
          //只发送一个唯一校验的请求到后台
          const x = (): Promise<string> => {
            return new Promise((resolve) => {
              if (isUniqueConstraint) {
                Iif (uniqueTimeout != null) {
                  clearTimeout(uniqueTimeout);
                }
                uniqueTimeout = setTimeout(() => resolve('done!'),
                  TRIGGER_BACKGROUND_VALIDATION_MINIMAL_TIME_MS);
              } else E{
                if (validateTimeout != null) {
                  clearTimeout(validateTimeout);
                }
                validateTimeout = setTimeout(() => resolve('done!'),
                  TRIGGER_BACKGROUND_VALIDATION_MINIMAL_TIME_MS);
              }
            });
          };
          const shouldCheck = (isUniqueConstraint) ? (latestUniqueValue !== "" && latestUniqueValue != null) : true;
          if (shouldCheck) {
            //字段不为空时,调用后台的唯一校验接口
            //对于 validation,字段为空的时候, 也调用后台的校验逻辑
            return x().then(() => {
              return validConstrain({
                domainName,
                column: column,
                value: isUniqueConstraint ? latestUniqueValue : latestValidateValue,
                id: (record == null || record.id == null) ? undefined : record.id,
                record,
                formValues: form.getFieldsValue(),
                validationType: constraintKey
              }).catch((ex) => {
                const msg = i18n.t('ValidationFailed', { columnTitle, statusText: ex?.response?.statusText });
                return Promise.reject(msg);
              }).then((validResult => {
                const isValid = validResult.valid;
                callback?.(isValid);
                Iif (!isValid) {
                  const errorMsg = isUniqueConstraint ?
                    i18n.t("UniqueValidationFailed", { domainTitle: displayDomainName, columnTitle, value: latestUniqueValue }) :
                    validResult.message;
                  const errorComponent = <ErrorMsg errorMsg={i18n.t(errorMsg ?? "Invalid value")} />;
                  return Promise.reject(errorComponent);
                } else Iif (isValid && !isUniqueConstraint && validResult.message !== '' && validResult.message != null) {
                  //TODO 支持 valid 返回提示信息,即后台返回 valid: true 然后 message 不为空
                  //  使用 antd 表单的 warning 形式的校验, 待调研
                  //  https://github.com/ant-design/ant-design/issues/29312
                  //const errorMsg = validResult.message;
                  return Promise.resolve();
                } else {
                  return Promise.resolve();
                }
              }));
            });
          } else Iif (nullableCheckBy === constraintKey) {
            //字段有校验且不能为空的情况下,如果字段为空,直接返回为空的校验结果
            const errorComponent = <ErrorMsg errorMsg={i18n.t('ColumnIsRequired', { columnTitle })} />;
            callback?.(false);
            return Promise.reject(errorComponent);
          } else {
            //字段有校验且可以为空的情况下,如果字段为空,直接返校回通过
            callback?.(true);
            return Promise.resolve();
          }
        }
      };
    case "nullable":
      return {
        required: true,
        // Attention: Tricky logic here
        // If nullableCheckBy is unique, means nullable will be handle by unique constraint
        // So empty string is returned to avoid duplicate error message shown
        validator: (_: RuleObject, value: StoreValue) => {
          let invalid = false;
          let errorComp = null;
          Iif (["unique", "validate", "email"].includes(nullableCheckBy ?? "")) {
            invalid = false;
          }
          //1. unique and validate constraint will not handle empty string scenario, so it's handle here
          //2. if value is false --> false is a valid value for boolean controller
          Eif (("" === value || value == null) && value !== false) {
            invalid = true;
            errorComp = (<ErrorMsg errorMsg={i18n.t('ColumnIsRequired', { columnTitle })} />);
          }
          callback?.(!invalid);
          Iif (errorComp == null) {
            return Promise.resolve();
          }
          return Promise.reject(errorComp);
        }
      };
    case 'min':
    case 'max':
      return {
        validator: (_: RuleObject, value: StoreValue) => {
          let invalid = false;
          switch (constraintKey) {
            case 'max':
              invalid = (constraintValue != null && value > constraintValue);
              break;
            case 'min':
              invalid = (constraintValue != null && value < constraintValue);
              break;
          }
          callback?.(!invalid);
          if (invalid) {
            const errorMsg = i18n.t('NumberExceedValidationFailed', { key: constraintKey, value: constraintValue });
            const errorComponent = (
              <ErrorMsg errorMsg={errorMsg} />
            );
            return Promise.reject(errorComponent);
          }
          return Promise.resolve();
        }
      };
  }
}