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

@ -73,55 +73,33 @@ exports[`Renderer:condition-builder 1`] = `
class="cxd-CBGroup"
>
<div
class="cxd-CBGroup-toolbar"
class="cxd-CBGroup-toolbarCondition"
>
<div
class="cxd-CBGroup-toolbarCondition"
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="downshift-0-label"
class="cxd-Select"
role="combobox"
tabindex="0"
>
<div
class="cxd-ButtonGroup"
class="cxd-Select-valueWrap"
>
<button
class="cxd-Button cxd-Button--default cxd-Button--xs is-active"
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
class="cxd-CBGroup-toolbarConditionAdd"
>
<div
class="cxd-ButtonGroup"
<span
class="cxd-Select-arrow"
>
<button
class="cxd-Button cxd-Button--default cxd-Button--xs"
type="button"
>
<icon-mock
classname="icon icon-plus"
icon="plus"
/>
添加条件
</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>
<icon-mock
classname="icon icon-caret"
icon="caret"
/>
</span>
</div>
</div>
<div
@ -133,6 +111,30 @@ exports[`Renderer:condition-builder 1`] = `
</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>
<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 变量即可。
@ -593,4 +684,4 @@ type Value = ValueGroup;
| fields | | | 字段配置 |
| showANDOR | `boolean` | | 用于 simple 模式下显示切换按钮 |
| showNot | `boolean` | | 是否显示「非」按钮 |
| searchable | `boolean` | | 字段是否可搜索 |
| searchable | `boolean` | | 字段是否可搜索 |

View File

