feat:ConditionBuilder支持自定义判断条件和右边渲染组件;优化ConditionBuilder样式 (#3887)

Co-authored-by: Qin,Haoyan <qinhaoyan@baidu.com>
This commit is contained in:
qinhaoyan 2022-04-07 14:54:33 +08:00 committed by GitHub
parent 42cbec8f44
commit b2d2b27031
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 587 additions and 295 deletions

View File

@ -71,57 +71,35 @@ exports[`Renderer:condition-builder 1`] = `
> >
<div <div
class="cxd-CBGroup" class="cxd-CBGroup"
>
<div
class="cxd-CBGroup-toolbar"
> >
<div <div
class="cxd-CBGroup-toolbarCondition" class="cxd-CBGroup-toolbarCondition"
> >
<div <div
class="cxd-ButtonGroup" aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="downshift-0-label"
class="cxd-Select"
role="combobox"
tabindex="0"
> >
<button <div
class="cxd-Button cxd-Button--default cxd-Button--xs is-active" class="cxd-Select-valueWrap"
type="button" >
<div
class="cxd-Select-value"
> >
并且 并且
</button>
<button
class="cxd-Button cxd-Button--default cxd-Button--xs"
type="button"
>
或者
</button>
</div> </div>
</div> </div>
<div <span
class="cxd-CBGroup-toolbarConditionAdd" class="cxd-Select-arrow"
>
<div
class="cxd-ButtonGroup"
>
<button
class="cxd-Button cxd-Button--default cxd-Button--xs"
type="button"
> >
<icon-mock <icon-mock
classname="icon icon-plus" classname="icon icon-caret"
icon="plus" icon="caret"
/> />
添加条件 </span>
</button>
<button
class="cxd-Button cxd-Button--default cxd-Button--xs"
type="button"
>
<icon-mock
classname="icon icon-plus-cicle"
icon="plus-cicle"
/>
添加条件组
</button>
</div>
</div> </div>
</div> </div>
<div <div
@ -133,6 +111,30 @@ exports[`Renderer:condition-builder 1`] = `
</div> </div>
</div> </div>
<div
class="cxd-CBGroup-toolbar"
>
<div
class="cxd-CBGroup-toolbarConditionAdd"
>
<div
class="cxd-ButtonGroup"
>
<button
class="cxd-Button cxd-Button--link cxd-Button--xs"
type="button"
>
添加条件
</button>
<button
class="cxd-Button cxd-Button--link cxd-Button--xs"
type="button"
>
添加条件组
</button>
</div>
</div>
</div>
</div> </div>
</div> </div>
<span <span

View File

@ -384,6 +384,97 @@ type Value = ValueGroup;
} }
``` ```
### 自定义
- `type` 字段配置中配置成 `"custom"`
- `label` 字段名称
- `placeholder` 占位符
- `operators` 默认为空,需配置自定义判断条件,支持字符串或 key-value 格式
- `value` 字段配置右边值需要渲染的组件,支持 amis 输入类组件或自定义输入类组件
```schema: scope="body"
{
"type": "form",
"debug": true,
"body": [
{
"type": "condition-builder",
"label": "条件组件",
"name": "conditions",
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
"fields": [
{
"label": "自定义",
"type": "custom",
"name": "a",
"value": {
"type": "input-color"
},
"operators": [
"equal",
{
"label": "等于(自定义)",
"value": "custom_equal"
}
]
}
]
}
]
}
```
其中`operators`通过配置 values 还支持右边多个组件的渲染,`right`值格式为对象,`key`为组件的`name`
```schema: scope="body"
{
"type": "form",
"debug": true,
"body": [
{
"type": "condition-builder",
"label": "条件组件",
"name": "conditions",
"description": "适合让用户自己拼查询条件,然后后端根据数据生成 query where",
"fields": [
{
"label": "自定义",
"type": "custom",
"name": "a",
"value": {
"type": "input-color"
},
"operators": [
{
"label": "等于(自定义)",
"value": "custom_equal"
},
{
"label": "属于",
"value": "belong",
"values": [
{
"type": "input-text",
"name": "color1"
},
{
"type": "tpl",
"tpl": "~"
},
{
"type": "input-text",
"name": "color2"
}
]
}
]
}
]
}
]
}
```
## 字段选项远程拉取 ## 字段选项远程拉取
- 方式 1 配置 `source` 接口返回的数据对象 `data` 中存在 fields 变量即可。 - 方式 1 配置 `source` 接口返回的数据对象 `data` 中存在 fields 变量即可。

View File

