lodash#unset TypeScript Examples

The following examples show how to use lodash#unset. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: withFriendlyChunkNames.ts    From nextclade with MIT License 6 votes vote down vote up
export default function withFriendlyChunkNames(nextConfig: NextConfig) {
  return addWebpackConfig(nextConfig, (nextConfig, webpackConfig, _options) => {
    if (
      typeof webpackConfig.optimization?.splitChunks !== 'boolean' &&
      webpackConfig.optimization?.splitChunks?.cacheGroups
    ) {
      unset(webpackConfig, 'optimization.splitChunks.cacheGroups.lib.name')
      unset(webpackConfig, 'optimization.splitChunks.cacheGroups.shared.name')
    }
    return webpackConfig
  })
}
Example #2
Source File: pivot-data-set.ts    From S2 with MIT License 5 votes vote down vote up
/**
   * Provide a way to append some drill-down data in indexesData
   * @param extraRowField
   * @param drillDownData
   * @param rowNode
   */
  public transformDrillDownData(
    extraRowField: string,
    drillDownData: DataType[],
    rowNode: Node,
  ) {
    const { columns, values: dataValues } = this.fields;
    const currentRowFields = Node.getFieldPath(rowNode, true);
    const nextRowFields = [...currentRowFields, extraRowField];
    const store = this.spreadsheet.store;

    // 1、通过values在data中注入额外的维度信息,并分离`明细数据`&`汇总数据`
    const transformedData = this.standardTransform(drillDownData, dataValues);

    const totalData = splitTotal(transformedData, {
      columns: this.fields.columns,
      rows: nextRowFields,
    });
    const originData = difference(transformedData, totalData);

    // 2. 检查该节点是否已经存在下钻维度
    const rowNodeId = rowNode?.id;
    const idPathMap = store.get('drillDownIdPathMap') ?? new Map();
    if (idPathMap.has(rowNodeId)) {
      // the current node has a drill-down field, clean it
      forEach(idPathMap.get(rowNodeId), (path: number[]) => {
        unset(this.indexesData, path);
      });
      deleteMetaById(this.rowPivotMeta, rowNodeId);
    }

    // 3、转换数据
    const {
      paths: drillDownDataPaths,
      indexesData,
      rowPivotMeta,
      colPivotMeta,
      sortedDimensionValues,
    } = transformIndexesData({
      rows: nextRowFields,
      columns,
      originData,
      totalData,
      indexesData: this.indexesData,
      sortedDimensionValues: this.sortedDimensionValues,
      rowPivotMeta: this.rowPivotMeta,
      colPivotMeta: this.colPivotMeta,
    });
    this.indexesData = indexesData;
    this.rowPivotMeta = rowPivotMeta;
    this.colPivotMeta = colPivotMeta;
    this.sortedDimensionValues = sortedDimensionValues;

    // 4、record data paths by nodeId
    // set new drill-down data path
    idPathMap.set(rowNodeId, drillDownDataPaths);
    store.set('drillDownIdPathMap', idPathMap);
  }
Example #3
Source File: pivot-data-set.ts    From S2 with MIT License 5 votes vote down vote up
/**
   * Clear drill down data by rowNodeId
   * rowNodeId is undefined => clear all
   * @param rowNodeId
   */
  public clearDrillDownData(rowNodeId?: string) {
    const store = this.spreadsheet.store;
    const idPathMap = store.get('drillDownIdPathMap');
    if (!idPathMap) {
      return;
    }
    const drillDownDataCache = store.get(
      'drillDownDataCache',
      [],
    ) as PartDrillDownDataCache[];

    if (rowNodeId) {
      // 1. 删除 indexesData 当前下钻层级对应数据
      const currentIdPathMap = idPathMap.get(rowNodeId);
      if (currentIdPathMap) {
        forEach(currentIdPathMap, (path) => {
          unset(this.indexesData, path);
        });
      }
      // 2. 删除 rowPivotMeta 当前下钻层级对应 meta 信息
      deleteMetaById(this.rowPivotMeta, rowNodeId);
      // 3. 删除下钻缓存路径
      idPathMap.delete(rowNodeId);

      // 4. 过滤清除的下钻缓存
      const restDataCache = filter(drillDownDataCache, (cache) =>
        idPathMap.has(cache?.rowId),
      );
      store.set('drillDownDataCache', restDataCache);

      // 5. 过滤清除的下钻层级
      const restDrillLevels = restDataCache.map((cache) => cache?.drillLevel);
      const drillDownFieldInLevel = store.get(
        'drillDownFieldInLevel',
        [],
      ) as PartDrillDownFieldInLevel[];
      const restFieldInLevel = drillDownFieldInLevel.filter((filed) =>
        includes(restDrillLevels, filed?.drillLevel),
      );

      store.set('drillDownFieldInLevel', restFieldInLevel);
    } else {
      idPathMap.clear();
      // 需要对应清空所有下钻后的dataCfg信息
      // 因此如果缓存有下钻前原始dataCfg,需要清空所有的下钻数据
      const originalDataCfg = this.spreadsheet.store.get('originalDataCfg');
      if (!isEmpty(originalDataCfg)) {
        this.spreadsheet.setDataCfg(originalDataCfg);
      }

      // 清空所有的下钻信息
      this.spreadsheet.store.set('drillItemsNum', -1);
      this.spreadsheet.store.set('drillDownDataCache', []);
      this.spreadsheet.store.set('drillDownFieldInLevel', []);
    }

    store.set('drillDownIdPathMap', idPathMap);
  }
Example #4
Source File: index.ts    From reaction-role with MIT License 5 votes vote down vote up
public async deleteMessage(message_id: string): Promise<IConfig> {
		unset(this.config, message_id);
		if (this.on_delete) await this.on_delete(message_id);
		return this.config;
	}
Example #5
Source File: i18n-utils.ts    From erda-ui with GNU Affero General Public License v3.0 5 votes vote down vote up
batchSwitchNamespace = async (originalResources: Obj<[Obj<Obj>, Obj<Obj>]>) => {
  const toSwitchWords = new Set<string>();
  const nsList = Object.values(originalResources).reduce<string[]>((acc, resource) => {
    const [zhResource] = resource;
    return acc.concat(Object.keys(zhResource));
  }, []);
  const { targetNs } = await inquirer.prompt({
    name: 'targetNs',
    type: 'list',
    message: 'Please select the new namespace name',
    choices: nsList.map((ns) => ({ value: ns, name: ns })),
  });
  // extract all i18n.r
  const promises = Object.values(internalSrcDirMap)
    .flat()
    .map((srcDir) => {
      return new Promise<void>((resolve) => {
        walker({
          root: srcDir,
          dealFile: (...args) => {
            extractPendingSwitchContent.apply(null, [...args, targetNs, toSwitchWords, resolve]);
          },
        });
      });
    });
  await Promise.all(promises);
  if (toSwitchWords.size) {
    const restorePromises = Object.values(internalSrcDirMap)
      .flat()
      .map((srcDir) => {
        return new Promise<void>((resolve) => {
          walker({
            root: srcDir,
            dealFile: (...args) => {
              switchSourceFileNs.apply(null, [...args, targetNs, toSwitchWords, resolve]);
            },
          });
        });
      });
    await Promise.all(restorePromises);
    // restore locale files
    for (const wordWithNs of toSwitchWords) {
      const wordArr = wordWithNs.split(':');
      const [currentNs, enWord] = wordArr.length === 2 ? wordArr : ['default', wordWithNs];
      const currentModuleName = getNamespaceModuleName(originalResources, currentNs);
      const targetModuleName = getNamespaceModuleName(originalResources, targetNs);
      if (!currentModuleName || !targetModuleName) {
        logError(`${currentModuleName} or ${targetModuleName} does not exist in locale files`);
        return;
      }

      // replace zh.json content
      const targetNsContent = originalResources[targetModuleName][0][targetNs];
      const currentNsContent = originalResources[currentModuleName][0][currentNs];
      if (!targetNsContent[enWord] || targetNsContent[enWord] === currentNsContent[enWord]) {
        targetNsContent[enWord] = currentNsContent[enWord];
      } else {
        // eslint-disable-next-line no-await-in-loop
        const confirm = await inquirer.prompt({
          name: 'confirm',
          type: 'confirm',
          message: `${chalk.red(enWord)} has translation in target namespace ${targetNs} with value ${chalk.yellow(
            targetNsContent[enWord],
          )}, Do you want to override it with ${chalk.yellow(currentNsContent[enWord])}?`,
        });
        if (confirm) {
          targetNsContent[enWord] = currentNsContent[enWord];
        }
      }
      currentNs !== targetNs && unset(currentNsContent, enWord);

      // replace en.json content
      const targetNsEnContent = originalResources[targetModuleName][1][targetNs];
      const currentNsEnContent = originalResources[currentModuleName][1][currentNs];
      if (!targetNsEnContent[enWord]) {
        targetNsEnContent[enWord] = currentNsEnContent[enWord];
      }
      currentNs !== targetNs && unset(currentNsEnContent, enWord);
    }
    for (const moduleName of Object.keys(originalResources)) {
      const [zhResource, enResource] = originalResources[moduleName];
      const localePath = internalLocalePathMap[moduleName];
      fs.writeFileSync(`${localePath}/zh.json`, JSON.stringify(zhResource, null, 2), 'utf8');
      fs.writeFileSync(`${localePath}/en.json`, JSON.stringify(enResource, null, 2), 'utf8');
    }
    logInfo('sort current locale files & remove unused translation');
    await writeLocaleFiles(false);
    logSuccess('switch namespace done.');
  } else {
    logWarn(`no ${chalk.red('i18n.r')} found in source code. program exit`);
  }
}
Example #6
Source File: pack-external-module.ts    From malagu with MIT License 4 votes vote down vote up
/**
 * Resolve the needed versions of production dependencies for external modules.
 * @this - The active plugin instance
 */