@ -28,6 +28,46 @@ export default {
description:
'适合让用户自己拼查询条件,然后后端根据数据生成 query where',
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: '文本',
type: 'text',

View File

@ -1,11 +1,36 @@
.#{$ns}CBGroup {
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 {
display: flex;
flex-direction: row;
padding-bottom: 10px;
margin-top: px2rem(8px);
.#{$ns}Button {
transition: padding var(--animation-duration);
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 {
transition: opacity var(--animation-duration);
display: flex;
align-items: center;
.#{$ns}ButtonGroup {
& > .cxd-Button:first-child {
margin-right: px2rem(24px);
}
}
.#{$ns}CBDelete {
margin-left: var(--gap-xs);
}
}
&:not(:hover) .#{$ns}CBGroup-toolbarConditionAdd {
opacity: 0;
}
}
.#{$ns}ResultBox {
padding-right: px2rem(3px);
}
&-field,
@ -76,92 +94,79 @@
&-placeholder {
color: var(--text--muted-color);
position: relative;
margin-left: 30px;
padding: 10px;
background: rgba(0, 0, 0, 0.03);
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 {
margin-left: 0;
&:before {
display: none;
}
&:after {
display: none;
}
}
}
&-toolbarCondition {
margin-right: var(--gap-base);
}
}
.#{$ns}CBDelete {
@include icon-color();
cursor: pointer;
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 {
position: relative;
margin-left: px2rem(30px);
& + & {
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 {
display: flex;
padding: 2px 7px;
border-radius: 5px;
flex-direction: row;
align-items: center;
position: relative;
background: rgba(0, 0, 0, 0.03);
transition: all var(--animation-duration) ease-out;
> .#{$ns}CBGroup {
margin: 3px;
&-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 {
margin: 0px;
}
}
}
&-body:not(:hover) .#{$ns}CBDelete {
opacity: 0;
&-item {
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 {
@ -179,59 +184,19 @@
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 {
flex-grow: 1;
}
&:hover > &-body {
background: rgba(0, 0, 0, 0.05);
& > &-body > &-body-group > &-dragbar,
& > &-body > &-body-item > &-dragbar {
opacity: 0;
}
&:hover > &-body > &-dragbar {
&:hover > &-body > &-body-item > &-dragbar {
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 {
margin-bottom: var(--gap-sm);
}

View File

@ -20,6 +20,7 @@ import {Config} from './config';
import InputBox from '../InputBox';
import Formula from './Formula';
import {FormulaPickerProps} from '../formula/Picker';
import {localeable, LocaleProps} from '../../locale';
/**
* 4
@ -30,7 +31,7 @@ import {FormulaPickerProps} from '../formula/Picker';
* 4.
*/
export interface ExpressionProps extends ThemeProps {
export interface ExpressionProps extends ThemeProps, LocaleProps {
value: ExpressionComplex;
data?: any;
index?: number;
@ -46,6 +47,7 @@ export interface ExpressionProps extends ThemeProps {
fieldClassName?: string;
formula?: FormulaPickerProps;
popOverContainer?: any;
renderEtrValue?: any;
}
const fieldMap = {
@ -137,7 +139,8 @@ export class Expression extends React.Component<ExpressionProps> {
disabled,
searchable,
formula,
popOverContainer
popOverContainer,
renderEtrValue
} = this.props;
const inputType =
((value as any)?.type === 'field'
@ -169,6 +172,7 @@ export class Expression extends React.Component<ExpressionProps> {
disabled={disabled}
formula={formula}
popOverContainer={popOverContainer}
renderEtrValue={renderEtrValue}
/>
) : 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 {localeable, LocaleProps} from '../../locale';
import {ThemeProps, themeable} from '../../theme';
import InputBox from '../InputBox';
export interface FormulaProps extends ThemeProps {
export interface FormulaProps extends ThemeProps, LocaleProps {
value: any;
onChange: (value: any) => void;
disabled?: boolean;
@ -10,7 +11,13 @@ export interface FormulaProps extends ThemeProps {
export class Formula extends React.Component<FormulaProps> {
render() {
const {classnames: cx, value, onChange, disabled} = this.props;
const {
classnames: cx,
value,
onChange,
disabled,
translate: __
} = this.props;
return (
<div className={cx('CBFormula')}>
@ -18,12 +25,16 @@ export class Formula extends React.Component<FormulaProps> {
disabled={disabled}
value={value}
onChange={onChange}
placeholder="请输入公式"
prefix={<span className={cx('CBFormula-label')}></span>}
placeholder={__('Condition.formula_placeholder')}
prefix={
<span className={cx('CBFormula-label')}>
{__('Condition.expression')}
</span>
}
/>
</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 Expression from './Expression';
import {Config} from './config';
import {localeable, LocaleProps} from '../../locale';
export interface ConditionFuncProps extends ThemeProps {
export interface ConditionFuncProps extends ThemeProps, LocaleProps {
value: ExpressionFunc;
onChange: (value: ExpressionFunc) => void;
disabled?: boolean;
@ -68,7 +69,14 @@ export class ConditionFunc extends React.Component<ConditionFuncProps> {
}
render() {
const {value, classnames: cx, fieldClassName, funcs, disabled} = this.props;
const {
value,
classnames: cx,
fieldClassName,
funcs,
disabled,
translate: __
} = this.props;
const func = value
? findTree(funcs!, item => (item as Func).type === value.func)
: null;
@ -100,7 +108,7 @@ export class ConditionFunc extends React.Component<ConditionFuncProps> {
result={func}
onResultChange={noop}
onResultClick={onClick}
placeholder="请选择字段"
placeholder={__('Condition.field_placeholder')}
disabled={disabled}
>
<span className={cx('CBGroup-fieldCaret')}>
@ -114,11 +122,13 @@ export class ConditionFunc extends React.Component<ConditionFuncProps> {
{func ? (
this.renderFunc(func as Func)
) : (
<span className={cx('CBFunc-error')}></span>
<span className={cx('CBFunc-error')}>
{__('Condition.fun_error')}
</span>
)}
</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 {localeable, LocaleProps} from '../../locale';
import {FormulaPickerProps} from '../formula/Picker';
import Select from '../Select';
export interface ConditionGroupProps extends ThemeProps, LocaleProps {
builderMode?: 'simple' | 'full';
@ -27,13 +28,14 @@ export interface ConditionGroupProps extends ThemeProps, LocaleProps {
fieldClassName?: string;
formula?: FormulaPickerProps;
popOverContainer?: any;
renderEtrValue?: any;
}
export class ConditionGroup extends React.Component<ConditionGroupProps> {
getValue() {
return {
id: guid(),
conjunction: 'and' as 'and',
conjunction: 'and',
...this.props.value
} as ConditionGroupValue;
}
@ -48,10 +50,10 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
}
@autobind
handleConjunctionClick() {
handleConjunctionChange(val: {value: 'or' | 'and'}) {
const onChange = this.props.onChange;
let value = this.getValue();
value.conjunction = value.conjunction === 'and' ? 'or' : 'and';
value.conjunction = val.value;
onChange(value);
}
@ -136,74 +138,42 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
searchable,
translate: __,
formula,
popOverContainer
popOverContainer,
renderEtrValue
} = this.props;
return (
<div className={cx('CBGroup')} data-group-id={value?.id}>
<div className={cx('CBGroup-toolbar')}>
{builderMode === 'simple' && showANDOR === false ? null : (
<div className={cx('CBGroup-toolbarCondition')}>
{showNot ? (
<Button
onClick={this.handleNotClick}
className="m-r-xs"
size="xs"
active={value?.not}
disabled={disabled}
>
{__('Condition.not')}
</Button>
) : null}
<div className={cx('ButtonGroup')}>
<Button
size="xs"
onClick={this.handleConjunctionClick}
active={value?.conjunction !== 'or'}
disabled={disabled}
>
{__('Condition.and')}
</Button>
<Button
size="xs"
onClick={this.handleConjunctionClick}
active={value?.conjunction === 'or'}
disabled={disabled}
>
{__('Condition.or')}
</Button>
</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')}
{builderMode === 'simple' && showANDOR === false ? null : (
<div className={cx('CBGroup-toolbarCondition')}>
{showNot ? (
<Button
onClick={this.handleNotClick}
className="m-r-xs"
size="xs"
active={value?.not}
disabled={disabled}
>
{__('Condition.not')}
</Button>
{builderMode === 'simple' ? null : (
<Button
onClick={this.handleAddGroup}
size="xs"
disabled={disabled}
>
<Icon icon="plus-cicle" className="icon" />
{__('Condition.add_cond_group')}
</Button>
)}
</div>
) : null}
<Select
options={[
{
label: __('Condition.and'),
value: 'and'
},
{
label: __('Condition.or'),
value: 'or'
}
]}
value={value?.conjunction || 'and'}
disabled={disabled}
onChange={this.handleConjunctionChange}
clearable={false}
/>
</div>
{removeable ? (
<a className={cx('CBDelete')} onClick={onRemove}>
<Icon icon="close" className="icon" />
</a>
) : null}
</div>
)}
<div className={cx('CBGroup-body')}>
{Array.isArray(value?.children) && value!.children.length ? (
value!.children.map((item, index) => (
@ -225,6 +195,7 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
builderMode={builderMode}
formula={formula}
popOverContainer={popOverContainer}
renderEtrValue={renderEtrValue}
/>
))
) : (
@ -239,6 +210,41 @@ export class ConditionGroup extends React.Component<ConditionGroupProps> {
</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>
);
}

View File

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

View File

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

View File

@ -18,6 +18,7 @@ export interface ValueProps extends ThemeProps, LocaleProps {
disabled?: boolean;
formula?: FormulaPickerProps;
popOverContainer?: any;
renderEtrValue?: any;
}
export class Value extends React.Component<ValueProps> {
@ -32,7 +33,8 @@ export class Value extends React.Component<ValueProps> {
data,
disabled,
formula,
popOverContainer
popOverContainer,
renderEtrValue
} = this.props;
let input: JSX.Element | undefined = undefined;
if (formula) {
@ -85,7 +87,7 @@ export class Value extends React.Component<ValueProps> {
input = (
<DatePicker
viewMode="time"
placeholder={__(field.placeholder) || 'Time.placeholder'}
placeholder={__(field.placeholder) || __('Time.placeholder')}
format={field.format || 'HH:mm'}
inputFormat={field.inputFormat || 'HH:mm'}
value={value ?? field.defaultValue}
@ -135,6 +137,23 @@ export class Value extends React.Component<ValueProps> {
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>;

View File

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

View File

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

View File

@ -263,6 +263,7 @@ register('de-DE', {
'Condition.or': 'oder',
'Condition.add_cond': 'und Bedingung',
'Condition.add_cond_group': 'Bedingungsgruppe hinzufügen',
'Condition.delete_cond_group': 'Konditionsgruppe löschen',
'Condition.equal': 'gleich',
'Condition.not_equal': 'ungleich',
'Condition.less': 'weniger',
@ -285,6 +286,9 @@ register('de-DE', {
'Condition.cond_placeholder': 'Bedingung auswählen',
'Condition.field_placeholder': 'Feld auswählen',
'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',
'Timeline.collapseText': 'Entfalten',
'Timeline.expandText': 'Falten',

View File

@ -265,6 +265,7 @@ register('en-US', {
'Condition.or': 'or',
'Condition.add_cond': 'add condition',
'Condition.add_cond_group': 'add condition group',
'Condition.delete_cond_group': 'delete condition group',
'Condition.equal': 'equal',
'Condition.not_equal': 'not equal',
'Condition.less': 'less',
@ -287,6 +288,9 @@ register('en-US', {
'Condition.cond_placeholder': 'select condition',
'Condition.field_placeholder': 'select field',
'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',
'Timeline.collapseText': 'Unfold',
'Timeline.expandText': 'Fold',

View File

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

View File

@ -9,6 +9,8 @@ import {
RemoteOptionsProps,
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> {
@autobind
renderEtrValue(schema: Schema, data: any) {
return this.props.render(
'inline',
Object.assign(schema, {label: false}),
data
);
}
render() {
const {className, classnames: cx, ...rest} = this.props;
return (
<div className={cx(`ConditionBuilderControl`, className)}>
<ConditionBuilderWithRemoteOptions {...rest} />
<ConditionBuilderWithRemoteOptions
renderEtrValue={this.renderEtrValue}
{...rest}
/>
</div>
);
}
@ -77,12 +90,14 @@ const ConditionBuilderWithRemoteOptions = withRemoteConfig({
RemoteOptionsProps & React.ComponentProps<typeof ConditionBuilder>
> {
render() {
const {loading, config, deferLoad, disabled, ...rest} = this.props;
const {loading, config, deferLoad, disabled, renderEtrValue, ...rest} =
this.props;
return (
<ConditionBuilder
{...rest}
fields={config || rest.fields || []}
disabled={disabled || loading}
renderEtrValue={renderEtrValue}
/>
);
}