@ -28,6 +28,46 @@ export default {
description: description:
'适合让用户自己拼查询条件,然后后端根据数据生成 query where', '适合让用户自己拼查询条件,然后后端根据数据生成 query where',
fields: [ fields: [
{
name: 'switch',
type: 'custom',
label: '开关',
value: {
name: 'checkbox',
type: 'checkbox',
label: '勾选框',
option: '选项说明'
},
operators: [
'equal',
{
label: '属于',
value: 'belong'
},
{
label: '不属于',
value: 'not_belong',
values: [
{
type: 'input-date',
name: 'date1',
label: '日期',
inputFormat: 'YYYY年MM月DD日'
},
{
type: 'tpl',
tpl: '~'
},
{
type: 'input-date',
name: 'date2',
label: '日期',
inputFormat: 'YYYY年MM月DD日'
}
]
}
]
},
{ {
label: '文本', label: '文本',
type: 'text', type: 'text',

View File

@ -1,11 +1,36 @@
.#{$ns}CBGroup { .#{$ns}CBGroup {
font-size: var(--fontSizeSm); font-size: var(--fontSizeSm);
position: relative;
border: 1px solid #e8e9eb;
border-radius: 4px;
border-left: px2rem(3px) solid #e6f0ff;
padding: px2rem(30px) px2rem(27px) px2rem(17px);
margin-top: px2rem(30px);
&-toolbarCondition {
text-align: center;
margin-top: px2rem(-44px);
margin-bottom: px2rem(16px);
.#{$ns}Select {
font-size: 12px;
height: px2rem(28px);
width: px2rem(62px);
background: #d4e5ff;
border: none;
color: #0832a6;
font-weight: 500;
padding: 0;
padding-left: px2rem(8px);
min-height: px2rem(28px);
&:hover {
background: #d4e5ff;
}
}
}
&-toolbar { &-toolbar {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
padding-bottom: 10px; margin-top: px2rem(8px);
.#{$ns}Button { .#{$ns}Button {
transition: padding var(--animation-duration); transition: padding var(--animation-duration);
min-width: unset; min-width: unset;
@ -18,29 +43,22 @@
} }
} }
&:not(:hover)
.#{$ns}CBGroup-toolbarCondition
.#{$ns}Button:not(.is-active) {
width: 0;
padding: 0;
overflow: hidden;
opacity: 0;
margin: 0;
border: 0;
}
.#{$ns}CBGroup-toolbarConditionAdd { .#{$ns}CBGroup-toolbarConditionAdd {
transition: opacity var(--animation-duration);
display: flex; display: flex;
align-items: center; align-items: center;
.#{$ns}ButtonGroup {
& > .cxd-Button:first-child {
margin-right: px2rem(24px);
}
}
.#{$ns}CBDelete { .#{$ns}CBDelete {
margin-left: var(--gap-xs); margin-left: var(--gap-xs);
} }
} }
&:not(:hover) .#{$ns}CBGroup-toolbarConditionAdd {
opacity: 0;
} }
.#{$ns}ResultBox {
padding-right: px2rem(3px);
} }
&-field, &-field,
@ -76,92 +94,79 @@
&-placeholder { &-placeholder {
color: var(--text--muted-color); color: var(--text--muted-color);
position: relative; position: relative;
margin-left: 30px;
padding: 10px; padding: 10px;
background: rgba(0, 0, 0, 0.03); background: rgba(0, 0, 0, 0.03);
border-radius: 5px; border-radius: 5px;
&:before {
position: absolute;
content: '';
top: -10px;
left: -30px;
width: var(--gap-md);
border-left: solid 1px var(--borderColor);
bottom: 0;
}
&:after {
position: absolute;
content: '';
top: 50%;
width: var(--gap-md);
left: -30px;
border-top: solid 1px var(--borderColor);
}
&:last-child {
&:before {
border-bottom-left-radius: 5px;
border-bottom: solid 1px var(--borderColor);
bottom: 50%;
}
&:after {
display: none;
}
}
&.simple { &.simple {
margin-left: 0; margin-left: 0;
&:before {
display: none;
} }
&:after {
display: none;
}
}
}
&-toolbarCondition {
margin-right: var(--gap-base);
} }
} }
.#{$ns}CBDelete { .#{$ns}CBDelete {
@include icon-color();
cursor: pointer; cursor: pointer;
margin-left: auto; margin-left: auto;
transition: opacity var(--animation-duration); }
.#{$ns}CBGroupOrItem-body-group--hover {
& > .#{$ns}CBGroupOrItem-dragbar {
opacity: 1 !important;
}
& > .#{$ns}CBGroup {
border-left-color: #2468f1;
}
} }
.#{$ns}CBGroupOrItem { .#{$ns}CBGroupOrItem {
position: relative; position: relative;
margin-left: px2rem(30px);
& + & { & + & {
margin-top: px2rem(10px); margin-top: px2rem(10px);
} }
&-dragbar {
cursor: move;
width: 20px;
margin-left: -5px;
opacity: 0.6;
text-align: center;
transition: opacity var(--animation-duration) ease-out;
@include icon-color();
}
&-body { &-body {
display: flex; display: flex;
padding: 2px 7px;
border-radius: 5px;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
position: relative; position: relative;
background: rgba(0, 0, 0, 0.03);
transition: all var(--animation-duration) ease-out; transition: all var(--animation-duration) ease-out;
&-group {
width: 100%;
flex-direction: row;
display: flex;
align-items: center;
margin-top: px2rem(16px);
> .#{$ns}CBGroupOrItem-dragbar {
left: px2rem(-16px);
position: absolute;
}
> .#{$ns}CBGroup { > .#{$ns}CBGroup {
margin: 3px; margin: 0px;
} }
} }
&-body:not(:hover) .#{$ns}CBDelete { &-item {
opacity: 0; background-color: #f7f7f9;
width: 100%;
padding: px2rem(12px);
padding-left: px2rem(28px);
display: flex;
flex-direction: row;
align-items: center;
> .#{$ns}CBGroupOrItem-dragbar {
left: px2rem(10px);
position: absolute;
}
}
} }
&.is-dragging { &.is-dragging {
@ -179,59 +184,19 @@
background: rgba($info, 0.2); background: rgba($info, 0.2);
} }
&-dragbar {
cursor: move;
width: 20px;
margin-left: -5px;
opacity: 0.6;
text-align: center;
transition: opacity var(--animation-duration) ease-out;
@include icon-color();
}
.#{$ns}CBGroup { .#{$ns}CBGroup {
flex-grow: 1; flex-grow: 1;
} }
&:hover > &-body { & > &-body > &-body-group > &-dragbar,
background: rgba(0, 0, 0, 0.05); & > &-body > &-body-item > &-dragbar {
opacity: 0;
} }
&:hover > &-body > &-dragbar { &:hover > &-body > &-body-item > &-dragbar {
opacity: 1; opacity: 1;
} }
&:before {
position: absolute;
content: '';
top: -10px;
left: -30px;
width: 20px;
border-left: solid 1px var(--borderColor);
bottom: 0;
}
&:after {
position: absolute;
content: '';
top: 50%;
width: 20px;
left: -30px;
border-top: solid 1px var(--borderColor);
}
&:last-child {
&:before {
border-bottom-left-radius: 5px;
border-bottom: solid 1px var(--borderColor);
bottom: 50%;
}
&:after {
display: none;
}
}
&-simple { &-simple {
margin-bottom: var(--gap-sm); margin-bottom: var(--gap-sm);
} }

