diff --git a/packages/amis-ui/src/components/condition-builder/Item.tsx b/packages/amis-ui/src/components/condition-builder/Item.tsx index 311fd752d..24fa5d566 100644 --- a/packages/amis-ui/src/components/condition-builder/Item.tsx +++ b/packages/amis-ui/src/components/condition-builder/Item.tsx @@ -96,7 +96,9 @@ export class ConditionItem extends React.Component { @autobind handleOperatorChange(op: OperatorType) { - const {fields, value, index, onChange} = this.props; + const {fields, value, index, onChange, formula} = this.props; + const useFormulaInput = + formula?.mode === 'input-group' && formula?.inputSettings; const leftFieldSchema: FieldSimple = findTree( fields, (i: FieldSimple) => i.name === (value?.left as ExpressionField)?.field @@ -104,7 +106,10 @@ export class ConditionItem extends React.Component { const result = { ...value, op: op, - right: value.right ?? leftFieldSchema?.defaultValue + /** 使用公式编辑器模式时,因为不同条件下值格式不一致(比如select类型下包含和等于对应的multiple会变化),所以变化条件时需要清空right值 */ + right: useFormulaInput + ? leftFieldSchema?.defaultValue + : value.right ?? leftFieldSchema?.defaultValue }; onChange(result, index); diff --git a/packages/amis-ui/src/components/formula/Editor.tsx b/packages/amis-ui/src/components/formula/Editor.tsx index a66f936bb..8eaa112ea 100644 --- a/packages/amis-ui/src/components/formula/Editor.tsx +++ b/packages/amis-ui/src/components/formula/Editor.tsx @@ -191,6 +191,15 @@ export class FormulaEditor extends React.Component< return; } + if (typeof value !== 'string') { + try { + value = JSON.stringify(value); + } catch (error) { + console.error('[amis][formula] given value is not a string'); + value = ''; + } + } + const varMap: { [propname: string]: string; } = {}; diff --git a/packages/amis-ui/src/components/formula/Input.tsx b/packages/amis-ui/src/components/formula/Input.tsx index ee9927675..2b1109678 100644 --- a/packages/amis-ui/src/components/formula/Input.tsx +++ b/packages/amis-ui/src/components/formula/Input.tsx @@ -9,7 +9,8 @@ import { LocaleProps, uncontrollable, findTree, - isExpression + isExpression, + isObject } from 'amis-core'; import {FormulaEditor, VariableItem} from './Editor'; @@ -96,9 +97,35 @@ const FormulaInput: React.FC = props => { if (schemaType === 'boolean') { result = origin.value; } else if (schemaType === 'select') { - result = Array.isArray(origin) - ? origin.map(item => item.value) - : origin.value; + const { + joinValues, + extractValue, + delimiter, + multiple, + valueField = 'value' + } = inputSettings; + + if (joinValues) { + if (multiple) { + result = Array.isArray(origin) + ? (origin.map(item => item[valueField]).join(delimiter) as string) + : origin + ? origin[valueField] + : ''; + } else { + result = origin ? origin[valueField] : ''; + } + } else if (extractValue) { + if (multiple) { + result = Array.isArray(origin) + ? origin.map(item => item[valueField]) + : origin + ? [origin[valueField || 'value']] + : []; + } else { + result = origin ? origin[valueField] : ''; + } + } } onChange?.(result); }, @@ -123,26 +150,58 @@ const FormulaInput: React.FC = props => { : cmptValue === item?.value; }) : null; + let useVariable = !!(isExpression(cmptValue) || targetVariable); - if ( - isExpression(cmptValue) || - targetVariable || - (schemaType === 'number' && - cmptValue != null && - typeof cmptValue !== 'number') || - (['date', 'time', 'datetime'].includes(schemaType) && - !moment(cmptValue).isValid()) || - (schemaType === 'select' && - cmptValue != null && - !(inputSettings?.options ?? []).some( - (item: any) => item?.value === cmptValue - )) || - (schemaType === 'boolean' && - cmptValue != null && - typeof cmptValue !== 'boolean') - ) { + /** 判断value是否为变量,如果是变量,使用ResultBox渲染 */ + if (!useVariable) { + if (schemaType === 'number') { + useVariable = cmptValue != null && typeof cmptValue !== 'number'; + } else if (['date', 'time', 'datetime'].includes(schemaType)) { + useVariable = !moment(cmptValue).isValid(); + } else if (schemaType === 'select') { + const { + options, + joinValues, + extractValue, + delimiter, + multiple, + valueField = 'value' + } = inputSettings; + let selctedValue: any[] = []; + + if (multiple) { + if (joinValues) { + selctedValue = + typeof cmptValue === 'string' ? cmptValue.split(delimiter) : []; + } else { + selctedValue = Array.isArray(cmptValue) + ? extractValue + ? cmptValue + : cmptValue.map(i => i?.[valueField]) + : []; + } + } else { + if (joinValues) { + selctedValue = typeof cmptValue === 'string' ? [cmptValue] : []; + } else { + selctedValue = isObject(cmptValue) ? [cmptValue?.[valueField]] : []; + } + } + + /** 选项类型清空后是空字符串, */ + useVariable = + cmptValue && + !(options ?? []).some((item: any) => + selctedValue.includes(item?.value) + ); + } else if (schemaType === 'boolean') { + useVariable = cmptValue != null && typeof cmptValue !== 'boolean'; + } + } + + if (useVariable) { const varName = - cmptValue && mixedMode + typeof cmptValue === 'string' && cmptValue && mixedMode ? cmptValue.replace(/^\$\{/, '').replace(/\}$/, '') : cmptValue; const resultValue = targetVariable?.value ?? varName; diff --git a/packages/amis/__tests__/renderers/Form/conditionBuilder.test.tsx b/packages/amis/__tests__/renderers/Form/conditionBuilder.test.tsx index b81deb1d1..5d6a9dc4f 100644 --- a/packages/amis/__tests__/renderers/Form/conditionBuilder.test.tsx +++ b/packages/amis/__tests__/renderers/Form/conditionBuilder.test.tsx @@ -14,10 +14,17 @@ */ import React from 'react'; -import {fireEvent, render, screen, waitFor} from '@testing-library/react'; +import {fireEvent, render, screen, cleanup, waitFor} from '@testing-library/react'; import '../../../src'; -import {render as amisRender} from '../../../src'; +import {render as amisRender, clearStoresCache} from '../../../src'; import {makeEnv, replaceReactAriaIds, wait} from '../../helper'; +import { Select } from 'packages/amis-ui/lib/components/Select'; + +afterEach(() => { + cleanup(); + clearStoresCache(); + jest.useRealTimers(); +}); const testSchema = { type: 'page', @@ -801,3 +808,341 @@ test('Renderer:condition-builder with not embed', async () => { baseElement.querySelector('.cxd-Modal .cxd-CBGroup') ).toBeInTheDocument(); }); + +/** + * 组合条件使用公式编辑器 + * 1. 7种类型的公式编辑器正常渲染 + * 2. 选项类型(select)字段,切换操作符(包含 -> 等于),字段值清空且正常渲染(等于和包含对应的multiple不一样,所以值格式不一样) + * 3. 先使用其他类型字段,再切换到select类型,条件选择包含,值正常渲染 + */ +describe.only('Renderer: condition-builder with formula', () => { + const onSubmit = jest.fn(); + test('condition-builder with different fields', async () => { + const {container} = render(amisRender({ + "type": "form", + "data": { + "conditions": { + "id": "68bddc1495e9", + "conjunction": "and", + "children": [ + { + "id": "b9cc34dae93a", + "left": { + "type": "field", + "field": "text" + }, + "op": "equal" + }, + { + "id": "4c718986c321", + "left": { + "type": "field", + "field": "number" + }, + "op": "equal" + }, + { + "id": "7ee79c416422", + "left": { + "type": "field", + "field": "boolean" + }, + "op": "equal" + }, + { + "id": "9cd76d8a6522", + "left": { + "type": "field", + "field": "select" + }, + "op": "select_equals" + }, + { + "id": "20a65e9df546", + "left": { + "type": "field", + "field": "date" + }, + "op": "equal" + }, + { + "id": "e729b32ea9e8", + "left": { + "type": "field", + "field": "time" + }, + "op": "equal" + }, + { + "id": "a5f48e000557", + "left": { + "type": "field", + "field": "datetime" + }, + "op": "equal" + } + ] + } + }, + "body": [ + { + "type": "condition-builder", + "label": "条件组件", + "name": "conditions", + "searchable": true, + "formula": { + "mode": "input-group", + "inputSettings": {}, + "allowInput": true, + "mixedMode": true, + "variables": [] + }, + "fields": [ + { + "label": "文本", + "type": "text", + "name": "text" + }, + { + "label": "数字", + "type": "number", + "name": "number" + }, + { + "label": "布尔", + "type": "boolean", + "name": "boolean" + }, + { + "label": "选项", + "type": "select", + "name": "select", + "options": [ + { + "label": "A", + "value": "a" + }, + { + "label": "B", + "value": "b" + }, + { + "label": "C", + "value": "c" + } + ] + }, + { + "label": "日期", + "children": [ + { + "label": "日期", + "type": "date", + "name": "date" + }, + { + "label": "时间", + "type": "time", + "name": "time" + }, + { + "label": "日期时间", + "type": "datetime", + "name": "datetime" + } + ] + } + ] + } + ] + }, {onSubmit}, makeEnv({}))); + + replaceReactAriaIds(container); + // 7种类型都存在 + expect(container.querySelectorAll('.cxd-FormulaPicker-input')?.length).toEqual(7); + expect(container.querySelector('.cxd-FormulaPicker--text')).toBeInTheDocument(); + expect(container.querySelector('.cxd-FormulaPicker-input-number')).toBeInTheDocument(); + expect(container.querySelector('.cxd-FormulaPicker-input-boolean')).toBeInTheDocument(); + expect(container.querySelector('.cxd-FormulaPicker-input-select')).toBeInTheDocument(); + expect(container.querySelector('.cxd-FormulaPicker-input-date')).toBeInTheDocument(); + expect(container.querySelector('.cxd-FormulaPicker-input-time')).toBeInTheDocument(); + expect(container.querySelector('.cxd-FormulaPicker-input-datetime')).toBeInTheDocument(); + }); + + test('condition-builder with select field and change operator', async () => { + const {container, findByText} = render(amisRender({ + "type": "form", + "data": { + "conditions": { + "id": "68bddc1495e9", + "conjunction": "and", + "children": [ + { + "id": "9cd76d8a6522", + "left": { + "type": "field", + "field": "select" + }, + "op": "select_equals" + } + ] + } + }, + "body": [ + { + "type": "condition-builder", + "label": "条件组件", + "name": "conditions", + "searchable": true, + "formula": { + "mode": "input-group", + "inputSettings": {}, + "allowInput": true, + "mixedMode": true, + "variables": [] + }, + "fields": [ + { + "label": "选项", + "type": "select", + "name": "select", + "options": [ + { + "label": "A", + "value": "a" + }, + { + "label": "B", + "value": "b" + }, + { + "label": "C", + "value": "c" + } + ] + } + ] + } + ] + }, {}, makeEnv({}))); + + replaceReactAriaIds(container); + + // 选中第一个选项(Form中默认值是等于操作) + let fieldValueControl = container.querySelector('.cxd-FormulaPicker-input-select')!; + fireEvent.click(fieldValueControl); + await wait(100); + fireEvent.click(await findByText('A')); + expect(container.querySelector('.cxd-Tag-text')?.innerHTML).toEqual('A'); + + // 切换操作符,字段值清空,需要重新选择,且下拉选项变成多选 + const opControl = container.querySelector('.cxd-CBGroup-operatorInput')!; + fireEvent.click(opControl); + await wait(100); + fireEvent.click(await findByText('包含')); + await wait(100); + expect(container.querySelector('.cxd-Select-placeholder')).toBeInTheDocument(); + fieldValueControl = container.querySelector('.cxd-FormulaPicker-input-select')!; + fireEvent.click(fieldValueControl); + await wait(100); + expect(container.querySelectorAll('.cxd-Select-option-checkbox').length).toEqual(3); + }); + + test('condition-builder with field type change', async () => { + const onSubmit = jest.fn(); + const {container, findByText, findByPlaceholderText} = render(amisRender({ + "type": "form", + "data": { + "conditions": { + "id": "68bddc1495e9", + "conjunction": "and", + "children": [ + { + "id": "b9cc34dae93a", + "left": { + "type": "field", + "field": "text" + }, + "op": "equal" + } + ] + } + }, + "body": [ + { + "type": "condition-builder", + "label": "条件组件", + "name": "conditions", + "searchable": true, + "formula": { + "mode": "input-group", + "inputSettings": {}, + "allowInput": true, + "mixedMode": true, + "variables": [] + }, + "fields": [ + { + "label": "文本", + "type": "text", + "name": "text" + }, + { + "label": "选项", + "type": "select", + "name": "select", + "options": [ + { + "label": "A", + "value": "a" + }, + { + "label": "B", + "value": "b" + }, + { + "label": "C", + "value": "c" + } + ] + } + ] + } + ] + }, {onSubmit}, makeEnv({}))); + + replaceReactAriaIds(container); + + // 切换字段类型,对应字段值控件更新 + const fieldControl = container.querySelector('.cxd-DropDownSelection-input')!; + fireEvent.click(fieldControl); + await wait(100); + fireEvent.click(await findByText('选项')); + await wait(100); + let selectValueControl = container.querySelector('.cxd-FormulaPicker-input-select')!; + expect(selectValueControl).toBeInTheDocument(); + + // 切换操作符,下拉选项变成多选 + const opControl = container.querySelector('.cxd-CBGroup-operatorInput')!; + fireEvent.click(opControl); + await wait(100); + fireEvent.click(await findByText('包含')); + await wait(100); + expect(container.querySelector('.cxd-Select-placeholder')).toBeInTheDocument(); + selectValueControl = container.querySelector('.cxd-FormulaPicker-input-select')!; + fireEvent.click(selectValueControl); + await wait(100); + expect(container.querySelectorAll('.cxd-Select-option-checkbox').length).toEqual(3); + + // 选择2个选项,绑定值变化 + fireEvent.click(await findByText('A')); + fireEvent.click(await findByText('C')); + const selectedValues = []; + const nodes = container.querySelectorAll('.cxd-Select-valueLabel'); + for (const el of nodes.values()) { + selectedValues.push(el?.innerHTML); + } + expect(selectedValues.length).toEqual(2); + expect(selectedValues.join(',')).toEqual('A,C'); + }); +})