mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 03:58:07 +08:00
feat: condition-builder支持selectMode为chained的选项层级显示 (#7120)
* feat: condition-builder支持selectMode为chained的选项层级显示
This commit is contained in:
parent
125d860c2b
commit
75952bd558
@ -490,7 +490,7 @@ type Value = ValueGroup;
|
|||||||
"label": "条件组件",
|
"label": "条件组件",
|
||||||
"name": "conditions",
|
"name": "conditions",
|
||||||
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
|
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
|
||||||
"source": "/api/condition-fields?a=${a}&waitSeconds=2"
|
"source": "/api/condition-fields/custom?a=${a}&waitSeconds=2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -500,7 +500,7 @@ type Value = ValueGroup;
|
|||||||
|
|
||||||
> 2.3.0 及以上版本
|
> 2.3.0 及以上版本
|
||||||
|
|
||||||
通过 selectMode 配置组合条件左侧选项类型,可配置项为`list`、`tree`,默认为`list`。两者数据格式相同,只是下拉框展示方式不同,当存在多层 children 嵌套时,建议使用`tree`。
|
通过 selectMode 配置组合条件左侧选项类型,可配置项为`list`、`tree`、`chained`,默认为`list`。这三个数据格式基本类似,只是下拉框展示方式不同,`tree`是树形下拉,`chained`为多个级联的下拉。当存在多层 children 嵌套时,建议使用`tree`。
|
||||||
|
|
||||||
selectMode 为`list`时
|
selectMode 为`list`时
|
||||||
|
|
||||||
@ -640,6 +640,84 @@ selectMode 为`tree`时
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 3.2.0 及以上版本
|
||||||
|
|
||||||
|
selectMode 为`chained`时,使用`fields`字段
|
||||||
|
|
||||||
|
```schema: scope="body"
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"debug": true,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"type": "condition-builder",
|
||||||
|
"label": "条件组件",
|
||||||
|
"name": "conditions",
|
||||||
|
"selectMode": "chained",
|
||||||
|
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"label": "文本",
|
||||||
|
"type": "text",
|
||||||
|
"name": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "数字",
|
||||||
|
"type": "number",
|
||||||
|
"name": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "布尔",
|
||||||
|
"type": "boolean",
|
||||||
|
"name": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "链式结构",
|
||||||
|
"name": "chained",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "Folder A",
|
||||||
|
"name": "Folder_A",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "file A",
|
||||||
|
"name": "file_A",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "file B",
|
||||||
|
"name": "file_B",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
selectMode 为`chained`时,使用`source`字段
|
||||||
|
|
||||||
|
```schema: scope="body"
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"debug": true,
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"type": "condition-builder",
|
||||||
|
"label": "条件组件",
|
||||||
|
"name": "conditions",
|
||||||
|
"selectMode": "chained",
|
||||||
|
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
|
||||||
|
"source": "/api/condition-fields/chained"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 简易模式
|
## 简易模式
|
||||||
|
|
||||||
通过 builderMode 配置为简易模式,在这个模式下将不开启树形分组功能,输出结果只有一层,方便后端实现简单的 SQL 生成。
|
通过 builderMode 配置为简易模式,在这个模式下将不开启树形分组功能,输出结果只有一层,方便后端实现简单的 SQL 生成。
|
||||||
@ -918,7 +996,7 @@ selectMode 为`tree`时
|
|||||||
|
|
||||||
## 属性表
|
## 属性表
|
||||||
|
|
||||||
| 属性名 | 类型 | 默认值 | 说明 |
|
| 属性名 | 类型 | 默认值 | 说明 |
|
||||||
| -------------- | ------------------ | -------- | ------------------------------ |
|
| -------------- | ------------------ | -------- | ------------------------------ |
|
||||||
| className | `string` | | 外层 dom 类名 |
|
| className | `string` | | 外层 dom 类名 |
|
||||||
| fieldClassName | `string` | | 输入字段的类名 |
|
| fieldClassName | `string` | | 输入字段的类名 |
|
||||||
@ -929,4 +1007,6 @@ selectMode 为`tree`时
|
|||||||
| showANDOR | `boolean` | | 用于 simple 模式下显示切换按钮 |
|
| showANDOR | `boolean` | | 用于 simple 模式下显示切换按钮 |
|
||||||
| showNot | `boolean` | | 是否显示「非」按钮 |
|
| showNot | `boolean` | | 是否显示「非」按钮 |
|
||||||
| searchable | `boolean` | | 字段是否可搜索 |
|
| searchable | `boolean` | | 字段是否可搜索 |
|
||||||
| selectMode | `'list'`、`'tree'` | `'list'` | 组合条件左侧选项类型 |
|
| selectMode | `'list'` \| `'tree'` \| `'chained'` | `'list'` | 组合条件左侧选项类型。`'chained'`模式需要`3.2.0及以上版本` |
|
||||||
|
| addBtnVisibleOn | `string` | | 表达式:控制按钮“添加条件”的显示。参数为`depth`、`breadth`,分别代表深度、长度。表达式需要返回`boolean`类型`3.2.0及以上版本` |
|
||||||
|
| addGroupBtnVisibleOn | `string` | | 表达式:控制按钮“添加条件组”的显示。参数为`depth`、`breadth`,分别代表深度、长度。表达式需要返回`boolean`类型`3.2.0及以上版本` |
|
||||||
|
44
mock/cfc/mock/condition-fields/chained.json
Normal file
44
mock/cfc/mock/condition-fields/chained.json
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"status": 0,
|
||||||
|
"msg": "",
|
||||||
|
"data": {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"label": "文本",
|
||||||
|
"type": "text",
|
||||||
|
"name": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "数字",
|
||||||
|
"type": "number",
|
||||||
|
"name": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "布尔",
|
||||||
|
"type": "boolean",
|
||||||
|
"name": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "日期",
|
||||||
|
"name": "date",
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"label": "日期1",
|
||||||
|
"type": "date",
|
||||||
|
"name": "date1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "时间2",
|
||||||
|
"type": "time",
|
||||||
|
"name": "time2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "日期时间3",
|
||||||
|
"type": "datetime",
|
||||||
|
"name": "datetime"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
@ -417,3 +417,38 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.#{$ns}ChainedDropdownSelection {
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&-item {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$ns}DropDownSelection {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0.1875rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
&-caret {
|
||||||
|
transition: transform var(--animation-duration) ease-out;
|
||||||
|
margin: 5px;
|
||||||
|
display: flex;
|
||||||
|
color: var(--Form-select-caret-iconColor);
|
||||||
|
&:hover {
|
||||||
|
color: var(--Form-select-caret-onHover-iconColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
> svg {
|
||||||
|
width: px2rem(10px);
|
||||||
|
height: px2rem(10px);
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&-input.is-active &-caret {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
141
packages/amis-ui/src/components/ChainedDropdownSelection.tsx
Normal file
141
packages/amis-ui/src/components/ChainedDropdownSelection.tsx
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import omit from 'lodash/omit';
|
||||||
|
import {
|
||||||
|
uncontrollable,
|
||||||
|
autobind,
|
||||||
|
ThemeProps,
|
||||||
|
themeable,
|
||||||
|
localeable,
|
||||||
|
LocaleProps
|
||||||
|
} from 'amis-core';
|
||||||
|
|
||||||
|
import {Options} from './Select';
|
||||||
|
import {BaseSelection, BaseSelectionProps} from './Selection';
|
||||||
|
|
||||||
|
import DropDownSelection from './DropDownSelection';
|
||||||
|
|
||||||
|
export interface ChainedDropDownSelectionProps
|
||||||
|
extends ThemeProps,
|
||||||
|
LocaleProps,
|
||||||
|
BaseSelectionProps {
|
||||||
|
options: Array<any>;
|
||||||
|
value: any;
|
||||||
|
onChange: (value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
searchable?: boolean;
|
||||||
|
popOverContainer?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChainedDropdownSelectionState {
|
||||||
|
stacks: Array<Options>;
|
||||||
|
values: Array<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChainedDropdownSelection extends BaseSelection<
|
||||||
|
ChainedDropDownSelectionProps,
|
||||||
|
ChainedDropdownSelectionState
|
||||||
|
> {
|
||||||
|
constructor(props: ChainedDropDownSelectionProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = this.computed(props.value, props.options);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: ChainedDropDownSelectionProps) {
|
||||||
|
const {options, value} = this.props;
|
||||||
|
if (options !== prevProps.options || prevProps.value !== value) {
|
||||||
|
this.setState(this.computed(value, options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
computed(value: string, options: Options) {
|
||||||
|
const {valueField} = this.props;
|
||||||
|
let values: Array<string> = [];
|
||||||
|
const getValues = (opts: Options, arr: Array<string> = []) => {
|
||||||
|
opts.forEach(item => {
|
||||||
|
const cValue = valueField ? item[valueField] : item?.value ?? '';
|
||||||
|
if (cValue === value) {
|
||||||
|
values = [...arr, cValue];
|
||||||
|
} else if (item.children) {
|
||||||
|
getValues(item.children, [...arr, cValue]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
getValues(options);
|
||||||
|
return {
|
||||||
|
values,
|
||||||
|
stacks: this.computedStask(values)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getFlatOptions(options: Options) {
|
||||||
|
return options.map(item => omit(item, 'children'));
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
handleSelect(index: number, value: string) {
|
||||||
|
// 当前层级点击时,需要重新设置下values的值,以及重新计算stacks列表
|
||||||
|
const {values} = this.state;
|
||||||
|
values.splice(index, values.length - index);
|
||||||
|
value && values.push(value);
|
||||||
|
const stacks = this.computedStask(values);
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
stacks,
|
||||||
|
values
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.props?.onChange?.(value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据树结构层级,寻找最后一层
|
||||||
|
computedStask(values: string[]) {
|
||||||
|
const {options, valueField} = this.props;
|
||||||
|
const getDeep = (opts: Options, index: number, tems: Array<Options>) => {
|
||||||
|
tems.push(this.getFlatOptions(opts));
|
||||||
|
opts.forEach(op => {
|
||||||
|
const cValue = valueField ? op[valueField] : op?.value ?? '';
|
||||||
|
if (
|
||||||
|
cValue === values[index] &&
|
||||||
|
op.children &&
|
||||||
|
values.length - 1 >= index
|
||||||
|
) {
|
||||||
|
getDeep(op.children, index + 1, tems);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return tems;
|
||||||
|
};
|
||||||
|
|
||||||
|
return getDeep(options, 0, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {stacks, values} = this.state;
|
||||||
|
const {className, classnames: cx} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cx('ChainedDropdownSelection', className)}>
|
||||||
|
{stacks.map((item, index) => (
|
||||||
|
<div className={cx('ChainedDropdownSelection-item')} key={index}>
|
||||||
|
<DropDownSelection
|
||||||
|
{...this.props}
|
||||||
|
value={values[index]}
|
||||||
|
options={item}
|
||||||
|
onChange={value => this.handleSelect(index, value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default themeable(
|
||||||
|
localeable(
|
||||||
|
uncontrollable(ChainedDropdownSelection, {
|
||||||
|
value: 'onChange'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
172
packages/amis-ui/src/components/DropDownSelection.tsx
Normal file
172
packages/amis-ui/src/components/DropDownSelection.tsx
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {findDOMNode} from 'react-dom';
|
||||||
|
import {BaseSelection, BaseSelectionProps} from './Selection';
|
||||||
|
import {SpinnerExtraProps} from './Spinner';
|
||||||
|
import PopOverContainer from './PopOverContainer';
|
||||||
|
import ListSelection from './GroupedSelection';
|
||||||
|
import TreeSelection from './TreeSelection';
|
||||||
|
import ResultBox from './ResultBox';
|
||||||
|
import {
|
||||||
|
ThemeProps,
|
||||||
|
themeable,
|
||||||
|
localeable,
|
||||||
|
LocaleProps,
|
||||||
|
findTree,
|
||||||
|
filterTree,
|
||||||
|
noop,
|
||||||
|
isMobile
|
||||||
|
} from 'amis-core';
|
||||||
|
import {matchSorter} from 'match-sorter';
|
||||||
|
|
||||||
|
import {Icon} from './icons';
|
||||||
|
import SearchBox from './SearchBox';
|
||||||
|
import {Option} from './Select';
|
||||||
|
|
||||||
|
export interface DropDownSelectionProps
|
||||||
|
extends ThemeProps,
|
||||||
|
LocaleProps,
|
||||||
|
SpinnerExtraProps,
|
||||||
|
BaseSelectionProps {
|
||||||
|
options: Array<any>;
|
||||||
|
value: any;
|
||||||
|
onChange: (value: any) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
searchable?: boolean;
|
||||||
|
popOverContainer?: any;
|
||||||
|
mode?: 'list' | 'tree';
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DropDownSelectionState {
|
||||||
|
searchText: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DropDownSelection extends BaseSelection<
|
||||||
|
DropDownSelectionProps,
|
||||||
|
DropDownSelectionState
|
||||||
|
> {
|
||||||
|
constructor(props: DropDownSelectionProps) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
searchText: ''
|
||||||
|
};
|
||||||
|
this.onSearch = this.onSearch.bind(this);
|
||||||
|
this.filterOptions = this.filterOptions.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch(text: string) {
|
||||||
|
this.setState({searchText: text});
|
||||||
|
}
|
||||||
|
|
||||||
|
filterOptions(options: any[]) {
|
||||||
|
const {valueField = 'value', labelField} = this.props;
|
||||||
|
const text = this.state.searchText;
|
||||||
|
if (!text) {
|
||||||
|
return this.props.options;
|
||||||
|
}
|
||||||
|
return filterTree(
|
||||||
|
options,
|
||||||
|
(option: Option, key: number, level: number, paths: Array<Option>) => {
|
||||||
|
return !!(
|
||||||
|
(Array.isArray(option.children) && option.children.length) ||
|
||||||
|
!!matchSorter([option].concat(paths), text, {
|
||||||
|
keys: [labelField || 'label', valueField || 'value']
|
||||||
|
}).length
|
||||||
|
);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选了值,还原options
|
||||||
|
onPopClose(onClose: () => void) {
|
||||||
|
this.setState({searchText: ''});
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
value,
|
||||||
|
classnames: cx,
|
||||||
|
disabled,
|
||||||
|
translate: __,
|
||||||
|
searchable,
|
||||||
|
mode = 'list',
|
||||||
|
valueField = 'value',
|
||||||
|
option2value,
|
||||||
|
loadingConfig,
|
||||||
|
popOverContainer
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PopOverContainer
|
||||||
|
useMobileUI
|
||||||
|
popOverContainer={popOverContainer || (() => findDOMNode(this))}
|
||||||
|
popOverRender={({onClose}) => (
|
||||||
|
<div>
|
||||||
|
{searchable ? (
|
||||||
|
<SearchBox mini={false} onSearch={this.onSearch} />
|
||||||
|
) : null}
|
||||||
|
{mode === 'list' ? (
|
||||||
|
<ListSelection
|
||||||
|
multiple={false}
|
||||||
|
onClick={() => this.onPopClose(onClose)}
|
||||||
|
options={this.filterOptions(this.props.options)}
|
||||||
|
value={value}
|
||||||
|
option2value={option2value}
|
||||||
|
onChange={(value: any) => {
|
||||||
|
onChange(Array.isArray(value) ? value[0] : value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TreeSelection
|
||||||
|
className={'is-scrollable'}
|
||||||
|
multiple={false}
|
||||||
|
options={this.filterOptions(this.props.options)}
|
||||||
|
value={value}
|
||||||
|
loadingConfig={loadingConfig}
|
||||||
|
onChange={(value: any) => {
|
||||||
|
this.onPopClose(onClose);
|
||||||
|
onChange(value[valueField]);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{({onClick, ref, isOpened}) => (
|
||||||
|
<div className={cx('DropDownSelection')}>
|
||||||
|
<ResultBox
|
||||||
|
className={cx(
|
||||||
|
'DropDownSelection-input',
|
||||||
|
isOpened ? 'is-active' : ''
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
allowInput={false}
|
||||||
|
result={
|
||||||
|
value
|
||||||
|
? findTree(options, item => item[valueField] === value)
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
onResultChange={noop}
|
||||||
|
onResultClick={onClick}
|
||||||
|
placeholder={__('Condition.field_placeholder')}
|
||||||
|
disabled={disabled}
|
||||||
|
useMobileUI
|
||||||
|
>
|
||||||
|
{!isMobile() ? (
|
||||||
|
<span className={cx('DropDownSelection-caret')}>
|
||||||
|
<Icon icon="caret" className="icon" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</ResultBox>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</PopOverContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default themeable(localeable(DropDownSelection));
|
@ -50,7 +50,7 @@ export interface ExpressionProps extends ThemeProps, LocaleProps {
|
|||||||
formula?: FormulaPickerProps;
|
formula?: FormulaPickerProps;
|
||||||
popOverContainer?: any;
|
popOverContainer?: any;
|
||||||
renderEtrValue?: any;
|
renderEtrValue?: any;
|
||||||
selectMode?: 'list' | 'tree';
|
selectMode?: 'list' | 'tree' | 'chained';
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldMap = {
|
const fieldMap = {
|
||||||
|
@ -1,23 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {findDOMNode} from 'react-dom';
|
import {ThemeProps, themeable, localeable, LocaleProps} from 'amis-core';
|
||||||
import PopOverContainer from '../PopOverContainer';
|
|
||||||
import ListSelection from '../GroupedSelection';
|
|
||||||
import ResultBox from '../ResultBox';
|
|
||||||
import {
|
|
||||||
ClassNamesFn,
|
|
||||||
ThemeProps,
|
|
||||||
themeable,
|
|
||||||
utils,
|
|
||||||
localeable,
|
|
||||||
LocaleProps,
|
|
||||||
findTree,
|
|
||||||
noop,
|
|
||||||
isMobile
|
|
||||||
} from 'amis-core';
|
|
||||||
import {Icon} from '../icons';
|
|
||||||
import SearchBox from '../SearchBox';
|
|
||||||
import TreeSelection from '../TreeSelection';
|
|
||||||
import {SpinnerExtraProps} from '../Spinner';
|
import {SpinnerExtraProps} from '../Spinner';
|
||||||
|
import DropDownSelection from '../DropDownSelection';
|
||||||
|
import ChainedDropdownSelection from '../ChainedDropdownSelection';
|
||||||
|
|
||||||
export interface ConditionFieldProps
|
export interface ConditionFieldProps
|
||||||
extends ThemeProps,
|
extends ThemeProps,
|
||||||
@ -30,10 +15,10 @@ export interface ConditionFieldProps
|
|||||||
fieldClassName?: string;
|
fieldClassName?: string;
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
popOverContainer?: any;
|
popOverContainer?: any;
|
||||||
selectMode?: 'list' | 'tree';
|
selectMode?: 'list' | 'tree' | 'chained';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConditionFieldState {
|
export interface FieldState {
|
||||||
searchText: string;
|
searchText: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,137 +26,68 @@ const option2value = (item: any) => item.name;
|
|||||||
|
|
||||||
export class ConditionField extends React.Component<
|
export class ConditionField extends React.Component<
|
||||||
ConditionFieldProps,
|
ConditionFieldProps,
|
||||||
ConditionFieldState
|
FieldState
|
||||||
> {
|
> {
|
||||||
constructor(props: ConditionFieldProps) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
searchText: ''
|
|
||||||
};
|
|
||||||
this.onSearch = this.onSearch.bind(this);
|
|
||||||
this.filterOptions = this.filterOptions.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
onSearch(text: string) {
|
|
||||||
let txt = text.toLowerCase();
|
|
||||||
|
|
||||||
this.setState({searchText: txt});
|
|
||||||
}
|
|
||||||
|
|
||||||
filterOptions(options: any[]) {
|
|
||||||
const txt = this.state.searchText;
|
|
||||||
if (!txt) {
|
|
||||||
return this.props.options;
|
|
||||||
}
|
|
||||||
return options
|
|
||||||
.map((item: any) => {
|
|
||||||
if (item.children) {
|
|
||||||
let children = item.children.filter((child: any) => {
|
|
||||||
return (
|
|
||||||
child.name.toLowerCase().includes(txt) ||
|
|
||||||
child.label.toLowerCase().includes(txt)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return children.length > 0
|
|
||||||
? Object.assign({}, item, {children}) // 需要copy一份,防止覆盖原始数据
|
|
||||||
: false;
|
|
||||||
} else {
|
|
||||||
return item.name.toLowerCase().includes(txt) ||
|
|
||||||
item.label.toLowerCase().includes(txt)
|
|
||||||
? item
|
|
||||||
: false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter((item: any) => {
|
|
||||||
return !!item;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选了值,还原options
|
|
||||||
onPopClose(onClose: () => void) {
|
|
||||||
this.setState({searchText: ''});
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
options,
|
|
||||||
onChange,
|
onChange,
|
||||||
value,
|
value,
|
||||||
classnames: cx,
|
classnames: cx,
|
||||||
fieldClassName,
|
|
||||||
disabled,
|
disabled,
|
||||||
translate: __,
|
translate,
|
||||||
searchable,
|
searchable,
|
||||||
popOverContainer,
|
|
||||||
selectMode = 'list',
|
selectMode = 'list',
|
||||||
|
options,
|
||||||
loadingConfig
|
loadingConfig
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return selectMode === 'chained' ? (
|
||||||
<PopOverContainer
|
<ChainedDropdownSelection
|
||||||
useMobileUI
|
multiple={false}
|
||||||
popOverContainer={popOverContainer || (() => findDOMNode(this))}
|
classnames={cx}
|
||||||
popOverRender={({onClose}) => (
|
translate={translate}
|
||||||
<div>
|
options={options}
|
||||||
{searchable ? (
|
value={value}
|
||||||
<SearchBox mini={false} onSearch={this.onSearch} />
|
valueField="name"
|
||||||
) : null}
|
option2value={option2value}
|
||||||
{selectMode === 'tree' ? (
|
searchable={searchable}
|
||||||
<TreeSelection
|
disabled={disabled}
|
||||||
className={'is-scrollable'}
|
onChange={(value: any) => {
|
||||||
multiple={false}
|
onChange(Array.isArray(value) ? value[0] : value);
|
||||||
options={this.filterOptions(this.props.options)}
|
}}
|
||||||
value={value}
|
/>
|
||||||
loadingConfig={loadingConfig}
|
) : selectMode === 'tree' ? (
|
||||||
onChange={(value: any) => {
|
<DropDownSelection
|
||||||
this.onPopClose(onClose);
|
className={'is-scrollable'}
|
||||||
onChange(value.name);
|
classnames={cx}
|
||||||
}}
|
translate={translate}
|
||||||
/>
|
multiple={false}
|
||||||
) : (
|
option2value={option2value}
|
||||||
<ListSelection
|
searchable={searchable}
|
||||||
multiple={false}
|
disabled={disabled}
|
||||||
onClick={() => this.onPopClose(onClose)}
|
valueField={'name'}
|
||||||
options={this.filterOptions(this.props.options)}
|
mode={'tree'}
|
||||||
value={[value]}
|
options={options}
|
||||||
option2value={option2value}
|
value={value}
|
||||||
onChange={(value: any) =>
|
loadingConfig={loadingConfig}
|
||||||
onChange(Array.isArray(value) ? value[0] : value)
|
onChange={(value: any) => {
|
||||||
}
|
onChange(value);
|
||||||
/>
|
}}
|
||||||
)}
|
/>
|
||||||
</div>
|
) : (
|
||||||
)}
|
<DropDownSelection
|
||||||
>
|
classnames={cx}
|
||||||
{({onClick, ref, isOpened}) => (
|
translate={translate}
|
||||||
<div className={cx('CBGroup-field')}>
|
options={options}
|
||||||
<ResultBox
|
value={value}
|
||||||
className={cx(
|
valueField={'name'}
|
||||||
'CBGroup-fieldInput',
|
option2value={option2value}
|
||||||
fieldClassName,
|
searchable={searchable}
|
||||||
isOpened ? 'is-active' : ''
|
disabled={disabled}
|
||||||
)}
|
onChange={(value: any) =>
|
||||||
ref={ref}
|
onChange(Array.isArray(value) ? value[0] : value)
|
||||||
allowInput={false}
|
}
|
||||||
result={
|
/>
|
||||||
value ? findTree(options, item => item.name === value) : ''
|
|
||||||
}
|
|
||||||
onResultChange={noop}
|
|
||||||
onResultClick={onClick}
|
|
||||||
placeholder={__('Condition.field_placeholder')}
|
|
||||||
disabled={disabled}
|
|
||||||
useMobileUI
|
|
||||||
>
|
|
||||||
{!isMobile() ? (
|
|
||||||
<span className={cx('CBGroup-fieldCaret')}>
|
|
||||||
<Icon icon="caret" className="icon" />
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</ResultBox>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</PopOverContainer>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,12 @@ import {
|
|||||||
ThemeProps,
|
ThemeProps,
|
||||||
themeable,
|
themeable,
|
||||||
autobind,
|
autobind,
|
||||||
utils,
|
|
||||||
localeable,
|
localeable,
|
||||||
LocaleProps,
|
LocaleProps,
|
||||||
guid,
|
guid,
|
||||||
ConditionGroupValue
|
ConditionGroupValue,
|
||||||
|
isPureVariable,
|
||||||
|
resolveVariableAndFilter
|
||||||
} from 'amis-core';
|
} from 'amis-core';
|
||||||
import Button from '../Button';
|
import Button from '../Button';
|
||||||
import GroupOrItem from './GroupOrItem';
|
import GroupOrItem from './GroupOrItem';
|
||||||
@ -42,8 +43,11 @@ export interface ConditionGroupProps extends ThemeProps, LocaleProps {
|
|||||||
formula?: FormulaPickerProps;
|
formula?: FormulaPickerProps;
|
||||||
popOverContainer?: any;
|
popOverContainer?: any;
|
||||||
renderEtrValue?: any;
|
renderEtrValue?: any;
|
||||||
selectMode?: 'list' | 'tree';
|
selectMode?: 'list' | 'tree' | 'chained';
|
||||||
isCollapsed?: boolean; // 是否折叠
|
isCollapsed?: boolean; // 是否折叠
|
||||||
|
depth: number;
|
||||||
|
isAddBtnVisibleOn?: (param: {depth: number; breadth: number}) => boolean;
|
||||||
|
isAddGroupBtnVisibleOn?: (param: {depth: number; breadth: number}) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConditionGroup extends React.Component<
|
export class ConditionGroup extends React.Component<
|
||||||
@ -185,7 +189,10 @@ export class ConditionGroup extends React.Component<
|
|||||||
popOverContainer,
|
popOverContainer,
|
||||||
selectMode,
|
selectMode,
|
||||||
renderEtrValue,
|
renderEtrValue,
|
||||||
draggable
|
draggable,
|
||||||
|
depth,
|
||||||
|
isAddBtnVisibleOn,
|
||||||
|
isAddGroupBtnVisibleOn
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const {isCollapsed} = this.state;
|
const {isCollapsed} = this.state;
|
||||||
|
|
||||||
@ -196,6 +203,11 @@ export class ConditionGroup extends React.Component<
|
|||||||
: value!.children
|
: value!.children
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const param = {depth, breadth: body?.length ?? 0};
|
||||||
|
const addConditionVisibleBool = isAddBtnVisibleOn?.(param) ?? true;
|
||||||
|
const addConditionGroupVisibleBool =
|
||||||
|
isAddGroupBtnVisibleOn?.(param) ?? true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx('CBGroup')} data-group-id={value?.id}>
|
<div className={cx('CBGroup')} data-group-id={value?.id}>
|
||||||
{builderMode === 'simple' && showANDOR === false ? null : (
|
{builderMode === 'simple' && showANDOR === false ? null : (
|
||||||
@ -268,6 +280,9 @@ export class ConditionGroup extends React.Component<
|
|||||||
renderEtrValue={renderEtrValue}
|
renderEtrValue={renderEtrValue}
|
||||||
selectMode={selectMode}
|
selectMode={selectMode}
|
||||||
isCollapsed={isCollapsed}
|
isCollapsed={isCollapsed}
|
||||||
|
depth={depth}
|
||||||
|
isAddBtnVisibleOn={isAddBtnVisibleOn}
|
||||||
|
isAddGroupBtnVisibleOn={isAddGroupBtnVisibleOn}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
@ -304,15 +319,17 @@ export class ConditionGroup extends React.Component<
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={cx('ButtonGroup')}>
|
<div className={cx('ButtonGroup')}>
|
||||||
<Button
|
{addConditionVisibleBool ? (
|
||||||
level="link"
|
<Button
|
||||||
onClick={this.handleAdd}
|
level="link"
|
||||||
size="xs"
|
onClick={this.handleAdd}
|
||||||
disabled={disabled}
|
size="xs"
|
||||||
>
|
disabled={disabled}
|
||||||
{__('Condition.add_cond')}
|
>
|
||||||
</Button>
|
{__('Condition.add_cond')}
|
||||||
{builderMode === 'simple' ? null : (
|
</Button>
|
||||||
|
) : null}
|
||||||
|
{addConditionGroupVisibleBool && builderMode !== 'simple' ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={this.handleAddGroup}
|
onClick={this.handleAddGroup}
|
||||||
size="xs"
|
size="xs"
|
||||||
@ -321,7 +338,7 @@ export class ConditionGroup extends React.Component<
|
|||||||
>
|
>
|
||||||
{__('Condition.add_cond_group')}
|
{__('Condition.add_cond_group')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
) : null}
|
||||||
{removeable ? (
|
{removeable ? (
|
||||||
<Button
|
<Button
|
||||||
onClick={onRemove}
|
onClick={onRemove}
|
||||||
@ -334,11 +351,6 @@ export class ConditionGroup extends React.Component<
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* {removeable ? (
|
|
||||||
<a className={cx('CBDelete')} onClick={onRemove}>
|
|
||||||
{__('Condition.delete_cond_group')}
|
|
||||||
</a>
|
|
||||||
) : null} */}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,8 +28,11 @@ export interface CBGroupOrItemProps extends ThemeProps {
|
|||||||
formula?: FormulaPickerProps;
|
formula?: FormulaPickerProps;
|
||||||
popOverContainer?: any;
|
popOverContainer?: any;
|
||||||
renderEtrValue?: any;
|
renderEtrValue?: any;
|
||||||
selectMode?: 'list' | 'tree';
|
selectMode?: 'list' | 'tree' | 'chained';
|
||||||
isCollapsed?: boolean;
|
isCollapsed?: boolean;
|
||||||
|
depth: number;
|
||||||
|
isAddBtnVisibleOn?: (param: {depth: number; breadth: number}) => boolean;
|
||||||
|
isAddGroupBtnVisibleOn?: (param: {depth: number; breadth: number}) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
|
export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
|
||||||
@ -82,7 +85,10 @@ export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
|
|||||||
popOverContainer,
|
popOverContainer,
|
||||||
selectMode,
|
selectMode,
|
||||||
renderEtrValue,
|
renderEtrValue,
|
||||||
isCollapsed
|
isCollapsed,
|
||||||
|
depth,
|
||||||
|
isAddBtnVisibleOn,
|
||||||
|
isAddGroupBtnVisibleOn
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -116,6 +122,7 @@ export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
|
|||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
searchable={searchable}
|
searchable={searchable}
|
||||||
|
selectMode={selectMode}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
config={config}
|
config={config}
|
||||||
fields={fields}
|
fields={fields}
|
||||||
@ -127,6 +134,9 @@ export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
|
|||||||
onRemove={this.handleItemRemove}
|
onRemove={this.handleItemRemove}
|
||||||
data={data}
|
data={data}
|
||||||
renderEtrValue={renderEtrValue}
|
renderEtrValue={renderEtrValue}
|
||||||
|
depth={depth + 1}
|
||||||
|
isAddBtnVisibleOn={isAddBtnVisibleOn}
|
||||||
|
isAddGroupBtnVisibleOn={isAddGroupBtnVisibleOn}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -53,7 +53,7 @@ export interface ConditionItemProps extends ThemeProps, LocaleProps {
|
|||||||
formula?: FormulaPickerProps;
|
formula?: FormulaPickerProps;
|
||||||
popOverContainer?: any;
|
popOverContainer?: any;
|
||||||
renderEtrValue?: any;
|
renderEtrValue?: any;
|
||||||
selectMode?: 'list' | 'tree';
|
selectMode?: 'list' | 'tree' | 'chained';
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConditionItem extends React.Component<ConditionItemProps> {
|
export class ConditionItem extends React.Component<ConditionItemProps> {
|
||||||
|
@ -42,7 +42,9 @@ export interface ConditionBuilderProps extends ThemeProps, LocaleProps {
|
|||||||
formula?: FormulaPickerProps;
|
formula?: FormulaPickerProps;
|
||||||
popOverContainer?: any;
|
popOverContainer?: any;
|
||||||
renderEtrValue?: any;
|
renderEtrValue?: any;
|
||||||
selectMode?: 'list' | 'tree';
|
selectMode?: 'list' | 'tree' | 'chained';
|
||||||
|
isAddBtnVisibleOn?: (param: {depth: number; breadth: number}) => boolean;
|
||||||
|
isAddGroupBtnVisibleOn?: (param: {depth: number; breadth: number}) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConditionBuilderState {
|
export interface ConditionBuilderState {
|
||||||
@ -252,7 +254,9 @@ export class QueryBuilder extends React.Component<
|
|||||||
builderMode,
|
builderMode,
|
||||||
formula,
|
formula,
|
||||||
renderEtrValue,
|
renderEtrValue,
|
||||||
selectMode
|
selectMode,
|
||||||
|
isAddBtnVisibleOn,
|
||||||
|
isAddGroupBtnVisibleOn
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const normalizedValue = Array.isArray(value?.children)
|
const normalizedValue = Array.isArray(value?.children)
|
||||||
@ -292,6 +296,9 @@ export class QueryBuilder extends React.Component<
|
|||||||
renderEtrValue={renderEtrValue}
|
renderEtrValue={renderEtrValue}
|
||||||
popOverContainer={popOverContainer}
|
popOverContainer={popOverContainer}
|
||||||
selectMode={selectMode}
|
selectMode={selectMode}
|
||||||
|
depth={1}
|
||||||
|
isAddBtnVisibleOn={isAddBtnVisibleOn}
|
||||||
|
isAddGroupBtnVisibleOn={isAddGroupBtnVisibleOn}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -554,7 +554,7 @@ test('Renderer:condition-builder with source fields', async () => {
|
|||||||
type: 'condition-builder',
|
type: 'condition-builder',
|
||||||
label: '条件组件',
|
label: '条件组件',
|
||||||
name: 'conditions',
|
name: 'conditions',
|
||||||
source: '/api/condition-fields'
|
source: '/api/condition-fields/custom'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -670,7 +670,7 @@ test('Renderer:condition-builder with selectMode', async () => {
|
|||||||
fireEvent.click(await findByText('请选择字段'));
|
fireEvent.click(await findByText('请选择字段'));
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
container.querySelector('.cxd-CBGroup-field .cxd-TreeSelection')
|
container.querySelector('.cxd-TreeSelection')
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
// expect(container).toMatchSnapshot();
|
// expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
@ -5,7 +5,8 @@ import {
|
|||||||
FormBaseControl,
|
FormBaseControl,
|
||||||
Schema,
|
Schema,
|
||||||
isPureVariable,
|
isPureVariable,
|
||||||
resolveVariableAndFilter
|
resolveVariableAndFilter,
|
||||||
|
createObject
|
||||||
} from 'amis-core';
|
} from 'amis-core';
|
||||||
import {
|
import {
|
||||||
FormBaseControlSchema,
|
FormBaseControlSchema,
|
||||||
@ -75,6 +76,16 @@ export interface ConditionBuilderControlSchema extends FormBaseControlSchema {
|
|||||||
* 是否显示并或切换键按钮,只在简单模式下有用
|
* 是否显示并或切换键按钮,只在简单模式下有用
|
||||||
*/
|
*/
|
||||||
showANDOR?: boolean;
|
showANDOR?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表达式:控制按钮“添加条件”的显示
|
||||||
|
*/
|
||||||
|
addBtnVisibleOn?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表达式:控制按钮“添加条件组”的显示
|
||||||
|
*/
|
||||||
|
addConditionVisible?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConditionBuilderProps
|
export interface ConditionBuilderProps
|
||||||
@ -99,6 +110,30 @@ export default class ConditionBuilderControl extends React.PureComponent<Conditi
|
|||||||
return pickerIcon ? render('picker-icon', pickerIcon) : undefined;
|
return pickerIcon ? render('picker-icon', pickerIcon) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
getAddBtnVisible(param: {depth: number; breadth: number}) {
|
||||||
|
const {data, addBtnVisibleOn} = this.props;
|
||||||
|
if (addBtnVisibleOn && isPureVariable(addBtnVisibleOn)) {
|
||||||
|
return resolveVariableAndFilter(
|
||||||
|
addBtnVisibleOn,
|
||||||
|
createObject(data, param)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@autobind
|
||||||
|
getAddGroupBtnVisible(param: {depth: number; breadth: number}) {
|
||||||
|
const {data, addGroupBtnVisibleOn} = this.props;
|
||||||
|
if (addGroupBtnVisibleOn && isPureVariable(addGroupBtnVisibleOn)) {
|
||||||
|
return resolveVariableAndFilter(
|
||||||
|
addGroupBtnVisibleOn,
|
||||||
|
createObject(data, param)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {className, classnames: cx, style, pickerIcon, ...rest} = this.props;
|
const {className, classnames: cx, style, pickerIcon, ...rest} = this.props;
|
||||||
|
|
||||||
@ -124,6 +159,8 @@ export default class ConditionBuilderControl extends React.PureComponent<Conditi
|
|||||||
<ConditionBuilderWithRemoteOptions
|
<ConditionBuilderWithRemoteOptions
|
||||||
renderEtrValue={this.renderEtrValue}
|
renderEtrValue={this.renderEtrValue}
|
||||||
pickerIcon={this.renderPickerIcon()}
|
pickerIcon={this.renderPickerIcon()}
|
||||||
|
isAddBtnVisibleOn={this.getAddBtnVisible}
|
||||||
|
isAddGroupBtnVisibleOn={this.getAddGroupBtnVisible}
|
||||||
{...rest}
|
{...rest}
|
||||||
formula={formula}
|
formula={formula}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user