function getProdModules(externalModules: any[], packagePath: string, dependencyGraph: any, forceExcludes: string[], runtime?: string): any[] {
    const packageJson = readJSONSync(packagePath);
    const prodModules: string[] = [];

    // only process the module stated in dependencies section
    if (!packageJson.dependencies) {
        return [];
    }

    // Get versions of all transient modules

    for (const module of externalModules) {
        let moduleVersion = packageJson.dependencies[module.external];
        if (moduleVersion) {
            prodModules.push(`${module.external}@${moduleVersion}`);
            // Check if the module has any peer dependencies and include them too
            try {
                const modulePackagePath = join(
                    dirname(packagePath),
                    'node_modules',
                    module.external,
                    'package.json'
                );
                const { peerDependencies, peerDependenciesMeta } = readJSONSync(modulePackagePath);
                if (!isEmpty(peerDependencies)) {

                    if (!isEmpty(peerDependenciesMeta)) {
                        for (const key of Object.keys(peerDependencies)) {
                            if (peerDependenciesMeta[key]?.optional === true) {
                                unset(peerDependencies, key);
                            }
                        }
                    }
                    if (!isEmpty(peerDependencies)) {
                        const peerModules = getProdModules(Object.keys(peerDependencies).map(value => ({ external: value })),
                            packagePath,
                            dependencyGraph,
                            forceExcludes,
                            runtime
                        );
                        prodModules.push(...peerModules);
                    }
                }
            } catch (e) {
                console.log(`WARNING: Could not check for peer dependencies of ${module.external}`);
            }
        } else {
            if (!packageJson.devDependencies || !packageJson.devDependencies[module.external]) {
                // Add transient dependencies if they appear not in the service's dev dependencies
                const originInfo = get(dependencyGraph, 'dependencies', {})[module.origin] || {};
                moduleVersion = get(get(originInfo, 'dependencies', {})[module.external], 'version');
                if (typeof moduleVersion === 'object') {
                    moduleVersion = moduleVersion.optional;
                }
                if (!moduleVersion) {
                    moduleVersion = get(get(dependencyGraph, 'dependencies', {})[module.external], 'version');
                    if (!moduleVersion) {
                        console.log(`WARNING: Could not determine version of module ${module.external}`);
                    }
                }
                prodModules.push(moduleVersion ? `${module.external}@${moduleVersion}` : module.external);
            } else if (
                packageJson.devDependencies &&
                packageJson.devDependencies[module.external] &&
                !(forceExcludes.indexOf(module.external) !== -1)
            ) {
                // To minimize the chance of breaking setups we whitelist packages available on AWS here. These are due to the previously missing check
                // most likely set in devDependencies and should not lead to an error now.
                const ignoredDevDependencies = ['aws-sdk'];

                if (ignoredDevDependencies.indexOf(module.external) !== -1) {
                    // Runtime dependency found in devDependencies but not forcefully excluded
                    console.error(
                        `ERROR: Runtime dependency '${module.external}' found in devDependencies. Move it to dependencies or use forceExclude to explicitly exclude it.`
                    );
                    throw new Error(`Serverless-webpack dependency error: ${module.external}.`);
                }
                console.log(
                    `INFO: Runtime dependency '${module.external}' found in devDependencies. It has been excluded automatically.`
                );
            }
        }
    }
    return prodModules;
}
Example #7
Source File: i18n-extract.ts    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
function customFlush(done: () => void) {
  const enToZhWords: Obj = zhWordMap;
  // @ts-ignore api
  const { resStore } = this.parser;
  // @ts-ignore api
  const { resource, removeUnusedKeys, sort, defaultValue } = this.parser.options;

  Object.keys(resStore).forEach((lng) => {
    const namespaces = resStore[lng];
    // The untranslated English value and key are consistent
    if (lng === 'en') {
      Object.keys(namespaces).forEach((_ns) => {
        const obj = namespaces[_ns];
        Object.keys(obj).forEach((k) => {
          if (obj[k] === defaultValue) {
            obj[k] = k.replace('&#58;', ':');
          }
        });
      });
    }

    const filePath = resource.savePath.replace('{{lng}}', lng);
    let oldContent = lng === 'zh' ? originalZhJson : originalEnJson;

    // Remove obsolete keys
    if (removeUnusedKeys) {
      const namespaceKeys = flattenObjectKeys(namespaces);
      const oldContentKeys = flattenObjectKeys(oldContent);
      const unusedKeys = differenceWith(oldContentKeys, namespaceKeys, isEqual);

      for (let i = 0; i < unusedKeys.length; ++i) {
        unset(oldContent, unusedKeys[i]);
      }

      oldContent = omitEmptyObject(oldContent);
    }

    // combine old content
    let output = merge(namespaces, oldContent);
    if (sort) {
      output = sortObject(output);
    }

    // substitution zh locale
    if (lng === 'zh') {
      Object.keys(output).forEach((_ns) => {
        const obj = output[_ns];
        Object.keys(obj).forEach((k) => {
          if (obj[k] === defaultValue) {
            const zh = enToZhWords[k] || enToZhWords[`${_ns}:${k}`];
            if (zh) {
              obj[k] = zh;
            } else {
              logError(`there is untranslated content in zh.json: ${k}, please handle it manually`);
            }
          }
        });
      });
    }

    if (isExternal) {
      fs.writeFile(filePath, `${JSON.stringify(output, null, resource.jsonIndent)}\n`, 'utf8', (writeErr) => {
        if (writeErr) return logError(`writing failed:${lng}`, writeErr);
      });
    } else {
      const { default: defaultContent, ...restContent } = output;
      fs.writeFile(filePath, `${JSON.stringify(restContent, null, resource.jsonIndent)}\n`, 'utf8', (writeErr) => {
        if (writeErr) return logError(`writing failed:${lng}`, writeErr);
      });
      const defaultLocalePath = `${internalLocalePathMap.default}/${lng}.json`;

      fs.writeFile(
        defaultLocalePath,
        `${JSON.stringify({ default: defaultContent }, null, resource.jsonIndent)}\n`,
        'utf8',
        (writeErr) => {
          if (writeErr) return logError(`writing failed:${lng}`, writeErr);
        },
      );
    }
  });

  done();
}
Example #8
Source File: index.ts    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
export function enhanceAPI<T extends FN>(_apiFn: T, config?: APIConfig<T>) {
  const { globalKey, mock } = config || {};

  let _toggleLoading: undefined | ((p: boolean) => void);
  let _setData: Obj<Function> = {};

  const onResponse = (body: PICK_BODY<T>, params?: Parameters<T>[0]) => {
    // standard response
    if ('success' in body && ('err' in body || 'data' in body)) {
      const { data, success, err, userInfo } = body;
      if (userInfo) {
        setUserMap(userInfo);
      }

      if (isObject(data) && Object.keys(params ?? {}).includes('pageNo')) {
        if ('list' in data && 'total' in data) {
          if (data.list === null) {
            data.list = [];
          }
          const { total } = data;
          const { pageNo, pageSize } = params ?? ({} as Parameters<T>[0]);
          const hasMore = Math.ceil(total / +pageSize) > +pageNo;
          (data as any).paging = { pageNo, pageSize, total, hasMore };
        }
      }

      if (success) {
        const successMsg = params?.$options?.successMsg || config?.successMsg;
        successMsg && getConfig('onAPISuccess')?.(successMsg);
      } else if (err) {
        const errorMsg = err?.msg || params?.$options?.errorMsg || config?.errorMsg;
        errorMsg && getConfig('onAPIFail')?.('error', errorMsg);
      }
    }
  };

  let apiFn = _apiFn;
  if (mock) {
    apiFn = ((params?: Parameters<T>[0]) => {
      return new Promise((resolve) =>
        setTimeout(() => resolve({ success: true, data: mock(params), err: null }), Math.floor(Math.random() * 300)),
      );
    }) as any;
  }

  const service = (params?: Parameters<T>[0]): ReturnType<T> =>
    apiFn(params).then((body: PICK_BODY<T>) => {
      onResponse(body, params);
      return body;
    });

  /**
   * when calling useData or useState, it may be called by multiple components at the same time,
   * so when fetch finished, each setData function should be invoked one by one
   * @param subscribeIdRef each subscribe maintains one unique id, used to unsubscribe when unmount component
   * @param setData subscribe callback, which to set state data
   */
  const subscribe = (subscribeIdRef: React.MutableRefObject<string>, setData: Function) => {
    if (!subscribeIdRef.current) {
      const uid = uniqueId('id_');
      subscribeIdRef.current = uid;
      _setData = { ..._setData, [uid]: setData };
    }
  };

  return Object.assign(service, {
    fetch: (params?: Parameters<T>[0]): ReturnType<T> => {
      _toggleLoading?.(true);
      return apiFn(params)
        .then((body: PICK_BODY<T>) => {
          onResponse(body, params);
          forEach(_setData, (fn) => fn(body?.data));
          return body;
        })
        .finally(() => {
          _toggleLoading?.(false);
        });
    },
    useData: (): PICK_DATA<T> | null => {
      const [data, setData] = React.useState(null);
      const subscribeIdRef = React.useRef('');

      React.useEffect(() => {
        return () => {
          unset(_setData, subscribeIdRef.current);
        };
      }, []);

      if (globalKey) {
        subscribe(subscribeIdRef, (d: PICK_DATA<T>) => apiDataStore.reducers.setData(globalKey, d));
        return apiDataStore.useStore((s) => s.data[globalKey]) as PICK_DATA<T>;
      }

      subscribe(subscribeIdRef, setData);
      return data;
    },
    useLoading: (): boolean => {
      const [loading, setLoading] = React.useState(false);

      if (globalKey) {
        _toggleLoading = (isLoading: boolean) => apiDataStore.reducers.setLoading(globalKey, isLoading);
        return apiDataStore.useStore((s) => !!s.loading[globalKey]);
      }
      _toggleLoading = setLoading;

      return loading;
    },
    useState: (): [PICK_DATA<T> | null, boolean] => {
      const [loading, setLoading] = React.useState(false);
      const [data, setData] = React.useState(null);
      const subscribeIdRef = React.useRef('');

      React.useEffect(() => {
        return () => {
          unset(_setData, subscribeIdRef.current);
        };
      }, []);

      if (globalKey) {
        _toggleLoading = (isLoading: boolean) => apiDataStore.reducers.setLoading(globalKey, isLoading);
        subscribe(subscribeIdRef, (d: PICK_DATA<T>) => apiDataStore.reducers.setData(globalKey, d));
        return apiDataStore.useStore((s) => [s.data[globalKey], !!s.loading[globalKey]]) as [PICK_DATA<T>, boolean];
      }
      _toggleLoading = setLoading;

      subscribe(subscribeIdRef, setData);
      return [data, loading];
    },
    getData: () => {
      return globalKey ? apiDataStore.getState((s) => s.data[globalKey]) : undefined;
    },
    clearData: () => {
      return globalKey ? apiDataStore.reducers.setData(globalKey, undefined) : undefined;
    },
  });
}
Example #9
Source File: resource.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
ApiResource = (props: Merge<CP_API_RESOURCE.Props, API_SETTING.IResourceProps>) => {
  const { quotePathMap, onQuoteChange, apiName, apiDetail } = props;

  const [
    { currentMethod, apiMethodDetail, curTabKey, apiDisplayName, tempApiData, pathParams, open },
    updater,
    update,
  ] = useUpdate({
    currentMethod: API_METHODS.get as API_SETTING.ApiMethod,
    apiMethodDetail: {} as Obj,
    curTabKey: API_RESOURCE_TAB.Summary,
    apiDisplayName: '',
    tempApiData: {},
    pathParams: null,
    open: false,
  });

  const { apiData, execOperation, operations } = props?.data || {};
  const formRef = React.useRef<IFormExtendType>({} as any);

  const [openApiDoc, apiLockState, formErrorNum] = apiDesignStore.useStore((s) => [
    s.openApiDoc,
    s.apiLockState,
    s.formErrorNum,
  ]);
  const { updateOpenApiDoc, updateFormErrorNum } = apiDesignStore;

  const dataPath = React.useMemo(() => [apiName, currentMethod], [apiName, currentMethod]);

  React.useEffect(() => {
    if (apiData?.apiMethod) {
      // 适配组件化协议的内容
      updater.apiMethodDetail(apiData);
      updater.tempApiData(!isEmpty(tempApiData) ? tempApiData : apiData);
    } else {
      // 点击左侧api列表导致的内容变化,更新第一个不为空的method,更新resource内容,resource内容切换到summary
      let initialMethod = API_METHODS.get;
      some(API_METHODS, (method) => {
        if (!isEmpty(apiDetail[method])) {
          initialMethod = method;
          return true;
        } else {
          return false;
        }
      });
      updater.currentMethod(initialMethod);

      const _apiMethodDetail = apiDetail[initialMethod] || {};
      updater.apiMethodDetail(_apiMethodDetail);
    }
    updater.pathParams(null);
    updater.curTabKey(API_RESOURCE_TAB.Summary); // API切换后重置tab
  }, [apiDetail, apiData, updater, tempApiData]);

  React.useEffect(() => {
    let _name = '';
    if (apiData) {
      _name = tempApiData.apiName || apiData?.apiName;
    } else if (apiName) {
      _name = apiName;
    }
    if (_name) {
      updater.apiDisplayName(_name);
      setTimeout(() => {
        formRef.current.setFieldsValue({ apiName: _name });
      });
    }
  }, [apiData, apiName, tempApiData.apiName, updater]);

  React.useEffect(() => {
    if (!pathParams) return;

    const prefixPath = !apiData ? ['paths', apiName] : [];
    const tempDetail = produce(openApiDoc, (draft) => {
      if (!isEmpty(pathParams)) {
        const _pathParams = map(pathParams, (name) => {
          return {
            ...DEFAULT_PATH_PARAM,
            name,
          };
        });
        set(draft, [...prefixPath, 'parameters'], _pathParams);
      } else {
        set(draft, [...prefixPath, 'parameters'], []);
        updater.pathParams(null);
      }
    });
    updateOpenApiDoc(tempDetail);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [apiData, apiName, pathParams, updateOpenApiDoc]);

  const setFieldHandle = React.useCallback(
    (key: string, fieldData: Obj, extraProps?: Obj) => {
      const prefixPath = !apiData?.apiMethod ? ['paths', ...dataPath] : [];
      const baseFormData = !apiData?.apiMethod ? openApiDoc : tempApiData;

      if (onQuoteChange && extraProps?.typeQuotePath) {
        const newTypeQuotePath = extraProps?.typeQuotePath ? ['paths', ...dataPath, ...extraProps.typeQuotePath] : [];

        const tempQuotePathMap = produce(quotePathMap, (draft) => {
          if (extraProps?.quoteTypeName) {
            const { quoteTypeName } = extraProps;
            draft[quoteTypeName] = draft[quoteTypeName] || [];
            draft[quoteTypeName].push(newTypeQuotePath);
          }
        });
        onQuoteChange(tempQuotePathMap);
      }

      const tempDetail = produce(baseFormData, (draft) => {
        if (key !== 'responses') {
          const responseData = get(draft, [...prefixPath, 'responses']);
          // 设置默认的response
          if (!responseData) {
            set(draft, [...prefixPath, 'responses'], DEFAULT_RESPONSE);
          }

          if (key === 'summary') {
            set(draft, [...prefixPath, fieldData?.propertyName], fieldData?.propertyData);
            if (fieldData?.propertyName === 'operationId') {
              set(draft, [...prefixPath, 'summary'], fieldData?.propertyData);
            }
            if (fieldData?.newTags) {
              set(draft, 'tags', fieldData?.newTags);
              message.success(i18n.t('dop:category created successfully'));
            }
          } else if (key === 'query' || key === 'header') {
            set(draft, [...prefixPath, 'parameters'], fieldData?.parameters);
          }
        }
        if (key === 'responses' || key === 'requestBody') {
          set(draft, [...prefixPath, key], fieldData[key]);
        }
        // 设置默认的operationId
        if (!get(draft, [...prefixPath, 'operationId'])) {
          const _operationIdList: string[] = [];
          const { paths } = openApiDoc;
          forEach(keys(paths), (pathName: string) => {
            const methodData = paths[pathName];
            forEach(keys(methodData), (item) => {
              methodData[item]?.operationId && _operationIdList.push(methodData[item]?.operationId);
            });
          });
          let _operationId = 'operationId';
          while (_operationIdList.includes(_operationId)) {
            _operationId += '1';
          }
          set(draft, [...prefixPath, 'operationId'], _operationId);
        }
        // 设置默认的tags
        if (!get(draft, [...prefixPath, 'tags'])) {
          set(draft, [...prefixPath, 'tags'], ['other']);
        }
        if (!draft.tags) {
          set(draft, 'tags', [{ name: 'other' }]);
        }
      });

      if (!apiData?.apiMethod) {
        updateOpenApiDoc(tempDetail);
      } else {
        updater.tempApiData(tempDetail);
      }
    },
    [apiData, dataPath, onQuoteChange, openApiDoc, quotePathMap, tempApiData, updateOpenApiDoc, updater],
  );

  const iconClassMap = React.useMemo(() => {
    const classMap = {};
    const emptyIcon = {};
    forEach(API_METHODS, (method) => {
      const tempMethodDetail = get(openApiDoc, ['paths', apiName, method]);
      const emptyMethodClass = !tempMethodDetail || isEmpty(tempMethodDetail) ? 'btn-icon-empty' : '';
      classMap[method] = `btn-icon btn-icon-${method} ${emptyMethodClass}`;
      emptyIcon[method] = `${emptyMethodClass}`;
    });
    return { classMap, emptyIcon };
  }, [apiName, openApiDoc]);

  const deleteMethod = React.useCallback(
    (methodKey: API_METHODS) => {
      updater.open(false);
      const tempDetail = produce(openApiDoc, (draft) => {
        unset(draft, ['paths', apiName, methodKey]);
      });
      updateOpenApiDoc(tempDetail);
      if (currentMethod === methodKey) {
        updater.apiMethodDetail({});
      }
    },
    [apiName, currentMethod, openApiDoc, updateOpenApiDoc, updater],
  );

  const onApiNameChange = React.useCallback(
    (name: string) => {
      updater.apiDisplayName(name);
      props.onApiNameChange(name);

      // 获取api中的path parameters
      const _pathParams = map(name.match(pathParamReg), (item) => {
        return item.slice(1, item.length - 1);
      });
      updater.pathParams(_pathParams);

      if (onQuoteChange && !isEmpty(quotePathMap)) {
        const tempQuotePathMap = produce(quotePathMap, (draft) => {
          forEach(keys(draft), (k) => {
            forEach(draft[k], (path, i) => {
              if (path.includes(apiDisplayName)) {
                const oldPathArray = path.slice(0, path.length - 1);
                draft[k][i] = [...oldPathArray, name];
              }
            });
          });
        });
        onQuoteChange(tempQuotePathMap);
      }

      if (!apiData?.apiMethod) {
        const tempDetail = produce(openApiDoc, (draft) => {
          const apiTempData = get(draft, ['paths', apiName]);
          set(draft, ['paths', name], apiTempData);
          unset(draft, ['paths', apiName]);
        });
        updateOpenApiDoc(tempDetail);
      } else {
        const tempDetail = produce(tempApiData, (draft) => {
          set(draft, 'apiName', name);
        });
        updater.tempApiData(tempDetail);
      }
    },
    [
      apiData,
      apiDisplayName,
      apiName,
      onQuoteChange,
      openApiDoc,
      props,
      quotePathMap,
      tempApiData,
      updateOpenApiDoc,
      updater,
    ],
  );

  const hasBody = !['get', 'head'].includes(currentMethod);

  const onSaveApiData = React.useCallback(() => {
    execOperation &&
      execOperation(operations.submit, {
        apiData: { ...tempApiData, apiMethod: apiData?.apiMethod, apiName: tempApiData?.name || apiData?.apiName },
      });
  }, [apiData, execOperation, operations, tempApiData]);

  const fieldList = React.useMemo(() => {
    const existApiPathNames = keys(openApiDoc?.paths).filter((n) => n !== apiDisplayName);
    return [
      {
        type: Input,
        name: 'apiName',
        colSpan: 24,
        required: false,
        isHoldLabel: false,
        wrapperClassName: 'pl-0',
        customProps: {
          className: 'name-input',
          maxLength: INPUT_MAX_LENGTH,
          disabled: apiLockState,
          addonBefore: apiData?.apiMethod,
          placeholder: i18n.t('Please enter the {name}', { name: i18n.t('API path') }),
          onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
            const newApiName = e.target.value;

            if (newApiName && !existApiPathNames.includes(newApiName) && newApiName.startsWith('/')) {
              onApiNameChange(newApiName);
              updateFormErrorNum(0);
            } else {
              updateFormErrorNum(1);
            }
          },
        },
        rules: [
          {
            validator: (_rule: any, value: string, callback: (msg?: string) => void) => {
              if (existApiPathNames.includes(value)) {
                callback(i18n.t('the same {key} exists', { key: i18n.t('Name') }));
              } else if (!value) {
                callback(i18n.t('can not be empty'));
              } else if (!value.startsWith('/')) {
                callback(i18n.t('dop:path must start with /'));
              } else {
                callback();
              }
            },
          },
        ],
      },
    ];
  }, [apiData, apiDisplayName, apiLockState, onApiNameChange, openApiDoc, updateFormErrorNum]);

  const popconfirmRef = React.useRef(null as any);
  const selectRef = React.useRef(null) as any;

  useClickAway(selectRef, () => {
    updater.open(false);
  });

  const maskClick = React.useCallback(
    (e: any) => {
      e.stopPropagation();
      updater.open(true);
    },
    [updater],
  );

  const labelClick = React.useCallback(
    (e: any, methodKey: string) => {
      e.stopPropagation();
      updater.open(false);

      const nextHandle = () => {
        const _apiMethodDetail = get(openApiDoc, ['paths', apiName, methodKey]) || {};
        update({
          currentMethod: methodKey as API_SETTING.ApiMethod,
          curTabKey: API_RESOURCE_TAB.Summary,
          apiMethodDetail: _apiMethodDetail,
        });

        updateFormErrorNum(0);
        formRef.current.setFieldsValue({ apiName });
      };

      if (formErrorNum > 0) {
        confirm({
          title: i18n.t('dop:Are you sure to leave, with the error message not saved?'),
          onOk() {
            nextHandle();
          },
        });
      } else {
        nextHandle();
      }
    },
    [apiName, formErrorNum, openApiDoc, update, updateFormErrorNum, updater],
  );

  const renderSelectMenu = () => {
    return (
      <div className="select-container" ref={selectRef}>
        {!apiData?.apiMethod ? (
          <Select
            getPopupContainer={(triggerNode) => triggerNode.parentElement as HTMLElement}
            style={{ marginRight: '8px', width: '141px' }}
            defaultValue={currentMethod}
            open={open}
            value={currentMethod}
          >
            {map(API_METHODS, (methodKey) => {
              let item = (
                <div className="circle-container flex-all-center">
                  {iconClassMap.emptyIcon[methodKey] ? (
                    <div className={`${iconClassMap.classMap[methodKey]}`} />
                  ) : (
                    <ErdaIcon type="check" className={iconClassMap.classMap[methodKey]} />
                  )}
                </div>
              );
              if (get(openApiDoc, ['paths', apiName, methodKey])) {
                item = (
                  <Popconfirm
                    title={`${i18n.t('common:confirm to delete')}?`}
                    onConfirm={() => deleteMethod(methodKey)}
                    placement="right"
                    // disabled={apiLockState}
                    overlayClassName="popconfirm-container"
                    getPopupContainer={() => popconfirmRef?.current}
                    onCancel={(e: any) => {
                      e.stopPropagation();
                      updater.open(false);
                    }}
                  >
                    {item}
                  </Popconfirm>
                );
              }
              return (
                <Option value={methodKey} key={methodKey}>
                  <div
                    className={`api-method-option ${currentMethod === methodKey ? 'api-method-option-active' : ''}`}
                    key={methodKey}
                  >
                    <div
                      onClick={(e) => {
                        e.stopPropagation();
                        labelClick(e, methodKey);
                      }}
                    >
                      {methodKey.toUpperCase()}
                    </div>
                    {item}
                  </div>
                </Option>
              );
            })}
          </Select>
        ) : undefined}
        <div className="mask" onClick={maskClick} />
      </div>
    );
  };

  const onTabChange = (tabKey: string) => {
    const nextHandle = () => {
      updater.curTabKey(tabKey as API_RESOURCE_TAB);
      const _apiMethodDetail = get(openApiDoc, ['paths', apiName, currentMethod]) || {};
      updater.apiMethodDetail(_apiMethodDetail);
      updateFormErrorNum(0);
      formRef.current.setFieldsValue({ apiName });
    };

    if (formErrorNum > 0) {
      confirm({
        title: i18n.t('dop:Are you sure to leave, with the error message not saved?'),
        onOk() {
          nextHandle();
        },
      });
    } else {
      nextHandle();
    }
  };

  return (
    <div className="api-resource" ref={popconfirmRef}>
      <div className="popover">
        {renderSelectMenu()}
        <FormBuilder ref={formRef} className="w-full">
          <Fields fields={fieldList} />
        </FormBuilder>
      </div>

      <div className="api-resource-tabs">
        <Tabs activeKey={curTabKey} onChange={onTabChange}>
          <TabPane tab={API_RESOURCE_TAB.Summary} key={API_RESOURCE_TAB.Summary}>
            <ResourceSummary formData={apiMethodDetail} onChange={setFieldHandle} isEditMode={!apiLockState} />
          </TabPane>
          <TabPane tab={API_RESOURCE_TAB.Params} key={API_RESOURCE_TAB.Params}>
            <QueryParamsConfig
              formData={apiMethodDetail}
              paramIn="query"
              onChange={setFieldHandle}
              isEditMode={!apiLockState}
              resourceKey={curTabKey}
            />
          </TabPane>
          <TabPane tab={API_RESOURCE_TAB.Headers} key={API_RESOURCE_TAB.Headers}>
            <QueryParamsConfig
              formData={apiMethodDetail}
              paramIn="header"
              onChange={setFieldHandle}
              isEditMode={!apiLockState}
              resourceKey={curTabKey}
            />
          </TabPane>
          <TabPane tab={API_RESOURCE_TAB.Body} key={API_RESOURCE_TAB.Body} disabled={!hasBody}>
            {hasBody && (
              <ResponseConfig
                formData={apiMethodDetail}
                paramIn="requestBody"
                onChange={setFieldHandle}
                dataPath={dataPath}
                isEditMode={!apiLockState}
                resourceKey={curTabKey}
              />
            )}
          </TabPane>
          <TabPane tab={API_RESOURCE_TAB.Response} key={API_RESOURCE_TAB.Response}>
            <ResponseConfig
              formData={apiMethodDetail}
              paramIn="responses"
              onChange={setFieldHandle}
              dataPath={dataPath}
              isEditMode={!apiLockState}
              resourceKey={curTabKey}
            />
          </TabPane>
          {apiData?.apiMethod && <TabPane tab={API_RESOURCE_TAB.Test} key={API_RESOURCE_TAB.Test} />}
        </Tabs>
        {apiData?.apiMethod && (
          <div className="flex items-center flex-wrap justify-end">
            <Button type="primary" onClick={onSaveApiData}>
              {i18n.t('Save')}
            </Button>
          </div>
        )}
      </div>
    </div>
  );
}
Example #10
Source File: basic-params-config.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
PropertyItemForm = React.memo((props: IPropertyItemForm) => {
  const { formData, onChange, formType = 'Query', isEditMode = true, index } = props;

  const [
    {
      detailVisible,
      curPropertyType,
      innerParamList,
      dataTempStorage,
      paramListTempStorage,
      paramsModalVisible,
      arrayItemDataStorage,
      errorMap,
      formErrorMap,
    },
    updater,
    update,
  ] = useUpdate({
    detailVisible: props.detailVisible || false,
    curPropertyType: formData.type || initialTypeMap[formType] || ('string' as API_SETTING.PropertyType),
    innerParamList: [],
    dataTempStorage: {},
    paramListTempStorage: [],
    paramsModalVisible: false,
    arrayItemDataStorage: null,

    errorMap: {
      name: false,
      maxLength: false,
      minLength: false,
      maximum: false,
      minimum: false,
    },
    formErrorMap: {},
  });

  const formRef = React.useRef<IFormExtendType>({} as any);
  const paramListTempStorageRef = React.useRef<any[]>([]);
  const dataTempStorageRef = React.useRef<Obj>({});

  React.useImperativeHandle(paramListTempStorageRef, () => paramListTempStorage);
  React.useImperativeHandle(dataTempStorageRef, () => dataTempStorage);

  const isCurTypeOf = React.useCallback(
    (type: BASE_DATA_TYPE) => {
      return curPropertyType === type || get(props, ['extraDataTypes', curPropertyType, 'type']) === type;
    },
    [curPropertyType, props],
  );

  const isBasicDataType = React.useMemo(() => {
    return some(BASE_DATA_TYPE, (item) => item === curPropertyType);
  }, [curPropertyType]);

  const getRefTypePath = React.useCallback((data: Obj): string => {
    return get(data, [QUOTE_PREFIX, 0, '$ref']) || get(data, [QUOTE_PREFIX_NO_EXTENDED]) || '';
  }, []);

  const getExampleData = React.useCallback(
    (data: Obj, extraTypes?: Obj) => {
      if (!data) return '';
      const _extraTypes = extraTypes || props?.extraDataTypes;

      const refTypePath = getRefTypePath(data);
      const customType = refTypePath.split('/').slice(-1)[0];
      const customTypeData = get(_extraTypes, [customType]) || customType || {};

      if (typeof customTypeData === 'string') {
        const _type =
          get(props, ['allExtraDataTypes', customType, 'type']) || get(props, ['extraDataTypes', customType, 'type']);
        return _type === 'array' ? [] : {};
      }

      const curType = data.type || customTypeData.type;

      if (curType === 'object') {
        const newExtraTypes = produce(_extraTypes, (draft) => {
          draft && (draft[customType] = null);
        });

        const newExample: Obj = refTypePath ? getExampleData(customTypeData, newExtraTypes) : {};

        const customProperties = data.properties || {};
        forEach(keys(customProperties), (pName) => {
          const propertyItem = customProperties[pName];
          newExample[pName] = getExampleData(propertyItem, newExtraTypes);
        });

        return newExample;
      } else if (curType === 'array') {
        if (refTypePath) {
          const newItemExtraTypes = produce(_extraTypes, (draft) => {
            draft && (draft[customType] = null);
          });
          return getExampleData(customTypeData, newItemExtraTypes);
        } else {
          return [getExampleData(data.items, _extraTypes)];
        }
      } else if (refTypePath && customTypeData.example !== undefined) {
        return customTypeData.example;
      } else if (data.example !== undefined) {
        return data.example;
      } else {
        return DATATYPE_EXAMPLE_MAP[curType] || '';
      }
    },
    [getRefTypePath, props],
  );

  // 表单初始化加载
  React.useEffect(() => {
    formRef.current.resetFields();
    update({
      errorMap: {
        name: false,
        maxLength: false,
        minLength: false,
        maximum: false,
        minimum: false,
      },
      formErrorMap: {},
    });

    const innerProperties = formData?.properties;
    const requiredNames = formData?.required || [];
    updater.dataTempStorage(formData);

    const tempList = map(keys(innerProperties), (pKey: string) => {
      const _temp = { ...innerProperties[pKey] };
      if (formData?.type === 'object' && Array.isArray(formData?.required)) {
        _temp[API_PROPERTY_REQUIRED] = requiredNames.includes(pKey);
      }
      _temp[API_FORM_KEY] = pKey;

      return _temp;
    });
    updater.innerParamList(tempList);
    updater.paramListTempStorage(tempList);

    let _curPropertyType = formData?.type || formData?.schema?.type || 'object';

    const tempFormData = { ...formData };

    const refTypePath = getRefTypePath(formData);
    if (refTypePath) {
      const customType = refTypePath.split('/').slice(-1)[0];
      tempFormData.type = customType;
      _curPropertyType = customType;
    }

    updater.curPropertyType(_curPropertyType);

    setTimeout(() => {
      formRef.current!.setFieldsValue(tempFormData);
      if (isEmpty(formData)) {
        const _formData = formRef.current!.getFieldsValue();
        updater.dataTempStorage(_formData);
      } else {
        updater.dataTempStorage(formData);
      }
    });
  }, [updater, formData, getRefTypePath, update]);

  const AllDataTypes = React.useMemo(() => {
    return filter(props?.allDataTypes, (item) => item !== dataTempStorage[API_FORM_KEY]) || [];
  }, [dataTempStorage, props]);

  const onToggleDetail = React.useCallback(
    (visible) => {
      if (visible) {
        const omitList = getRefTypePath(dataTempStorage) ? ['type', API_FORM_KEY] : [API_FORM_KEY];
        const tempFormData = omit(dataTempStorage, omitList);
        const example = getExampleData(tempFormData);
        setTimeout(() => formRef.current!.setFieldsValue({ ...tempFormData, example }));
      }
      updater.dataTempStorage(dataTempStorageRef.current);
      if (curPropertyType === 'array' && arrayItemDataStorage) {
        updater.dataTempStorage(arrayItemDataStorage);
      } else {
        updater.dataTempStorage(dataTempStorageRef.current);
      }
      updater.innerParamList(paramListTempStorageRef.current);
      updater.detailVisible(visible);
    },
    [arrayItemDataStorage, curPropertyType, dataTempStorage, getExampleData, getRefTypePath, updater],
  );

  const propertyNameMap = React.useMemo(() => {
    const list = props?.siblingProperties || [];
    return map(list, (item) => item[API_FORM_KEY]);
  }, [props]);

  const setFields = React.useCallback(
    (fieldProps: ISetFieldProps) => {
      const { propertyKey = '', propertyData } = fieldProps;
      if (propertyKey === API_MEDIA && props.onSetMediaType) {
        props.onSetMediaType(fieldProps as { propertyKey: string; propertyData: string });
        return;
      }
      if (propertyKey === 'operation') {
        updater.detailVisible(propertyData);
        return;
      }
      if (formRef?.current) {
        const newFormData = produce(dataTempStorageRef.current, (draft: any) => {
          if (
            curPropertyType === 'array' &&
            !['description', 'type', API_FORM_KEY, API_PROPERTY_REQUIRED].includes(propertyKey)
          ) {
            set(draft, `items.${propertyKey}`, propertyData);
          } else {
            set(draft, propertyKey, propertyData);
          }

          if (propertyKey === 'type') {
            const curType = propertyData;
            updater.curPropertyType(curType);
            unset(draft, QUOTE_PREFIX);
            unset(draft, QUOTE_PREFIX_NO_EXTENDED);
            unset(draft, 'default');
            unset(draft, 'enum');

            if (curType === 'object' || curType === 'array') {
              unset(draft, 'pattern');
              unset(draft, 'maxLength');
              unset(draft, 'minLength');
              unset(draft, 'format');
              unset(draft, 'maximum');
              unset(draft, 'minimum');

              if (curType === 'object') {
                set(draft, 'properties', {});
                set(draft, 'required', []);
                unset(draft, 'items');
              }
              if (curType === 'array') {
                const tempItemData = {
                  type: 'string',
                  example: 'Example',
                };
                tempItemData[API_FORM_KEY] = 'items';
                set(draft, 'items', tempItemData);
                unset(draft, 'properties');
                updater.innerParamList([]);
                updater.paramListTempStorage([]);
              }
            } else if (['boolean', 'string', 'number', 'integer'].includes(curType)) {
              unset(draft, 'items');
              unset(draft, 'properties');
              unset(draft, 'required');
              if (curType !== 'number') {
                unset(draft, 'format');
                unset(draft, 'maximum');
                unset(draft, 'minimum');
              }
              if (curType !== 'string') {
                unset(draft, 'pattern');
                unset(draft, 'maxLength');
                unset(draft, 'minLength');
              }
              updater.innerParamList([]);
              updater.paramListTempStorage([]);
            }
            set(draft, 'example', DATATYPE_EXAMPLE_MAP[curType]);
          }
        });

        if (propertyKey === 'type') {
          if (!DATATYPE_EXAMPLE_MAP[propertyData]) {
            const customTypeData = get(props, ['extraDataTypes', propertyData]) || {};
            const _newTypeData = {
              ...omit(dataTempStorage, [QUOTE_PREFIX, QUOTE_PREFIX_NO_EXTENDED]),
              example: customTypeData.example || getExampleData(customTypeData),
              properties: customTypeData.type === 'object' ? {} : undefined,
              required: dataTempStorage.required,
              type: customTypeData.type,
            };
            // object类型的引用类型支持可拓展编辑
            if (customTypeData.type === 'object') {
              _newTypeData[QUOTE_PREFIX] = [{ $ref: `#/components/schemas/${propertyData}` }];
            } else {
              _newTypeData[QUOTE_PREFIX_NO_EXTENDED] = `#/components/schemas/${propertyData}`;
            }

            const typeQuotePath = _newTypeData[API_FORM_KEY];

            update({
              dataTempStorage: _newTypeData,
              innerParamList: [],
              paramListTempStorage: [],
            });

            formRef.current.setFieldsValue({ ..._newTypeData, type: propertyData });
            onChange(dataTempStorage[API_FORM_KEY], _newTypeData, { typeQuotePath, quoteTypeName: propertyData });
            return;
          }
        }

        updater.dataTempStorage(newFormData);
        onChange(dataTempStorage[API_FORM_KEY], newFormData);
      }
    },
    [curPropertyType, dataTempStorage, onChange, props, update, updater],
  );

  const dataTypeOptions = React.useMemo(() => {
    if (!props?.extraDataTypes) {
      return map(BASE_DATA_TYPE, (item) => (
        <Option key={item} value={item}>
          {item.slice(0, 1).toUpperCase() + item.slice(1)}
        </Option>
      ));
    } else {
      const basicDataTypeOptions = map(BASE_DATA_TYPE, (item) => (
        <Option key={item} value={item}>
          {item.slice(0, 1).toUpperCase() + item.slice(1)}
        </Option>
      ));
      const extraOptions =
        map(keys(props.extraDataTypes), (typeName) => (
          <Option key={typeName} value={typeName}>
            {typeName}
          </Option>
        )) || [];

      return [...basicDataTypeOptions, ...extraOptions];
    }
  }, [props]);

  const updateErrorNum = React.useCallback(
    (num, name) => {
      const _formErrorMap: IFormErrorMap = {};
      _formErrorMap[name] = num;
      if (curPropertyType === 'object') {
        forEach(paramListTempStorage, (param) => {
          const pName = param[API_FORM_KEY];
          if (pName === name) {
            _formErrorMap[pName] = num;
          } else {
            _formErrorMap[pName] = formErrorMap[pName] || 0;
          }
        });
      }

      updater.formErrorMap(_formErrorMap);
      const totalError = reduce(values(_formErrorMap), (total, cur) => total + cur, 0);

      props.updateErrorNum && props.updateErrorNum(totalError, dataTempStorage[API_FORM_KEY]);
      props.onFormErrorNumChange && props.onFormErrorNumChange(totalError, dataTempStorage[API_FORM_KEY]);
    },
    [curPropertyType, dataTempStorage, formErrorMap, paramListTempStorage, props, updater],
  );

  const onChangePropertyName = React.useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      const newName = e.target.value;
      const { pattern } = regRules.specialLetter;
      const nameMap = formType === 'DataType' ? AllDataTypes : propertyNameMap;
      let isErrorValue = true;

      const isSameWithBaseType = formType === 'DataType' && newName.toLocaleLowerCase() in BASE_DATA_TYPE;
      if (newName !== '' && !pattern.test(newName) && !nameMap.includes(newName) && !isSameWithBaseType) {
        const temp = produce(dataTempStorageRef.current, (draft) => {
          set(draft, API_FORM_KEY, newName);
        });
        isErrorValue = false;

        updater.dataTempStorage(temp);
        onChange(dataTempStorageRef.current[API_FORM_KEY], temp, {
          typeQuotePath: newName,
          quoteTypeName: curPropertyType,
        });
      }

      // 统计name类型错误数量
      const newErrorMap = { ...errorMap };
      if (isErrorValue && !errorMap.name) {
        newErrorMap.name = true;
        updater.errorMap(newErrorMap);
        const errorNum = reduce(values(newErrorMap), (total, cur) => (cur ? total + 1 : total), 0);

        updateErrorNum(errorNum, dataTempStorage[API_FORM_KEY]);
      } else if (!isErrorValue && errorMap.name) {
        newErrorMap.name = false;
        updater.errorMap(newErrorMap);
        const errorNum = reduce(values(newErrorMap), (total, cur) => (cur ? total + 1 : total), 0);

        updateErrorNum(errorNum, dataTempStorage[API_FORM_KEY]);
      }
    },
    [
      AllDataTypes,
      curPropertyType,
      dataTempStorage,
      errorMap,
      formType,
      onChange,
      propertyNameMap,
      updateErrorNum,
      updater,
    ],
  );

  const onChangeNumberValue = React.useCallback(
    ({ propertyKey, propertyData }: { propertyKey: string; propertyData: number }) => {
      let isErrorValue = true;

      if (propertyKey === 'minLength') {
        const maxLength = dataTempStorageRef.current?.maxLength;
        if (maxLength === undefined || maxLength >= propertyData) {
          setFields({ propertyKey, propertyData });
          isErrorValue = false;
        }
      } else if (propertyKey === 'maxLength') {
        const minLength = dataTempStorageRef.current?.minLength;
        if (minLength === undefined || minLength <= propertyData) {
          setFields({ propertyKey, propertyData });
          isErrorValue = false;
        }
      } else if (propertyKey === 'minimum') {
        const maximum = dataTempStorageRef.current?.maximum;
        if (maximum === undefined || maximum >= propertyData) {
          setFields({ propertyKey, propertyData });
          isErrorValue = false;
        }
      } else {
        const minimum = dataTempStorageRef.current?.minimum;
        if (minimum === undefined || minimum <= propertyData) {
          setFields({ propertyKey, propertyData });
          isErrorValue = false;
        }
      }

      // 统计number类型错误数量
      const newErrorMap = { ...errorMap };
      if (isErrorValue && !errorMap[propertyKey]) {
        newErrorMap[propertyKey] = true;
        updater.errorMap(newErrorMap);
        const errorNum = reduce(values(newErrorMap), (total, cur) => (cur ? total + 1 : total), 0);

        updateErrorNum(errorNum, dataTempStorage[API_FORM_KEY]);
      } else if (!isErrorValue && errorMap[propertyKey]) {
        newErrorMap[propertyKey] = false;
        updater.errorMap(newErrorMap);
        const errorNum = reduce(values(newErrorMap), (total, cur) => (cur ? total + 1 : total), 0);

        updateErrorNum(errorNum, dataTempStorage[API_FORM_KEY]);
      }
    },
    [errorMap, dataTempStorage, setFields, updater, updateErrorNum],
  );

  //  参数详情选项
  const propertyFields = React.useMemo(() => {
    const fields = formType !== 'Parameters' ? [descriptionField] : [];
    if (!getRefTypePath(dataTempStorage)) {
      const tempFields = getPropertyDetailFields({ type: curPropertyType, curPropertyType, formData: dataTempStorage });
      fields.push(...tempFields);
    } else if (get(props, `extraDataTypes.${curPropertyType}.type`)) {
      const tempFields = getPropertyDetailFields({
        type: curPropertyType,
        curPropertyType: get(props, ['extraDataTypes', curPropertyType, 'type']),
        formData: dataTempStorage,
      });
      fields.push(...tempFields);
    }

    return map(fields, (fieldItem) => {
      const tempFieldItem = produce(fieldItem, (draft) => {
        if (['minLength', 'maxLength', 'minimum', 'maximum'].includes(draft.name)) {
          set(draft, 'customProps.onChange', (e: React.ChangeEvent<HTMLInputElement> | any) => {
            const newNum = !(Object.prototype.toString.call(e) === '[object Object]') ? e : +e.target.value;
            onChangeNumberValue({ propertyKey: fieldItem?.name, propertyData: newNum });
          });
        } else {
          set(draft, 'customProps.onChange', (e: React.ChangeEvent<HTMLInputElement> | any) => {
            const newVal = !(Object.prototype.toString.call(e) === '[object Object]') ? e : e.target.value;
            setFields({ propertyKey: fieldItem?.name, propertyData: newVal });
          });
        }
        set(draft, 'customProps.disabled', !isEditMode);
      });
      return tempFieldItem;
    });
  }, [formType, getRefTypePath, dataTempStorage, props, curPropertyType, isEditMode, onChangeNumberValue, setFields]);

  const onArrayItemChange = (_formKey: string, _formData: any, extraProps?: Obj) => {
    const newExample = [getExampleData(_formData)];
    const tempData = produce(dataTempStorageRef.current, (draft) => {
      draft.items = _formData;
      draft.example = newExample;
    });
    const _extraProps = {
      quoteTypeName: extraProps?.quoteTypeName,
      typeQuotePath: extraProps?.typeQuotePath
        ? `${dataTempStorageRef.current[API_FORM_KEY]}.${extraProps.typeQuotePath}`
        : '',
    };
    props.onChange(dataTempStorageRef.current[API_FORM_KEY], tempData, _extraProps);
    updater.arrayItemDataStorage(tempData);
  };

  const updateInnerParamList = (formKey: string, _formData: any, extraProps?: Obj) => {
    const tempList = produce(paramListTempStorageRef.current, (draft) => {
      forEach(draft, (item, index) => {
        if (item[API_FORM_KEY] === formKey) {
          draft[index] = _formData;
        }
      });
    });
    const requiredNames: string[] = [];

    const refTypePath = getRefTypePath(dataTempStorage);
    const customDataType = refTypePath ? refTypePath.split('/').slice(-1)[0] : '';

    const objectExample: Obj = { ...getExampleData(get(props, ['extraDataTypes', customDataType])) };

    forEach(tempList, (item) => {
      const _example = item?.example || DATATYPE_EXAMPLE_MAP[item?.type];
      if (item[API_PROPERTY_REQUIRED]) {
        requiredNames.push(item[API_FORM_KEY]);
      }
      objectExample[item[API_FORM_KEY]] = _example;
    });

    updater.paramListTempStorage(tempList);

    if (props.onChange && tempList?.length) {
      const newProperties = {};
      forEach(tempList, (item) => {
        newProperties[item[API_FORM_KEY]] = item;
      });
      const tempData = produce(dataTempStorageRef.current, (draft) => {
        draft.properties = newProperties;
        draft.type = 'object';
        draft.required = requiredNames;
        draft.example = objectExample;
      });
      updater.dataTempStorage(tempData);
      const typeQuotePath = extraProps?.typeQuotePath
        ? `${tempData[API_FORM_KEY] || 'schema'}.properties.${extraProps?.typeQuotePath}`
        : '';
      props.onChange(dataTempStorageRef.current[API_FORM_KEY], tempData, {
        typeQuotePath,
        quoteTypeName: extraProps?.quoteTypeName,
      });
    }
  };

  const deleteParamByFormKey = (_data: Obj, index: number) => {
    const tempList = paramListTempStorage.filter((_record, i) => index !== i);

    updater.innerParamList(tempList);
    updater.paramListTempStorage(tempList);

    const tempProperties = {};
    const newExample = {};
    const requiredNames: string[] = [];

    forEach(tempList, (item) => {
      tempProperties[item[API_FORM_KEY]] = item;
      newExample[item[API_FORM_KEY]] = item?.example;
      item[API_PROPERTY_REQUIRED] && requiredNames.push(item[API_FORM_KEY]);
    });

    const newFormData = produce(dataTempStorage, (draft) => {
      set(draft, 'properties', tempProperties);
      set(draft, 'example', newExample);
      set(draft, 'required', requiredNames);
    });
    updater.dataTempStorage(newFormData);
    props?.onChange && props.onChange(newFormData[API_FORM_KEY], newFormData);
  };

  const getExistNames = React.useCallback(() => {
    const existNames = map(paramListTempStorage, (item) => item[API_FORM_KEY]);

    const refTypePath = getRefTypePath(dataTempStorage);
    const customDataType = refTypePath ? refTypePath.split('/').slice(-1)[0] : '';

    if (refTypePath && get(props, `extraDataTypes.${curPropertyType}.type`) === 'object') {
      const _extraProperties = get(props, ['extraDataTypes', customDataType, 'properties']);
      existNames.push(...keys(_extraProperties));
    }
    return existNames;
  }, [curPropertyType, dataTempStorage, getRefTypePath, paramListTempStorage, props]);

  // object类型的批量添加参数
  const addParamList = React.useCallback(
    (newList: Obj[]) => {
      const refTypePath = getRefTypePath(dataTempStorage);
      const customDataType = refTypePath ? refTypePath.split('/').slice(-1)[0] : '';

      const tempList = [...paramListTempStorage, ...newList];
      updater.innerParamList(tempList);
      updater.paramListTempStorage(tempList);

      const tempProperties = {};
      forEach(tempList, (item) => {
        tempProperties[item[API_FORM_KEY]] = item;
      });
      const newExample = refTypePath ? { ...get(props, `extraDataTypes.${customDataType}.example`) } : {};

      const requiredNames: string[] = [];
      forEach(tempList, (property) => {
        property[API_PROPERTY_REQUIRED] && requiredNames.push(property[API_FORM_KEY]);
        newExample[property[API_FORM_KEY]] = property?.example;
      });

      const newFormData = produce(dataTempStorage, (draft) => {
        set(draft, 'properties', tempProperties);
        set(draft, 'type', 'object');
        set(draft, 'example', newExample);
        set(draft, 'required', requiredNames);
      });
      updater.dataTempStorage(newFormData);
      props?.onChange && props.onChange(dataTempStorage[API_FORM_KEY], newFormData);
    },
    [dataTempStorage, getRefTypePath, paramListTempStorage, props, updater],
  );

  // object添加单个参数
  const addParam = React.useCallback(() => {
    let newPropertyName = `propertyName${innerParamList?.length + 1}`;
    const existNames = getExistNames();

    while (existNames.includes(newPropertyName)) {
      newPropertyName += '1';
    }

    const tempObj = {
      type: 'string',
      example: 'Example',
    };
    tempObj[API_PROPERTY_REQUIRED] = true;
    tempObj[API_FORM_KEY] = newPropertyName;

    addParamList([tempObj]);
  }, [addParamList, getExistNames, innerParamList]);

  // 更新设置example示例
  React.useEffect(() => {
    const tempData = isCurTypeOf(BASE_DATA_TYPE.array) && arrayItemDataStorage ? arrayItemDataStorage : dataTempStorage;
    if (tempData.example && typeof tempData.example === 'object') {
      const newExample = getExampleData(tempData);
      formRef.current.resetFields(['example']);
      formRef.current.setFieldsValue({ example: newExample });
    }
  }, [arrayItemDataStorage, curPropertyType, dataTempStorage, getExampleData, isCurTypeOf]);

  const onCloseParamsModal = () => updater.paramsModalVisible(false);

  const onImport = (importedParams: Obj[]) => {
    onCloseParamsModal();
    addParamList(importedParams);
  };

  const formFieldsSelector = React.useMemo(() => {
    const tempFields = getPropertyFormSelector({
      formType,
      dataTypeOptions,
      propertyNameMap,
      AllDataTypes,
      detailVisible,
      index,
    });
    return map(tempFields, (fieldItem: any) => {
      const tempFieldItem = produce(fieldItem, (draft: { name: string }) => {
        if (draft.name === API_FORM_KEY && ['DataType', 'Query'].includes(formType)) {
          set(draft, 'customProps.onChange', onChangePropertyName);
        } else if (draft.name === 'operation') {
          set(draft, 'customProps.onChange', onToggleDetail);
        } else {
          set(draft, 'customProps.onChange', (e: React.ChangeEvent<HTMLInputElement> | string | boolean) => {
            const newVal = typeof e === 'string' || typeof e === 'boolean' ? e : e.target.value;
            setFields({ propertyKey: fieldItem?.name, propertyData: newVal });
          });
        }
        set(draft, 'customProps.disabled', !isEditMode);
      });
      return tempFieldItem;
    });
  }, [
    formType,
    dataTypeOptions,
    propertyNameMap,
    AllDataTypes,
    detailVisible,
    isEditMode,
    onChangePropertyName,
    onToggleDetail,
    setFields,
    index,
  ]);

  const detailType = React.useMemo(() => {
    if (detailVisible) {
      if (isCurTypeOf(BASE_DATA_TYPE.object)) {
        return 'object';
      } else if (isCurTypeOf(BASE_DATA_TYPE.array)) {
        return 'array';
      } else if (!isBasicDataType) {
        return 'example';
      }
    }
    return '';
  }, [detailVisible, isBasicDataType, isCurTypeOf]);

  return (
    <FormBuilder isMultiColumn ref={formRef}>
      {props?.formType !== 'Parameters' && <Fields fields={formFieldsSelector} />}
      {detailVisible && isBasicDataType && <Fields fields={propertyFields} />}
      {detailType === 'object' && (
        <div>
          {map(innerParamList, (record, index) => {
            return (
              <div className="param-form" key={record[API_FORM_KEY]}>
                {isEditMode && (
                  <div className="param-form-operation">
                    <Popconfirm
                      title={`${i18n.t('common:confirm to delete')}?`}
                      onConfirm={() => deleteParamByFormKey(record, index)}
                    >
                      <CustomIcon type="shanchu" className="param-form-operation-btn cursor-pointer" />
                    </Popconfirm>
                  </div>
                )}
                <div className="param-form-content">
                  <FormBuilder isMultiColumn>
                    <PropertyItemForm
                      key={record[API_FORM_KEY]}
                      updateErrorNum={updateErrorNum}
                      formData={record}
                      isEditMode={isEditMode}
                      onChange={updateInnerParamList}
                      extraDataTypes={props?.extraDataTypes}
                      allExtraDataTypes={props?.allExtraDataTypes}
                      siblingProperties={filter(
                        paramListTempStorage,
                        (item) => item[API_FORM_KEY] !== record[API_FORM_KEY],
                      )}
                      index={index}
                    />
                  </FormBuilder>
                </div>
              </div>
            );
          })}
          {isEditMode && (
            <>
              <Button className="operation-btn mb-4" onClick={addParam}>
                {i18n.t('common:add parameter')}
              </Button>
              <Button className="operation-btn mb-4 ml-2" onClick={() => updater.paramsModalVisible(true)}>
                {i18n.t('dop:import parameters')}
              </Button>
            </>
          )}
          {props?.formType !== 'Parameters' && <Fields fields={[objectExampleField]} />}
        </div>
      )}
      {detailType === 'array' && (
        <>
          {isBasicDataType && (
            <div className="array-form">
              <PropertyItemForm
                formType="Array"
                updateErrorNum={updateErrorNum}
                formData={dataTempStorage.items || {}}
                detailVisible
                onChange={onArrayItemChange}
                isEditMode={isEditMode}
                extraDataTypes={props?.extraDataTypes}
                allExtraDataTypes={props?.allExtraDataTypes}
                siblingProperties={filter(
                  paramListTempStorage,
                  (item) => item[API_FORM_KEY] !== dataTempStorage.items[API_FORM_KEY],
                )}
              />
            </div>
          )}
          <Fields fields={[objectExampleField]} />
        </>
      )}
      {detailType === 'example' && <Fields fields={[objectExampleField]} />}
      <ApiParamsModal
        visible={paramsModalVisible}
        onImport={onImport}
        onClose={onCloseParamsModal}
        paramList={paramListTempStorage}
      />
    </FormBuilder>
  );
})
Example #11
Source File: datatype-config.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
DataTypeConfig = (props: IProps) => {
  const [{ propertyFormData }, updater] = useUpdate({
    propertyFormData: {} as Obj,
  });
  const { formData, onQuoteNameChange, quotePathMap, isEditMode, dataType, onDataTypeNameChange, dataTypeNameMap } =
    props;
  const { updateOpenApiDoc, updateFormErrorNum } = apiDesignStore;
  const [openApiDoc] = apiDesignStore.useStore((s) => [s.openApiDoc]);

  const openApiDocRef = useLatest(openApiDoc);
  const quotePathMapRef = useLatest(quotePathMap);
  const formRef = React.useRef<IFormExtendType>(null);

  React.useEffect(() => {
    const newFormData = { ...omit(formData, 'name') };
    newFormData[API_FORM_KEY] = formData[API_FORM_KEY] || formData.name;
    updater.propertyFormData(newFormData);
  }, [updater, formData]);

  const extraDataTypes = React.useMemo(() => {
    const tempTypes = get(openApiDoc, 'components.schemas') || {};
    const types = {};
    const forbiddenTypes = get(tempTypes, [dataType, 'x-dice-forbidden-types']) || [dataType];
    forEach(keys(tempTypes), (typeName) => {
      if (!forbiddenTypes?.includes(typeName)) {
        types[typeName] = tempTypes[typeName];
      }
    });
    return types;
  }, [dataType, openApiDoc]);

  const allExtraDataTypes = React.useMemo(() => {
    return get(openApiDoc, 'components.schemas') || {};
  }, [openApiDoc]);

  const getForbiddenTypes = React.useCallback((data: Obj, storage: string[]) => {
    if (data?.properties) {
      forEach(values(data.properties), (item) => {
        const refTypePath = get(item, [QUOTE_PREFIX, 0, '$ref']) || data[QUOTE_PREFIX_NO_EXTENDED];
        if (refTypePath) {
          const tempQuoteName = refTypePath.split('#/components/schemas/')[1];
          if (!storage.includes(tempQuoteName)) {
            storage.push(tempQuoteName);
          }
        }
        getForbiddenTypes(item, storage);
      });
    } else if (data?.items) {
      const itemsData = data.items;
      const refTypePath = get(itemsData, [QUOTE_PREFIX, 0, '$ref']) || itemsData[QUOTE_PREFIX_NO_EXTENDED];
      if (refTypePath) {
        const tempQuoteName = refTypePath.split('#/components/schemas/')[1];
        if (!storage.includes(tempQuoteName)) {
          storage.push(tempQuoteName);
        }
        getForbiddenTypes(itemsData, storage);
      }
    }
  }, []);

  const onFormChange = React.useCallback(
    (_formKey: string, _formData: any, extraProps?: Obj) => {
      const quotedType = extraProps?.quoteTypeName;

      const nName: string = _formData[API_FORM_KEY]; // 当前正在编辑的类型的新name
      const tempMap = {};
      const originalDataTypeMap = get(openApiDocRef.current, ['components', 'schemas']);

      const usedCustomTypes: string[] = []; // 获取所有引用当前类型的其他类型
      const refTypePath = get(_formData, [QUOTE_PREFIX, 0, '$ref']) || _formData[QUOTE_PREFIX_NO_EXTENDED];
      if (refTypePath) {
        const tempQuoteName = refTypePath.split('#/components/schemas/')[1];
        usedCustomTypes.push(tempQuoteName);
      }

      getForbiddenTypes(_formData, usedCustomTypes);

      forEach(keys(originalDataTypeMap), (typeKey: string) => {
        if (typeKey !== dataType) {
          const isSelfUsed = usedCustomTypes.includes(typeKey); // 是否引用了当前正在编辑的类型
          const _originalForbiddenTypes = filter(
            get(originalDataTypeMap, [typeKey, 'x-dice-forbidden-types']) || [],
            (item) => item !== dataType,
          );

          if (!_originalForbiddenTypes.includes(typeKey)) {
            // 每个类型禁止引用自己
            _originalForbiddenTypes.push(typeKey);
          }

          // 若当前编辑的类型引用了该类型,则禁该止引用当前正在编辑的类型
          if (isSelfUsed || quotedType === typeKey) {
            _originalForbiddenTypes.push(nName);
          }

          tempMap[typeKey] = {
            ...originalDataTypeMap[typeKey],
            'x-dice-forbidden-types': _originalForbiddenTypes,
          };
        }
      });

      let originalForbiddenTypes: string[] = _formData['x-dice-forbidden-types'] || [dataType];
      if (dataType !== nName) {
        originalForbiddenTypes = originalForbiddenTypes.filter(
          (item) => item !== dataType && originalDataTypeMap[item],
        );
        originalForbiddenTypes.push(nName);
      }
      tempMap[nName] = {
        ..._formData,
        'x-dice-forbidden-types': originalForbiddenTypes,
      };

      if (dataType !== nName) {
        onDataTypeNameChange(nName);

        const newPathMap = produce(quotePathMapRef.current, (draft) => {
          draft[nName] = filter(draft[dataType], (itemPath) => get(openApiDocRef.current, itemPath));
          unset(draft, dataType);
        });

        const newHref =
          _formData.type === 'object' ? [{ $ref: `#/components/schemas/${nName}` }] : `#/components/schemas/${nName}`;
        const quotePrefix = _formData.type === 'object' ? QUOTE_PREFIX : QUOTE_PREFIX_NO_EXTENDED;

        const _tempDocDetail = produce(openApiDocRef.current, (draft) => {
          set(draft, 'components.schemas', tempMap);
        });

        const tempDocDetail = produce(_tempDocDetail, (draft) => {
          forEach(quotePathMapRef.current[dataType], (curTypeQuotePath) => {
            if (
              get(draft, [...curTypeQuotePath, QUOTE_PREFIX]) ||
              get(draft, [...curTypeQuotePath, QUOTE_PREFIX_NO_EXTENDED])
            ) {
              unset(draft, [...curTypeQuotePath, QUOTE_PREFIX]);
              unset(draft, [...curTypeQuotePath, QUOTE_PREFIX_NO_EXTENDED]);
              set(draft, [...curTypeQuotePath, quotePrefix], newHref);
            }
          });
        });
        updateOpenApiDoc(tempDocDetail);
        onQuoteNameChange(newPathMap);
      } else {
        if (quotedType && extraProps?.typeQuotePath) {
          const prefixPath = `components.schemas.${extraProps.typeQuotePath}`;

          const isQuotedBefore =
            get(openApiDocRef.current, `${prefixPath}.${QUOTE_PREFIX}.0.$ref`) ||
            get(openApiDocRef.current, `${prefixPath}.${QUOTE_PREFIX_NO_EXTENDED}`);
          const oldQuotedType = isQuotedBefore ? isQuotedBefore.split('/').slice(-1)[0] : '';

          const newPathMap = produce(quotePathMapRef.current, (draft) => {
            draft[quotedType] = draft[quotedType] || [];
            draft[quotedType].push(prefixPath.split('.'));
            if (oldQuotedType) {
              draft[oldQuotedType] = filter(draft[oldQuotedType], (item) => item.join('.') !== prefixPath);
            }
          });
          onQuoteNameChange(newPathMap);
        }

        const tempDocDetail = produce(openApiDocRef.current, (draft) => {
          set(draft, 'components.schemas', tempMap);
        });
        updateOpenApiDoc(tempDocDetail);
      }
    },
    [
      dataType,
      getForbiddenTypes,
      onDataTypeNameChange,
      onQuoteNameChange,
      openApiDocRef,
      quotePathMapRef,
      updateOpenApiDoc,
    ],
  );

  return (
    <div className="basic-params-config">
      <FormBuilder isMultiColumn className="param-config-form" ref={formRef}>
        <PropertyItemForm
          onChange={onFormChange}
          onFormErrorNumChange={updateFormErrorNum}
          detailVisible
          formType="DataType"
          formData={propertyFormData}
          extraDataTypes={extraDataTypes}
          allExtraDataTypes={allExtraDataTypes}
          isEditMode={isEditMode}
          allDataTypes={dataTypeNameMap}
        />
      </FormBuilder>
    </div>
  );
}
Example #12
Source File: index.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
ApiDesign = () => {
  const [
    {
      contentKey,
      dataTypeFormData,
      filterKey,
      apiResourceList,
      apiDataTypeList,

      quotePathMap,
      treeModalVisible,
      apiModalVisible,
      curTreeNodeData,
      curApiName,
      curDataType,
      newTreeNode,
      popVisible,
      apiDetail,
    },
    updater,
    update,
  ] = useUpdate({
    contentKey: 'SUMMARY',
    dataTypeFormData: {},
    filterKey: '',
    apiResourceList: [] as string[],
    apiDataTypeList: [] as string[],

    quotePathMap: {} as Obj,
    treeModalVisible: false,
    apiModalVisible: false,
    curTreeNodeData: {},
    curApiName: '',

    curDataType: '',
    newTreeNode: {} as API_SETTING.IFileTree,
    popVisible: false,
    apiDetail: {},
  });

  const { inode: inodeQuery, pinode: pinodeQuery } = routeInfoStore.useStore((s) => s.query);

  React.useEffect(() => {
    const [key] = contentKey.split('&DICE&');
    if (key === 'RESOURCE') {
      curApiName && updater.contentKey(`${key}&DICE&${curApiName}`);
    } else {
      curDataType && updater.contentKey(`${key}&DICE&${curDataType}`);
    }
  }, [curApiName, contentKey, updater, curDataType]);
  const { isExternalRepo, repoConfig } = appStore.useStore((s) => s.detail);

  const [openApiDoc, apiWs, apiLockState, isDocChanged, wsQuery, formErrorNum, isApiReadOnly, lockUser, docValidData] =
    apiDesignStore.useStore((s) => [
      s.openApiDoc,
      s.apiWs,
      s.apiLockState,
      s.isDocChanged,
      s.wsQuery,
      s.formErrorNum,
      s.isApiReadOnly,
      s.lockUser,
      s.docValidData,
    ]);

  const {
    updateOpenApiDoc,
    createTreeNode,
    commitSaveApi,
    getApiDetail,
    publishApi,
    updateFormErrorNum,
    resetDocValidData,
  } = apiDesignStore;

  const [getApiDocDetailLoading, commitSaveApiLoading, getTreeListLoading] = useLoading(apiDesignStore, [
    'getApiDetail',
    'commitSaveApi',
    'getTreeList',
  ]);

  useMount(() => {
    window.addEventListener('beforeunload', beforeunload);
  });

  useUnmount(() => {
    updateOpenApiDoc({});
    apiWs && apiWs.close();
    window.removeEventListener('beforeunload', beforeunload);
  });

  const changeRef = React.useRef(null as any);

  React.useEffect(() => {
    changeRef.current = isDocChanged;
  }, [isDocChanged]);

  const beforeunload = React.useCallback((e) => {
    const msg = `${i18n.t('dop:not saved yet, confirm to leave')}?`;
    if (changeRef.current) {
      // eslint-disable-next-line no-param-reassign
      (e || window.event).returnValue = msg;
    }

    return msg;
  }, []);

  const apiResourceMap = React.useMemo(() => {
    const tempMap = openApiDoc?.paths || {};
    const fullKeys = keys(tempMap);
    let tempList = [];
    if (filterKey) {
      tempList = filter(keys(tempMap), (name) => name.indexOf(filterKey) > -1);
    } else {
      tempList = fullKeys;
    }
    updater.apiResourceList(tempList);
    return tempMap;
  }, [filterKey, openApiDoc, updater]);

  const apiDataTypeMap = React.useMemo(() => {
    const tempMap = openApiDoc?.components?.schemas || {};
    const fullKeys = keys(tempMap);
    let tempList = [];
    if (filterKey) {
      tempList = filter(fullKeys, (name) => name.indexOf(filterKey) > -1);
    } else {
      tempList = fullKeys;
    }
    updater.apiDataTypeList(tempList);
    return tempMap;
  }, [filterKey, openApiDoc, updater]);

  const onCreateDoc = (values: { name: string; pinode: string }) => {
    createTreeNode(values).then((res) => {
      updater.newTreeNode(res);
    });
    updater.treeModalVisible(false);
  };

  const onContentChange = React.useCallback(
    (contentName: string) => {
      const nextHandle = () => {
        updateFormErrorNum(0);
        const [, name] = contentName.split('&DICE&');
        updater.contentKey(contentName);
        if (contentName.startsWith('RESOURCE') && name) {
          updater.curApiName(name);
          const tempApiDetail = get(openApiDoc, ['paths', name]) || {};
          updater.apiDetail(tempApiDetail);
        }
        if (contentName.startsWith('DATATYPE')) {
          const _fromData = apiDataTypeMap[name] || { type: 'string', example: 'Example', 'x-dice-name': name };
          updater.dataTypeFormData({ ..._fromData, name });
          updater.curDataType(name);
        }
      };

      if (formErrorNum > 0) {
        confirm({
          title: i18n.t('dop:Are you sure to leave, with the error message not saved?'),
          onOk() {
            nextHandle();
          },
        });
      } else {
        nextHandle();
      }
    },
    [apiDataTypeMap, formErrorNum, openApiDoc, updateFormErrorNum, updater],
  );

  const dataTypeNameMap = React.useMemo(() => {
    return keys(get(openApiDoc, ['components', 'schemas']));
  }, [openApiDoc]);

  const apiNameMap = React.useMemo(() => {
    return keys(openApiDoc.paths || {});
  }, [openApiDoc]);

  const onAddHandle = (addKey: IListKey) => {
    let newData = {};
    let newName = `/api/new${apiResourceList.length}`;
    while (apiNameMap.includes(newName)) {
      newName += '1';
    }
    let dataPath = ['paths', newName];

    if (addKey === 'DATATYPE') {
      newName = `NewDataType${apiDataTypeList.length}`;
      newData = { type: 'string', example: 'Example', 'x-dice-name': newName };
      dataPath = ['components', 'schemas', newName];
    }

    const tempDocDetail = produce(openApiDoc, (draft) => set(draft, dataPath, newData));
    updateOpenApiDoc(tempDocDetail);

    onContentChange(`${addKey}&DICE&${newName}`);
  };

  const onDeleteHandle = (itemKey: string) => {
    const [key, name] = itemKey.split('&DICE&');

    if (key === 'DATATYPE') {
      const newQuoteMap = getQuoteMap(openApiDoc);
      if (newQuoteMap[name]?.length) {
        message.warning(i18n.t('dop:this type is referenced and cannot be deleted'));
        return;
      }
    } else if (key === 'RESOURCE') {
      const paths = keys(openApiDoc.paths);
      if (paths.length === 1) {
        message.warning(i18n.t('dop:at least one API needs to be kept'));
        return;
      }
    }
    const dataPath = key === 'RESOURCE' ? ['paths', name] : ['components', 'schemas', name];
    const tempDocDetail = produce(openApiDoc, (draft) => {
      unset(draft, dataPath);
    });
    updateOpenApiDoc(tempDocDetail);
    onContentChange('SUMMARY');
  };

  // 左侧列表头部渲染
  const renderPanelHead = (titleKey: IListKey) => (
    <div className="list-panel-head inline-flex justify-between items-center">
      <span className="font-bold">{LIST_TITLE_MAP[titleKey]}</span>
      {!apiLockState && (
        <ErdaIcon
          type="plus"
          className="mr-0 cursor-pointer"
          size="16px"
          onClick={(e) => {
            e.stopPropagation();
            onAddHandle(titleKey);
          }}
        />
      )}
    </div>
  );

  // 左侧列表渲染
  const renderListItem = (listKey: IListKey, name: string) => {
    const apiData = apiResourceMap[name] || {};
    const key = `${listKey}&DICE&${name}`;
    return (
      <LazyRender key={name} minHeight={listKey === 'RESOURCE' ? '58px' : '37px'}>
        <div
          className={`list-title ${contentKey === key ? 'list-title-active' : ''}`}
          onClick={() => onContentChange(key)}
        >
          <div className="flex justify-between items-center">
            <Ellipsis title={name}>
              <div className="list-title-name w-full nowrap mr-1">{name}</div>
            </Ellipsis>
            <Popconfirm
              title={`${i18n.t('common:confirm to delete')}?`}
              onConfirm={(e: any) => {
                e.stopPropagation();
                onDeleteHandle(key);
              }}
              onCancel={(e: any) => e.stopPropagation()}
            >
              {!apiLockState && (
                <CustomIcon
                  type="shanchu"
                  className="list-title-btn cursor-pointer"
                  onClick={(e) => e?.stopPropagation()}
                />
              )}
            </Popconfirm>
          </div>
          {listKey === 'RESOURCE' && (
            <div className="method-list">
              {map(API_METHODS, (methodKey: API_SETTING.ApiMethod) => {
                const methodIconClass = !isEmpty(apiData[methodKey]) ? `method-icon-${methodKey}` : '';
                return (
                  <Tooltip title={methodKey} key={methodKey}>
                    <div className={`method-icon mr-2 ${methodIconClass}`} />
                  </Tooltip>
                );
              })}
            </div>
          )}
        </div>
      </LazyRender>
    );
  };

  // 获取所有引用的pathMap
  const getQuoteMap = React.useCallback(
    (data: Obj) => {
      const getQuotePath = (innerData: Obj, prefixPath: Array<number | string>, pathMap: Obj) => {
        const refTypePath = get(innerData, [QUOTE_PREFIX, 0, '$ref']) || innerData[QUOTE_PREFIX_NO_EXTENDED];
        if (refTypePath) {
          const _type = refTypePath.split('/').slice(-1)[0];
          // eslint-disable-next-line no-param-reassign
          !pathMap[_type] && (pathMap[_type] = []);
          if (!pathMap[_type].includes(prefixPath)) {
            pathMap[_type].push(prefixPath);
          }
        }
        if (innerData?.properties) {
          forEach(keys(innerData.properties), (item) => {
            getQuotePath(innerData.properties[item], [...prefixPath, 'properties', item], pathMap);
          });
        }
        if (innerData?.items) {
          getQuotePath(innerData.items, [...prefixPath, 'items'], pathMap);
        }
      };

      const tempMap = {};
      const pathMap = data.paths;
      forEach(keys(pathMap), (path) => {
        const pathData = pathMap[path];
        forEach(keys(pathData), (method) => {
          const methodData = pathData[method];
          const _path = ['paths', path, method];

          forEach(API_MEDIA_TYPE, (mediaType) => {
            // responses
            const responsePath = ['responses', '200', 'content', mediaType, 'schema'];
            const responseData = get(methodData, responsePath) || {};
            getQuotePath(responseData, [..._path, ...responsePath], tempMap);

            // requestBody;
            const requestBodyPath = ['requestBody', 'content', mediaType, 'schema'];
            const requestBody = get(methodData, requestBodyPath) || {};
            getQuotePath(requestBody, [..._path, ...requestBodyPath], tempMap);
          });

          // parameters
          const parametersData = methodData.parameters || [];
          forEach(parametersData, (pData, index) => {
            getQuotePath(pData, [..._path, 'parameters', index], tempMap);
          });
        });
      });

      // datatype中的引用
      const dataTypeData = data?.components?.schemas || {};
      forEach(keys(dataTypeData), (dataTypeName) => {
        getQuotePath(dataTypeData[dataTypeName], ['components', 'schemas', dataTypeName], tempMap);
      });
      updater.quotePathMap(tempMap);
      return tempMap;
    },
    [updater],
  );

  const onQuotePathMapChange = React.useCallback(
    (pathMap: Obj) => {
      updater.quotePathMap(pathMap);
    },
    [updater],
  );

  const onApiNameChange = React.useCallback(
    (name: string) => {
      updater.curApiName(name);
    },
    [updater],
  );

  const renderContent = (key: string) => {
    if (key.startsWith('RESOURCE')) {
      return (
        <ApiResource
          onQuoteChange={onQuotePathMapChange}
          onApiNameChange={onApiNameChange}
          quotePathMap={quotePathMap}
          apiName={curApiName}
          apiDetail={apiDetail}
        />
      );
    } else if (key.startsWith('DATATYPE')) {
      return (
        <DataTypeConfig
          quotePathMap={quotePathMap}
          dataTypeNameMap={dataTypeNameMap}
          formData={dataTypeFormData}
          key={dataTypeFormData?.name}
          dataType={curDataType}
          onQuoteNameChange={onQuotePathMapChange}
          onDataTypeNameChange={(name) => updater.curDataType(name)}
          isEditMode={!apiLockState}
        />
      );
    } else {
      return <ApiSummary />;
    }
  };

  const isDocLocked = React.useMemo(() => {
    return wsQuery?.sessionID && apiLockState;
  }, [apiLockState, wsQuery]);

  const LockTipVisible = React.useMemo(() => isApiReadOnly || isDocLocked, [isApiReadOnly, isDocLocked]);

  const docLockTip = React.useMemo(() => {
    if (isApiReadOnly) {
      return i18n.t('dop:protect branch, not editable');
    } else if (isDocLocked) {
      return lockUser + API_LOCK_WARNING;
    } else {
      return '';
    }
  }, [isApiReadOnly, isDocLocked, lockUser]);

  const errorData = React.useMemo(() => {
    return {
      branchName: curTreeNodeData.branchName,
      docName: `${curTreeNodeData.apiDocName}.yaml`,
      msg: docValidData.msg,
    };
  }, [curTreeNodeData, docValidData]);

  const onEditDocHandle = () => {
    if (!apiWs) {
      initApiWs({ inode: inodeQuery, pinode: pinodeQuery });
    } else if (isDocLocked) {
      message.warning(lockUser + API_LOCK_WARNING);
    }
  };

  const onPublishApi = React.useCallback(
    (values: any) => {
      publishApi(values).then(() => {
        apiWs && apiWs.close();
        getApiDetail(inodeQuery as string).then((data: any) => {
          getQuoteMap(data.openApiDoc);
          updater.curTreeNodeData({
            ...curTreeNodeData,
            asset: data.asset,
          });
        });
      });
    },
    [apiWs, curTreeNodeData, getApiDetail, getQuoteMap, inodeQuery, publishApi, updater],
  );

  const onSelectDoc = React.useCallback(
    (nodeData, reset) => {
      if (reset) {
        updateOpenApiDoc({});
        resetDocValidData();
      }
      onContentChange('Summary');
      update({
        contentKey: 'SUMMARY',
        curTreeNodeData: nodeData,
        newTreeNode: {} as API_SETTING.IFileTree,
        filterKey: '',
      });
    },
    [onContentChange, resetDocValidData, update, updateOpenApiDoc],
  );

  const onToggleTreeVisible = React.useCallback(
    (val: boolean) => {
      updater.popVisible(val);
    },
    [updater],
  );

  const onConfirmPublish = React.useCallback(() => {
    if (isDocChanged) {
      confirm({
        title: i18n.t('dop:The current document has not been saved. Publish the saved document?'),
        onOk() {
          updater.apiModalVisible(true);
        },
      });
    } else {
      updater.apiModalVisible(true);
    }
  }, [isDocChanged, updater]);

  const showErrorDocTip = React.useMemo(() => {
    return !docValidData.valid && !isDocChanged && !isEmpty(openApiDoc);
  }, [docValidData.valid, isDocChanged, openApiDoc]);

  return isExternalRepo === undefined ? (
    <EmptyHolder relative />
  ) : (
    <div className="api-design">
      <TopButtonGroup>
        <Button type="primary" onClick={() => updater.treeModalVisible(true)}>
          {i18n.t('dop:New Document')}
        </Button>
      </TopButtonGroup>
      <div className="api-design-wrap">
        <div className="search-wrap mb-4 flex items-center justify-start">
          <ApiDocTree
            treeNodeData={curTreeNodeData}
            newTreeNode={newTreeNode}
            getQuoteMap={getQuoteMap}
            onSelectDoc={onSelectDoc}
            popVisible={popVisible}
            onVisibleChange={onToggleTreeVisible}
          />
          {LockTipVisible && (
            <span className="ml-4">
              <CustomIcon type="lock" />
              {docLockTip}
            </span>
          )}
          {showErrorDocTip && <ErrorPopover {...errorData} />}
          {inodeQuery && !isEmpty(curTreeNodeData) && (
            <div className="flex items-center flex-wrap justify-end flex-1">
              {!apiWs || isDocLocked ? (
                <WithAuth pass={!isApiReadOnly && docValidData.valid}>
                  <Button type="ghost" onClick={onEditDocHandle}>
                    {i18n.t('Edit')}
                  </Button>
                </WithAuth>
              ) : (
                <Button type="ghost" disabled={formErrorNum > 0} onClick={() => commitSaveApi()}>
                  {i18n.t('Save')}
                </Button>
              )}
              <WithAuth pass={inodeQuery && docValidData.valid}>
                <Button type="primary" className="ml-2" onClick={onConfirmPublish}>
                  {i18n.t('publisher:Release')}
                </Button>
              </WithAuth>
            </div>
          )}
        </div>
        <Spin spinning={getApiDocDetailLoading || commitSaveApiLoading || getTreeListLoading}>
          {isEmpty(openApiDoc) ? (
            <ErrorEmptyHolder {...errorData} isLoading={getTreeListLoading} />
          ) : (
            <div className="api-design-content">
              <div className="api-design-content-list flex flex-col justify-start">
                <Input
                  placeholder={i18n.t('Search by keyword')}
                  className="mx-2 my-3 api-filter-input"
                  prefix={<ErdaIcon type="search1" size="14" className="mr-0.5 mt-0.5" />}
                  onInput={(e: React.ChangeEvent<HTMLInputElement>) => updater.filterKey(e.target.value)}
                />

                <div
                  className={`list-title py-3 border-bottom font-bold ${
                    contentKey === 'SUMMARY' ? 'list-title-active' : ''
                  }`}
                  onClick={() => onContentChange('SUMMARY')}
                >
                  {i18n.t('dop:API overview')}
                </div>
                <div className="panel-list">
                  <Collapse
                    accordion
                    bordered={false}
                    defaultActiveKey={['RESOURCE']}
                    className="api-overview-collapse"
                  >
                    <Panel header={renderPanelHead('RESOURCE')} key="RESOURCE">
                      {!isEmpty(apiResourceList) ? (
                        map(apiResourceList, (name) => renderListItem('RESOURCE', name))
                      ) : (
                        <EmptyHolder relative />
                      )}
                    </Panel>
                    <Panel header={renderPanelHead('DATATYPE')} key="DATATYPE">
                      {!isEmpty(apiDataTypeList) ? (
                        map(apiDataTypeList, (name) => renderListItem('DATATYPE', name))
                      ) : (
                        <EmptyHolder relative />
                      )}
                    </Panel>
                  </Collapse>
                </div>
              </div>
              <div className="api-design-content-detail px-4 py-3">{renderContent(contentKey)}</div>
            </div>
          )}
        </Spin>
        <ApiDocAddModal
          visible={treeModalVisible}
          onClose={() => updater.treeModalVisible(false)}
          onSubmit={onCreateDoc}
        />
        <ApiPublishModal
          visible={apiModalVisible}
          treeNodeData={curTreeNodeData as API_SETTING.ITreeNodeData}
          onSubmit={onPublishApi}
          onClose={() => updater.apiModalVisible(false)}
        />
      </div>
      <Prompt
        when={isDocChanged}
        message={(location: Location) => {
          if (location.pathname.endsWith('apiDesign')) {
            return false;
          }
          return `${i18n.t('dop:not saved yet, confirm to leave')}?`;
        }}
      />
    </div>
  );
}
Example #13
Source File: backlog.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
Backlog = () => {
  const [backlogIssues, backlogIssuesPaging] = iterationStore.useStore((s) => [s.backlogIssues, s.backlogIssuesPaging]);
  const { pageSize, total, pageNo } = backlogIssuesPaging;
  const { getBacklogIssues, createIssue } = iterationStore.effects;
  const { clearBacklogIssues } = iterationStore.reducers;
  const { deleteIssue, updateIssue } = issueStore.effects;
  const labelList = labelStore.useStore((s) => s.list);
  const { getLabels } = labelStore.effects;
  const [loading] = useLoading(iterationStore, ['getBacklogIssues']);
  const [{ projectId }, { id: queryId, issueType: queryType, ...restQuery }] = routeInfoStore.getState((s) => [
    s.params,
    s.query,
  ]);
  const workflowStateList = issueWorkflowStore.useStore((s) => s.workflowStateList);

  const allStateIds = React.useRef<number[]>([]);

  const [{ isAdding, curIssueDetail, drawerVisible, filterState }, updater, update] = useUpdate({
    isAdding: false,
    curIssueDetail: {} as ISSUE.Issue,
    drawerVisible: false,
    filterState: { ...restQuery } as Obj,
  });

  const stateCollection: Array<{ label: string | React.ReactNode; children: Array<{ label: string; value: string }> }> =
    React.useMemo(() => {
      const stateIds: number[] = [];
      const initState: string[] = [];
      const typeArr = ['REQUIREMENT', 'TASK', 'BUG'];
      const collection =
        workflowStateList?.reduce((acc, current) => {
          const { issueType, stateName, stateID, stateBelong } = current;
          if (!typeArr.includes(issueType)) {
            return acc;
          }
          if (!['CLOSED', 'DONE'].includes(stateBelong)) {
            initState.push(`${stateID}`);
          }
          if (acc[issueType]) {
            acc[issueType].push({ label: stateName, value: `${stateID}` });
          } else {
            acc[issueType] = [{ label: stateName, value: `${stateID}` }];
          }
          if (!allStateIds.current.length) {
            stateIds.push(stateID);
          }
          return acc;
        }, {}) || {};
      if (!allStateIds.current.length) {
        allStateIds.current = stateIds;
      }

      updater.filterState((prev: Obj) => ({ state: initState, ...prev }));
      const options = sortBy(
        map(collection, (stateArray, issueType) => {
          const label = ISSUE_TYPE_ICON_MAP[issueType];
          return {
            label: (
              <span className="flex-h-center">
                {label.icon}
                {label.name}
              </span>
            ),
            labelValue: label.value,
            children: stateArray,
          };
        }),
        (item) => indexOf(typeArr, item.labelValue),
      );
      return options;
    }, [workflowStateList]);

  React.useEffect(() => {
    getList();
  }, [stateCollection]);

  useEffectOnce(() => {
    if (!labelList.length) {
      getLabels({ type: 'issue' });
    }
    if (queryId && queryType) {
      update({
        curIssueDetail: { id: queryId, type: queryType } as ISSUE.Issue,
        drawerVisible: true,
      });
    }
    return () => {
      clearBacklogIssues();
    };
  });

  const addAuth = usePerm((s) => s.project.requirement.create.pass); // 目前迭代、任务、缺陷添加权限都一致

  const onIssueDrop = (val: ISSUE.IssueType) => {
    return updateIssue({ ...val, iterationID: -1 }).then(() => {
      getList({ pageNo: 1 });
    });
  };

  const [{ isOver }, drop] = useDrop({
    accept: BACKLOG_ISSUE_TYPE.iterationIssue,
    drop: (item: any) => ({ res: onIssueDrop(item.data) }), // drop需要返回一个Obj,如果直接返回Promise是无效的
    collect: (monitor) => ({
      isOver: monitor.isOver(),
    }),
  });

  React.useEffect(() => {
    updateSearch(filterState);
  }, [filterState]);

  const getList = React.useCallback(
    (filters: Obj = {}, goTop = true) => {
      goTop && (listRef.current.scrollTop = 0);
      const submitValues = { ...filterState, ...filters };
      const { finishedAtStartEnd, createdAtStartEnd, state } = submitValues;
      if (finishedAtStartEnd) {
        unset(submitValues, 'finishedAtStartEnd');
        submitValues.startFinishedAt = finishedAtStartEnd[0];
        submitValues.endFinishedAt = finishedAtStartEnd[1];
      }
      if (createdAtStartEnd) {
        unset(submitValues, 'createdAtStartEnd');
        submitValues.startCreatedAt = createdAtStartEnd[0];
        submitValues.endCreatedAt = createdAtStartEnd[1];
      }
      return getBacklogIssues({ ...submitValues, state: (state as number[])?.length ? state : allStateIds.current });
    },
    [filterState, getBacklogIssues],
  );

  const onDelete = (val: ISSUE.Issue) => {
    deleteIssue(val.id).then(() => {
      getList({ pageNo: 1 });
    });
  };

  const onAdd = () => updater.isAdding(true);

  const onClickIssue = (val: ISSUE.Issue) => {
    update({
      drawerVisible: true,
      curIssueDetail: val,
    });
  };

  const closeDrawer = ({ hasEdited, isCreate, isDelete }: CloseDrawerParam) => {
    update({
      drawerVisible: false,
      curIssueDetail: {} as ISSUE.Issue,
    });
    if (hasEdited || isCreate || isDelete) {
      getList();
    }
  };
  const conditionsFilter = React.useMemo(
    () => [
      {
        type: 'select',
        key: 'type',
        label: i18n.t('Type'),
        placeholder: i18n.t('filter by {name}', { name: i18n.t('Type') }),
        fixed: false,
        emptyText: i18n.t('dop:All'),
        showIndex: 1,
        options: [ISSUE_TYPE_MAP.REQUIREMENT, ISSUE_TYPE_MAP.TASK, ISSUE_TYPE_MAP.BUG],
      },
      {
        key: 'priority',
        label: i18n.t('dop:Priority'),
        emptyText: i18n.t('dop:All'),
        fixed: false,
        showIndex: 2,
        type: 'select' as const,
        placeholder: i18n.t('filter by {name}', { name: i18n.t('dop:Priority') }),
        options: map(ISSUE_PRIORITY_MAP),
      },
      {
        key: 'state',
        label: i18n.t('dop:Status'),
        type: 'select' as const,
        options: stateCollection,
        allowClear: false,
        fixed: false,
        showIndex: 3,
      },
      {
        key: 'label',
        label: i18n.t('label'),
        emptyText: i18n.t('dop:All'),
        fixed: false,
        haveFilter: true,
        type: 'select' as const,
        placeholder: i18n.t('filter by {name}', { name: i18n.t('label') }),
        options: map(labelList, (item) => ({ label: item.name, value: `${item.id}` })),
      },
      {
        key: 'assignee',
        label: i18n.t('dop:assignee'),
        fixed: false,
        type: 'memberSelector',
        customProps: {
          mode: 'multiple',
        },
      },
      {
        key: 'creator',
        label: i18n.t('Creator'),
        fixed: false,
        type: 'memberSelector',
        customProps: {
          mode: 'multiple',
        },
      },
      {
        key: 'finishedAtStartEnd',
        label: i18n.t('End date'),
        fixed: false,
        type: 'dateRange',
      },
      {
        key: 'createdAtStartEnd',
        label: i18n.t('dop:creation date'),
        fixed: false,
        type: 'dateRange',
      },
      {
        key: 'title',
        emptyText: i18n.t('dop:All'),
        fixed: true,
        placeholder: i18n.t('dop:please enter the title or ID'),
        type: 'input' as const,
      },
    ],
    [labelList, stateCollection],
  );

  const onFilter = (val: Obj) => {
    updater.filterState(val);
    getList({ ...val, pageNo: 1 });
  };

  const curType = isEmpty(filterState.type) ? map(ISSUE_OPTION) : filterState.type;

  const handleChangePage = (curPage: number, curSize?: number) => {
    getList({
      pageNo: curPage,
      pageSize: curSize,
    });
  };

  const listRef = React.useRef<{ scrollTop: number }>(null);
  const isHide = !!listRef.current && listRef.current.scrollTop;

  const tabs = [
    {
      key: 'export',
      text: i18n.t('Export'),
    },
    {
      key: 'record',
      text: i18n.t('Records'),
    },
  ];
  return (
    <div className="backlog-issues flex flex-col justify-center h-full" ref={drop}>
      <div className="backlog-issues-title flex justify-between items-center mb-2">
        <div className="flex items-center">
          <span className="font-bold text-base mr-2">{i18n.t('dop:Backlog')}</span>
          <Tooltip
            placement="right"
            title={i18n.t('dop:For issues that have not been scheduled for a specific iteration')}
          >
            <ErdaIcon type="help" className="cursor-pointer mr-2" />
          </Tooltip>
          <span className="text-desc">{i18n.t('{num} issues', { num: total })}</span>
        </div>
        <div>
          <ImportExport
            tabs={tabs}
            title={i18n.t('Export')}
            extraQuery={{ iterationID: -1 }}
            queryObj={{ ...filterState, projectID: +projectId }}
            issueType={curType}
            projectId={projectId}
          />

          <WithAuth pass={addAuth}>
            <Button className="ml-2" type="primary" onClick={onAdd}>
              <CustomIcon type="cir-add" className="mr-1" />
              {i18n.t('Add')}
            </Button>
          </WithAuth>
        </div>
      </div>
      <div className={'backlog-filter'}>
        <ContractiveFilter delay={1000} conditions={conditionsFilter} initValue={filterState} onChange={onFilter} />
      </div>
      <div className={`backlog-issues-content spin-full-height ${isOver ? 'drag-over' : ''}`} ref={drop}>
        <Spin spinning={!isHide && loading}>
          {isEmpty(backlogIssues) && !isAdding && <EmptyBacklog addAuth={addAuth} onAdd={onAdd} />}
          <div className="list-container">
            {
              <div className="backlog-issues-list p-2 bg-default-02" ref={listRef}>
                {isAdding ? (
                  <IssueForm
                    key="add"
                    className="backlog-issue-item hover-active-bg"
                    onCancel={() => updater.isAdding(false)}
                    onOk={(val: ISSUE.BacklogIssueCreateBody) => {
                      return createIssue({ ...val }).finally(() => {
                        updater.isAdding(true);
                        getList();
                      });
                    }}
                  />
                ) : null}
                {map(backlogIssues, (item) => (
                  <IssueItem
                    data={item}
                    key={item.id}
                    onDelete={onDelete}
                    issueType={BACKLOG_ISSUE_TYPE.undoneIssue}
                    onDragDelete={() => {
                      getList({ pageNo: 1 });
                    }}
                    onClickIssue={onClickIssue}
                  />
                ))}
              </div>
            }
            <Pagination
              className="flex items-center flex-wrap justify-end pt-2"
              showSizeChanger
              total={total}
              current={pageNo}
              pageSize={pageSize}
              onChange={handleChangePage}
            />
          </div>
        </Spin>
      </div>

      {drawerVisible ? (
        <EditIssueDrawer
          iterationID={-1}
          id={curIssueDetail.id}
          shareLink={`${location.href.split('?')[0]}?${mergeSearch(
            { id: curIssueDetail.id, type: curIssueDetail.type },
            true,
          )}`}
          issueType={curIssueDetail.type}
          visible={drawerVisible}
          closeDrawer={closeDrawer}
        />
      ) : null}
    </div>
  );
}