View File

@ -20,6 +20,7 @@ import {Config} from './config';
import InputBox from '../InputBox'; import InputBox from '../InputBox';
import Formula from './Formula'; import Formula from './Formula';
import {FormulaPickerProps} from '../formula/Picker'; import {FormulaPickerProps} from '../formula/Picker';
import {localeable, LocaleProps} from '../../locale';
/** /**
* 4 * 4
@ -30,7 +31,7 @@ import {FormulaPickerProps} from '../formula/Picker';
* 4. * 4.
*/ */
export interface ExpressionProps extends ThemeProps { export interface ExpressionProps extends ThemeProps, LocaleProps {
value: ExpressionComplex; value: ExpressionComplex;
data?: any; data?: any;
index?: number; index?: number;
@ -46,6 +47,7 @@ export interface ExpressionProps extends ThemeProps {
fieldClassName?: string; fieldClassName?: string;
formula?: FormulaPickerProps; formula?: FormulaPickerProps;
popOverContainer?: any; popOverContainer?: any;
renderEtrValue?: any;
} }
const fieldMap = { const fieldMap = {
@ -137,7 +139,8 @@ export class Expression extends React.Component<ExpressionProps> {
disabled, disabled,
searchable, searchable,
formula, formula,
popOverContainer popOverContainer,
renderEtrValue
} = this.props; } = this.props;
const inputType = const inputType =
((value as any)?.type === 'field' ((value as any)?.type === 'field'
@ -169,6 +172,7 @@ export class Expression extends React.Component<ExpressionProps> {
disabled={disabled} disabled={disabled}
formula={formula} formula={formula}
popOverContainer={popOverContainer} popOverContainer={popOverContainer}
renderEtrValue={renderEtrValue}
/> />
) : null} ) : null}
@ -231,4 +235,4 @@ export class Expression extends React.Component<ExpressionProps> {
} }
} }
export default themeable(Expression); export default themeable(localeable(Expression));

