feat: condition-builder支持selectMode为chained的选项层级显示 (#7120)

* feat: condition-builder支持selectMode为chained的选项层级显示
This commit is contained in:
sqzhou 2023-06-12 12:58:22 +08:00 committed by GitHub
parent 125d860c2b
commit 75952bd558
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 625 additions and 171 deletions

View File

@ -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及以上版本` |

View 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"
}
]
}
]
}
}

View File

@ -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);
}
}

View 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'
})
)
);

View 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));

View File

@ -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 = {

View File

@ -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>
); );
} }
} }

View File

@ -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>

View File

@ -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>
) : ( ) : (

View File

@ -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> {

View File

@ -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}
/> />
); );
} }

View File

@ -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();
}); });

View File

@ -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}
/> />