mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 03:58:07 +08:00
chore: 公式编辑器 input 换成 codemirror 提升交互体验 (#9821)
This commit is contained in:
parent
95787b98ef
commit
90ced059b1
@ -102,8 +102,8 @@ order: 21
|
||||
"name": "formula",
|
||||
"label": "公式",
|
||||
"variableMode": "tree",
|
||||
"evalMode": false,
|
||||
"value": "${SUM(1 , 2)}",
|
||||
"evalMode": true,
|
||||
"value": "SUM(1 , 2)",
|
||||
"inputMode": "button",
|
||||
"variables": [
|
||||
{
|
||||
@ -368,77 +368,6 @@ Tab 结构:
|
||||
}
|
||||
```
|
||||
|
||||
## 高亮文本
|
||||
|
||||
通过配置`allowInput`为`false`可以高亮文本内容,但是只能在编辑器中编辑
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "form",
|
||||
"debug": true,
|
||||
"body": [
|
||||
{
|
||||
"type": "input-formula",
|
||||
"name": "formula",
|
||||
"allowInput": false,
|
||||
"label": "公式",
|
||||
"evalMode": true,
|
||||
"value": "SUM(1, 2)",
|
||||
"variables": [
|
||||
{
|
||||
"label": "表单字段",
|
||||
"children": [
|
||||
{
|
||||
"label": "文章名",
|
||||
"value": "name",
|
||||
"tag": "文本"
|
||||
},
|
||||
{
|
||||
"label": "作者",
|
||||
"value": "author",
|
||||
"tag": "文本"
|
||||
},
|
||||
{
|
||||
"label": "售价",
|
||||
"value": "price",
|
||||
"tag": "数字"
|
||||
},
|
||||
{
|
||||
"label": "出版时间",
|
||||
"value": "time",
|
||||
"tag": "时间"
|
||||
},
|
||||
{
|
||||
"label": "版本号",
|
||||
"value": "version",
|
||||
"tag": "数字"
|
||||
},
|
||||
{
|
||||
"label": "出版社",
|
||||
"value": "publisher",
|
||||
"tag": "文本"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "流程字段",
|
||||
"children": [
|
||||
{
|
||||
"label": "联系电话",
|
||||
"value": "telphone"
|
||||
},
|
||||
{
|
||||
"label": "地址",
|
||||
"value": "addr"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 模板模式
|
||||
|
||||
当配置 `evalMode` 为 false 时则为模板模式,意思是说默认不当做表达式,只有 `${`和`}`包裹的部分才是表达式。
|
||||
|
@ -59,7 +59,10 @@ export function StatusScoped<
|
||||
} = {
|
||||
statusStore: this.store!
|
||||
};
|
||||
const refConfig = ComposedComponent.prototype?.isReactComponent
|
||||
const refConfig =
|
||||
ComposedComponent.prototype?.isReactComponent ||
|
||||
(ComposedComponent as any).$$typeof ===
|
||||
Symbol.for('react.forward_ref')
|
||||
? {ref: this.childRef}
|
||||
: {forwardedRef: this.childRef};
|
||||
|
||||
|
@ -162,7 +162,10 @@ export function localeable<
|
||||
translate: translate!
|
||||
};
|
||||
moment.locale(momentLocaleMap?.[locale] ?? locale);
|
||||
const refConfig = ComposedComponent.prototype?.isReactComponent
|
||||
const refConfig =
|
||||
ComposedComponent.prototype?.isReactComponent ||
|
||||
(ComposedComponent as any).$$typeof ===
|
||||
Symbol.for('react.forward_ref')
|
||||
? {ref: this.childRef}
|
||||
: {forwardedRef: this.childRef};
|
||||
|
||||
|
@ -182,7 +182,10 @@ export function themeable<
|
||||
classnames: config.classnames,
|
||||
theme
|
||||
};
|
||||
const refConfig = ComposedComponent.prototype?.isReactComponent
|
||||
const refConfig =
|
||||
ComposedComponent.prototype?.isReactComponent ||
|
||||
(ComposedComponent as any).$$typeof ===
|
||||
Symbol.for('react.forward_ref')
|
||||
? {ref: this.childRef}
|
||||
: {forwardedRef: this.childRef};
|
||||
|
||||
|
@ -212,7 +212,7 @@ export function isExpression(expression: any): boolean {
|
||||
// 备注1: "\\${xxx}"不作为表达式,至少含一个${xxx}才算是表达式
|
||||
|
||||
// 备注2: safari 不支持 /(?<!\\)(\${).+(\})/.test(expression)
|
||||
return /(^|[^\\])\$\{.+\}/.test(expression);
|
||||
return /(^|[^\\])\$\{[\s\S]+\}/.test(expression);
|
||||
}
|
||||
|
||||
// 用于判断是否需要执行表达式:
|
||||
|
@ -38,4 +38,6 @@
|
||||
}
|
||||
.btn-configured-tooltip {
|
||||
font-size: 12px;
|
||||
min-width: 320px;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
@ -33,9 +33,9 @@
|
||||
& > .CodeMirror {
|
||||
height: 100%;
|
||||
font-family: inherit;
|
||||
span[class^='cm-'] {
|
||||
color: var(--input-default-default-color);
|
||||
}
|
||||
// span[class^='cm-'] {
|
||||
// color: var(--input-default-default-color);
|
||||
// }
|
||||
|
||||
// 解决上下 pre标签中表达式浮层遮挡问题
|
||||
.CodeMirror-measure + div {
|
||||
|
@ -48,9 +48,9 @@
|
||||
color: var(--Form-input-color);
|
||||
font-family: inherit;
|
||||
|
||||
span[class^='cm-'] {
|
||||
color: var(--input-default-default-color);
|
||||
}
|
||||
// span[class^='cm-'] {
|
||||
// color: var(--input-default-default-color);
|
||||
// }
|
||||
|
||||
// 解决上下 pre标签中表达式浮层遮挡问题
|
||||
.CodeMirror-measure + div {
|
||||
|
@ -890,7 +890,7 @@ export class BaseCRUDPlugin extends BasePlugin {
|
||||
step: 10,
|
||||
min: 1000
|
||||
},
|
||||
getSchemaTpl('tplFormulaControl', {
|
||||
getSchemaTpl('expressionFormulaControl', {
|
||||
name: 'stopAutoRefreshWhen',
|
||||
label: tipedLabel(
|
||||
'停止条件',
|
||||
|
@ -6,9 +6,8 @@ import React from 'react';
|
||||
import {autobind, FormControlProps} from 'amis-core';
|
||||
import cx from 'classnames';
|
||||
import {FormItem, Button, Icon, PickerContainer} from 'amis';
|
||||
import {FormulaEditor} from 'amis-ui';
|
||||
import {FormulaCodeEditor, FormulaEditor} from 'amis-ui';
|
||||
import type {VariableItem} from 'amis-ui';
|
||||
import {renderFormulaValue} from './FormulaControl';
|
||||
import {reaction} from 'mobx';
|
||||
import {getVariables} from 'amis-editor-core';
|
||||
|
||||
@ -76,6 +75,12 @@ export default class ExpressionFormulaControl extends React.Component<
|
||||
this.appCorpusData = editorStore?.appCorpusData;
|
||||
}
|
||||
);
|
||||
|
||||
// 要高亮,初始就要加载
|
||||
const variablesArr = await getVariables(this);
|
||||
this.setState({
|
||||
variables: variablesArr
|
||||
});
|
||||
}
|
||||
|
||||
async componentDidUpdate(prevProps: ExpressionFormulaControlProps) {
|
||||
@ -133,13 +138,6 @@ export default class ExpressionFormulaControl extends React.Component<
|
||||
const {value, className, variableMode, header, size, ...rest} = this.props;
|
||||
const {formulaPickerValue, variables} = this.state;
|
||||
|
||||
const highlightValue = FormulaEditor.highlightValue(
|
||||
formulaPickerValue,
|
||||
variables
|
||||
) || {
|
||||
html: formulaPickerValue
|
||||
};
|
||||
|
||||
// 自身字段
|
||||
const selfName = this.props?.data?.name;
|
||||
return (
|
||||
@ -180,11 +178,25 @@ export default class ExpressionFormulaControl extends React.Component<
|
||||
mouseLeaveDelay: 20,
|
||||
content: value,
|
||||
tooltipClassName: 'btn-configured-tooltip',
|
||||
children: () => renderFormulaValue(highlightValue)
|
||||
children: () => (
|
||||
<FormulaCodeEditor
|
||||
readOnly
|
||||
value={value}
|
||||
variables={variables}
|
||||
evalMode={false}
|
||||
editorTheme="dark"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
onClick={e => this.handleOnClick(e, onClick)}
|
||||
>
|
||||
{renderFormulaValue(highlightValue)}
|
||||
<FormulaCodeEditor
|
||||
singleLine
|
||||
readOnly
|
||||
value={value}
|
||||
variables={variables}
|
||||
evalMode={false}
|
||||
/>
|
||||
<Icon
|
||||
icon="input-clear"
|
||||
className="icon"
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
TooltipWrapper
|
||||
} from 'amis';
|
||||
import {FormulaExec, isExpression} from 'amis';
|
||||
import {FormulaEditor} from 'amis-ui';
|
||||
import {FormulaCodeEditor, FormulaEditor} from 'amis-ui';
|
||||
|
||||
import FormulaPicker, {
|
||||
CustomFormulaPickerProps
|
||||
@ -29,7 +29,7 @@ import {JSONPipeOut, autobind, translateSchema} from 'amis-editor-core';
|
||||
import type {
|
||||
VariableItem,
|
||||
FuncGroup
|
||||
} from 'amis-ui/lib/components/formula/Editor';
|
||||
} from 'amis-ui/lib/components/formula/CodeEditor';
|
||||
import {FormControlProps} from 'amis-core';
|
||||
import type {BaseEventContext} from 'amis-editor-core';
|
||||
import {EditorManager} from 'amis-editor-core';
|
||||
@ -42,6 +42,11 @@ export enum FormulaDateType {
|
||||
IsRange // 日期时间范围类
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 废弃,直接用 codemirror 渲染输入框即可,自带高亮
|
||||
* @param item
|
||||
* @returns
|
||||
*/
|
||||
export function renderFormulaValue(item: any) {
|
||||
const html = {__html: typeof item === 'string' ? item : item?.html};
|
||||
// bca-disable-next-line
|
||||
@ -189,6 +194,9 @@ export default class FormulaControl extends React.Component<
|
||||
this.appCorpusData = editorStore?.appCorpusData;
|
||||
}
|
||||
);
|
||||
|
||||
const variables = await getVariables(this);
|
||||
this.setState({variables});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -346,8 +354,8 @@ export default class FormulaControl extends React.Component<
|
||||
transExpr(str: string) {
|
||||
if (
|
||||
typeof str === 'string' &&
|
||||
str?.slice(0, 2) === '${' &&
|
||||
str?.slice(-1) === '}'
|
||||
str.slice(0, 2) === '${' &&
|
||||
str.slice(-1) === '}'
|
||||
) {
|
||||
// 非最外层内容还存在表达式情况
|
||||
if (isExpression(str.slice(2, -1))) {
|
||||
@ -580,12 +588,6 @@ export default class FormulaControl extends React.Component<
|
||||
|
||||
const FormulaPickerCmp = customFormulaPicker ?? FormulaPicker;
|
||||
|
||||
const highlightValue = isExpression(value)
|
||||
? FormulaEditor.highlightValue(exprValue, variables) || {
|
||||
html: exprValue
|
||||
}
|
||||
: value;
|
||||
|
||||
// 公式表达式弹窗内容过滤
|
||||
const filterValue = isExpression(value)
|
||||
? exprValue
|
||||
@ -651,40 +653,38 @@ export default class FormulaControl extends React.Component<
|
||||
tooltipTheme: 'dark',
|
||||
mouseLeaveDelay: 20,
|
||||
content: exprValue,
|
||||
children: () => renderFormulaValue(highlightValue)
|
||||
}}
|
||||
>
|
||||
<div className="ae-editor-FormulaControl-tooltipBox">
|
||||
<div
|
||||
className="ae-editor-FormulaControl-ResultBox-wrapper"
|
||||
onClick={this.handleFormulaClick}
|
||||
>
|
||||
<ResultBox
|
||||
className={cx(
|
||||
'ae-editor-FormulaControl-ResultBox',
|
||||
isError ? 'is-error' : ''
|
||||
)}
|
||||
allowInput={false}
|
||||
tooltipClassName: 'btn-configured-tooltip',
|
||||
children: () => (
|
||||
<FormulaCodeEditor
|
||||
readOnly
|
||||
value={value}
|
||||
result={{
|
||||
html: this.hasDateShortcutkey(value)
|
||||
? value
|
||||
: highlightValue?.html
|
||||
}}
|
||||
itemRender={renderFormulaValue}
|
||||
onChange={this.handleInputChange}
|
||||
onResultChange={() => {
|
||||
this.handleInputChange(undefined);
|
||||
}}
|
||||
variables={variables}
|
||||
evalMode={false}
|
||||
editorTheme="dark"
|
||||
/>
|
||||
</div>
|
||||
{value && (
|
||||
<Icon
|
||||
icon="input-clear"
|
||||
className="input-clear-icon"
|
||||
onClick={() => this.handleInputChange('')}
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div className={cx('ae-editor-FormulaControl-tooltipBox')}>
|
||||
<InputBox
|
||||
onClick={this.handleFormulaClick}
|
||||
hasError={isError}
|
||||
inputRender={({value, onChange, onFocus, onBlur}: any) => (
|
||||
<FormulaCodeEditor
|
||||
singleLine
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
functions={[]}
|
||||
variables={variables}
|
||||
evalMode={false}
|
||||
readOnly
|
||||
/>
|
||||
)}
|
||||
value={value}
|
||||
onChange={this.handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</TooltipWrapper>
|
||||
)}
|
||||
|
@ -5,12 +5,11 @@
|
||||
import React from 'react';
|
||||
import cx from 'classnames';
|
||||
import {reaction} from 'mobx';
|
||||
import {CodeMirrorEditor, FormulaEditor} from 'amis-ui';
|
||||
import {CodeMirrorEditor, FormulaCodeEditor, FormulaEditor} from 'amis-ui';
|
||||
import type {VariableItem, CodeMirror} from 'amis-ui';
|
||||
import {Icon, Button, FormItem, TooltipWrapper} from 'amis';
|
||||
import {autobind, FormControlProps} from 'amis-core';
|
||||
import {FormulaPlugin, editorFactory} from './textarea-formula/plugin';
|
||||
import {renderFormulaValue} from './FormulaControl';
|
||||
import FormulaPicker, {
|
||||
CustomFormulaPickerProps
|
||||
} from './textarea-formula/FormulaPicker';
|
||||
@ -383,13 +382,6 @@ export class TplFormulaControl extends React.Component<
|
||||
|
||||
const FormulaPickerCmp = customFormulaPicker ?? FormulaPicker;
|
||||
|
||||
const highlightValue = FormulaEditor.highlightValue(
|
||||
formulaPickerValue,
|
||||
variables
|
||||
) || {
|
||||
html: formulaPickerValue
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('ae-TplFormulaControl', className, {
|
||||
@ -441,11 +433,20 @@ export class TplFormulaControl extends React.Component<
|
||||
|
||||
<TooltipWrapper
|
||||
trigger="hover"
|
||||
placement="top"
|
||||
placement="auto"
|
||||
style={{fontSize: '12px'}}
|
||||
tooltip={{
|
||||
tooltipTheme: 'dark',
|
||||
children: () => renderFormulaValue(highlightValue)
|
||||
tooltipClassName: 'btn-configured-tooltip',
|
||||
children: () => (
|
||||
<FormulaCodeEditor
|
||||
readOnly
|
||||
value={formulaPickerValue}
|
||||
variables={variables}
|
||||
evalMode={true}
|
||||
editorTheme="dark"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
@ -2926,8 +2926,10 @@ export const getEventControlConfig = (
|
||||
|
||||
return {
|
||||
showOldEntry:
|
||||
!!context.schema.actionType ||
|
||||
['submit', 'reset'].includes(context.schema.type),
|
||||
!!(
|
||||
context.schema.actionType &&
|
||||
!['dialog', 'drawer'].includes(context.schema.type)
|
||||
) || ['submit', 'reset'].includes(context.schema.type),
|
||||
actions: manager?.pluginActions,
|
||||
events: manager?.pluginEvents,
|
||||
actionTree,
|
||||
|
@ -49,7 +49,7 @@ import {
|
||||
} from 'amis-editor-core';
|
||||
export * from './helper';
|
||||
import {i18n as _i18n} from 'i18n-runtime';
|
||||
import type {VariableItem} from 'amis-ui/lib/components/formula/Editor';
|
||||
import type {VariableItem} from 'amis-ui/src/components/formula/CodeEditor';
|
||||
import {reaction} from 'mobx';
|
||||
import {updateComponentContext} from 'amis-editor-core';
|
||||
|
||||
|
@ -2,7 +2,7 @@ import React, {useEffect} from 'react';
|
||||
import {Modal, Button} from 'amis';
|
||||
import {FormControlProps} from 'amis-core';
|
||||
import cx from 'classnames';
|
||||
import FormulaEditor from 'amis-ui/lib/components/formula/Editor';
|
||||
import {FormulaEditor} from 'amis-ui';
|
||||
|
||||
export interface FormulaPickerProps extends FormControlProps {
|
||||
onConfirm: (data: string | undefined) => void;
|
||||
|
@ -10,7 +10,7 @@ import type {SchemaObject} from 'amis';
|
||||
import flatten from 'lodash/flatten';
|
||||
import {InputComponentName} from '../component/InputComponentName';
|
||||
import {FormulaDateType} from '../renderer/FormulaControl';
|
||||
import {VariableItem} from 'amis-ui/lib/components/formula/Editor';
|
||||
import type {VariableItem} from 'amis-ui/src/components/formula/CodeEditor';
|
||||
import reduce from 'lodash/reduce';
|
||||
import map from 'lodash/map';
|
||||
import omit from 'lodash/omit';
|
||||
|
@ -452,6 +452,7 @@
|
||||
fill: var(--Form-input-clearBtn-color);
|
||||
width: var(--Form-input-clearBtn-size);
|
||||
height: var(--Form-input-clearBtn-size);
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&:hover svg {
|
||||
|
@ -112,6 +112,7 @@
|
||||
position: relative;
|
||||
&-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&-collapse {
|
||||
@ -139,6 +140,7 @@
|
||||
|
||||
&-body-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&-toolbar {
|
||||
@ -323,6 +325,8 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
> .#{$ns}CBGroupOrItem-dragbar {
|
||||
left: px2rem(10px);
|
||||
position: absolute;
|
||||
@ -431,11 +435,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}CBItem {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
> * {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}CBValue {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: px2rem(3px);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.#{$ns}CBFormula {
|
||||
@ -461,6 +477,7 @@
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
align-self: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
overflow: visible;
|
||||
max-width: 100%;
|
||||
box-sizing: content-box;
|
||||
min-height: px2rem(450px);
|
||||
|
||||
@mixin scrollbar {
|
||||
&::-webkit-scrollbar {
|
||||
@ -31,10 +32,59 @@
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
max-width: #{px2rem(530px)};
|
||||
min-width: 0;
|
||||
border-top: var(--Form-input-borderWidth) solid
|
||||
var(--Form-input-borderColor);
|
||||
border-bottom: var(--Form-input-borderWidth) solid
|
||||
var(--Form-input-borderColor);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&-runPanel {
|
||||
&.in {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
height: px2rem(200px);
|
||||
transition: height 0.1s ease-out;
|
||||
border-top: 1px solid var(--Form-input-borderColor);
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-direction: row;
|
||||
|
||||
&-context,
|
||||
&-result {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> header {
|
||||
@include panel-header();
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&-context {
|
||||
border-right: 1px solid var(--Form-input-borderColor);
|
||||
}
|
||||
|
||||
&-result {
|
||||
> div {
|
||||
padding: 0 px2rem(10px);
|
||||
}
|
||||
|
||||
&.is-error {
|
||||
color: var(--Form-input-onError-borderColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-header {
|
||||
@ -51,8 +101,8 @@
|
||||
|
||||
&-editor {
|
||||
@include scrollbar();
|
||||
min-height: px2rem(200px);
|
||||
height: calc(100% - 35px);
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: #{px2rem(5px)};
|
||||
padding-right: 0;
|
||||
|
||||
@ -418,27 +468,6 @@
|
||||
overflow: hidden;
|
||||
background: var(--InputFormula-code-bgColor);
|
||||
}
|
||||
|
||||
.cm-field,
|
||||
.cm-func {
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
margin: 0 1px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.cm-field {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.cm-field {
|
||||
background: #007bff;
|
||||
}
|
||||
.cm-func {
|
||||
color: #ae4597;
|
||||
font-weight: bold;
|
||||
line-height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}FormulaPicker {
|
||||
@ -459,8 +488,9 @@
|
||||
|
||||
&-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: #{px2rem(10px)};
|
||||
padding-right: 0;
|
||||
padding-right: px2rem(4px);
|
||||
max-width: calc(100% - #{px2rem(42px)});
|
||||
}
|
||||
|
||||
@ -515,7 +545,8 @@
|
||||
height: var(--Form-input-height);
|
||||
|
||||
&.#{$ns}FormulaPicker--text {
|
||||
padding: var(--Form-input-paddingY) var(--Form-input-paddingX);
|
||||
padding: var(--Form-input-paddingY) var(--Form-input-paddingX)
|
||||
var(--Form-input-paddingY) px2rem(5px);
|
||||
}
|
||||
|
||||
.#{$ns}FormulaPicker-input {
|
||||
@ -534,8 +565,8 @@
|
||||
box-sizing: none;
|
||||
}
|
||||
|
||||
&-number,
|
||||
&-select,
|
||||
&-number,
|
||||
&-boolean,
|
||||
&-date,
|
||||
&-time,
|
||||
@ -548,6 +579,7 @@
|
||||
.#{$ns}Number-handler-wrap {
|
||||
height: unset; /* 避免调节器超出Input框 */
|
||||
}
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&-variable {
|
||||
@ -575,3 +607,67 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}FormulaCodeEditor {
|
||||
.cm-field,
|
||||
.cm-func {
|
||||
border-radius: 3px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.cm-field {
|
||||
padding: 2px 5px;
|
||||
}
|
||||
|
||||
.cm-field {
|
||||
background: #007bff;
|
||||
}
|
||||
.cm-func {
|
||||
color: #ae4597;
|
||||
font-weight: bold;
|
||||
line-height: 14px;
|
||||
}
|
||||
.cm-error-token {
|
||||
background-position: left bottom;
|
||||
background-repeat: repeat-x;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==);
|
||||
}
|
||||
|
||||
.CodeMirror-placeholder {
|
||||
color: var(--Form-input-placeholderColor) !important;
|
||||
}
|
||||
|
||||
.lint-error {
|
||||
color: var(--Form-input-onError-borderColor);
|
||||
}
|
||||
|
||||
&--singleLine {
|
||||
max-width: 100%;
|
||||
> .CodeMirror {
|
||||
height: 21px;
|
||||
|
||||
.CodeMirror-hscrollbar,
|
||||
.CodeMirror-vscrollbar {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.CodeMirror-sizer {
|
||||
min-height: 21px !important;
|
||||
}
|
||||
.CodeMirror-scroll {
|
||||
height: 21px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
.CodeMirror-lines {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.#{$ns}InputBox > .#{$ns}FormulaCodeEditor {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
@ -1,13 +1,17 @@
|
||||
import React from 'react';
|
||||
// import 'codemirror/lib/codemirror.css';
|
||||
import type CodeMirror from 'codemirror';
|
||||
import {autobind} from 'amis-core';
|
||||
import {autobind, changedEffect} from 'amis-core';
|
||||
import {resizeSensor} from 'amis-core';
|
||||
|
||||
import 'codemirror/theme/base16-dark.css';
|
||||
// import 'codemirror/theme/base16-light.css';
|
||||
|
||||
export interface CodeMirrorEditorProps {
|
||||
className?: string;
|
||||
style?: any;
|
||||
value?: string;
|
||||
readOnly?: boolean;
|
||||
onChange?: (value: string) => void;
|
||||
onFocus?: (e: any) => void;
|
||||
onBlur?: (e: any) => void;
|
||||
@ -37,14 +41,17 @@ export class CodeMirrorEditor extends React.Component<CodeMirrorEditorProps> {
|
||||
await import('codemirror/mode/htmlmixed/htmlmixed');
|
||||
await import('codemirror/addon/mode/simple');
|
||||
await import('codemirror/addon/mode/multiplex');
|
||||
await import('codemirror/addon/display/placeholder');
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dom.current!.innerHTML = '';
|
||||
this.editor =
|
||||
this.props.editorFactory?.(this.dom.current!, cm, this.props) ??
|
||||
cm(this.dom.current!, {
|
||||
value: this.props.value || ''
|
||||
value: this.props.value || '',
|
||||
readOnly: this.props.readOnly ? 'nocursor' : false
|
||||
});
|
||||
|
||||
this.props.editorDidMount?.(cm, this.editor);
|
||||
@ -52,6 +59,8 @@ export class CodeMirrorEditor extends React.Component<CodeMirrorEditorProps> {
|
||||
this.editor.on('blur', this.handleBlur);
|
||||
this.editor.on('focus', this.handleFocus);
|
||||
|
||||
this.setValue(this.props.value);
|
||||
|
||||
this.toDispose.push(
|
||||
resizeSensor(this.dom.current as HTMLElement, () =>
|
||||
this.editor?.refresh()
|
||||
@ -70,6 +79,10 @@ export class CodeMirrorEditor extends React.Component<CodeMirrorEditorProps> {
|
||||
if (props.value !== prevProps.value) {
|
||||
this.editor && this.setValue(props.value);
|
||||
}
|
||||
|
||||
changedEffect(['readOnly'], prevProps, this.props, (changes: any) => {
|
||||
this.editor?.setOption('readOnly', changes.readOnly ? 'nocursor' : false);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -97,9 +110,9 @@ export class CodeMirrorEditor extends React.Component<CodeMirrorEditorProps> {
|
||||
|
||||
setValue(value?: string) {
|
||||
const doc = this.editor!.getDoc();
|
||||
if (value && value !== doc.getValue()) {
|
||||
if (value !== doc.getValue()) {
|
||||
const cursor = doc.getCursor();
|
||||
doc.setValue(value);
|
||||
doc.setValue(value || '');
|
||||
doc.setCursor(cursor);
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ export interface InputBoxProps
|
||||
children?: React.ReactNode | Array<React.ReactNode>;
|
||||
borderMode?: 'full' | 'half' | 'none';
|
||||
testid?: string;
|
||||
inputRender?: (props: any, ref?: any) => JSX.Element;
|
||||
}
|
||||
|
||||
export interface InputBoxState {
|
||||
@ -49,7 +50,7 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
|
||||
@autobind
|
||||
handleChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const onChange = this.props.onChange;
|
||||
onChange && onChange(e.currentTarget.value);
|
||||
onChange && onChange(e.currentTarget ? e.currentTarget.value : (e as any));
|
||||
}
|
||||
|
||||
@autobind
|
||||
@ -86,6 +87,7 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
|
||||
onClick,
|
||||
mobileUI,
|
||||
testid,
|
||||
inputRender,
|
||||
...rest
|
||||
} = this.props;
|
||||
const isFocused = this.state.isFocused;
|
||||
@ -104,6 +106,18 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
|
||||
>
|
||||
{result}
|
||||
|
||||
{typeof inputRender === 'function' ? (
|
||||
inputRender({
|
||||
...rest,
|
||||
value: value ?? '',
|
||||
onChange: this.handleChange as any,
|
||||
placeholder,
|
||||
onFocus: this.handleFocus,
|
||||
onBlur: this.handleBlur,
|
||||
disabled,
|
||||
...buildTestId(testid)
|
||||
})
|
||||
) : (
|
||||
<Input
|
||||
{...rest}
|
||||
value={value ?? ''}
|
||||
@ -115,6 +129,7 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
|
||||
disabled={disabled}
|
||||
{...buildTestId(testid)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
|
||||
|
@ -27,7 +27,7 @@ export interface TooltipObject {
|
||||
/**
|
||||
* 浮层出现位置
|
||||
*/
|
||||
placement?: 'top' | 'right' | 'bottom' | 'left';
|
||||
placement?: 'top' | 'right' | 'bottom' | 'left' | 'auto';
|
||||
/**
|
||||
* 主题样式
|
||||
*/
|
||||
@ -95,7 +95,7 @@ export interface TooltipWrapperProps {
|
||||
tooltip?: string | TooltipObject;
|
||||
classPrefix: string;
|
||||
classnames: ClassNamesFn;
|
||||
placement: 'top' | 'right' | 'bottom' | 'left';
|
||||
placement: 'top' | 'right' | 'bottom' | 'left' | 'auto';
|
||||
container?: HTMLElement | (() => HTMLElement | null | undefined);
|
||||
trigger: Trigger | Array<Trigger>;
|
||||
rootClose: boolean;
|
||||
|
235
packages/amis-ui/src/components/formula/CodeEditor.tsx
Normal file
235
packages/amis-ui/src/components/formula/CodeEditor.tsx
Normal file
@ -0,0 +1,235 @@
|
||||
import {ThemeProps, themeable} from 'amis-core';
|
||||
import React from 'react';
|
||||
import CodeMirrorEditor, {CodeMirrorEditorProps} from '../CodeMirror';
|
||||
import {FormulaPlugin, editorFactory as createEditor} from './plugin';
|
||||
import type CodeMirror from 'codemirror';
|
||||
|
||||
export interface VariableItem {
|
||||
label: string;
|
||||
value?: string;
|
||||
path?: string; // 路径(label)
|
||||
children?: Array<VariableItem>;
|
||||
type?: string;
|
||||
tag?: string;
|
||||
selectMode?: 'tree' | 'tabs';
|
||||
isMember?: boolean; // 是否是数组成员
|
||||
// chunks?: string[]; // 内容块,作为一个整体进行高亮标记
|
||||
}
|
||||
|
||||
export interface FuncGroup {
|
||||
groupName: string;
|
||||
items: Array<FuncItem>;
|
||||
}
|
||||
|
||||
export interface FuncItem {
|
||||
name: string; // 函数名
|
||||
example?: string; // 示例
|
||||
description?: string; // 描述
|
||||
[propName: string]: any;
|
||||
}
|
||||
|
||||
export interface CodeEditorProps
|
||||
extends ThemeProps,
|
||||
Omit<CodeMirrorEditorProps, 'style' | 'editorFactory' | 'editorDidMount'> {
|
||||
readOnly?: boolean;
|
||||
|
||||
/**
|
||||
* 是否为单行模式,默认为 false
|
||||
*/
|
||||
singleLine?: boolean;
|
||||
|
||||
/**
|
||||
* evalMode 即直接就是表达式,否则
|
||||
* 需要 ${这里面才是表达式}
|
||||
* 默认为 true
|
||||
*/
|
||||
evalMode?: boolean;
|
||||
|
||||
autoFocus?: boolean;
|
||||
|
||||
editorTheme?: 'dark' | 'light';
|
||||
|
||||
editorOptions?: any;
|
||||
|
||||
/**
|
||||
* 用于提示的变量集合,默认为空
|
||||
*/
|
||||
variables?: Array<VariableItem>;
|
||||
|
||||
/**
|
||||
* 函数集合,默认不需要传,即 amis-formula 里面那个函数
|
||||
* 如果有扩充,则需要传。
|
||||
*/
|
||||
functions?: Array<FuncGroup>;
|
||||
|
||||
placeholder?: string;
|
||||
|
||||
editorDidMount?: (
|
||||
cm: typeof CodeMirror,
|
||||
editor: CodeMirror.Editor,
|
||||
plugin: FormulaPlugin
|
||||
) => void;
|
||||
}
|
||||
|
||||
function CodeEditor(props: CodeEditorProps, ref: any) {
|
||||
const {
|
||||
classnames: cx,
|
||||
className,
|
||||
value,
|
||||
onChange,
|
||||
editorDidMount,
|
||||
onFocus,
|
||||
onBlur,
|
||||
functions,
|
||||
variables,
|
||||
evalMode,
|
||||
singleLine,
|
||||
autoFocus,
|
||||
editorTheme,
|
||||
theme: defaultTheme,
|
||||
editorOptions,
|
||||
placeholder
|
||||
} = props;
|
||||
const pluginRef = React.useRef<FormulaPlugin>();
|
||||
|
||||
const editorFactory = React.useCallback((dom: HTMLElement, cm: any) => {
|
||||
let theme =
|
||||
(editorTheme ??
|
||||
((defaultTheme || '').includes('dark') ? 'dark' : 'light')) === 'dark'
|
||||
? 'base16-dark'
|
||||
: 'default';
|
||||
let options: any = {
|
||||
autoFocus,
|
||||
indentUnit: 2,
|
||||
lineNumbers: true,
|
||||
lineWrapping: true, // 自动换行
|
||||
theme,
|
||||
placeholder,
|
||||
...editorOptions
|
||||
};
|
||||
if (singleLine) {
|
||||
options = {
|
||||
lineNumbers: false,
|
||||
indentWithTabs: false,
|
||||
indentUnit: 4,
|
||||
lineWrapping: false,
|
||||
scrollbarStyle: null,
|
||||
theme,
|
||||
placeholder,
|
||||
...editorOptions
|
||||
};
|
||||
}
|
||||
|
||||
return createEditor(dom, cm, props, options);
|
||||
}, []);
|
||||
|
||||
const [readOnly, setReadOnly] = React.useState(props.readOnly);
|
||||
|
||||
React.useEffect(() => setReadOnly(props.readOnly), [props.readOnly]);
|
||||
React.useEffect(
|
||||
() => pluginRef.current?.editor?.setOption('placeholder', placeholder),
|
||||
[placeholder]
|
||||
);
|
||||
|
||||
// singleLine 模式下,禁止输入换行符
|
||||
const onEditorBeforeChange = React.useCallback((cm: any, event: any) => {
|
||||
// Identify typing events that add a newline to the buffer.
|
||||
const hasTypedNewline =
|
||||
event.origin === '+input' &&
|
||||
typeof event.text === 'object' &&
|
||||
event.text.join('') === '';
|
||||
|
||||
// Prevent newline characters from being added to the buffer.
|
||||
if (hasTypedNewline) {
|
||||
return event.cancel();
|
||||
}
|
||||
|
||||
// Identify paste events.
|
||||
const hasPastedNewline =
|
||||
event.origin === 'paste' &&
|
||||
typeof event.text === 'object' &&
|
||||
event.text.length > 1;
|
||||
|
||||
// Format pasted text to replace newlines with spaces.
|
||||
if (hasPastedNewline) {
|
||||
const newText = event.text.join(' ');
|
||||
return event.update(null, null, [newText]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
const onEditorMount = React.useCallback(
|
||||
(cm: any, editor: any) => {
|
||||
const plugin = (pluginRef.current = new FormulaPlugin(editor, cm));
|
||||
plugin.setEvalMode(!!evalMode);
|
||||
plugin.setFunctions(functions || []);
|
||||
plugin.setVariables(variables || []);
|
||||
editorDidMount?.(cm, editor, plugin);
|
||||
plugin.autoMarkText();
|
||||
|
||||
// 单行模式,不允许输入换行,同时原来的换行符也要去掉
|
||||
if (singleLine) {
|
||||
editor.on('beforeChange', onEditorBeforeChange);
|
||||
|
||||
const value = editor.getValue();
|
||||
if (value && /[\n\r]/.test(value)) {
|
||||
// 初始数据有换行,不允许直接编辑
|
||||
// 只能弹窗弹出非单行模式编辑
|
||||
setReadOnly(true);
|
||||
editor.setValue(value.replace(/[\n\r]+/g, ''));
|
||||
}
|
||||
}
|
||||
},
|
||||
[evalMode, functions, variables]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
pluginRef.current?.editor.off('beforeChange', onEditorBeforeChange);
|
||||
pluginRef.current?.dispose();
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useImperativeHandle(ref, () => {
|
||||
return {
|
||||
insertContent: (value: any, type: 'variable' | 'func') =>
|
||||
pluginRef.current?.insertContent(value, type),
|
||||
setValue: (value: any) => pluginRef.current?.setValue(value),
|
||||
getValue: () => pluginRef.current?.getValue(),
|
||||
setDisableAutoMark: (value: boolean) =>
|
||||
pluginRef.current?.setDisableAutoMark(value)
|
||||
};
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
const plugin = pluginRef.current;
|
||||
if (!plugin) {
|
||||
return;
|
||||
}
|
||||
|
||||
plugin.setEvalMode(!!evalMode);
|
||||
plugin.setFunctions(functions || []);
|
||||
plugin.setVariables(variables || []);
|
||||
plugin.autoMarkText();
|
||||
}, [evalMode, functions, variables, value]);
|
||||
|
||||
return (
|
||||
<CodeMirrorEditor
|
||||
className={cx(
|
||||
'FormulaCodeEditor',
|
||||
className,
|
||||
singleLine ? 'FormulaCodeEditor--singleLine' : ''
|
||||
)}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
editorFactory={editorFactory}
|
||||
editorDidMount={onEditorMount}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default themeable(React.forwardRef(CodeEditor));
|
@ -2,50 +2,43 @@
|
||||
* @file 公式编辑器
|
||||
*/
|
||||
import React from 'react';
|
||||
import {mapTree, uncontrollable} from 'amis-core';
|
||||
import {
|
||||
eachTree,
|
||||
resolveVariableAndFilterForAsync,
|
||||
uncontrollable
|
||||
} from 'amis-core';
|
||||
import {
|
||||
parse,
|
||||
autobind,
|
||||
utils,
|
||||
themeable,
|
||||
ThemeProps,
|
||||
localeable,
|
||||
LocaleProps,
|
||||
eachTree
|
||||
LocaleProps
|
||||
} from 'amis-core';
|
||||
import {functionDocs} from 'amis-formula';
|
||||
import type {FunctionDocMap} from 'amis-formula/lib/types';
|
||||
|
||||
import {FormulaPlugin, editorFactory} from './plugin';
|
||||
import {editorFactory} from './plugin';
|
||||
import FuncList from './FuncList';
|
||||
import VariableList from './VariableList';
|
||||
import CodeMirrorEditor from '../CodeMirror';
|
||||
import {toast} from '../Toast';
|
||||
import Switch from '../Switch';
|
||||
import CodeEditor, {FuncGroup, FuncItem, VariableItem} from './CodeEditor';
|
||||
import {functionDocs} from 'amis-formula';
|
||||
import Transition, {
|
||||
EXITED,
|
||||
ENTERING,
|
||||
EXITING
|
||||
} from 'react-transition-group/Transition';
|
||||
import MonacoEditor from '../Editor';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export interface VariableItem {
|
||||
label: string;
|
||||
value?: string;
|
||||
path?: string; // 路径(label)
|
||||
children?: Array<VariableItem>;
|
||||
type?: string;
|
||||
tag?: string;
|
||||
selectMode?: 'tree' | 'tabs';
|
||||
isMember?: boolean; // 是否是数组成员
|
||||
// chunks?: string[]; // 内容块,作为一个整体进行高亮标记
|
||||
}
|
||||
|
||||
export interface FuncGroup {
|
||||
groupName: string;
|
||||
items: Array<FuncItem>;
|
||||
}
|
||||
|
||||
export interface FuncItem {
|
||||
name: string; // 函数名
|
||||
example?: string; // 示例
|
||||
description?: string; // 描述
|
||||
[propName: string]: any;
|
||||
}
|
||||
const collapseStyles: {
|
||||
[propName: string]: string;
|
||||
} = {
|
||||
[EXITED]: 'out',
|
||||
[EXITING]: 'out',
|
||||
[ENTERING]: 'in'
|
||||
};
|
||||
|
||||
export interface FormulaEditorProps extends ThemeProps, LocaleProps {
|
||||
onChange?: (value: string) => void;
|
||||
@ -106,11 +99,14 @@ export interface FunctionProps {
|
||||
}
|
||||
|
||||
export interface FormulaState {
|
||||
functions: FuncGroup[];
|
||||
focused: boolean;
|
||||
isCodeMode: boolean;
|
||||
showRunPanel: boolean;
|
||||
expandTree: boolean;
|
||||
normalizeVariables?: Array<VariableItem>;
|
||||
functions?: Array<FuncGroup>;
|
||||
runContext: string;
|
||||
runResult: string;
|
||||
runValid: boolean;
|
||||
}
|
||||
|
||||
export class FormulaEditor extends React.Component<
|
||||
@ -120,12 +116,15 @@ export class FormulaEditor extends React.Component<
|
||||
state: FormulaState = {
|
||||
focused: false,
|
||||
isCodeMode: false,
|
||||
showRunPanel: false,
|
||||
expandTree: false,
|
||||
normalizeVariables: [],
|
||||
functions: []
|
||||
functions: this.props.functions,
|
||||
runContext: '{\n}',
|
||||
runResult: '',
|
||||
runValid: false
|
||||
};
|
||||
editorPlugin?: FormulaPlugin;
|
||||
unmounted: boolean = false;
|
||||
editor = React.createRef<any>();
|
||||
|
||||
static buildDefaultFunctions(
|
||||
doc: Array<{
|
||||
@ -159,6 +158,25 @@ export class FormulaEditor extends React.Component<
|
||||
}));
|
||||
}
|
||||
|
||||
static async buildFunctions(
|
||||
functions?: Array<any>,
|
||||
functionsFilter?: (functions: Array<FuncGroup>) => Array<FuncGroup>
|
||||
): Promise<any> {
|
||||
const {doc} = await import('amis-formula/lib/doc');
|
||||
const customFunctions = Array.isArray(functions) ? functions : [];
|
||||
const functionList = [
|
||||
...FormulaEditor.buildDefaultFunctions(doc),
|
||||
...FormulaEditor.buildCustomFunctions(functionDocs),
|
||||
...customFunctions
|
||||
];
|
||||
|
||||
if (functionsFilter) {
|
||||
return functionsFilter(functionList);
|
||||
}
|
||||
|
||||
return functionList;
|
||||
}
|
||||
|
||||
static defaultProps: Pick<FormulaEditorProps, 'variables' | 'evalMode'> = {
|
||||
variables: [],
|
||||
evalMode: true
|
||||
@ -182,6 +200,15 @@ export class FormulaEditor extends React.Component<
|
||||
return new RegExp(reg);
|
||||
}
|
||||
|
||||
/**
|
||||
* 干不掉,太多地方使用了,但是要废弃了。
|
||||
* 不要用了,输入框也换成 codemirror 了,本身就支持高亮
|
||||
* @deprecated
|
||||
* @param value
|
||||
* @param variables
|
||||
* @param evalMode
|
||||
* @returns
|
||||
*/
|
||||
static highlightValue(
|
||||
value: string,
|
||||
variables: Array<VariableItem>,
|
||||
@ -252,78 +279,38 @@ export class FormulaEditor extends React.Component<
|
||||
return {html};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
const {variables} = this.props;
|
||||
this.normalizeVariables(variables as VariableItem[]);
|
||||
this.buildFunctions();
|
||||
constructor(props: FormulaEditorProps) {
|
||||
super(props);
|
||||
this.runCode = debounce(this.runCode.bind(this), 250, {
|
||||
leading: false,
|
||||
trailing: true
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(
|
||||
prevProps: Readonly<FormulaEditorProps>,
|
||||
prevState: Readonly<FormulaState>,
|
||||
snapshot?: any
|
||||
): void {
|
||||
if (prevProps.variables !== this.props.variables) {
|
||||
this.normalizeVariables(this.props.variables as VariableItem[]);
|
||||
}
|
||||
|
||||
if (prevProps.functions !== this.props.functions) {
|
||||
this.buildFunctions();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.editorPlugin?.dispose();
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
async buildFunctions() {
|
||||
const {doc} = await import('amis-formula/lib/doc');
|
||||
async componentDidMount() {
|
||||
if (!this.state.functions) {
|
||||
const functionList = await FormulaEditor.buildFunctions();
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
const customFunctions = Array.isArray(this.props.functions)
|
||||
? this.props.functions
|
||||
: [];
|
||||
const functionList = [
|
||||
...FormulaEditor.buildDefaultFunctions(doc),
|
||||
...FormulaEditor.buildCustomFunctions(functionDocs),
|
||||
...customFunctions
|
||||
];
|
||||
|
||||
this.setState({
|
||||
functions: functionList
|
||||
});
|
||||
}
|
||||
|
||||
normalizeVariables(variables?: Array<VariableItem>) {
|
||||
if (!variables) {
|
||||
return;
|
||||
}
|
||||
// 追加path,用于分级高亮
|
||||
const list = mapTree(
|
||||
variables,
|
||||
(item: any, key: number, level: number, paths: any[]) => {
|
||||
const path = paths?.reduce((prev, next) => {
|
||||
return !next.value
|
||||
? prev
|
||||
: `${prev}${prev ? '.' : ''}${next.label ?? next.value}`;
|
||||
}, '');
|
||||
|
||||
return {
|
||||
...item,
|
||||
path: `${path}${path ? '.' : ''}${item.label}`,
|
||||
// 自己是数组成员或者父级有数组成员
|
||||
...(item.isMember || paths.some(item => item.isMember)
|
||||
? {
|
||||
memberDepth: paths?.filter((item: any) => item.type === 'array')
|
||||
?.length
|
||||
componentDidUpdate(prevProps: FormulaEditorProps): void {
|
||||
if (prevProps.functions !== this.props.functions) {
|
||||
this.setState({
|
||||
functions: this.props.functions
|
||||
});
|
||||
}
|
||||
: {})
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
this.setState({normalizeVariables: list});
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
(this.runCode as any).cancel();
|
||||
}
|
||||
|
||||
@autobind
|
||||
@ -340,17 +327,17 @@ export class FormulaEditor extends React.Component<
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
insertValue(value: any, type: 'variable' | 'func') {
|
||||
this.editorPlugin?.insertContent(value, type);
|
||||
getEditor() {
|
||||
let ref = this.editor.current;
|
||||
while (ref?.getWrappedInstance) {
|
||||
ref = ref.getWrappedInstance();
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleEditorMounted(cm: any, editor: any) {
|
||||
this.editorPlugin = new FormulaPlugin(editor, cm, () => ({
|
||||
...this.props,
|
||||
variables: this.state.normalizeVariables
|
||||
}));
|
||||
insertValue(value: any, type: 'variable' | 'func') {
|
||||
this.getEditor()?.insertContent(value, type);
|
||||
}
|
||||
|
||||
@autobind
|
||||
@ -372,12 +359,12 @@ export class FormulaEditor extends React.Component<
|
||||
|
||||
@autobind
|
||||
handleFunctionSelect(item: FuncItem) {
|
||||
this.editorPlugin?.insertContent(`${item.name}`, 'func');
|
||||
this.getEditor()?.insertContent(`${item.name}`, 'func');
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleVariableSelect(item: VariableItem) {
|
||||
const {evalMode, selfVariableName} = this.props;
|
||||
const {selfVariableName} = this.props;
|
||||
|
||||
if (
|
||||
item &&
|
||||
@ -393,7 +380,7 @@ export class FormulaEditor extends React.Component<
|
||||
return;
|
||||
}
|
||||
|
||||
this.editorPlugin?.insertContent(
|
||||
this.getEditor()?.insertContent(
|
||||
item.isMember
|
||||
? item.value
|
||||
: {
|
||||
@ -412,23 +399,72 @@ export class FormulaEditor extends React.Component<
|
||||
handleOnChange(value: any) {
|
||||
const onChange = this.props.onChange;
|
||||
onChange?.(value);
|
||||
this.runCode();
|
||||
}
|
||||
|
||||
@autobind
|
||||
editorFactory(dom: HTMLElement, cm: any) {
|
||||
const {editorOptions, ...rest} = this.props;
|
||||
return editorFactory(dom, cm, rest, {
|
||||
lineWrapping: true // 自动换行
|
||||
lineWrapping: true, // 自动换行
|
||||
autoFocus: true
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleIsCodeModeChange(showCode: boolean) {
|
||||
// 重置一下value
|
||||
this.editorPlugin?.setValue(this.editorPlugin?.getValue());
|
||||
// this.getEditor()?.setValue(this.getEditor()?.getValue());
|
||||
// 非源码模式,则mark一下
|
||||
!showCode && this.editorPlugin?.autoMarkText();
|
||||
this.setState({isCodeMode: showCode});
|
||||
// !showCode && this.getEditor()?.autoMarkText();
|
||||
this.setState({isCodeMode: showCode}, () =>
|
||||
this.getEditor()?.setDisableAutoMark(showCode ? true : false)
|
||||
);
|
||||
}
|
||||
|
||||
@autobind
|
||||
toggleRunPanel() {
|
||||
this.setState(
|
||||
{
|
||||
showRunPanel: !this.state.showRunPanel
|
||||
},
|
||||
this.runCode
|
||||
);
|
||||
}
|
||||
@autobind
|
||||
handleRunContextChange(value: string) {
|
||||
this.setState({runContext: value}, this.runCode);
|
||||
}
|
||||
|
||||
async runCode() {
|
||||
const value = this.props.value || '';
|
||||
if (!this.state.showRunPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 因为 resolveVariableAndFilterForAsync 不会报语法错误
|
||||
parse(value, {
|
||||
evalMode: this.props.evalMode
|
||||
});
|
||||
|
||||
const runContext = JSON.parse(this.state.runContext);
|
||||
let code = this.props.evalMode ? `\${${value}}` : value;
|
||||
|
||||
const result = await resolveVariableAndFilterForAsync(code, runContext);
|
||||
|
||||
this.unmounted ||
|
||||
this.setState({
|
||||
runValid: true,
|
||||
runResult: JSON.stringify(result)
|
||||
});
|
||||
} catch (e) {
|
||||
this.unmounted ||
|
||||
this.setState({
|
||||
runValid: false,
|
||||
runResult: e.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
@ -440,21 +476,25 @@ export class FormulaEditor extends React.Component<
|
||||
const {
|
||||
header,
|
||||
value,
|
||||
functions,
|
||||
variables,
|
||||
variableMode,
|
||||
translate: __,
|
||||
classnames: cx,
|
||||
variableClassName,
|
||||
functionClassName,
|
||||
classPrefix,
|
||||
selfVariableName
|
||||
selfVariableName,
|
||||
evalMode
|
||||
} = this.props;
|
||||
const {
|
||||
focused,
|
||||
isCodeMode,
|
||||
showRunPanel,
|
||||
expandTree,
|
||||
normalizeVariables,
|
||||
functions: functionList
|
||||
functions,
|
||||
runContext,
|
||||
runResult,
|
||||
runValid
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
@ -467,15 +507,19 @@ export class FormulaEditor extends React.Component<
|
||||
<FuncList
|
||||
className={functionClassName}
|
||||
title={__('FormulaEditor.function')}
|
||||
data={functionList}
|
||||
data={functions || []}
|
||||
onSelect={this.handleFunctionSelect}
|
||||
/>
|
||||
|
||||
<div className={cx(`FormulaEditor-content`)}>
|
||||
<header className={cx(`FormulaEditor-header`)}>
|
||||
{__(header || 'FormulaEditor.title')}
|
||||
<div className={cx(`FormulaEditor-header-toolbar m-l`)}>
|
||||
<span>{__('FormulaEditor.run')}</span>
|
||||
<Switch value={showRunPanel} onChange={this.toggleRunPanel} />
|
||||
</div>
|
||||
<div className={cx(`FormulaEditor-header-toolbar`)}>
|
||||
<span>源码模式</span>
|
||||
<span>{__('FormulaEditor.sourceMode')}</span>
|
||||
<Switch
|
||||
value={isCodeMode}
|
||||
onChange={this.handleIsCodeModeChange}
|
||||
@ -483,15 +527,61 @@ export class FormulaEditor extends React.Component<
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<CodeMirrorEditor
|
||||
<CodeEditor
|
||||
evalMode={evalMode}
|
||||
functions={functions}
|
||||
variables={variables}
|
||||
className={cx('FormulaEditor-editor')}
|
||||
value={value}
|
||||
onChange={this.handleOnChange}
|
||||
editorFactory={this.editorFactory}
|
||||
editorDidMount={this.handleEditorMounted}
|
||||
ref={this.editor}
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleBlur}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<Transition
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
key="run-panel"
|
||||
in={showRunPanel}
|
||||
timeout={300}
|
||||
>
|
||||
{(status: string) => {
|
||||
return (
|
||||
<div
|
||||
className={cx(
|
||||
`FormulaEditor-runPanel`,
|
||||
collapseStyles[status]
|
||||
)}
|
||||
>
|
||||
<div className={cx(`FormulaEditor-runPanel-context`)}>
|
||||
<header>{__('FormulaEditor.runContext')}</header>
|
||||
<div>
|
||||
<MonacoEditor
|
||||
value={runContext}
|
||||
onChange={this.handleRunContextChange}
|
||||
language="json"
|
||||
options={{
|
||||
tabSize: 2,
|
||||
lineNumbers: false
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cx(
|
||||
`FormulaEditor-runPanel-result`,
|
||||
runValid ? '' : 'is-error'
|
||||
)}
|
||||
>
|
||||
<header>{__('FormulaEditor.runResult')}</header>
|
||||
<div>{runResult}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Transition>
|
||||
</div>
|
||||
<div className={cx('FormulaEditor-panel', 'right')}>
|
||||
{variableMode !== 'tabs' ? (
|
||||
@ -499,7 +589,7 @@ export class FormulaEditor extends React.Component<
|
||||
{__('FormulaEditor.variable')}
|
||||
{variableMode === 'tree' ? (
|
||||
<div className={cx(`FormulaEditor-header-toolbar`)}>
|
||||
<span>展开全部</span>
|
||||
<span>{__('FormulaEditor.toggleAll')}</span>
|
||||
<Switch
|
||||
value={expandTree}
|
||||
onChange={this.handleExpandTreeChange}
|
||||
@ -523,7 +613,7 @@ export class FormulaEditor extends React.Component<
|
||||
)}
|
||||
expandTree={expandTree}
|
||||
selectMode={variableMode}
|
||||
data={normalizeVariables!}
|
||||
data={variables!}
|
||||
onSelect={this.handleVariableSelect}
|
||||
selfVariableName={selfVariableName}
|
||||
/>
|
||||
|
@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
import {themeable, ThemeProps} from 'amis-core';
|
||||
import {mapTree, themeable, ThemeProps} from 'amis-core';
|
||||
import Collapse from '../Collapse';
|
||||
import CollapseGroup from '../CollapseGroup';
|
||||
import SearchBox from '../SearchBox';
|
||||
import type {FuncGroup, FuncItem} from './Editor';
|
||||
import type {FuncGroup, FuncItem} from './CodeEditor';
|
||||
import TooltipWrapper from '../TooltipWrapper';
|
||||
import {Icon} from '../icons';
|
||||
|
||||
@ -25,26 +25,32 @@ export function FuncList(props: FuncListProps) {
|
||||
descClassName,
|
||||
mobileUI
|
||||
} = props;
|
||||
const [term, setTerm] = React.useState('');
|
||||
const [filteredFuncs, setFiteredFuncs] = React.useState(props.data);
|
||||
const [activeFunc, setActiveFunc] = React.useState<any>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
setFiteredFuncs(props.data);
|
||||
}, [props.data]);
|
||||
|
||||
function onSearch(term: string) {
|
||||
const onSearch = React.useCallback(
|
||||
(term: string) => {
|
||||
const filtered = props.data
|
||||
.map(item => {
|
||||
return {
|
||||
...item,
|
||||
items: term
|
||||
? item.items.filter(item => ~item.name.indexOf(term.toUpperCase()))
|
||||
? item.items.filter(
|
||||
(item: any) => ~item.name.indexOf(term.toUpperCase())
|
||||
)
|
||||
: item.items
|
||||
};
|
||||
})
|
||||
.filter(item => item.items.length);
|
||||
setFiteredFuncs(filtered);
|
||||
}
|
||||
},
|
||||
[props.data]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
onSearch(term);
|
||||
}, [props.data]);
|
||||
|
||||
return (
|
||||
<div className={cx('FormulaEditor-panel', 'left', className)}>
|
||||
@ -57,7 +63,13 @@ export function FuncList(props: FuncListProps) {
|
||||
<div className={cx('FormulaEditor-panel-header')}>{title}</div>
|
||||
<div className={cx('FormulaEditor-panel-body')}>
|
||||
<div className={cx('FormulaEditor-FuncList-searchBox')}>
|
||||
<SearchBox mini={false} onSearch={onSearch} mobileUI={mobileUI} />
|
||||
<SearchBox
|
||||
value={term}
|
||||
onChange={setTerm}
|
||||
mini={false}
|
||||
onSearch={onSearch}
|
||||
mobileUI={mobileUI}
|
||||
/>
|
||||
</div>
|
||||
<div className={cx('FormulaEditor-FuncList-body', bodyClassName)}>
|
||||
<CollapseGroup
|
||||
@ -81,7 +93,7 @@ export function FuncList(props: FuncListProps) {
|
||||
header={item.groupName}
|
||||
key={item.groupName}
|
||||
>
|
||||
{item.items.map(item => (
|
||||
{item.items.map((item: any) => (
|
||||
<div
|
||||
className={cx('FormulaEditor-FuncList-item', {
|
||||
'is-active': item.name === activeFunc?.name
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
isObject
|
||||
} from 'amis-core';
|
||||
|
||||
import {FormulaEditor, VariableItem} from './Editor';
|
||||
import {FormulaEditor} from './Editor';
|
||||
import ResultBox from '../ResultBox';
|
||||
import Select from '../Select';
|
||||
import NumberInput from '../NumberInput';
|
||||
@ -21,6 +21,8 @@ import DatePicker from '../DatePicker';
|
||||
import Tag from '../Tag';
|
||||
|
||||
import type {FormulaPickerProps} from './Picker';
|
||||
import CodeEditor, {FuncGroup, VariableItem} from './CodeEditor';
|
||||
import InputBox from '../InputBox';
|
||||
|
||||
export interface FormulaInputProps
|
||||
extends Pick<
|
||||
@ -42,9 +44,19 @@ export interface FormulaInputProps
|
||||
*/
|
||||
value?: string;
|
||||
|
||||
/**
|
||||
* 就是 evalMode 的反义词
|
||||
* 混合模式,意味着这个输入框既可以输入不同文本
|
||||
* 也可以输入公式。
|
||||
* 当输入公式时,值格式为 ${公式内容}
|
||||
* 其他内容当字符串。
|
||||
*/
|
||||
mixedMode?: boolean;
|
||||
|
||||
autoFoucs?: boolean;
|
||||
|
||||
variables?: VariableItem[];
|
||||
functions?: Array<FuncGroup>;
|
||||
|
||||
popOverContainer?: any;
|
||||
|
||||
@ -59,7 +71,7 @@ export interface FormulaInputProps
|
||||
itemRender?: (value: any) => JSX.Element | string;
|
||||
}
|
||||
|
||||
const FormulaInput: React.FC<FormulaInputProps> = props => {
|
||||
const FormulaInput = (props: FormulaInputProps, ref: any) => {
|
||||
const {
|
||||
translate: __,
|
||||
className,
|
||||
@ -71,6 +83,7 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
||||
mixedMode,
|
||||
value,
|
||||
variables,
|
||||
functions,
|
||||
inputSettings = {type: 'text'},
|
||||
popOverContainer,
|
||||
onChange,
|
||||
@ -81,9 +94,16 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
||||
const sharedProps = pick(props, ['disabled', 'clearable']);
|
||||
const pipInValue = useCallback(
|
||||
(value?: any) => {
|
||||
/** 数据来源可能是从 query中下发的(CRUD查询表头),导致数字或者布尔值被转为 string 格式,这里预处理一下 */
|
||||
if (schemaType === 'number') {
|
||||
value = isNaN(+value) ? value : +value;
|
||||
} else if (schemaType === 'boolean') {
|
||||
value = value === 'true' ? true : value === 'false' ? false : value;
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
['value']
|
||||
[schemaType]
|
||||
);
|
||||
const pipOutValue = useCallback(
|
||||
(origin: any) => {
|
||||
@ -133,106 +153,9 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
||||
);
|
||||
|
||||
let cmptValue = pipInValue(value ?? inputSettings.defaultValue);
|
||||
const isExpr = isExpression(cmptValue);
|
||||
|
||||
/** 数据来源可能是从 query中下发的(CRUD查询表头),导致数字或者布尔值被转为 string 格式,这里预处理一下 */
|
||||
if (schemaType === 'number') {
|
||||
cmptValue = isNaN(+cmptValue) ? cmptValue : +cmptValue;
|
||||
} else if (schemaType === 'boolean') {
|
||||
cmptValue =
|
||||
cmptValue === 'true' ? true : cmptValue === 'false' ? false : cmptValue;
|
||||
}
|
||||
|
||||
const targetVariable =
|
||||
variables && cmptValue != null && typeof cmptValue === 'string'
|
||||
? findTree(variables, item => {
|
||||
return mixedMode
|
||||
? cmptValue.replace(/^\$\{/, '').replace(/\}$/, '') === item?.value
|
||||
: cmptValue === item?.value;
|
||||
})
|
||||
: null;
|
||||
let useVariable = !!(isExpression(cmptValue) || targetVariable);
|
||||
|
||||
/** 判断value是否为变量,如果是变量,使用ResultBox渲染 */
|
||||
if (!useVariable) {
|
||||
if (schemaType === 'number') {
|
||||
useVariable = cmptValue != null && typeof cmptValue !== 'number';
|
||||
} else if (['date', 'time', 'datetime'].includes(schemaType)) {
|
||||
useVariable = !moment(cmptValue).isValid();
|
||||
} else if (schemaType === 'select') {
|
||||
const {
|
||||
options,
|
||||
joinValues,
|
||||
extractValue,
|
||||
delimiter,
|
||||
multiple,
|
||||
valueField = 'value'
|
||||
} = inputSettings;
|
||||
let selctedValue: any[] = [];
|
||||
|
||||
if (multiple) {
|
||||
if (joinValues) {
|
||||
selctedValue =
|
||||
typeof cmptValue === 'string' ? cmptValue.split(delimiter) : [];
|
||||
} else {
|
||||
selctedValue = Array.isArray(cmptValue)
|
||||
? extractValue
|
||||
? cmptValue
|
||||
: cmptValue.map(i => i?.[valueField])
|
||||
: [];
|
||||
}
|
||||
} else {
|
||||
if (joinValues) {
|
||||
selctedValue = typeof cmptValue === 'string' ? [cmptValue] : [];
|
||||
} else {
|
||||
selctedValue = isObject(cmptValue) ? [cmptValue?.[valueField]] : [];
|
||||
}
|
||||
}
|
||||
|
||||
/** 选项类型清空后是空字符串, */
|
||||
useVariable =
|
||||
cmptValue &&
|
||||
!(options ?? []).some((item: any) =>
|
||||
selctedValue.includes(item?.value)
|
||||
);
|
||||
} else if (schemaType === 'boolean') {
|
||||
useVariable = cmptValue != null && typeof cmptValue !== 'boolean';
|
||||
}
|
||||
}
|
||||
|
||||
if (useVariable) {
|
||||
const varName =
|
||||
typeof cmptValue === 'string' && cmptValue && mixedMode
|
||||
? cmptValue.replace(/^\$\{/, '').replace(/\}$/, '')
|
||||
: cmptValue;
|
||||
const resultValue = targetVariable?.value ?? varName;
|
||||
|
||||
return (
|
||||
<ResultBox
|
||||
className={cx(`FormulaPicker-input-variable`)}
|
||||
allowInput={allowInput}
|
||||
// value={resultValue}
|
||||
result={
|
||||
resultValue == null
|
||||
? void 0
|
||||
: FormulaEditor.highlightValue(resultValue, variables!, evalMode)
|
||||
}
|
||||
itemRender={(item: any) => {
|
||||
return (
|
||||
<div
|
||||
className={cx('FormulaPicker-ResultBox')}
|
||||
dangerouslySetInnerHTML={{__html: item.html}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
onResultChange={noop}
|
||||
onChange={pipOutValue}
|
||||
onClear={() => pipOutValue(undefined)}
|
||||
clearable={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (schemaType === 'number') {
|
||||
if (!isExpr && schemaType === 'number') {
|
||||
return (
|
||||
<NumberInput
|
||||
{...sharedProps}
|
||||
@ -247,9 +170,7 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
||||
onChange={pipOutValue}
|
||||
/>
|
||||
);
|
||||
} else if (schemaType === 'date') {
|
||||
const cmptValue = pipInValue(value ?? inputSettings.defaultValue);
|
||||
|
||||
} else if (!isExpr && schemaType === 'date') {
|
||||
return (
|
||||
<DatePicker
|
||||
{...sharedProps}
|
||||
@ -265,7 +186,7 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
||||
onChange={pipOutValue}
|
||||
/>
|
||||
);
|
||||
} else if (schemaType === 'time') {
|
||||
} else if (!isExpr && schemaType === 'time') {
|
||||
return (
|
||||
<DatePicker
|
||||
{...sharedProps}
|
||||
@ -279,11 +200,11 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
||||
dateFormat=""
|
||||
timeFormat={inputSettings.format || 'HH:mm'}
|
||||
popOverContainer={popOverContainer}
|
||||
value={pipInValue(value ?? inputSettings.defaultValue)}
|
||||
value={cmptValue}
|
||||
onChange={pipOutValue}
|
||||
/>
|
||||
);
|
||||
} else if (schemaType === 'datetime') {
|
||||
} else if (!isExpr && schemaType === 'datetime') {
|
||||
return (
|
||||
<DatePicker
|
||||
{...sharedProps}
|
||||
@ -295,11 +216,11 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
||||
inputFormat={inputSettings.inputFormat || 'YYYY-MM-DD HH:mm'}
|
||||
timeFormat={inputSettings.timeFormat || 'HH:mm'}
|
||||
popOverContainer={popOverContainer}
|
||||
value={pipInValue(value ?? inputSettings.defaultValue)}
|
||||
value={cmptValue}
|
||||
onChange={pipOutValue}
|
||||
/>
|
||||
);
|
||||
} else if (schemaType === 'select' || schemaType === 'boolean') {
|
||||
} else if (!isExpr && (schemaType === 'select' || schemaType === 'boolean')) {
|
||||
return (
|
||||
<Select
|
||||
{...sharedProps}
|
||||
@ -335,21 +256,25 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ResultBox
|
||||
{...sharedProps}
|
||||
className={cx(className)}
|
||||
allowInput={allowInput}
|
||||
borderMode={borderMode}
|
||||
<InputBox
|
||||
className={cx('FormulaPicker-input')}
|
||||
inputRender={({value, onChange, onFocus, onBlur, placeholder}: any) => (
|
||||
<CodeEditor
|
||||
singleLine
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
functions={functions}
|
||||
variables={variables}
|
||||
evalMode={evalMode}
|
||||
placeholder={placeholder}
|
||||
value={pipInValue(value)}
|
||||
result={
|
||||
allowInput || !value
|
||||
? void 0
|
||||
: FormulaEditor.highlightValue(value, variables!, evalMode)
|
||||
}
|
||||
itemRender={itemRender}
|
||||
onResultChange={noop}
|
||||
/>
|
||||
)}
|
||||
borderMode={borderMode}
|
||||
value={cmptValue}
|
||||
onChange={pipOutValue}
|
||||
placeholder={__(placeholder ?? 'placeholder.enter')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -357,7 +282,7 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
||||
|
||||
export default themeable(
|
||||
localeable(
|
||||
uncontrollable(FormulaInput, {
|
||||
uncontrollable(React.forwardRef(FormulaInput), {
|
||||
value: 'onChange'
|
||||
})
|
||||
)
|
||||
|
@ -5,12 +5,7 @@ import {
|
||||
uncontrollable
|
||||
} from 'amis-core';
|
||||
import React from 'react';
|
||||
import {
|
||||
FormulaEditor,
|
||||
FormulaEditorProps,
|
||||
FuncGroup,
|
||||
VariableItem
|
||||
} from './Editor';
|
||||
import {FormulaEditor, FormulaEditorProps} from './Editor';
|
||||
import {
|
||||
autobind,
|
||||
noop,
|
||||
@ -28,6 +23,8 @@ import {Icon} from '../icons';
|
||||
import Modal from '../Modal';
|
||||
import PopUp from '../PopUp';
|
||||
import FormulaInput from './Input';
|
||||
import {FuncGroup, VariableItem} from './CodeEditor';
|
||||
import {functionDocs} from 'amis-formula';
|
||||
|
||||
export const InputSchemaType = [
|
||||
'text',
|
||||
@ -142,6 +139,8 @@ export interface FormulaPickerProps
|
||||
*/
|
||||
onPickerOpen?: (props: FormulaPickerProps) => any;
|
||||
|
||||
functionsFilter?: (functions: Array<FuncGroup>) => Array<FuncGroup>;
|
||||
|
||||
children?: (props: {
|
||||
onClick: (e: React.MouseEvent) => void;
|
||||
setState: (state: any) => void;
|
||||
@ -180,6 +179,7 @@ export class FormulaPicker extends React.Component<
|
||||
evalMode: true
|
||||
};
|
||||
|
||||
unmounted = false;
|
||||
constructor(props: FormulaPickerProps) {
|
||||
super(props);
|
||||
this.props.onRef && this.props.onRef(this);
|
||||
@ -205,6 +205,7 @@ export class FormulaPicker extends React.Component<
|
||||
);
|
||||
this.setState({variables: result});
|
||||
}
|
||||
this.buildFunctions();
|
||||
}
|
||||
|
||||
async componentDidUpdate(prevProps: FormulaPickerProps) {
|
||||
@ -231,12 +232,47 @@ export class FormulaPicker extends React.Component<
|
||||
this.setState({variables: result});
|
||||
}
|
||||
}
|
||||
|
||||
if (prevProps.functions !== this.props.functions) {
|
||||
this.buildFunctions();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
async buildFunctions(
|
||||
functions = this.props.functions,
|
||||
setState = true
|
||||
): Promise<any> {
|
||||
const functionList = await FormulaEditor.buildFunctions(
|
||||
functions,
|
||||
this.props.functionsFilter
|
||||
);
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!setState) {
|
||||
return functionList;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
functions: functionList
|
||||
});
|
||||
}
|
||||
|
||||
value2EditorValue(props: FormulaPickerProps) {
|
||||
const {value} = props;
|
||||
const {value, mixedMode, inputSettings} = props;
|
||||
|
||||
if (!this.isTextInput()) {
|
||||
if (
|
||||
mixedMode &&
|
||||
typeof value === 'string' &&
|
||||
/^\s*\$\{([\s\S]+)\}\s*$/.test(value)
|
||||
) {
|
||||
return RegExp.$1;
|
||||
} else if (typeof value !== 'string') {
|
||||
let editorValue = '';
|
||||
|
||||
try {
|
||||
@ -244,22 +280,11 @@ export class FormulaPicker extends React.Component<
|
||||
} catch (error) {}
|
||||
|
||||
return editorValue;
|
||||
}
|
||||
|
||||
if (props.mixedMode) {
|
||||
if (
|
||||
typeof props.value === 'string' &&
|
||||
/^\s*\$\{(.+?)\}\s*$/.test(props.value)
|
||||
) {
|
||||
return RegExp.$1;
|
||||
} else {
|
||||
return '';
|
||||
return value ? (mixedMode ? `\`${value}\`` : value) : '';
|
||||
}
|
||||
}
|
||||
|
||||
return String(props.value || '');
|
||||
}
|
||||
|
||||
isTextInput() {
|
||||
const {inputSettings} = this.props;
|
||||
|
||||
@ -281,22 +306,6 @@ export class FormulaPicker extends React.Component<
|
||||
}
|
||||
}
|
||||
|
||||
@autobind
|
||||
renderFormulaValue(item: any) {
|
||||
const {allowInput, classnames: cx} = this.props;
|
||||
const html = {__html: item.html};
|
||||
if (allowInput) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cx('FormulaPicker-ResultBox')}
|
||||
dangerouslySetInnerHTML={html}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
|
||||
@autobind
|
||||
handleInputChange(value: string) {
|
||||
this.setState({value}, () => this.handleConfirm());
|
||||
@ -321,22 +330,17 @@ export class FormulaPicker extends React.Component<
|
||||
const {translate: __, inputSettings} = this.props;
|
||||
const {editorValue} = this.state;
|
||||
|
||||
if (this.isTextInput()) {
|
||||
return this.confirm(editorValue);
|
||||
} else if (inputSettings) {
|
||||
if (
|
||||
inputSettings?.type &&
|
||||
['boolean', 'number'].includes(inputSettings?.type)
|
||||
) {
|
||||
let result = editorValue;
|
||||
const schemaType = inputSettings?.type;
|
||||
// const schemaType = inputSettings?.type;
|
||||
|
||||
try {
|
||||
const ast = parse(editorValue, {evalMode: true, allowFilter: false});
|
||||
|
||||
if (
|
||||
schemaType === 'select' &&
|
||||
inputSettings.multiple &&
|
||||
ast.type === 'array'
|
||||
) {
|
||||
result = ast.members.map((i: any) => i.value);
|
||||
} else if (ast.type === 'literal' || ast.type === 'string') {
|
||||
if (ast.type === 'literal' || ast.type === 'string') {
|
||||
result = ast.value ?? '';
|
||||
}
|
||||
} catch (error) {
|
||||
@ -347,19 +351,28 @@ export class FormulaPicker extends React.Component<
|
||||
this.setState({isError: false});
|
||||
return this.confirm(result);
|
||||
}
|
||||
return this.confirm(editorValue);
|
||||
}
|
||||
|
||||
confirm(value: string) {
|
||||
confirm(value: any) {
|
||||
const {mixedMode} = this.props;
|
||||
const validate = this.validate(value);
|
||||
|
||||
if (validate === true) {
|
||||
this.setState(
|
||||
{value: mixedMode && value ? `\${${value}}` : value},
|
||||
() => {
|
||||
this.close(undefined, () => this.handleConfirm());
|
||||
let result = value;
|
||||
|
||||
if (mixedMode && typeof value === 'string') {
|
||||
result =
|
||||
!value.includes('$') &&
|
||||
value[0] === '`' &&
|
||||
value[value.length - 1] === '`'
|
||||
? value.substring(1, value.length - 1)
|
||||
: `\${${value}}`;
|
||||
}
|
||||
);
|
||||
|
||||
this.setState({value: result}, () => {
|
||||
this.close(undefined, () => this.handleConfirm());
|
||||
});
|
||||
} else {
|
||||
this.setState({isError: validate});
|
||||
}
|
||||
@ -375,6 +388,9 @@ export class FormulaPicker extends React.Component<
|
||||
isOpened: true
|
||||
};
|
||||
|
||||
if (state.functions) {
|
||||
state.functions = await this.buildFunctions(state.functions, false);
|
||||
}
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
@ -431,7 +447,7 @@ export class FormulaPicker extends React.Component<
|
||||
try {
|
||||
value &&
|
||||
parse(value, {
|
||||
evalMode: this.props.mixedMode ? true : this.props.evalMode,
|
||||
evalMode: this.props.mixedMode ? false : this.props.evalMode,
|
||||
allowFilter: false
|
||||
});
|
||||
|
||||
@ -529,31 +545,25 @@ export class FormulaPicker extends React.Component<
|
||||
)}
|
||||
{mode === 'input-button' && (
|
||||
<>
|
||||
<ResultBox
|
||||
<FormulaInput
|
||||
className={cx(
|
||||
'FormulaPicker-input',
|
||||
isOpened ? 'is-active' : '',
|
||||
!!isError ? 'is-error' : ''
|
||||
)}
|
||||
inputSettings={inputSettings}
|
||||
allowInput={allowInput}
|
||||
clearable={clearable}
|
||||
evalMode={mixedMode ? false : evalMode}
|
||||
variables={this.state.variables!}
|
||||
functions={this.state.functions ?? functions}
|
||||
value={value}
|
||||
result={
|
||||
allowInput
|
||||
? void 0
|
||||
: FormulaEditor.highlightValue(
|
||||
value,
|
||||
this.state.variables!,
|
||||
this.props.evalMode
|
||||
)
|
||||
}
|
||||
itemRender={this.renderFormulaValue}
|
||||
onResultChange={noop}
|
||||
onChange={this.handleInputChange}
|
||||
disabled={disabled}
|
||||
borderMode={borderMode}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className={cx('FormulaPicker-action')}
|
||||
onClick={this.handleClick}
|
||||
@ -578,11 +588,10 @@ export class FormulaPicker extends React.Component<
|
||||
inputSettings={inputSettings}
|
||||
allowInput={allowInput}
|
||||
clearable={clearable}
|
||||
evalMode={evalMode}
|
||||
mixedMode={mixedMode}
|
||||
evalMode={mixedMode ? false : evalMode}
|
||||
variables={this.state.variables!}
|
||||
functions={this.state.functions ?? functions}
|
||||
value={value}
|
||||
itemRender={this.renderFormulaValue}
|
||||
onChange={this.handleInputChange}
|
||||
disabled={disabled}
|
||||
borderMode={borderMode}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React, {useEffect} from 'react';
|
||||
|
||||
import {themeable, ThemeProps, filterTree} from 'amis-core';
|
||||
import {themeable, ThemeProps, filterTree, mapTree} from 'amis-core';
|
||||
import GroupedSelection from '../GroupedSelection';
|
||||
import Tabs, {Tab} from '../Tabs';
|
||||
import TreeSelection from '../TreeSelection';
|
||||
import SearchBox from '../SearchBox';
|
||||
|
||||
import type {VariableItem} from './Editor';
|
||||
import type {VariableItem} from './CodeEditor';
|
||||
import type {ItemRenderStates} from '../Selection';
|
||||
import type {Option} from '../Select';
|
||||
import type {TabsMode} from '../Tabs';
|
||||
@ -85,7 +85,6 @@ export interface VariableListProps extends ThemeProps, SpinnerExtraProps {
|
||||
function VariableList(props: VariableListProps) {
|
||||
const variableListRef = React.useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
data: list,
|
||||
className,
|
||||
classnames: cx,
|
||||
tabsMode = 'line',
|
||||
@ -97,14 +96,37 @@ function VariableList(props: VariableListProps) {
|
||||
selfVariableName,
|
||||
expandTree
|
||||
} = props;
|
||||
const [filterVars, setFilterVars] = React.useState(list);
|
||||
const [variables, setVariables] = React.useState<Array<VariableItem>>([]);
|
||||
const [filterVars, setFilterVars] = React.useState<Array<VariableItem>>([]);
|
||||
const classPrefix = `${themePrefix}FormulaEditor-VariableList`;
|
||||
|
||||
useEffect(() => {
|
||||
const {data} = props;
|
||||
if (data) {
|
||||
setFilterVars(data);
|
||||
React.useEffect(() => {
|
||||
// 追加path,用于分级高亮
|
||||
const list = mapTree(
|
||||
props.data,
|
||||
(item: any, key: number, level: number, paths: any[]) => {
|
||||
const path = paths?.reduce((prev, item) => {
|
||||
return !item.value
|
||||
? prev
|
||||
: `${prev}${prev ? '.' : ''}${item.label ?? item.value}`;
|
||||
}, '');
|
||||
|
||||
return {
|
||||
...item,
|
||||
path: `${path}${path ? '.' : ''}${item.label}`,
|
||||
// 自己是数组成员或者父级有数组成员
|
||||
...(item.isMember || paths.some(item => item.isMember)
|
||||
? {
|
||||
memberDepth: paths?.filter((item: any) => item.type === 'array')
|
||||
?.length
|
||||
}
|
||||
: {})
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
setVariables(list);
|
||||
setFilterVars(list);
|
||||
}, [props.data]);
|
||||
|
||||
const itemRender =
|
||||
@ -217,7 +239,7 @@ function VariableList(props: VariableListProps) {
|
||||
|
||||
function onSearch(term: string) {
|
||||
const tree = filterTree(
|
||||
list,
|
||||
variables,
|
||||
(i: any, key: number, level: number, paths: any[]) => {
|
||||
return !!(
|
||||
(Array.isArray(i.children) && i.children.length) ||
|
||||
@ -231,7 +253,7 @@ function VariableList(props: VariableListProps) {
|
||||
true
|
||||
);
|
||||
|
||||
setFilterVars(!term ? list : tree);
|
||||
setFilterVars(!term ? variables : tree);
|
||||
}
|
||||
|
||||
function renderSearchBox() {
|
||||
|
@ -3,8 +3,10 @@
|
||||
*/
|
||||
|
||||
import type CodeMirror from 'codemirror';
|
||||
import {eachTree} from 'amis-core';
|
||||
import {FormulaEditorProps, VariableItem, FormulaEditor} from './Editor';
|
||||
import {findTree} from 'amis-core';
|
||||
import {FuncGroup, VariableItem} from './CodeEditor';
|
||||
import {parse} from 'amis-formula';
|
||||
import debounce from 'lodash/debounce';
|
||||
|
||||
export function editorFactory(
|
||||
dom: HTMLElement,
|
||||
@ -16,30 +18,88 @@ export function editorFactory(
|
||||
|
||||
return cm(dom, {
|
||||
value: props.value || '',
|
||||
autofocus: true,
|
||||
autofocus: false,
|
||||
mode: props.evalMode ? 'text/formula' : 'text/formula-template',
|
||||
readOnly: props.readOnly ? 'nocursor' : false,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
function traverseAst(ast: any, iterator: (ast: any) => void | false) {
|
||||
if (!ast || !ast.type) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ret = iterator(ast);
|
||||
if (ret === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(ast).forEach(key => {
|
||||
const value = ast[key];
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(child => traverseAst(child, iterator));
|
||||
} else {
|
||||
traverseAst(value, iterator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export class FormulaPlugin {
|
||||
/**
|
||||
* 用于提示的变量集合,默认为空
|
||||
*/
|
||||
variables: Array<VariableItem> = [];
|
||||
|
||||
/**
|
||||
* 函数集合,默认不需要传,即 amis-formula 里面那个函数
|
||||
* 如果有扩充,则需要传。
|
||||
*/
|
||||
functions: Array<FuncGroup> = [];
|
||||
|
||||
/**
|
||||
* evalMode 即直接就是表达式,否则就是混合模式
|
||||
*/
|
||||
evalMode: boolean = true;
|
||||
|
||||
disableAutoMark = false;
|
||||
|
||||
constructor(
|
||||
readonly editor: CodeMirror.Editor,
|
||||
readonly cm: typeof CodeMirror,
|
||||
readonly getProps: () => FormulaEditorProps
|
||||
readonly cm: typeof CodeMirror
|
||||
) {
|
||||
// editor.on('change', this.autoMarkText);
|
||||
this.autoMarkText();
|
||||
// this.autoMarkText();
|
||||
this.autoMarkText = debounce(this.autoMarkText.bind(this), 250, {
|
||||
leading: false,
|
||||
trailing: true
|
||||
});
|
||||
|
||||
editor.on('blur', () => this.autoMarkText());
|
||||
}
|
||||
|
||||
autoMarkText() {
|
||||
const {functions, variables, value} = this.getProps();
|
||||
|
||||
if (value) {
|
||||
// todo functions 也需要自动替换
|
||||
this.autoMark(variables!);
|
||||
this.focus(value);
|
||||
setVariables(variables: Array<VariableItem>) {
|
||||
this.variables = Array.isArray(variables) ? variables : [];
|
||||
}
|
||||
|
||||
setFunctions(functions: Array<FuncGroup>) {
|
||||
this.functions = Array.isArray(functions) ? functions : [];
|
||||
}
|
||||
|
||||
setEvalMode(evalMode: boolean) {
|
||||
this.evalMode = evalMode;
|
||||
}
|
||||
|
||||
setDisableAutoMark(disableAutoMark: boolean) {
|
||||
this.disableAutoMark = disableAutoMark;
|
||||
this.autoMarkText(true);
|
||||
}
|
||||
|
||||
autoMarkText(forceClear = false) {
|
||||
if (forceClear || !this.editor.hasFocus()) {
|
||||
this.editor?.getAllMarks().forEach(mark => mark.clear());
|
||||
}
|
||||
this.disableAutoMark || this.autoMark();
|
||||
}
|
||||
|
||||
// 计算 `${`、`}` 括号的位置,如 ${a}+${b}, 结果是 [ { from: 0, to: 3 }, { from: 5, to: 8 } ]
|
||||
@ -108,50 +168,19 @@ export class FormulaPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
insertContent(
|
||||
value: any,
|
||||
type?: 'variable' | 'func',
|
||||
className: string = 'cm-field',
|
||||
toMark: boolean = true
|
||||
) {
|
||||
insertContent(value: any, type?: 'variable' | 'func') {
|
||||
let from = this.editor.getCursor();
|
||||
const {evalMode} = this.getProps();
|
||||
const evalMode = this.evalMode;
|
||||
|
||||
if (type === 'variable') {
|
||||
this.editor.replaceSelection(value.key);
|
||||
const to = this.editor.getCursor();
|
||||
|
||||
if (toMark) {
|
||||
// 路径中每个变量分别进行标记
|
||||
let markFrom = from.ch;
|
||||
value.path.split('.').forEach((label: string, index: number) => {
|
||||
const val = value.key.split('.')[index];
|
||||
this.markText(
|
||||
{line: from.line, ch: markFrom},
|
||||
{line: to.line, ch: markFrom + val.length},
|
||||
label,
|
||||
className
|
||||
);
|
||||
markFrom += 1 + val.length;
|
||||
});
|
||||
}
|
||||
|
||||
!evalMode && this.insertBraces(from, to);
|
||||
} else if (type === 'func') {
|
||||
this.editor.replaceSelection(`${value}()`);
|
||||
const to = this.editor.getCursor();
|
||||
|
||||
toMark &&
|
||||
this.markText(
|
||||
from,
|
||||
{
|
||||
line: to.line,
|
||||
ch: to.ch - 2
|
||||
},
|
||||
value,
|
||||
'cm-func'
|
||||
);
|
||||
|
||||
this.editor.setCursor({
|
||||
line: to.line,
|
||||
ch: to.ch - 1
|
||||
@ -167,7 +196,6 @@ export class FormulaPlugin {
|
||||
} else if (typeof value === 'string') {
|
||||
this.editor.replaceSelection(value);
|
||||
// 非变量、非函数,可能是组合模式,也需要标记
|
||||
toMark && setTimeout(() => this.autoMarkText(), 0);
|
||||
}
|
||||
|
||||
this.editor.focus();
|
||||
@ -190,88 +218,175 @@ export class FormulaPlugin {
|
||||
const text = document.createElement('span');
|
||||
text.className = className;
|
||||
text.innerText = label;
|
||||
this.editor.markText(from, to, {
|
||||
return this.editor.markText(from, to, {
|
||||
atomic: true,
|
||||
replacedWith: text
|
||||
});
|
||||
}
|
||||
|
||||
autoMark(variables: Array<VariableItem>) {
|
||||
if (!Array.isArray(variables) || !variables.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const varMap: {
|
||||
[propname: string]: string;
|
||||
} = {};
|
||||
|
||||
eachTree(variables, item => {
|
||||
if (item.value) {
|
||||
varMap[item.value] = item.path ?? item.label;
|
||||
}
|
||||
});
|
||||
|
||||
const vars = Object.keys(varMap).sort((a, b) => b.length - a.length);
|
||||
widgets: any[] = [];
|
||||
marks: any[] = [];
|
||||
autoMark() {
|
||||
const editor = this.editor;
|
||||
const lines = editor.lineCount();
|
||||
const {evalMode = true} = this.getProps();
|
||||
for (let line = 0; line < lines; line++) {
|
||||
const content = editor.getLine(line);
|
||||
const value = editor.getValue();
|
||||
const functions = this.functions;
|
||||
const variables = this.variables;
|
||||
|
||||
// 标记方法调用
|
||||
content.replace(/([A-Z]+)\s*\(/g, (_, func, pos) => {
|
||||
// 把旧的清掉
|
||||
this.widgets.forEach(widget => editor.removeLineWidget(widget));
|
||||
this.widgets = [];
|
||||
|
||||
this.marks.forEach(mark => mark.clear());
|
||||
this.marks = [];
|
||||
|
||||
try {
|
||||
const ast = parse(value, {
|
||||
evalMode: this.evalMode,
|
||||
variableMode: false
|
||||
});
|
||||
traverseAst(ast, (ast: any): any => {
|
||||
if (ast.type === 'func_call') {
|
||||
const funName = ast.identifier;
|
||||
const exists = functions.some(item =>
|
||||
item.items.some(i => i.name === funName)
|
||||
);
|
||||
if (exists) {
|
||||
this.markText(
|
||||
{
|
||||
line: line,
|
||||
ch: pos
|
||||
line: ast.start.line - 1,
|
||||
ch: ast.start.column - 1
|
||||
},
|
||||
{
|
||||
line: line,
|
||||
ch: pos + func.length
|
||||
line: ast.start.line - 1,
|
||||
ch: ast.start.column + funName.length - 1
|
||||
},
|
||||
func,
|
||||
funName,
|
||||
'cm-func'
|
||||
);
|
||||
return _;
|
||||
});
|
||||
|
||||
const REPLACE_KEY = 'AMIS_FORMULA_REPLACE_KEY';
|
||||
// 标记变量
|
||||
vars.forEach(v => {
|
||||
let from = 0;
|
||||
let idx = -1;
|
||||
|
||||
while (~(idx = content.indexOf(v, from))) {
|
||||
const encode = FormulaEditor.replaceStrByIndex(
|
||||
content,
|
||||
idx,
|
||||
v,
|
||||
REPLACE_KEY
|
||||
}
|
||||
} else if (ast.type === 'getter') {
|
||||
// 获取对象中的变量
|
||||
const list = [ast];
|
||||
let current = ast;
|
||||
while (current?.type === 'getter') {
|
||||
current = current.host;
|
||||
list.unshift(current);
|
||||
}
|
||||
const host = list.shift();
|
||||
if (host?.type === 'variable') {
|
||||
const variable = findTree(
|
||||
variables,
|
||||
item => item.value === host.name
|
||||
);
|
||||
const reg = FormulaEditor.getRegExpByMode(evalMode, REPLACE_KEY);
|
||||
|
||||
if (reg.test(encode)) {
|
||||
let markFrom = idx;
|
||||
v.split('.').forEach((val: string, index: number) => {
|
||||
if (variable) {
|
||||
// 先标记顶层对象
|
||||
this.markText(
|
||||
{
|
||||
line: line,
|
||||
ch: markFrom
|
||||
line: host.start.line - 1,
|
||||
ch: host.start.column - 1
|
||||
},
|
||||
{
|
||||
line: line,
|
||||
ch: markFrom + val.length
|
||||
line: host.end.line - 1,
|
||||
ch: host.end.column - 1
|
||||
},
|
||||
varMap[v].split('.')[index],
|
||||
variable.label,
|
||||
'cm-field'
|
||||
);
|
||||
markFrom += 1 + val.length;
|
||||
});
|
||||
}
|
||||
|
||||
from = idx + v.length;
|
||||
// 再标记子对象
|
||||
let path = host.name + '.';
|
||||
let vars = variable.children || [];
|
||||
for (let i = 0, len = list.length; i < len; i++) {
|
||||
const item = list[i]?.key;
|
||||
|
||||
// 只能识别这种固定下标的情况
|
||||
if (item?.type === 'identifier') {
|
||||
const variable =
|
||||
findTree(vars, v => v.value === path + item.name) ??
|
||||
findTree(
|
||||
vars,
|
||||
v => v.value === item.name // 兼容不带路径的情况
|
||||
);
|
||||
if (variable) {
|
||||
this.markText(
|
||||
{
|
||||
line: item.start.line - 1,
|
||||
ch: item.start.column - 1
|
||||
},
|
||||
{
|
||||
line: item.end.line - 1,
|
||||
ch: item.end.column - 1
|
||||
},
|
||||
variable.label,
|
||||
'cm-field'
|
||||
);
|
||||
path += item.name + '.';
|
||||
vars = variable.children || [];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (ast.type === 'variable') {
|
||||
// 直接就是变量
|
||||
const variable = findTree(variables, item => item.value === ast.name);
|
||||
if (variable) {
|
||||
this.markText(
|
||||
{
|
||||
line: ast.start.line - 1,
|
||||
ch: ast.start.column - 1
|
||||
},
|
||||
{
|
||||
line: ast.end.line - 1,
|
||||
ch: ast.end.column - 1
|
||||
},
|
||||
variable.label,
|
||||
'cm-field'
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
const reg = /^Unexpected\stoken\s(.+)\sin\s(\d+):(\d+)$/.exec(e.message);
|
||||
if (reg) {
|
||||
const token = reg[1];
|
||||
const line = parseInt(reg[2], 10);
|
||||
const column = parseInt(reg[3], 10);
|
||||
const msg = document.createElement('div');
|
||||
const icon = msg.appendChild(document.createElement('span'));
|
||||
icon.innerText = '!!';
|
||||
icon.className = 'lint-error-icon';
|
||||
msg.appendChild(
|
||||
document.createTextNode(`Unexpected token \`${token}\``)
|
||||
);
|
||||
msg.className = 'lint-error';
|
||||
this.widgets.push(
|
||||
editor.addLineWidget(line - 1, msg, {
|
||||
coverGutter: false,
|
||||
noHScroll: true
|
||||
})
|
||||
);
|
||||
|
||||
this.marks.push(
|
||||
this.markText(
|
||||
{
|
||||
line: line - 1,
|
||||
ch: column - 1
|
||||
},
|
||||
{
|
||||
line: line - 1,
|
||||
ch: column + token.length - 1
|
||||
},
|
||||
token,
|
||||
'cm-error-token'
|
||||
)
|
||||
);
|
||||
}
|
||||
console.warn('synax error, ignore it');
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,7 +398,9 @@ export class FormulaPlugin {
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {}
|
||||
dispose() {
|
||||
(this.autoMarkText as any).cancel();
|
||||
}
|
||||
|
||||
validate() {}
|
||||
}
|
||||
@ -295,6 +412,8 @@ function registerLaunguageMode(cm: typeof CodeMirror) {
|
||||
}
|
||||
modeRegisted = true;
|
||||
|
||||
// TODO 自定义语言规则
|
||||
|
||||
// 对应 evalMode
|
||||
cm.defineMode('formula', (config: any, parserConfig: any) => {
|
||||
var formula = cm.getMode(config, 'javascript');
|
||||
@ -306,7 +425,6 @@ function registerLaunguageMode(cm: typeof CodeMirror) {
|
||||
mode: formula
|
||||
});
|
||||
});
|
||||
|
||||
cm.defineMIME('text/formula', {name: 'formula'});
|
||||
cm.defineMIME('text/formula-template', {name: 'formula', base: 'htmlmixed'});
|
||||
}
|
||||
|
@ -74,7 +74,8 @@ import SchemaVariableList from './schema-editor/SchemaVariableList';
|
||||
import VariableList from './formula/VariableList';
|
||||
import FormulaPicker from './formula/Picker';
|
||||
import {FormulaEditor} from './formula/Editor';
|
||||
import type {VariableItem} from './formula/Editor';
|
||||
import FormulaCodeEditor from './formula/CodeEditor';
|
||||
import type {VariableItem} from './formula/CodeEditor';
|
||||
import PickerContainer from './PickerContainer';
|
||||
import InputJSONSchema from './json-schema';
|
||||
import {Badge, withBadge} from './Badge';
|
||||
@ -202,6 +203,7 @@ export {
|
||||
PickerContainer,
|
||||
ConfirmBox,
|
||||
FormulaPicker,
|
||||
FormulaCodeEditor,
|
||||
VariableItem,
|
||||
FormulaEditor,
|
||||
InputJSONSchema,
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
themeable,
|
||||
ThemeProps
|
||||
} from 'amis-core';
|
||||
import {VariableItem} from '../formula/Editor';
|
||||
import {VariableItem} from '../formula/CodeEditor';
|
||||
import VariableList from '../formula/VariableList';
|
||||
import TooltipWrapper from '../TooltipWrapper';
|
||||
|
||||
|
@ -393,6 +393,11 @@ register('de-DE', {
|
||||
'expand': 'Entfalten',
|
||||
'FormulaEditor.btnLabel': 'Formel Bearbeiten',
|
||||
'FormulaEditor.title': 'Formel Editor',
|
||||
'FormulaEditor.run': 'Laufen',
|
||||
'FormulaEditor.sourceMode': 'Source Mode',
|
||||
'FormulaEditor.runContext': 'Run Context',
|
||||
'FormulaEditor.runResult': 'Run Result',
|
||||
'FormulaEditor.toggleAll': 'Expand All',
|
||||
'FormulaEditor.variable': 'Variable',
|
||||
'FormulaEditor.function': 'Funktion',
|
||||
'FormulaEditor.invalidData':
|
||||
|
@ -377,6 +377,11 @@ register('en-US', {
|
||||
'expand': 'Expand',
|
||||
'FormulaEditor.btnLabel': 'Formula Edit',
|
||||
'FormulaEditor.title': 'Formula Editor',
|
||||
'FormulaEditor.run': 'Run',
|
||||
'FormulaEditor.sourceMode': 'Source Mode',
|
||||
'FormulaEditor.runContext': 'Run Context',
|
||||
'FormulaEditor.runResult': 'Run Result',
|
||||
'FormulaEditor.toggleAll': 'Expand All',
|
||||
'FormulaEditor.variable': 'Variable',
|
||||
'FormulaEditor.function': 'Function',
|
||||
'FormulaEditor.invalidData': 'invalid data, position or reason is {{err}}',
|
||||
|
@ -370,6 +370,11 @@ register('zh-CN', {
|
||||
'expand': '展开',
|
||||
'FormulaEditor.btnLabel': '公式编辑',
|
||||
'FormulaEditor.title': '公式编辑器',
|
||||
'FormulaEditor.run': '运行',
|
||||
'FormulaEditor.sourceMode': '源码模式',
|
||||
'FormulaEditor.runContext': '上下文数据',
|
||||
'FormulaEditor.runResult': '运行结果',
|
||||
'FormulaEditor.toggleAll': '展开全部',
|
||||
'FormulaEditor.variable': '变量',
|
||||
'FormulaEditor.function': '函数',
|
||||
'FormulaEditor.invalidData': '公式值校验错误,错误的位置/原因是 {{err}}',
|
||||
|
@ -131,3 +131,13 @@ Object.defineProperty(global, 'IntersectionObserver', {
|
||||
configurable: true,
|
||||
value: IntersectionObserver
|
||||
});
|
||||
|
||||
(global as any).document.createRange = () => ({
|
||||
selectNodeContents: jest.fn(),
|
||||
getBoundingClientRect: jest.fn(() => ({
|
||||
width: 500
|
||||
})),
|
||||
getClientRects: jest.fn(() => []),
|
||||
setStart: jest.fn(),
|
||||
setEnd: jest.fn()
|
||||
});
|
||||
|
@ -431,23 +431,143 @@ exports[`Renderer:input-formula input-group 1`] = `
|
||||
class="cxd-FormulaPicker is-input-group cxd-FormulaPicker--text cxd-Form-control"
|
||||
>
|
||||
<div
|
||||
class="cxd-ResultBox cxd-FormulaPicker-input cxd-ResultBox--borderFull"
|
||||
tabindex="-1"
|
||||
class="cxd-InputBox cxd-FormulaPicker-input cxd-InputBox--borderFull"
|
||||
>
|
||||
<div
|
||||
class="cxd-ResultBox-value-wrap"
|
||||
class="cxd-FormulaCodeEditor cxd-FormulaCodeEditor--singleLine"
|
||||
style="position: relative;"
|
||||
>
|
||||
<input
|
||||
class="cxd-ResultBox-value-input"
|
||||
placeholder="暂无数据"
|
||||
theme="cxd"
|
||||
type="text"
|
||||
value="SUM(1 + 2)"
|
||||
<div
|
||||
class="CodeMirror cm-s-default"
|
||||
translate="no"
|
||||
>
|
||||
<div
|
||||
style="overflow: hidden; position: relative; width: 3px; height: 0px;"
|
||||
>
|
||||
<textarea
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
style="position: absolute; bottom: -1em; padding: 0px; width: 1000px; height: 1em; min-height: 1em; outline: none;"
|
||||
tabindex="0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="cxd-ResultBox-actions"
|
||||
class="CodeMirror-scrollbar-filler"
|
||||
cm-not-content="true"
|
||||
/>
|
||||
<div
|
||||
class="CodeMirror-gutter-filler"
|
||||
cm-not-content="true"
|
||||
/>
|
||||
<div
|
||||
class="CodeMirror-scroll"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div
|
||||
class="CodeMirror-sizer"
|
||||
style="margin-left: 0px; min-width: 3px;"
|
||||
>
|
||||
<div
|
||||
style="position: relative;"
|
||||
>
|
||||
<div
|
||||
class="CodeMirror-lines"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
role="presentation"
|
||||
style="position: relative; outline: none;"
|
||||
>
|
||||
<div
|
||||
class="CodeMirror-measure"
|
||||
/>
|
||||
<div
|
||||
class="CodeMirror-measure"
|
||||
>
|
||||
<pre
|
||||
class="CodeMirror-line"
|
||||
role="presentation"
|
||||
>
|
||||
<span
|
||||
role="presentation"
|
||||
style="padding-right: .1px;"
|
||||
>
|
||||
SUM(1 + 2)
|
||||
</span>
|
||||
</pre>
|
||||
</div>
|
||||
<div
|
||||
style="position: relative; z-index: 1;"
|
||||
/>
|
||||
<div
|
||||
class="CodeMirror-cursors"
|
||||
/>
|
||||
<div
|
||||
class="CodeMirror-code"
|
||||
role="presentation"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="position: absolute; height: 50px; width: 1px;"
|
||||
/>
|
||||
<div
|
||||
class="CodeMirror-gutters"
|
||||
style="display: none;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="resize-sensor"
|
||||
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-expand"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
style="position: absolute; left: 0px; top: 0px; width: 10px; height: 10px;"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-shrink"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||
>
|
||||
|
||||
|
||||
<div
|
||||
style="position: absolute; left: 0; top: 0; width: 200%; height: 200%"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div
|
||||
class="resize-sensor-appear"
|
||||
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;animation-name: apearSensor; animation-duration: 0.2s;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
class="cxd-InputBox-clear"
|
||||
>
|
||||
<icon-mock
|
||||
classname="icon icon-input-clear"
|
||||
icon="input-clear"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
class="cxd-FormulaPicker-toggler"
|
||||
|
@ -14,11 +14,17 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {fireEvent, render, screen, cleanup, waitFor} from '@testing-library/react';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
cleanup,
|
||||
waitFor
|
||||
} from '@testing-library/react';
|
||||
import '../../../src';
|
||||
import {render as amisRender, clearStoresCache} from '../../../src';
|
||||
import {makeEnv, replaceReactAriaIds, wait} from '../../helper';
|
||||
import { Select } from 'packages/amis-ui/lib/components/Select';
|
||||
import {Select} from 'packages/amis-ui/lib/components/Select';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
@ -818,218 +824,248 @@ test('Renderer:condition-builder with not embed', async () => {
|
||||
describe('Renderer: condition-builder with formula', () => {
|
||||
const onSubmit = jest.fn();
|
||||
test('condition-builder with different fields', async () => {
|
||||
const {container} = render(amisRender({
|
||||
"type": "form",
|
||||
"data": {
|
||||
"conditions": {
|
||||
"id": "68bddc1495e9",
|
||||
"conjunction": "and",
|
||||
"children": [
|
||||
const {container} = render(
|
||||
amisRender(
|
||||
{
|
||||
"id": "b9cc34dae93a",
|
||||
"left": {
|
||||
"type": "field",
|
||||
"field": "text"
|
||||
type: 'form',
|
||||
data: {
|
||||
conditions: {
|
||||
id: '68bddc1495e9',
|
||||
conjunction: 'and',
|
||||
children: [
|
||||
{
|
||||
id: 'b9cc34dae93a',
|
||||
left: {
|
||||
type: 'field',
|
||||
field: 'text'
|
||||
},
|
||||
"op": "equal"
|
||||
op: 'equal'
|
||||
},
|
||||
{
|
||||
"id": "4c718986c321",
|
||||
"left": {
|
||||
"type": "field",
|
||||
"field": "number"
|
||||
id: '4c718986c321',
|
||||
left: {
|
||||
type: 'field',
|
||||
field: 'number'
|
||||
},
|
||||
"op": "equal"
|
||||
op: 'equal'
|
||||
},
|
||||
{
|
||||
"id": "7ee79c416422",
|
||||
"left": {
|
||||
"type": "field",
|
||||
"field": "boolean"
|
||||
id: '7ee79c416422',
|
||||
left: {
|
||||
type: 'field',
|
||||
field: 'boolean'
|
||||
},
|
||||
"op": "equal"
|
||||
op: 'equal'
|
||||
},
|
||||
{
|
||||
"id": "9cd76d8a6522",
|
||||
"left": {
|
||||
"type": "field",
|
||||
"field": "select"
|
||||
id: '9cd76d8a6522',
|
||||
left: {
|
||||
type: 'field',
|
||||
field: 'select'
|
||||
},
|
||||
"op": "select_equals"
|
||||
op: 'select_equals'
|
||||
},
|
||||
{
|
||||
"id": "20a65e9df546",
|
||||
"left": {
|
||||
"type": "field",
|
||||
"field": "date"
|
||||
id: '20a65e9df546',
|
||||
left: {
|
||||
type: 'field',
|
||||
field: 'date'
|
||||
},
|
||||
"op": "equal"
|
||||
op: 'equal'
|
||||
},
|
||||
{
|
||||
"id": "e729b32ea9e8",
|
||||
"left": {
|
||||
"type": "field",
|
||||
"field": "time"
|
||||
id: 'e729b32ea9e8',
|
||||
left: {
|
||||
type: 'field',
|
||||
field: 'time'
|
||||
},
|
||||
"op": "equal"
|
||||
op: 'equal'
|
||||
},
|
||||
{
|
||||
"id": "a5f48e000557",
|
||||
"left": {
|
||||
"type": "field",
|
||||
"field": "datetime"
|
||||
id: 'a5f48e000557',
|
||||
left: {
|
||||
type: 'field',
|
||||
field: 'datetime'
|
||||
},
|
||||
"op": "equal"
|
||||
op: 'equal'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"body": [
|
||||
body: [
|
||||
{
|
||||
"type": "condition-builder",
|
||||
"label": "条件组件",
|
||||
"name": "conditions",
|
||||
"searchable": true,
|
||||
"formula": {
|
||||
"mode": "input-group",
|
||||
"inputSettings": {},
|
||||
"allowInput": true,
|
||||
"mixedMode": true,
|
||||
"variables": []
|
||||
type: 'condition-builder',
|
||||
label: '条件组件',
|
||||
name: 'conditions',
|
||||
searchable: true,
|
||||
formula: {
|
||||
mode: 'input-group',
|
||||
inputSettings: {},
|
||||
allowInput: true,
|
||||
mixedMode: true,
|
||||
variables: []
|
||||
},
|
||||
"fields": [
|
||||
fields: [
|
||||
{
|
||||
"label": "文本",
|
||||
"type": "text",
|
||||
"name": "text"
|
||||
label: '文本',
|
||||
type: 'text',
|
||||
name: 'text'
|
||||
},
|
||||
{
|
||||
"label": "数字",
|
||||
"type": "number",
|
||||
"name": "number"
|
||||
label: '数字',
|
||||
type: 'number',
|
||||
name: 'number'
|
||||
},
|
||||
{
|
||||
"label": "布尔",
|
||||
"type": "boolean",
|
||||
"name": "boolean"
|
||||
label: '布尔',
|
||||
type: 'boolean',
|
||||
name: 'boolean'
|
||||
},
|
||||
{
|
||||
"label": "选项",
|
||||
"type": "select",
|
||||
"name": "select",
|
||||
"options": [
|
||||
label: '选项',
|
||||
type: 'select',
|
||||
name: 'select',
|
||||
options: [
|
||||
{
|
||||
"label": "A",
|
||||
"value": "a"
|
||||
label: 'A',
|
||||
value: 'a'
|
||||
},
|
||||
{
|
||||
"label": "B",
|
||||
"value": "b"
|
||||
label: 'B',
|
||||
value: 'b'
|
||||
},
|
||||
{
|
||||
"label": "C",
|
||||
"value": "c"
|
||||
label: 'C',
|
||||
value: 'c'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "日期",
|
||||
"children": [
|
||||
label: '日期',
|
||||
children: [
|
||||
{
|
||||
"label": "日期",
|
||||
"type": "date",
|
||||
"name": "date"
|
||||
label: '日期',
|
||||
type: 'date',
|
||||
name: 'date'
|
||||
},
|
||||
{
|
||||
"label": "时间",
|
||||
"type": "time",
|
||||
"name": "time"
|
||||
label: '时间',
|
||||
type: 'time',
|
||||
name: 'time'
|
||||
},
|
||||
{
|
||||
"label": "日期时间",
|
||||
"type": "datetime",
|
||||
"name": "datetime"
|
||||
label: '日期时间',
|
||||
type: 'datetime',
|
||||
name: 'datetime'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}, {onSubmit}, makeEnv({})));
|
||||
},
|
||||
{onSubmit},
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
|
||||
replaceReactAriaIds(container);
|
||||
// 7种类型都存在
|
||||
expect(container.querySelectorAll('.cxd-FormulaPicker-input')?.length).toEqual(7);
|
||||
expect(container.querySelector('.cxd-FormulaPicker--text')).toBeInTheDocument();
|
||||
expect(container.querySelector('.cxd-FormulaPicker-input-number')).toBeInTheDocument();
|
||||
expect(container.querySelector('.cxd-FormulaPicker-input-boolean')).toBeInTheDocument();
|
||||
expect(container.querySelector('.cxd-FormulaPicker-input-select')).toBeInTheDocument();
|
||||
expect(container.querySelector('.cxd-FormulaPicker-input-date')).toBeInTheDocument();
|
||||
expect(container.querySelector('.cxd-FormulaPicker-input-time')).toBeInTheDocument();
|
||||
expect(container.querySelector('.cxd-FormulaPicker-input-datetime')).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelectorAll('.cxd-FormulaPicker-input')?.length
|
||||
).toEqual(7);
|
||||
expect(
|
||||
container.querySelector('.cxd-FormulaPicker--text')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector('.cxd-FormulaPicker-input-number')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector('.cxd-FormulaPicker-input-boolean')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector('.cxd-FormulaPicker-input-select')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector('.cxd-FormulaPicker-input-date')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector('.cxd-FormulaPicker-input-time')
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
container.querySelector('.cxd-FormulaPicker-input-datetime')
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('condition-builder with select field and change operator', async () => {
|
||||
const {container, findByText} = render(amisRender({
|
||||
"type": "form",
|
||||
"data": {
|
||||
"conditions": {
|
||||
"id": "68bddc1495e9",
|
||||
"conjunction": "and",
|
||||
"children": [
|
||||
const {container, findByText} = render(
|
||||
amisRender(
|
||||
{
|
||||
"id": "9cd76d8a6522",
|
||||
"left": {
|
||||
"type": "field",
|
||||
"field": "select"
|
||||
type: 'form',
|
||||
data: {
|
||||
conditions: {
|
||||
id: '68bddc1495e9',
|
||||
conjunction: 'and',
|
||||
children: [
|
||||
{
|
||||
id: '9cd76d8a6522',
|
||||
left: {
|
||||
type: 'field',
|
||||
field: 'select'
|
||||
},
|
||||
"op": "select_equals"
|
||||
op: 'select_equals'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"body": [
|
||||
body: [
|
||||
{
|
||||
"type": "condition-builder",
|
||||
"label": "条件组件",
|
||||
"name": "conditions",
|
||||
"searchable": true,
|
||||
"formula": {
|
||||
"mode": "input-group",
|
||||
"inputSettings": {},
|
||||
"allowInput": true,
|
||||
"mixedMode": true,
|
||||
"variables": []
|
||||
type: 'condition-builder',
|
||||
label: '条件组件',
|
||||
name: 'conditions',
|
||||
searchable: true,
|
||||
formula: {
|
||||
mode: 'input-group',
|
||||
inputSettings: {},
|
||||
allowInput: true,
|
||||
mixedMode: true,
|
||||
variables: []
|
||||
},
|
||||
"fields": [
|
||||
fields: [
|
||||
{
|
||||
"label": "选项",
|
||||
"type": "select",
|
||||
"name": "select",
|
||||
"options": [
|
||||
label: '选项',
|
||||
type: 'select',
|
||||
name: 'select',
|
||||
options: [
|
||||
{
|
||||
"label": "A",
|
||||
"value": "a"
|
||||
label: 'A',
|
||||
value: 'a'
|
||||
},
|
||||
{
|
||||
"label": "B",
|
||||
"value": "b"
|
||||
label: 'B',
|
||||
value: 'b'
|
||||
},
|
||||
{
|
||||
"label": "C",
|
||||
"value": "c"
|
||||
label: 'C',
|
||||
value: 'c'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}, {}, makeEnv({})));
|
||||
},
|
||||
{},
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
|
||||
replaceReactAriaIds(container);
|
||||
|
||||
// 选中第一个选项(Form中默认值是等于操作)
|
||||
let fieldValueControl = container.querySelector('.cxd-FormulaPicker-input-select')!;
|
||||
let fieldValueControl = container.querySelector(
|
||||
'.cxd-FormulaPicker-input-select'
|
||||
)!;
|
||||
fireEvent.click(fieldValueControl);
|
||||
await wait(100);
|
||||
fireEvent.click(await findByText('A'));
|
||||
@ -1041,85 +1077,101 @@ describe('Renderer: condition-builder with formula', () => {
|
||||
await wait(100);
|
||||
fireEvent.click(await findByText('包含'));
|
||||
await wait(100);
|
||||
expect(container.querySelector('.cxd-Select-placeholder')).toBeInTheDocument();
|
||||
fieldValueControl = container.querySelector('.cxd-FormulaPicker-input-select')!;
|
||||
expect(
|
||||
container.querySelector('.cxd-Select-placeholder')
|
||||
).toBeInTheDocument();
|
||||
fieldValueControl = container.querySelector(
|
||||
'.cxd-FormulaPicker-input-select'
|
||||
)!;
|
||||
fireEvent.click(fieldValueControl);
|
||||
await wait(100);
|
||||
expect(container.querySelectorAll('.cxd-Select-option-checkbox').length).toEqual(3);
|
||||
expect(
|
||||
container.querySelectorAll('.cxd-Select-option-checkbox').length
|
||||
).toEqual(3);
|
||||
});
|
||||
|
||||
test('condition-builder with field type change', async () => {
|
||||
const onSubmit = jest.fn();
|
||||
const {container, findByText, findByPlaceholderText} = render(amisRender({
|
||||
"type": "form",
|
||||
"data": {
|
||||
"conditions": {
|
||||
"id": "68bddc1495e9",
|
||||
"conjunction": "and",
|
||||
"children": [
|
||||
const {container, findByText, findByPlaceholderText} = render(
|
||||
amisRender(
|
||||
{
|
||||
"id": "b9cc34dae93a",
|
||||
"left": {
|
||||
"type": "field",
|
||||
"field": "text"
|
||||
type: 'form',
|
||||
data: {
|
||||
conditions: {
|
||||
id: '68bddc1495e9',
|
||||
conjunction: 'and',
|
||||
children: [
|
||||
{
|
||||
id: 'b9cc34dae93a',
|
||||
left: {
|
||||
type: 'field',
|
||||
field: 'text'
|
||||
},
|
||||
"op": "equal"
|
||||
op: 'equal'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"body": [
|
||||
body: [
|
||||
{
|
||||
"type": "condition-builder",
|
||||
"label": "条件组件",
|
||||
"name": "conditions",
|
||||
"searchable": true,
|
||||
"formula": {
|
||||
"mode": "input-group",
|
||||
"inputSettings": {},
|
||||
"allowInput": true,
|
||||
"mixedMode": true,
|
||||
"variables": []
|
||||
type: 'condition-builder',
|
||||
label: '条件组件',
|
||||
name: 'conditions',
|
||||
searchable: true,
|
||||
formula: {
|
||||
mode: 'input-group',
|
||||
inputSettings: {},
|
||||
allowInput: true,
|
||||
mixedMode: true,
|
||||
variables: []
|
||||
},
|
||||
"fields": [
|
||||
fields: [
|
||||
{
|
||||
"label": "文本",
|
||||
"type": "text",
|
||||
"name": "text"
|
||||
label: '文本',
|
||||
type: 'text',
|
||||
name: 'text'
|
||||
},
|
||||
{
|
||||
"label": "选项",
|
||||
"type": "select",
|
||||
"name": "select",
|
||||
"options": [
|
||||
label: '选项',
|
||||
type: 'select',
|
||||
name: 'select',
|
||||
options: [
|
||||
{
|
||||
"label": "A",
|
||||
"value": "a"
|
||||
label: 'A',
|
||||
value: 'a'
|
||||
},
|
||||
{
|
||||
"label": "B",
|
||||
"value": "b"
|
||||
label: 'B',
|
||||
value: 'b'
|
||||
},
|
||||
{
|
||||
"label": "C",
|
||||
"value": "c"
|
||||
label: 'C',
|
||||
value: 'c'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}, {onSubmit}, makeEnv({})));
|
||||
},
|
||||
{onSubmit},
|
||||
makeEnv({})
|
||||
)
|
||||
);
|
||||
|
||||
replaceReactAriaIds(container);
|
||||
|
||||
// 切换字段类型,对应字段值控件更新
|
||||
const fieldControl = container.querySelector('.cxd-DropDownSelection-input')!;
|
||||
const fieldControl = container.querySelector(
|
||||
'.cxd-DropDownSelection-input'
|
||||
)!;
|
||||
fireEvent.click(fieldControl);
|
||||
await wait(100);
|
||||
fireEvent.click(await findByText('选项'));
|
||||
await wait(100);
|
||||
let selectValueControl = container.querySelector('.cxd-FormulaPicker-input-select')!;
|
||||
let selectValueControl = container.querySelector(
|
||||
'.cxd-FormulaPicker-input-select'
|
||||
)!;
|
||||
expect(selectValueControl).toBeInTheDocument();
|
||||
|
||||
// 切换操作符,下拉选项变成多选
|
||||
@ -1128,11 +1180,17 @@ describe('Renderer: condition-builder with formula', () => {
|
||||
await wait(100);
|
||||
fireEvent.click(await findByText('包含'));
|
||||
await wait(100);
|
||||
expect(container.querySelector('.cxd-Select-placeholder')).toBeInTheDocument();
|
||||
selectValueControl = container.querySelector('.cxd-FormulaPicker-input-select')!;
|
||||
expect(
|
||||
container.querySelector('.cxd-Select-placeholder')
|
||||
).toBeInTheDocument();
|
||||
selectValueControl = container.querySelector(
|
||||
'.cxd-FormulaPicker-input-select'
|
||||
)!;
|
||||
fireEvent.click(selectValueControl);
|
||||
await wait(100);
|
||||
expect(container.querySelectorAll('.cxd-Select-option-checkbox').length).toEqual(3);
|
||||
expect(
|
||||
container.querySelectorAll('.cxd-Select-option-checkbox').length
|
||||
).toEqual(3);
|
||||
|
||||
// 选择2个选项,绑定值变化
|
||||
fireEvent.click(await findByText('A'));
|
||||
@ -1145,4 +1203,4 @@ describe('Renderer: condition-builder with formula', () => {
|
||||
expect(selectedValues.length).toEqual(2);
|
||||
expect(selectedValues.join(',')).toEqual('A,C');
|
||||
});
|
||||
})
|
||||
});
|
||||
|
@ -78,9 +78,9 @@ test('Renderer:input-formula', async () => {
|
||||
)
|
||||
);
|
||||
|
||||
// await wait(500);
|
||||
await wait(500);
|
||||
|
||||
await findByDisplayValue('SUM(1 + 2)');
|
||||
// await findByDisplayValue('SUM(1 + 2)');
|
||||
|
||||
// TODO: 不知道为啥切换到 @swc/jest 后不支持
|
||||
// expect(container).toMatchSnapshot();
|
||||
|
@ -8,7 +8,7 @@ import {isPureVariable, resolveVariableAndFilter} from 'amis-core';
|
||||
import type {
|
||||
FuncGroup,
|
||||
VariableItem
|
||||
} from 'amis-ui/lib/components/formula/Editor';
|
||||
} from 'amis-ui/src/components/formula/CodeEditor';
|
||||
import type {FormulaPickerInputSettings} from 'amis-ui/lib/components/formula/Picker';
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user