View File

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import {localeable, LocaleProps} from '../../locale';
import {ThemeProps, themeable} from '../../theme'; import {ThemeProps, themeable} from '../../theme';
import InputBox from '../InputBox'; import InputBox from '../InputBox';
export interface FormulaProps extends ThemeProps { export interface FormulaProps extends ThemeProps, LocaleProps {
value: any; value: any;
onChange: (value: any) => void; onChange: (value: any) => void;
disabled?: boolean; disabled?: boolean;
@ -10,7 +11,13 @@ export interface FormulaProps extends ThemeProps {
export class Formula extends React.Component<FormulaProps> { export class Formula extends React.Component<FormulaProps> {
render() { render() {
const {classnames: cx, value, onChange, disabled} = this.props; const {
classnames: cx,
value,
onChange,
disabled,
translate: __
} = this.props;
return ( return (
<div className={cx('CBFormula')}> <div className={cx('CBFormula')}>
@ -18,12 +25,16 @@ export class Formula extends React.Component<FormulaProps> {
disabled={disabled} disabled={disabled}
value={value} value={value}
onChange={onChange} onChange={onChange}
placeholder="请输入公式" placeholder={__('Condition.formula_placeholder')}
prefix={<span className={cx('CBFormula-label')}></span>} prefix={
<span className={cx('CBFormula-label')}>
{__('Condition.expression')}
</span>
}
/> />
</div> </div>
); );
} }
} }
export default themeable(Formula); export default themeable(localeable(Formula));

View File

@ -8,8 +8,9 @@ import ResultBox from '../ResultBox';
import {Icon} from '../icons'; import {Icon} from '../icons';
import Expression from './Expression'; import Expression from './Expression';
import {Config} from './config'; import {Config} from './config';
import {localeable, LocaleProps} from '../../locale';
export interface ConditionFuncProps extends ThemeProps { export interface ConditionFuncProps extends ThemeProps, LocaleProps {
value: ExpressionFunc; value: ExpressionFunc;
onChange: (value: ExpressionFunc) => void; onChange: (value: ExpressionFunc) => void;
disabled?: boolean; disabled?: boolean;
@ -68,7 +69,14 @@ export class ConditionFunc extends React.Component<ConditionFuncProps> {
} }
render() { render() {
const {value, classnames: cx, fieldClassName, funcs, disabled} = this.props; const {
value,
classnames: cx,
fieldClassName,
funcs,
disabled,
translate: __
} = this.props;
const func = value const func = value
? findTree(funcs!, item => (item as Func).type === value.func) ? findTree(funcs!, item => (item as Func).type === value.func)
: null; : null;
@ -100,7 +108,7 @@ export class ConditionFunc extends React.Component<ConditionFuncProps> {
result={func} result={func}
onResultChange={noop} onResultChange={noop}
onResultClick={onClick} onResultClick={onClick}
placeholder="请选择字段" placeholder={__('Condition.field_placeholder')}
disabled={disabled} disabled={disabled}
> >
<span className={cx('CBGroup-fieldCaret')}> <span className={cx('CBGroup-fieldCaret')}>
@ -114,11 +122,13 @@ export class ConditionFunc extends React.Component<ConditionFuncProps> {
{func ? ( {func ? (
this.renderFunc(func as Func) this.renderFunc(func as Func)
) : ( ) : (
<span className={cx('CBFunc-error')}></span> <span className={cx('CBFunc-error')}>
{__('Condition.fun_error')}
</span>
)} )}
</div> </div>
); );
} }
} }
export default themeable(ConditionFunc); export default themeable(localeable(ConditionFunc));

View File

@ -8,6 +8,7 @@ import {Config} from './config';
import {Icon} from '../icons'; import {Icon} from '../icons';
import {localeable, LocaleProps} from '../../locale'; import {localeable, LocaleProps} from '../../locale';
import {FormulaPickerProps} from '../formula/Picker'; import {FormulaPickerProps} from '../formula/Picker';
import Select from '../Select';
export interface ConditionGroupProps extends ThemeProps, LocaleProps { export interface ConditionGroupProps extends ThemeProps, LocaleProps {
builderMode?: 'simple' | 'full'; builderMode?: 'simple' | 'full';
@ -27,13 +28,14 @@ export interface ConditionGroupProps extends ThemeProps, LocaleProps {
fieldClassName?: string; fieldClassName?: string;
formula?: FormulaPickerProps; formula?: FormulaPickerProps;
popOverContainer?: any; popOverContainer?: any;
renderEtrValue?: any;
} }
export class ConditionGroup extends React.Component<ConditionGroupProps> { export class ConditionGroup extends React.Component<ConditionGroupProps> {
getValue() { getValue() {
return { return {
id: guid(), id: guid(),
conjunction: 'and' as 'and', conjunction: 'and',
...this.props.value ...this.props.value
} as ConditionGroupValue; } as ConditionGroupValue;
} }
@ -48,10 +50,10 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
} }
@autobind @autobind
handleConjunctionClick() { handleConjunctionChange(val: {value: 'or' | 'and'}) {
const onChange = this.props.onChange; const onChange = this.props.onChange;
let value = this.getValue(); let value = this.getValue();
value.conjunction = value.conjunction === 'and' ? 'or' : 'and'; value.conjunction = val.value;
onChange(value); onChange(value);
} }
@ -136,11 +138,11 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
searchable, searchable,
translate: __, translate: __,
formula, formula,
popOverContainer popOverContainer,
renderEtrValue
} = this.props; } = this.props;
return ( return (
<div className={cx('CBGroup')} data-group-id={value?.id}> <div className={cx('CBGroup')} data-group-id={value?.id}>
<div className={cx('CBGroup-toolbar')}>
{builderMode === 'simple' && showANDOR === false ? null : ( {builderMode === 'simple' && showANDOR === false ? null : (
<div className={cx('CBGroup-toolbarCondition')}> <div className={cx('CBGroup-toolbarCondition')}>
{showNot ? ( {showNot ? (
@ -154,56 +156,24 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
{__('Condition.not')} {__('Condition.not')}
</Button> </Button>
) : null} ) : null}
<div className={cx('ButtonGroup')}> <Select
<Button options={[
size="xs" {
onClick={this.handleConjunctionClick} label: __('Condition.and'),
active={value?.conjunction !== 'or'} value: 'and'
},
{
label: __('Condition.or'),
value: 'or'
}
]}
value={value?.conjunction || 'and'}
disabled={disabled} disabled={disabled}
> onChange={this.handleConjunctionChange}
{__('Condition.and')} clearable={false}
</Button> />
<Button
size="xs"
onClick={this.handleConjunctionClick}
active={value?.conjunction === 'or'}
disabled={disabled}
>
{__('Condition.or')}
</Button>
</div>
</div> </div>
)} )}
<div
className={cx(
`CBGroup-toolbarConditionAdd${
builderMode === 'simple' ? '-simple' : ''
}`
)}
>
<div className={cx('ButtonGroup')}>
<Button onClick={this.handleAdd} size="xs" disabled={disabled}>
<Icon icon="plus" className="icon" />
{__('Condition.add_cond')}
</Button>
{builderMode === 'simple' ? null : (
<Button
onClick={this.handleAddGroup}
size="xs"
disabled={disabled}
>
<Icon icon="plus-cicle" className="icon" />
{__('Condition.add_cond_group')}
</Button>
)}
</div>
</div>
{removeable ? (
<a className={cx('CBDelete')} onClick={onRemove}>
<Icon icon="close" className="icon" />
</a>
) : null}
</div>
<div className={cx('CBGroup-body')}> <div className={cx('CBGroup-body')}>
{Array.isArray(value?.children) && value!.children.length ? ( {Array.isArray(value?.children) && value!.children.length ? (
value!.children.map((item, index) => ( value!.children.map((item, index) => (
@ -225,6 +195,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
builderMode={builderMode} builderMode={builderMode}
formula={formula} formula={formula}
popOverContainer={popOverContainer} popOverContainer={popOverContainer}
renderEtrValue={renderEtrValue}
/> />
)) ))
) : ( ) : (
@ -239,6 +210,41 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
</div> </div>
)} )}
</div> </div>
<div className={cx('CBGroup-toolbar')}>
<div
className={cx(
`CBGroup-toolbarConditionAdd${
builderMode === 'simple' ? '-simple' : ''
}`
)}
>
<div className={cx('ButtonGroup')}>
<Button
level="link"
onClick={this.handleAdd}
size="xs"
disabled={disabled}
>
{__('Condition.add_cond')}
</Button>
{builderMode === 'simple' ? null : (
<Button
onClick={this.handleAddGroup}
size="xs"
disabled={disabled}
level="link"
>
{__('Condition.add_cond_group')}
</Button>
)}
</div>
</div>
{removeable ? (
<a className={cx('CBDelete')} onClick={onRemove}>
{__('Condition.delete_cond_group')}
</a>
) : null}
</div>
</div> </div>
); );
} }

