Commit 816e81936e75b8fef6b092d41d1ee7c34c135b17

Authored by qiang.tian
1 parent dc1b1717

refactor: 优化 flow-node-selector

@@ -44,6 +44,7 @@ @@ -44,6 +44,7 @@
44 }, 44 },
45 "dependencies": { 45 "dependencies": {
46 "@ant-design/icons": "^5.2.5", 46 "@ant-design/icons": "^5.2.5",
  47 + "@qx/flow": "^1.0.0-alpha.23",
47 "@qx/ui": "0.0.3-beta.1", 48 "@qx/ui": "0.0.3-beta.1",
48 "@qx/utils": "0.0.58", 49 "@qx/utils": "0.0.58",
49 "ahooks": "^3.7.5", 50 "ahooks": "^3.7.5",
@@ -5,13 +5,13 @@ nav: @@ -5,13 +5,13 @@ nav:
5 order: 1 5 order: 1
6 group: 6 group:
7 path: /common 7 path: /common
8 - title: 基础条件配置 8 + title: 条件配置
9 order: 0 9 order: 0
10 --- 10 ---
11 11
12 -## QxBaseCondition 条件配置 12 +## QxBaseCondition 基础条件配置
13 13
14 -### 条件配置 14 +### 基础条件配置
15 15
16 ```tsx 16 ```tsx
17 import { QxBaseCondition } from '@qx/common'; 17 import { QxBaseCondition } from '@qx/common';
@@ -2,17 +2,11 @@ @@ -2,17 +2,11 @@
2 2
3 .qx-condition { 3 .qx-condition {
4 width: 100%; 4 width: 100%;
5 -  
6 - &-item { 5 +
  6 + &-header {
7 display: flex; 7 display: flex;
8 align-items: center; 8 align-items: center;
9 -  
10 -  
11 - &__idx {  
12 - color: @N9;  
13 - margin-right: 4px;  
14 - width: 24px;  
15 - display: inline-block;  
16 - } 9 + justify-content: space-between;
  10 + margin-bottom: 16px;
17 } 11 }
18 } 12 }
1 -import React, { useEffect } from 'react'; 1 +import { Button } from 'antd';
  2 +import React, { useEffect, useRef, useState } from 'react';
2 import { 3 import {
3 QxBaseCondition, 4 QxBaseCondition,
4 QxBaseConditionValueType, 5 QxBaseConditionValueType,
5 } from '../qx-base-condition'; 6 } from '../qx-base-condition';
  7 +import { QxBaseIcon } from '../qx-base-icon';
6 import { QxConditionSql, SqlType } from '../qx-condition-sql'; 8 import { QxConditionSql, SqlType } from '../qx-condition-sql';
7 -import { INode } from '../qx-flow-node-selector'; 9 +import QxFieldPopover from '../qx-field/src/popover';
  10 +import FieldsCheckboxGroup from '../qx-field/src/popover/checkbox';
  11 +import { INode, QxFlowNodeFieldSelector } from '../qx-flow-node-selector';
  12 +import { request } from '../utils';
  13 +
  14 +import './index.less';
  15 +
  16 +const excludeCodes = ['id'];
  17 +
  18 +export function getFieldsByFunId(funId: string, params?: { useKey: boolean }) {
  19 + return request.get(`/qx-apaas-lowcode/app/form/${funId}/field`, { params });
  20 +}
8 21
9 export const QxCondition: React.FC<QxConditionProps> = ({ 22 export const QxCondition: React.FC<QxConditionProps> = ({
10 value, 23 value,
@@ -14,14 +27,115 @@ export const QxCondition: React.FC<QxConditionProps> = ({ @@ -14,14 +27,115 @@ export const QxCondition: React.FC<QxConditionProps> = ({
14 showAssignment = true, 27 showAssignment = true,
15 onChange, 28 onChange,
16 node, 29 node,
17 - nodes 30 + nodes,
  31 + header,
  32 + headerLeft,
  33 + headerRight,
  34 + formId,
  35 + multiple,
  36 + showHeader = false
18 }) => { 37 }) => {
  38 + const [fields, setFields] = useState([]);
  39 +
  40 + const fieldSelectorRef = useRef();
  41 +
19 const handleChange = (opt: Partial<QxConditionValueType>) => { 42 const handleChange = (opt: Partial<QxConditionValueType>) => {
20 onChange?.(Object.assign({}, value, opt, { enabled })); 43 onChange?.(Object.assign({}, value, opt, { enabled }));
21 }; 44 };
22 45
  46 + const handleGetFieldsList = async (formId: string) => {
  47 + try {
  48 + const data = await getFieldsByFunId(formId);
  49 + setFields(data.filter((i) => !excludeCodes.includes(i.code)));
  50 + } catch (error) {
  51 + setFields([]);
  52 + }
  53 + };
  54 +
  55 + useEffect(() => {
  56 + if (formId) handleGetFieldsList(formId);
  57 + }, [formId]);
  58 +
  59 + const PopoverComponent = multiple ? FieldsCheckboxGroup : QxFieldPopover;
  60 +
  61 + const handleAddField = (val: any, opt: any) => {
  62 + handleChange({
  63 + operators: [
  64 + ...(value?.operators || []),
  65 + {
  66 + field: opt,
  67 + code: opt.code,
  68 + name: opt.name,
  69 + type: opt.extract?.fieldType,
  70 + opt: 'IS',
  71 + mappingValues: [],
  72 + },
  73 + ],
  74 + });
  75 + // setFieldValue(
  76 + // 'condition',
  77 + // Object.assign({}, condition, {
  78 + // operators: [
  79 + // ...(condition.operators || []),
  80 + // { field: opt, code: val },
  81 + // ],
  82 + // }),
  83 + // );
  84 + };
  85 +
  86 + // const fieldOpt = useMemo(() => {
  87 + // return fieldSelectorRef.current?.fieldMap || {};
  88 + // }, []);
  89 +
  90 + const RenderHeader = header ? (
  91 + header
  92 + ) : (
  93 + <div className="qx-condition-header">
  94 + {!headerLeft ? (
  95 + <div className="qx-condition-header__title">条件配置</div>
  96 + ) : (
  97 + headerLeft
  98 + )}
  99 + {!headerRight ? (
  100 + <div className="qx-condition-header__right">
  101 + {node ? (
  102 + <QxFlowNodeFieldSelector
  103 + ref={fieldSelectorRef}
  104 + node={node}
  105 + nodes={nodes!}
  106 + mode="variable"
  107 + onChange={handleAddField}
  108 + >
  109 + <Button size="small" type="link">
  110 + <QxBaseIcon style={{ fontSize: 16 }} type="qx-icon-plus" />
  111 + 添加字段
  112 + </Button>
  113 + </QxFlowNodeFieldSelector>
  114 + ) : (
  115 + <PopoverComponent
  116 + ref={fieldSelectorRef}
  117 + data={fields}
  118 + onSelect={handleAddField}
  119 + onChange={handleAddField}
  120 + >
  121 + <Button size="small" type="link">
  122 + <QxBaseIcon style={{ fontSize: 16 }} type="qx-icon-plus" />
  123 + 添加字段
  124 + </Button>
  125 + </PopoverComponent>
  126 + )}
  127 + </div>
  128 + ) : (
  129 + headerRight
  130 + )}
  131 + </div>
  132 + );
  133 +
  134 + console.log('operators', fieldSelectorRef);
  135 +
23 return ( 136 return (
24 - <div className={`qx-condition ${className}`} style={style}> 137 + <div className={`qx-condition ${className && className}`} style={style}>
  138 + {showHeader && RenderHeader}
25 <QxBaseCondition 139 <QxBaseCondition
26 mode="condition" 140 mode="condition"
27 nodes={nodes} 141 nodes={nodes}
@@ -69,7 +183,13 @@ export interface QxConditionProps { @@ -69,7 +183,13 @@ export interface QxConditionProps {
69 onChange?: (value: QxConditionValueType) => void; 183 onChange?: (value: QxConditionValueType) => void;
70 className?: string; 184 className?: string;
71 style?: React.CSSProperties; 185 style?: React.CSSProperties;
72 - node?: INode  
73 - nodes?: INode[]  
74 - showAssignment?: boolean 186 + node?: INode;
  187 + nodes?: INode[];
  188 + showAssignment?: boolean;
  189 + header?: React.ReactNode;
  190 + headerRight?: React.ReactNode;
  191 + headerLeft?: React.ReactNode;
  192 + multiple?: boolean;
  193 + formId?: string;
  194 + showHeader?: boolean;
75 } 195 }
  1 +---
  2 +nav:
  3 + path: /component
  4 + title: 组件
  5 + order: 1
  6 +group:
  7 + path: /flow
  8 + title: 流程
  9 + order: 0
  10 +---
  11 +
  12 +## QxFlowNodeFieldSelector 流程结果集选择器
  13 +
  14 +### 普通
  15 +
  16 +```tsx
  17 +import { QxFlowNodeFieldSelector } from '@qx/common';
  18 +
  19 +const node = {
  20 + id: 'dfc29d5b64fa42489a65b3cfaeb999da',
  21 + type: 'default_DF_CONDITION',
  22 + name: '条件',
  23 + iconColor: '#F77234',
  24 + data: {},
  25 + children: [],
  26 + previousId: 'b0a2c7925a7b45e884fe8f82a0f39e9b',
  27 +};
  28 +
  29 +const nodes = [
  30 + {
  31 + id: '4c4fc5213db149808c57d093b15e6295',
  32 + name: '开始',
  33 + type: 'default_DF_START',
  34 + data: {
  35 + nodeVersion: '3.0.0',
  36 + data: {
  37 + enablePropagation: false,
  38 + propagation: 'REQUIRED',
  39 + isolation: 'REPEATABLE_READ',
  40 + },
  41 + result: [
  42 + {
  43 + id: '9911c21704ba4d8da8651185f441f9d3',
  44 + code: '5sfuiz',
  45 + type: 'OBJECT',
  46 + title: '5sfuiz',
  47 + qxProps: {},
  48 + pid: '',
  49 + description: '',
  50 + valueMapping: { mappingValues: [] },
  51 + valuesObj: [],
  52 + child: [
  53 + {
  54 + id: '022c007b4c304c58850ed592ef5c1774',
  55 + type: 'STRING',
  56 + pid: '9911c21704ba4d8da8651185f441f9d3',
  57 + code: 'gcc8qd',
  58 + title: 'gcc8qd',
  59 + },
  60 + ],
  61 + },
  62 + {
  63 + id: '6289083b52474567aba6d3f5fe9eda90',
  64 + code: '3coizb',
  65 + type: 'FORM',
  66 + title: '3coizb',
  67 + qxProps: {
  68 + appId: 'HQIXKC0dxbuYENalZzP',
  69 + formId: 'VX1TdanWSgYrKYn3vT8',
  70 + isTree: false,
  71 + },
  72 + pid: '',
  73 + },
  74 + ],
  75 + },
  76 + children: [],
  77 + },
  78 + {
  79 + id: 'b0a2c7925a7b45e884fe8f82a0f39e9b',
  80 + name: '分支',
  81 + type: 'default_DF_BRANCH',
  82 + previousId: '4c4fc5213db149808c57d093b15e6295',
  83 + children: [
  84 + {
  85 + id: 'dfc29d5b64fa42489a65b3cfaeb999da',
  86 + type: 'default_DF_CONDITION',
  87 + previousId: 'b0a2c7925a7b45e884fe8f82a0f39e9b',
  88 + name: '条件',
  89 + data: {},
  90 + children: [],
  91 + },
  92 + {
  93 + id: '003ca991f70548a3ba8c4f9b8d0daad2',
  94 + name: '条件',
  95 + type: 'default_DF_CONDITION',
  96 + previousId: 'b0a2c7925a7b45e884fe8f82a0f39e9b',
  97 + children: [],
  98 + },
  99 + ],
  100 + },
  101 + {
  102 + id: '576f817ce67846318d1132f231128f05',
  103 + name: '结束',
  104 + previousId: 'b0a2c7925a7b45e884fe8f82a0f39e9b',
  105 + type: 'default_DF_END',
  106 + data: { nodeVersion: '3.0.0' },
  107 + children: [],
  108 + },
  109 +];
  110 +
  111 +export default () => {
  112 + return <QxFlowNodeFieldSelector node={node} nodes={nodes} />;
  113 +};
  114 +```
  115 +
  116 +<API id="NodeFieldSelectProps"></API>
@@ -7,8 +7,6 @@ import { FieldBaseType } from '../qx-base-condition'; @@ -7,8 +7,6 @@ import { FieldBaseType } from '../qx-base-condition';
7 import { QxBaseIcon } from '../qx-base-icon'; 7 import { QxBaseIcon } from '../qx-base-icon';
8 import { request } from '../utils'; 8 import { request } from '../utils';
9 import './index.less'; 9 import './index.less';
10 -// import type { FiledType } from '@/interface';  
11 -// import type { INode } from '@qx/flow';  
12 10
13 const getAppsFields = (params: string[], appId = 'default') => { 11 const getAppsFields = (params: string[], appId = 'default') => {
14 return request.post(`/qx-apaas-lowcode/app/${appId}/fields`, { 12 return request.post(`/qx-apaas-lowcode/app/${appId}/fields`, {
@@ -217,13 +215,16 @@ export const useNodeFieldDisplay = ({ @@ -217,13 +215,16 @@ export const useNodeFieldDisplay = ({
217 /** 215 /**
218 * 查询所有 formId 的字段,并给 result 添加 child 216 * 查询所有 formId 的字段,并给 result 添加 child
219 */ 217 */
220 - const handleGetAppsFields = async () => { 218 + const handleFormTypeAddChild = async () => {
221 const forms = findResultByFormId(sourceParentNodes); 219 const forms = findResultByFormId(sourceParentNodes);
  220 + if (!forms.length) return sourceParentNodes;
  221 +
222 const ids = forms.map((item) => item.qxProps?.formId); 222 const ids = forms.map((item) => item.qxProps?.formId);
223 if (Array.isArray(ids) && ids.length && subset) { 223 if (Array.isArray(ids) && ids.length && subset) {
224 try { 224 try {
225 const data = await getAppsFields(ids as any[]); 225 const data = await getAppsFields(ids as any[]);
226 Object.keys(data).forEach((id) => { 226 Object.keys(data).forEach((id) => {
  227 + // 此处是引用类型,会间接修改 sourceParentNodes
227 const form = forms.find((item) => item.qxProps?.formId === id); 228 const form = forms.find((item) => item.qxProps?.formId === id);
228 if (!form) return; 229 if (!form) return;
229 230
@@ -272,12 +273,12 @@ export const useNodeFieldDisplay = ({ @@ -272,12 +273,12 @@ export const useNodeFieldDisplay = ({
272 name: fields.title, 273 name: fields.title,
273 code: 274 code:
274 parent && ['ORG', 'FORM', 'USER'].includes(parent.type) 275 parent && ['ORG', 'FORM', 'USER'].includes(parent.type)
275 - ? `${parent.code}.${fields.code}`  
276 - : `${nodeId}|${fields.code}`, 276 + ? `${parent.code}.${fields.id}`
  277 + : `${nodeId}|${fields.id}`,
277 extract: { 278 extract: {
278 ...(fields.extract || {}), 279 ...(fields.extract || {}),
279 fieldType: fields.type, 280 fieldType: fields.type,
280 - fieldKey: fields.code, 281 + fieldKey: fields.id,
281 }, 282 },
282 }); 283 });
283 fields.child = correctionNodeField( 284 fields.child = correctionNodeField(
@@ -290,43 +291,31 @@ export const useNodeFieldDisplay = ({ @@ -290,43 +291,31 @@ export const useNodeFieldDisplay = ({
290 return fields; 291 return fields;
291 }; 292 };
292 293
  294 + /**
  295 + * 获取可被选择的节点和结果
  296 + */
293 const getOptionalNodes = async () => { 297 const getOptionalNodes = async () => {
294 - const targetParentNodes = await handleGetAppsFields(); 298 + const targetParentNodes = await handleFormTypeAddChild(); // 给 form 类型的字段添加 child
295 299
296 for (let i = 0; i < targetParentNodes.length; i++) { 300 for (let i = 0; i < targetParentNodes.length; i++) {
  301 + // TODO: 统一节点和 result 格式
297 correctionNodeField( 302 correctionNodeField(
298 targetParentNodes[i].data?.result || [], 303 targetParentNodes[i].data?.result || [],
299 targetParentNodes[i].id, 304 targetParentNodes[i].id,
300 ); 305 );
301 } 306 }
302 307
303 - if (!limitTypes || !limitTypes.length) {  
304 - setOptionalNodes(targetParentNodes);  
305 - renderInputDisplay(targetParentNodes);  
306 - return;  
307 - }  
308 -  
309 /** 308 /**
310 * 根据 limitType 获取可选择的字段 309 * 根据 limitType 获取可选择的字段
311 */ 310 */
312 function getEffectiveResult(result: FiledType[]) { 311 function getEffectiveResult(result: FiledType[]) {
313 const newResult = []; 312 const newResult = [];
  313 +
314 for (let i = 0; i < result.length; i++) { 314 for (let i = 0; i < result.length; i++) {
315 const resultItem = result[i] || {}; 315 const resultItem = result[i] || {};
316 - // correctionNodeField(resultItem, nodeId, parent);  
317 316
318 - if (resultItem.child) {  
319 - resultItem.child = getEffectiveResult(resultItem.child);  
320 - if (  
321 - (Array.isArray(resultItem.child) && resultItem.child.length) ||  
322 - limitTypes?.includes(  
323 - FieldBaseType[resultItem.type as keyof typeof FieldBaseType] ||  
324 - resultItem.type,  
325 - )  
326 - ) {  
327 - newResult.push(resultItem);  
328 - }  
329 - } else if ( 317 + if (
  318 + // 先将表单字段类型或流程参数类型转换为基础类型再做过滤
330 limitTypes?.includes( 319 limitTypes?.includes(
331 FieldBaseType[resultItem.type as keyof typeof FieldBaseType] || 320 FieldBaseType[resultItem.type as keyof typeof FieldBaseType] ||
332 resultItem.type, 321 resultItem.type,
@@ -334,6 +323,10 @@ export const useNodeFieldDisplay = ({ @@ -334,6 +323,10 @@ export const useNodeFieldDisplay = ({
334 ) { 323 ) {
335 newResult.push(resultItem); 324 newResult.push(resultItem);
336 } 325 }
  326 +
  327 + if (Array.isArray(resultItem.child) && resultItem.child.length) {
  328 + resultItem.child = getEffectiveResult(resultItem.child);
  329 + }
337 } 330 }
338 331
339 return newResult; 332 return newResult;
@@ -351,9 +344,15 @@ export const useNodeFieldDisplay = ({ @@ -351,9 +344,15 @@ export const useNodeFieldDisplay = ({
351 return nodes; 344 return nodes;
352 } 345 }
353 346
354 - const newNodes = getEffectiveNodes(targetParentNodes);  
355 - setOptionalNodes([...newNodes]);  
356 - renderInputDisplay([...newNodes]); 347 + let newNodes = targetParentNodes;
  348 +
  349 + // 有类型限制根据 limitType 筛选出可选的节点和 result
  350 + if (limitTypes && Array.isArray(limitTypes) && limitTypes.length) {
  351 + newNodes = getEffectiveNodes(targetParentNodes);
  352 + }
  353 +
  354 + setOptionalNodes(newNodes);
  355 + renderInputDisplay(newNodes);
357 }; 356 };
358 357
359 const getResultFieldMaps = (optionalNodes: INode[]) => { 358 const getResultFieldMaps = (optionalNodes: INode[]) => {
@@ -470,13 +469,18 @@ const SelectItem = (props: any) => { @@ -470,13 +469,18 @@ const SelectItem = (props: any) => {
470 ); 469 );
471 }; 470 };
472 471
473 -export const QxFlowNodeFieldSelector: React.FC<NodeFieldSelectProps> = (  
474 - props,  
475 -) => { 472 +export const QxFlowNodeFieldSelector = React.forwardRef<
  473 + any,
  474 + NodeFieldSelectProps
  475 +>((props, ref) => {
476 const [visible, setVisible] = useState(false); 476 const [visible, setVisible] = useState(false);
477 477
478 - const { optionalNodes, renderInputDisplay, inputDisplay } =  
479 - useNodeFieldDisplay(props); 478 + const {
  479 + optionalNodes,
  480 + renderInputDisplay,
  481 + inputDisplay,
  482 + } = useNodeFieldDisplay(props);
  483 + // console.log('111111', props, optionalNodes);
480 484
481 const getOptions = () => { 485 const getOptions = () => {
482 return optionalNodes.map((node) => ({ 486 return optionalNodes.map((node) => ({
@@ -528,8 +532,23 @@ export const QxFlowNodeFieldSelector: React.FC<NodeFieldSelectProps> = ( @@ -528,8 +532,23 @@ export const QxFlowNodeFieldSelector: React.FC<NodeFieldSelectProps> = (
528 } 532 }
529 }; 533 };
530 534
  535 + useEffect(() => {
  536 + const closeDropdown = () => {
  537 + setVisible(false);
  538 + };
  539 + document.body.addEventListener('click', closeDropdown);
  540 + return () => {
  541 + document.body.removeEventListener('click', closeDropdown);
  542 + };
  543 + }, []);
  544 +
531 return ( 545 return (
532 - <div className={cls('qx-node-select')}> 546 + <div
  547 + className={cls('qx-node-select')}
  548 + onClick={(e) => {
  549 + e.stopPropagation();
  550 + }}
  551 + >
533 <Dropdown 552 <Dropdown
534 destroyPopupOnHide 553 destroyPopupOnHide
535 trigger={['click']} 554 trigger={['click']}
@@ -556,7 +575,7 @@ export const QxFlowNodeFieldSelector: React.FC<NodeFieldSelectProps> = ( @@ -556,7 +575,7 @@ export const QxFlowNodeFieldSelector: React.FC<NodeFieldSelectProps> = (
556 </Dropdown> 575 </Dropdown>
557 </div> 576 </div>
558 ); 577 );
559 -}; 578 +});
560 579
561 export interface NodeFieldSelectProps { 580 export interface NodeFieldSelectProps {
562 node: INode; 581 node: INode;