View File

@ -7,6 +7,7 @@ import {autobind} from '../../utils/helper';
import ConditionGroup from './Group'; import ConditionGroup from './Group';
import ConditionItem from './Item'; import ConditionItem from './Item';
import {FormulaPickerProps} from '../formula/Picker'; import {FormulaPickerProps} from '../formula/Picker';
import Button from '../Button';
export interface CBGroupOrItemProps extends ThemeProps { export interface CBGroupOrItemProps extends ThemeProps {
builderMode?: 'simple' | 'full'; builderMode?: 'simple' | 'full';
@ -26,9 +27,13 @@ export interface CBGroupOrItemProps extends ThemeProps {
fieldClassName?: string; fieldClassName?: string;
formula?: FormulaPickerProps; formula?: FormulaPickerProps;
popOverContainer?: any; popOverContainer?: any;
renderEtrValue?: any;
} }
export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> { export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
state = {
hover: false
};
@autobind @autobind
handleItemChange(value: any) { handleItemChange(value: any) {
this.props.onChange(value, this.props.index); this.props.onChange(value, this.props.index);
@ -39,6 +44,21 @@ export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
this.props.onRemove?.(this.props.index); this.props.onRemove?.(this.props.index);
} }
@autobind
handlerHoverIn(e: any) {
e.stopPropagation();
this.setState({
hover: true
});
}
@autobind
handlerHoverOut(e: any) {
this.setState({
hover: false
});
}
render() { render() {
const { const {
builderMode, builderMode,
@ -54,7 +74,8 @@ export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
searchable, searchable,
onDragStart, onDragStart,
formula, formula,
popOverContainer popOverContainer,
renderEtrValue
} = this.props; } = this.props;
return ( return (
@ -65,6 +86,15 @@ export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
data-id={value?.id} data-id={value?.id}
> >
<div className={cx('CBGroupOrItem-body')}> <div className={cx('CBGroupOrItem-body')}>
{value?.conjunction ? (
<div
className={cx(
'CBGroupOrItem-body-group',
this.state.hover && 'CBGroupOrItem-body-group--hover'
)}
onMouseOver={this.handlerHoverIn}
onMouseOut={this.handlerHoverOut}
>
{draggable ? ( {draggable ? (
<a <a
draggable draggable
@ -74,8 +104,6 @@ export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
<Icon icon="drag-bar" className="icon" /> <Icon icon="drag-bar" className="icon" />
</a> </a>
) : null} ) : null}
{value?.conjunction ? (
<ConditionGroup <ConditionGroup
disabled={disabled} disabled={disabled}
searchable={searchable} searchable={searchable}
@ -89,9 +117,20 @@ export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
removeable removeable
onRemove={this.handleItemRemove} onRemove={this.handleItemRemove}
data={data} data={data}
renderEtrValue={renderEtrValue}
/> />
</div>
) : ( ) : (
<> <div className={cx('CBGroupOrItem-body-item')}>
{draggable ? (
<a
draggable
onDragStart={onDragStart}
className={cx('CBGroupOrItem-dragbar')}
>
<Icon icon="drag-bar" className="icon" />
</a>
) : null}
<ConditionItem <ConditionItem
disabled={disabled} disabled={disabled}
searchable={searchable} searchable={searchable}
@ -104,11 +143,15 @@ export class CBGroupOrItem extends React.Component<CBGroupOrItemProps> {
data={data} data={data}
formula={formula} formula={formula}
popOverContainer={popOverContainer} popOverContainer={popOverContainer}
renderEtrValue={renderEtrValue}
/> />
<a className={cx('CBDelete')} onClick={this.handleItemRemove}> <Button
<Icon icon="close" className="icon" /> className={cx('CBDelete')}
</a> onClick={this.handleItemRemove}
</> >
<Icon icon="remove" className="icon" />
</Button>
</div>
)} )}
</div> </div>
</div> </div>

View File

@ -23,6 +23,7 @@ import GroupedSelection from '../GroupedSelection';
import ResultBox from '../ResultBox'; import ResultBox from '../ResultBox';
import {localeable, LocaleProps} from '../../locale'; import {localeable, LocaleProps} from '../../locale';
import {FormulaPickerProps} from '../formula/Picker'; import {FormulaPickerProps} from '../formula/Picker';
import {PlainObject} from '../../types';
const option2value = (item: any) => item.value; const option2value = (item: any) => item.value;
@ -39,6 +40,7 @@ export interface ConditionItemProps extends ThemeProps, LocaleProps {
fieldClassName?: string; fieldClassName?: string;
formula?: FormulaPickerProps; formula?: FormulaPickerProps;
popOverContainer?: any; popOverContainer?: any;
renderEtrValue?: any;
} }
export class ConditionItem extends React.Component<ConditionItemProps> { export class ConditionItem extends React.Component<ConditionItemProps> {
@ -91,12 +93,22 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
onChange(value, this.props.index); onChange(value, this.props.index);
} }
handleRightSubChange(index: number, rightValue: any) { handleRightSubChange(
const origin = Array.isArray(this.props.value?.right) isCustom: boolean,
index: number | string,
rightValue: any
) {
let origin;
if (isCustom) {
origin = Object.assign({}, this.props.value?.right) as PlainObject;
origin[index] = rightValue;
} else {
origin = Array.isArray(this.props.value?.right)
? this.props.value.right.concat() ? this.props.value.right.concat()
: []; : [];
origin[index as number] = rightValue;
}
origin[index] = rightValue;
const value = {...this.props.value, right: origin}; const value = {...this.props.value, right: origin};
const onChange = this.props.onChange; const onChange = this.props.onChange;
@ -145,7 +157,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
popOverContainer popOverContainer
} = this.props; } = this.props;
const left = value?.left; const left = value?.left;
let operators: Array<string> = []; let operators: any[] = [];
if ((left as ExpressionFunc)?.type === 'func') { if ((left as ExpressionFunc)?.type === 'func') {
const func: Func = findTree( const func: Func = findTree(
@ -169,6 +181,16 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
if (Array.isArray(operators) && operators.length) { if (Array.isArray(operators) && operators.length) {
const __ = this.props.translate; const __ = this.props.translate;
const options = operators.map(operator => {
if (typeof operator === 'string') {
return {
label: __(OperationMap[operator as keyof typeof OperationMap]),
value: operator
};
} else {
return operator;
}
});
return ( return (
<PopOverContainer <PopOverContainer
popOverContainer={popOverContainer || (() => findDOMNode(this))} popOverContainer={popOverContainer || (() => findDOMNode(this))}
@ -177,10 +199,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
onClick={onClose} onClick={onClose}
option2value={option2value} option2value={option2value}
onChange={this.handleOperatorChange} onChange={this.handleOperatorChange}
options={operators.map(operator => ({ options={options}
label: __(OperationMap[operator as keyof typeof OperationMap]),
value: operator
}))}
value={value.op} value={value.op}
multiple={false} multiple={false}
/> />
@ -195,9 +214,10 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
)} )}
ref={ref} ref={ref}
allowInput={false} allowInput={false}
result={__( result={
OperationMap[value?.op as keyof typeof OperationMap] __(OperationMap[value?.op as keyof typeof OperationMap]) ||
)} options.find(option => option.value === value.op)?.label
}
onResultChange={noop} onResultChange={noop}
onResultClick={onClick} onResultClick={onClick}
disabled={disabled} disabled={disabled}
@ -263,13 +283,16 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
classnames: cx, classnames: cx,
disabled, disabled,
formula, formula,
popOverContainer popOverContainer,
renderEtrValue
} = this.props; } = this.props;
let field = { let field = {
...config.types[type], ...config.types[type],
type type
} as FieldSimple; } as FieldSimple;
let option;
if ((value?.left as ExpressionField)?.type === 'field') { if ((value?.left as ExpressionField)?.type === 'field') {
const leftField: FieldSimple = findTree( const leftField: FieldSimple = findTree(
fields, fields,
@ -281,6 +304,9 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
...field, ...field,
...leftField ...leftField
}; };
option = field.operators?.find(
option => typeof option !== 'string' && option?.value === op
);
} }
} }
@ -295,7 +321,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
valueField={field} valueField={field}
value={(value.right as Array<ExpressionComplex>)?.[0]} value={(value.right as Array<ExpressionComplex>)?.[0]}
data={data} data={data}
onChange={this.handleRightSubChange.bind(this, 0)} onChange={this.handleRightSubChange.bind(this, false, 0)}
fields={fields} fields={fields}
allowedTypes={ allowedTypes={
field?.valueTypes || field?.valueTypes ||
@ -304,6 +330,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
disabled={disabled} disabled={disabled}
formula={formula} formula={formula}
popOverContainer={popOverContainer} popOverContainer={popOverContainer}
renderEtrValue={renderEtrValue}
/> />
<span className={cx('CBSeprator')}>~</span> <span className={cx('CBSeprator')}>~</span>
@ -314,7 +341,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
valueField={field} valueField={field}
value={(value.right as Array<ExpressionComplex>)?.[1]} value={(value.right as Array<ExpressionComplex>)?.[1]}
data={data} data={data}
onChange={this.handleRightSubChange.bind(this, 1)} onChange={this.handleRightSubChange.bind(this, false, 1)}
fields={fields} fields={fields}
allowedTypes={ allowedTypes={
field?.valueTypes || field?.valueTypes ||
@ -323,11 +350,36 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
disabled={disabled} disabled={disabled}
formula={formula} formula={formula}
popOverContainer={popOverContainer} popOverContainer={popOverContainer}
renderEtrValue={renderEtrValue}
/> />
</> </>
); );
} else if (option && typeof option !== 'string' && option.values) {
return option.values.map((schema, i) => {
return (
<span key={i}>
<Expression
config={config}
op={op}
funcs={funcs}
valueField={schema}
value={value.right}
data={data}
onChange={this.handleRightSubChange.bind(this, true, schema.name)}
fields={fields}
allowedTypes={
field?.valueTypes ||
config.valueTypes || ['value', 'field', 'func', 'formula']
}
disabled={disabled}
formula={formula}
popOverContainer={popOverContainer}
renderEtrValue={renderEtrValue}
/>
</span>
);
});
} }
return ( return (
<Expression <Expression
config={config} config={config}
@ -345,6 +397,7 @@ export class ConditionItem extends React.Component<ConditionItemProps> {
disabled={disabled} disabled={disabled}
formula={formula} formula={formula}
popOverContainer={popOverContainer} popOverContainer={popOverContainer}
renderEtrValue={renderEtrValue}
/> />
); );
} }

View File

@ -18,6 +18,7 @@ export interface ValueProps extends ThemeProps, LocaleProps {
disabled?: boolean; disabled?: boolean;
formula?: FormulaPickerProps; formula?: FormulaPickerProps;
popOverContainer?: any; popOverContainer?: any;
renderEtrValue?: any;
} }
export class Value extends React.Component<ValueProps> { export class Value extends React.Component<ValueProps> {
@ -32,7 +33,8 @@ export class Value extends React.Component<ValueProps> {
data, data,
disabled, disabled,
formula, formula,
popOverContainer popOverContainer,
renderEtrValue
} = this.props; } = this.props;
let input: JSX.Element | undefined = undefined; let input: JSX.Element | undefined = undefined;
if (formula) { if (formula) {
@ -85,7 +87,7 @@ export class Value extends React.Component<ValueProps> {
input = ( input = (
<DatePicker <DatePicker
viewMode="time" viewMode="time"
placeholder={__(field.placeholder) || 'Time.placeholder'} placeholder={__(field.placeholder) || __('Time.placeholder')}
format={field.format || 'HH:mm'} format={field.format || 'HH:mm'}
inputFormat={field.inputFormat || 'HH:mm'} inputFormat={field.inputFormat || 'HH:mm'}
value={value ?? field.defaultValue} value={value ?? field.defaultValue}
@ -135,6 +137,23 @@ export class Value extends React.Component<ValueProps> {
disabled={disabled} disabled={disabled}
/> />
); );
} else if (field.type === 'custom') {
input = renderEtrValue
? renderEtrValue(field.value, {
data,
onChange,
value: value ?? field.defaultValue
})
: null;
} else {
const res = value ?? (field as any).defaultValue;
input = renderEtrValue
? renderEtrValue(field, {
data,
onChange,
value: res ? res[(field as any).name] : res
})
: null;
} }
return <div className={cx('CBValue')}>{input}</div>; return <div className={cx('CBValue')}>{input}</div>;

View File

@ -31,6 +31,7 @@ export interface ConditionBuilderProps extends ThemeProps, LocaleProps {
fieldClassName?: string; fieldClassName?: string;
formula?: FormulaPickerProps; formula?: FormulaPickerProps;
popOverContainer?: any; popOverContainer?: any;
renderEtrValue?: any;
} }
export class QueryBuilder extends React.Component<ConditionBuilderProps> { export class QueryBuilder extends React.Component<ConditionBuilderProps> {
@ -211,7 +212,8 @@ export class QueryBuilder extends React.Component<ConditionBuilderProps> {
searchable, searchable,
builderMode, builderMode,
formula, formula,
popOverContainer popOverContainer,
renderEtrValue
} = this.props; } = this.props;
const normalizedValue = Array.isArray(value?.children) const normalizedValue = Array.isArray(value?.children)
@ -249,6 +251,7 @@ export class QueryBuilder extends React.Component<ConditionBuilderProps> {
searchable={searchable} searchable={searchable}
formula={formula} formula={formula}
popOverContainer={popOverContainer} popOverContainer={popOverContainer}
renderEtrValue={renderEtrValue}
/> />
); );
} }

View File

@ -1,4 +1,4 @@
import {SchemaApi} from '../../Schema'; import {BaseSchema, SchemaApi} from '../../Schema';
import {Api} from '../../types'; import {Api} from '../../types';
export type FieldTypes = export type FieldTypes =
@ -8,7 +8,8 @@ export type FieldTypes =
| 'date' | 'date'
| 'time' | 'time'
| 'datetime' | 'datetime'
| 'select'; | 'select'
| 'custom';
export type OperatorType = export type OperatorType =
| 'equal' | 'equal'
@ -28,7 +29,11 @@ export type OperatorType =
| 'select_equals' | 'select_equals'
| 'select_not_equals' | 'select_not_equals'
| 'select_any_in' | 'select_any_in'
| 'select_not_any_in'; | 'select_not_any_in'
| {
label: string;
value: string;
};
export type FieldItem = { export type FieldItem = {
type: 'text'; type: 'text';
@ -78,11 +83,17 @@ export interface ConditionGroupValue {
export interface ConditionValue extends ConditionGroupValue {} export interface ConditionValue extends ConditionGroupValue {}
interface customOperator {
lable: string;
value: string;
values?: any[];
}
interface BaseField { interface BaseField {
type: FieldTypes; type: FieldTypes;
label: string; label: string;
valueTypes?: Array<'value' | 'field' | 'func' | 'formula'>; valueTypes?: Array<'value' | 'field' | 'func' | 'formula'>;
operators?: Array<string>; operators?: Array<string | customOperator>;
// valueTypes 里面配置 func 才有效。 // valueTypes 里面配置 func 才有效。
funcs?: Array<string>; funcs?: Array<string>;
@ -158,6 +169,12 @@ interface BooleanField extends BaseField {
name: string; name: string;
} }
interface CustomField extends BaseField {
type: 'custom';
name: string;
value: BaseSchema;
}
interface GroupField { interface GroupField {
type: 'group'; type: 'group';
label: string; label: string;
@ -172,7 +189,8 @@ export type FieldSimple =
| TimeField | TimeField
| DatetimeField | DatetimeField
| SelectField | SelectField
| BooleanField; | BooleanField
| CustomField;
export type Field = FieldSimple | FieldGroup | GroupField; export type Field = FieldSimple | FieldGroup | GroupField;

View File

@ -263,6 +263,7 @@ register('de-DE', {
'Condition.or': 'oder', 'Condition.or': 'oder',
'Condition.add_cond': 'und Bedingung', 'Condition.add_cond': 'und Bedingung',
'Condition.add_cond_group': 'Bedingungsgruppe hinzufügen', 'Condition.add_cond_group': 'Bedingungsgruppe hinzufügen',
'Condition.delete_cond_group': 'Konditionsgruppe löschen',
'Condition.equal': 'gleich', 'Condition.equal': 'gleich',
'Condition.not_equal': 'ungleich', 'Condition.not_equal': 'ungleich',
'Condition.less': 'weniger', 'Condition.less': 'weniger',
@ -285,6 +286,9 @@ register('de-DE', {
'Condition.cond_placeholder': 'Bedingung auswählen', 'Condition.cond_placeholder': 'Bedingung auswählen',
'Condition.field_placeholder': 'Feld auswählen', 'Condition.field_placeholder': 'Feld auswählen',
'Condition.blank': 'leer', 'Condition.blank': 'leer',
'Condition.expression': 'Ausdruck',
'Condition.formula_placeholder': 'Bitte geben Sie eine Formel ein',
'Condition.fun_error': 'Funktion ist undefiniert',
'InputTable.uniqueError': 'Column `{{label}}` unique validate failed', 'InputTable.uniqueError': 'Column `{{label}}` unique validate failed',
'Timeline.collapseText': 'Entfalten', 'Timeline.collapseText': 'Entfalten',
'Timeline.expandText': 'Falten', 'Timeline.expandText': 'Falten',

View File

@ -265,6 +265,7 @@ register('en-US', {
'Condition.or': 'or', 'Condition.or': 'or',
'Condition.add_cond': 'add condition', 'Condition.add_cond': 'add condition',
'Condition.add_cond_group': 'add condition group', 'Condition.add_cond_group': 'add condition group',
'Condition.delete_cond_group': 'delete condition group',
'Condition.equal': 'equal', 'Condition.equal': 'equal',
'Condition.not_equal': 'not equal', 'Condition.not_equal': 'not equal',
'Condition.less': 'less', 'Condition.less': 'less',
@ -287,6 +288,9 @@ register('en-US', {
'Condition.cond_placeholder': 'select condition', 'Condition.cond_placeholder': 'select condition',
'Condition.field_placeholder': 'select field', 'Condition.field_placeholder': 'select field',
'Condition.blank': 'blank', 'Condition.blank': 'blank',
'Condition.expression': 'expression',
'Condition.formula_placeholder': 'Please enter a formula',
'Condition.fun_error': 'Function is undefined',
'InputTable.uniqueError': 'Column `{{label}}` unique validate failed', 'InputTable.uniqueError': 'Column `{{label}}` unique validate failed',
'Timeline.collapseText': 'Unfold', 'Timeline.collapseText': 'Unfold',
'Timeline.expandText': 'Fold', 'Timeline.expandText': 'Fold',

View File

@ -272,6 +272,7 @@ register('zh-CN', {
'Condition.or': '或者', 'Condition.or': '或者',
'Condition.add_cond': '添加条件', 'Condition.add_cond': '添加条件',
'Condition.add_cond_group': '添加条件组', 'Condition.add_cond_group': '添加条件组',
'Condition.delete_cond_group': '删除组',
'Condition.equal': '等于', 'Condition.equal': '等于',
'Condition.not_equal': '不等于', 'Condition.not_equal': '不等于',
'Condition.less': '小于', 'Condition.less': '小于',
@ -294,6 +295,9 @@ register('zh-CN', {
'Condition.cond_placeholder': '请选择操作', 'Condition.cond_placeholder': '请选择操作',
'Condition.field_placeholder': '请选择字段', 'Condition.field_placeholder': '请选择字段',
'Condition.blank': '空', 'Condition.blank': '空',
'Condition.expression': '表达式',
'Condition.formula_placeholder': '请输入公式',
'Condition.fun_error': '方法未定义',
'InputTable.uniqueError': '列`{{label}}`没有通过唯一验证', 'InputTable.uniqueError': '列`{{label}}`没有通过唯一验证',
'Timeline.collapseText': '展开', 'Timeline.collapseText': '展开',
'Timeline.expandText': '折叠', 'Timeline.expandText': '折叠',

View File

@ -9,6 +9,8 @@ import {
RemoteOptionsProps, RemoteOptionsProps,
withRemoteConfig withRemoteConfig
} from '../../components/WithRemoteConfig'; } from '../../components/WithRemoteConfig';
import {Schema} from '../../types';
import {autobind} from '../../utils/helper';
/** /**
* *
@ -59,12 +61,23 @@ export interface ConditionBuilderProps
> {} > {}
export default class ConditionBuilderControl extends React.PureComponent<ConditionBuilderProps> { export default class ConditionBuilderControl extends React.PureComponent<ConditionBuilderProps> {
@autobind
renderEtrValue(schema: Schema, data: any) {
return this.props.render(
'inline',
Object.assign(schema, {label: false}),
data
);
}
render() { render() {
const {className, classnames: cx, ...rest} = this.props; const {className, classnames: cx, ...rest} = this.props;
return ( return (
<div className={cx(`ConditionBuilderControl`, className)}> <div className={cx(`ConditionBuilderControl`, className)}>
<ConditionBuilderWithRemoteOptions {...rest} /> <ConditionBuilderWithRemoteOptions
renderEtrValue={this.renderEtrValue}
{...rest}
/>
</div> </div>
); );
} }
@ -77,12 +90,14 @@ const ConditionBuilderWithRemoteOptions = withRemoteConfig({
RemoteOptionsProps & React.ComponentProps<typeof ConditionBuilder> RemoteOptionsProps & React.ComponentProps<typeof ConditionBuilder>
> { > {
render() { render() {
const {loading, config, deferLoad, disabled, ...rest} = this.props; const {loading, config, deferLoad, disabled, renderEtrValue, ...rest} =
this.props;
return ( return (
<ConditionBuilder <ConditionBuilder
{...rest} {...rest}
fields={config || rest.fields || []} fields={config || rest.fields || []}
disabled={disabled || loading} disabled={disabled || loading}
renderEtrValue={renderEtrValue}
/> />
); );
} }