Pick formula 更新、条件组合支持自定义、弹窗错误信息优化 (#9875)

* chore: 调整弹窗错误提示逻辑,显示后 3 秒消失 (#9868)

* chore: 公式编辑器 input 换成 codemirror 提升交互体验 (#9821)

* style: 调整公式部分样式

* feat: 条件组合与公式结合时支持自定义输入框类型 (#9871)
This commit is contained in:
liaoxuezhi 2024-03-26 09:48:54 +08:00 committed by GitHub
parent 7e03085faf
commit 0fa156f9a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 1789 additions and 997 deletions

View File

@ -102,8 +102,8 @@ order: 21
"name": "formula", "name": "formula",
"label": "公式", "label": "公式",
"variableMode": "tree", "variableMode": "tree",
"evalMode": false, "evalMode": true,
"value": "${SUM(1 , 2)}", "value": "SUM(1 , 2)",
"inputMode": "button", "inputMode": "button",
"variables": [ "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 时则为模板模式,意思是说默认不当做表达式,只有 `${`和`}`包裹的部分才是表达式。 当配置 `evalMode` 为 false 时则为模板模式,意思是说默认不当做表达式,只有 `${`和`}`包裹的部分才是表达式。

View File

@ -59,9 +59,12 @@ export function StatusScoped<
} = { } = {
statusStore: this.store! statusStore: this.store!
}; };
const refConfig = ComposedComponent.prototype?.isReactComponent const refConfig =
? {ref: this.childRef} ComposedComponent.prototype?.isReactComponent ||
: {forwardedRef: this.childRef}; (ComposedComponent as any).$$typeof ===
Symbol.for('react.forward_ref')
? {ref: this.childRef}
: {forwardedRef: this.childRef};
return ( return (
<ComposedComponent <ComposedComponent

View File

@ -162,9 +162,12 @@ export function localeable<
translate: translate! translate: translate!
}; };
moment.locale(momentLocaleMap?.[locale] ?? locale); moment.locale(momentLocaleMap?.[locale] ?? locale);
const refConfig = ComposedComponent.prototype?.isReactComponent const refConfig =
? {ref: this.childRef} ComposedComponent.prototype?.isReactComponent ||
: {forwardedRef: this.childRef}; (ComposedComponent as any).$$typeof ===
Symbol.for('react.forward_ref')
? {ref: this.childRef}
: {forwardedRef: this.childRef};
const body = ( const body = (
<ComposedComponent <ComposedComponent

View File

@ -182,9 +182,12 @@ export function themeable<
classnames: config.classnames, classnames: config.classnames,
theme theme
}; };
const refConfig = ComposedComponent.prototype?.isReactComponent const refConfig =
? {ref: this.childRef} ComposedComponent.prototype?.isReactComponent ||
: {forwardedRef: this.childRef}; (ComposedComponent as any).$$typeof ===
Symbol.for('react.forward_ref')
? {ref: this.childRef}
: {forwardedRef: this.childRef};
const body = ( const body = (
<ComposedComponent <ComposedComponent

View File

@ -212,7 +212,7 @@ export function isExpression(expression: any): boolean {
// 备注1: "\\${xxx}"不作为表达式,至少含一个${xxx}才算是表达式 // 备注1: "\\${xxx}"不作为表达式,至少含一个${xxx}才算是表达式
// 备注2: safari 不支持 /(?<!\\)(\${).+(\})/.test(expression) // 备注2: safari 不支持 /(?<!\\)(\${).+(\})/.test(expression)
return /(^|[^\\])\$\{.+\}/.test(expression); return /(^|[^\\])\$\{[\s\S]+\}/.test(expression);
} }
// 用于判断是否需要执行表达式: // 用于判断是否需要执行表达式:

View File

@ -38,4 +38,6 @@
} }
.btn-configured-tooltip { .btn-configured-tooltip {
font-size: 12px; font-size: 12px;
min-width: 320px;
min-height: 320px;
} }

View File

@ -33,9 +33,9 @@
& > .CodeMirror { & > .CodeMirror {
height: 100%; height: 100%;
font-family: inherit; font-family: inherit;
span[class^='cm-'] { // span[class^='cm-'] {
color: var(--input-default-default-color); // color: var(--input-default-default-color);
} // }
// 解决上下 pre标签中表达式浮层遮挡问题 // 解决上下 pre标签中表达式浮层遮挡问题
.CodeMirror-measure + div { .CodeMirror-measure + div {

View File

@ -48,9 +48,9 @@
color: var(--Form-input-color); color: var(--Form-input-color);
font-family: inherit; font-family: inherit;
span[class^='cm-'] { // span[class^='cm-'] {
color: var(--input-default-default-color); // color: var(--input-default-default-color);
} // }
// 解决上下 pre标签中表达式浮层遮挡问题 // 解决上下 pre标签中表达式浮层遮挡问题
.CodeMirror-measure + div { .CodeMirror-measure + div {

View File

@ -890,7 +890,7 @@ export class BaseCRUDPlugin extends BasePlugin {
step: 10, step: 10,
min: 1000 min: 1000
}, },
getSchemaTpl('tplFormulaControl', { getSchemaTpl('expressionFormulaControl', {
name: 'stopAutoRefreshWhen', name: 'stopAutoRefreshWhen',
label: tipedLabel( label: tipedLabel(
'停止条件', '停止条件',

View File

@ -6,9 +6,8 @@ import React from 'react';
import {autobind, FormControlProps} from 'amis-core'; import {autobind, FormControlProps} from 'amis-core';
import cx from 'classnames'; import cx from 'classnames';
import {FormItem, Button, Icon, PickerContainer} from 'amis'; 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 type {VariableItem} from 'amis-ui';
import {renderFormulaValue} from './FormulaControl';
import {reaction} from 'mobx'; import {reaction} from 'mobx';
import {getVariables} from 'amis-editor-core'; import {getVariables} from 'amis-editor-core';
@ -76,6 +75,12 @@ export default class ExpressionFormulaControl extends React.Component<
this.appCorpusData = editorStore?.appCorpusData; this.appCorpusData = editorStore?.appCorpusData;
} }
); );
// 要高亮,初始就要加载
const variablesArr = await getVariables(this);
this.setState({
variables: variablesArr
});
} }
async componentDidUpdate(prevProps: ExpressionFormulaControlProps) { 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 {value, className, variableMode, header, size, ...rest} = this.props;
const {formulaPickerValue, variables} = this.state; const {formulaPickerValue, variables} = this.state;
const highlightValue = FormulaEditor.highlightValue(
formulaPickerValue,
variables
) || {
html: formulaPickerValue
};
// 自身字段 // 自身字段
const selfName = this.props?.data?.name; const selfName = this.props?.data?.name;
return ( return (
@ -180,11 +178,25 @@ export default class ExpressionFormulaControl extends React.Component<
mouseLeaveDelay: 20, mouseLeaveDelay: 20,
content: value, content: value,
tooltipClassName: 'btn-configured-tooltip', tooltipClassName: 'btn-configured-tooltip',
children: () => renderFormulaValue(highlightValue) children: () => (
<FormulaCodeEditor
readOnly
value={value}
variables={variables}
evalMode={false}
editorTheme="dark"
/>
)
}} }}
onClick={e => this.handleOnClick(e, onClick)} onClick={e => this.handleOnClick(e, onClick)}
> >
{renderFormulaValue(highlightValue)} <FormulaCodeEditor
singleLine
readOnly
value={value}
variables={variables}
evalMode={false}
/>
<Icon <Icon
icon="input-clear" icon="input-clear"
className="icon" className="icon"

View File

@ -19,7 +19,7 @@ import {
TooltipWrapper TooltipWrapper
} from 'amis'; } from 'amis';
import {FormulaExec, isExpression} from 'amis'; import {FormulaExec, isExpression} from 'amis';
import {FormulaEditor} from 'amis-ui'; import {FormulaCodeEditor, FormulaEditor} from 'amis-ui';
import FormulaPicker, { import FormulaPicker, {
CustomFormulaPickerProps CustomFormulaPickerProps
@ -29,7 +29,7 @@ import {JSONPipeOut, autobind, translateSchema} from 'amis-editor-core';
import type { import type {
VariableItem, VariableItem,
FuncGroup FuncGroup
} from 'amis-ui/lib/components/formula/Editor'; } from 'amis-ui/lib/components/formula/CodeEditor';
import {FormControlProps} from 'amis-core'; import {FormControlProps} from 'amis-core';
import type {BaseEventContext} from 'amis-editor-core'; import type {BaseEventContext} from 'amis-editor-core';
import {EditorManager} from 'amis-editor-core'; import {EditorManager} from 'amis-editor-core';
@ -42,6 +42,11 @@ export enum FormulaDateType {
IsRange // 日期时间范围类 IsRange // 日期时间范围类
} }
/**
* @deprecated codemirror
* @param item
* @returns
*/
export function renderFormulaValue(item: any) { export function renderFormulaValue(item: any) {
const html = {__html: typeof item === 'string' ? item : item?.html}; const html = {__html: typeof item === 'string' ? item : item?.html};
// bca-disable-next-line // bca-disable-next-line
@ -189,6 +194,9 @@ export default class FormulaControl extends React.Component<
this.appCorpusData = editorStore?.appCorpusData; this.appCorpusData = editorStore?.appCorpusData;
} }
); );
const variables = await getVariables(this);
this.setState({variables});
} }
componentWillUnmount() { componentWillUnmount() {
@ -346,8 +354,8 @@ export default class FormulaControl extends React.Component<
transExpr(str: string) { transExpr(str: string) {
if ( if (
typeof str === 'string' && typeof str === 'string' &&
str?.slice(0, 2) === '${' && str.slice(0, 2) === '${' &&
str?.slice(-1) === '}' str.slice(-1) === '}'
) { ) {
// 非最外层内容还存在表达式情况 // 非最外层内容还存在表达式情况
if (isExpression(str.slice(2, -1))) { if (isExpression(str.slice(2, -1))) {
@ -580,12 +588,6 @@ export default class FormulaControl extends React.Component<
const FormulaPickerCmp = customFormulaPicker ?? FormulaPicker; const FormulaPickerCmp = customFormulaPicker ?? FormulaPicker;
const highlightValue = isExpression(value)
? FormulaEditor.highlightValue(exprValue, variables) || {
html: exprValue
}
: value;
// 公式表达式弹窗内容过滤 // 公式表达式弹窗内容过滤
const filterValue = isExpression(value) const filterValue = isExpression(value)
? exprValue ? exprValue
@ -651,40 +653,38 @@ export default class FormulaControl extends React.Component<
tooltipTheme: 'dark', tooltipTheme: 'dark',
mouseLeaveDelay: 20, mouseLeaveDelay: 20,
content: exprValue, content: exprValue,
children: () => renderFormulaValue(highlightValue) tooltipClassName: 'btn-configured-tooltip',
children: () => (
<FormulaCodeEditor
readOnly
value={value}
variables={variables}
evalMode={false}
editorTheme="dark"
/>
)
}} }}
> >
<div className="ae-editor-FormulaControl-tooltipBox"> <div className={cx('ae-editor-FormulaControl-tooltipBox')}>
<div <InputBox
className="ae-editor-FormulaControl-ResultBox-wrapper"
onClick={this.handleFormulaClick} onClick={this.handleFormulaClick}
> hasError={isError}
<ResultBox inputRender={({value, onChange, onFocus, onBlur}: any) => (
className={cx( <FormulaCodeEditor
'ae-editor-FormulaControl-ResultBox', singleLine
isError ? 'is-error' : '' value={value}
)} onChange={onChange}
allowInput={false} onFocus={onFocus}
value={value} onBlur={onBlur}
result={{ functions={[]}
html: this.hasDateShortcutkey(value) variables={variables}
? value evalMode={false}
: highlightValue?.html readOnly
}} />
itemRender={renderFormulaValue} )}
onChange={this.handleInputChange} value={value}
onResultChange={() => { onChange={this.handleInputChange}
this.handleInputChange(undefined); />
}}
/>
</div>
{value && (
<Icon
icon="input-clear"
className="input-clear-icon"
onClick={() => this.handleInputChange('')}
/>
)}
</div> </div>
</TooltipWrapper> </TooltipWrapper>
)} )}

View File

@ -5,12 +5,11 @@
import React from 'react'; import React from 'react';
import cx from 'classnames'; import cx from 'classnames';
import {reaction} from 'mobx'; 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 type {VariableItem, CodeMirror} from 'amis-ui';
import {Icon, Button, FormItem, TooltipWrapper} from 'amis'; import {Icon, Button, FormItem, TooltipWrapper} from 'amis';
import {autobind, FormControlProps} from 'amis-core'; import {autobind, FormControlProps} from 'amis-core';
import {FormulaPlugin, editorFactory} from './textarea-formula/plugin'; import {FormulaPlugin, editorFactory} from './textarea-formula/plugin';
import {renderFormulaValue} from './FormulaControl';
import FormulaPicker, { import FormulaPicker, {
CustomFormulaPickerProps CustomFormulaPickerProps
} from './textarea-formula/FormulaPicker'; } from './textarea-formula/FormulaPicker';
@ -383,13 +382,6 @@ export class TplFormulaControl extends React.Component<
const FormulaPickerCmp = customFormulaPicker ?? FormulaPicker; const FormulaPickerCmp = customFormulaPicker ?? FormulaPicker;
const highlightValue = FormulaEditor.highlightValue(
formulaPickerValue,
variables
) || {
html: formulaPickerValue
};
return ( return (
<div <div
className={cx('ae-TplFormulaControl', className, { className={cx('ae-TplFormulaControl', className, {
@ -441,11 +433,20 @@ export class TplFormulaControl extends React.Component<
<TooltipWrapper <TooltipWrapper
trigger="hover" trigger="hover"
placement="top" placement="auto"
style={{fontSize: '12px'}} style={{fontSize: '12px'}}
tooltip={{ tooltip={{
tooltipTheme: 'dark', tooltipTheme: 'dark',
children: () => renderFormulaValue(highlightValue) tooltipClassName: 'btn-configured-tooltip',
children: () => (
<FormulaCodeEditor
readOnly
value={formulaPickerValue}
variables={variables}
evalMode={true}
editorTheme="dark"
/>
)
}} }}
> >
<div <div

View File

@ -2926,8 +2926,10 @@ export const getEventControlConfig = (
return { return {
showOldEntry: 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, actions: manager?.pluginActions,
events: manager?.pluginEvents, events: manager?.pluginEvents,
actionTree, actionTree,

View File

@ -49,7 +49,7 @@ import {
} from 'amis-editor-core'; } from 'amis-editor-core';
export * from './helper'; export * from './helper';
import {i18n as _i18n} from 'i18n-runtime'; 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 {reaction} from 'mobx';
import {updateComponentContext} from 'amis-editor-core'; import {updateComponentContext} from 'amis-editor-core';

View File

@ -2,7 +2,7 @@ import React, {useEffect} from 'react';
import {Modal, Button} from 'amis'; import {Modal, Button} from 'amis';
import {FormControlProps} from 'amis-core'; import {FormControlProps} from 'amis-core';
import cx from 'classnames'; import cx from 'classnames';
import FormulaEditor from 'amis-ui/lib/components/formula/Editor'; import {FormulaEditor} from 'amis-ui';
export interface FormulaPickerProps extends FormControlProps { export interface FormulaPickerProps extends FormControlProps {
onConfirm: (data: string | undefined) => void; onConfirm: (data: string | undefined) => void;

View File

@ -10,7 +10,7 @@ import type {SchemaObject} from 'amis';
import flatten from 'lodash/flatten'; import flatten from 'lodash/flatten';
import {InputComponentName} from '../component/InputComponentName'; import {InputComponentName} from '../component/InputComponentName';
import {FormulaDateType} from '../renderer/FormulaControl'; 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 reduce from 'lodash/reduce';
import map from 'lodash/map'; import map from 'lodash/map';
import omit from 'lodash/omit'; import omit from 'lodash/omit';

View File

@ -62,7 +62,7 @@ export function parse(input: string, options?: ParserOptions): ASTNode {
function fatal() { function fatal() {
throw TypeError( throw TypeError(
`Unexpected token ${token!.value} in ${token!.start.line}:${ `Unexpected token ${token!.value || token.type} in ${token!.start.line}:${
token!.start.column token!.start.column
}` }`
); );

View File

@ -452,6 +452,7 @@
fill: var(--Form-input-clearBtn-color); fill: var(--Form-input-clearBtn-color);
width: var(--Form-input-clearBtn-size); width: var(--Form-input-clearBtn-size);
height: var(--Form-input-clearBtn-size); height: var(--Form-input-clearBtn-size);
top: 0;
} }
&:hover svg { &:hover svg {

View File

@ -112,6 +112,7 @@
position: relative; position: relative;
&-wrapper { &-wrapper {
flex: 1; flex: 1;
min-width: 0;
} }
&-collapse { &-collapse {
@ -139,6 +140,7 @@
&-body-wrapper { &-body-wrapper {
flex: 1; flex: 1;
min-width: 0;
} }
&-toolbar { &-toolbar {
@ -323,6 +325,8 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
flex: 1;
min-width: 0;
> .#{$ns}CBGroupOrItem-dragbar { > .#{$ns}CBGroupOrItem-dragbar {
left: px2rem(10px); left: px2rem(10px);
position: absolute; position: absolute;
@ -431,11 +435,24 @@
} }
} }
.#{$ns}CBItem {
display: flex;
flex: 1;
min-width: 0;
flex-wrap: wrap;
> * {
flex-shrink: 0;
}
}
.#{$ns}CBValue { .#{$ns}CBValue {
position: relative; position: relative;
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
margin: px2rem(3px); margin: px2rem(3px);
flex: 1;
min-width: px2rem(100px);
} }
.#{$ns}CBFormula { .#{$ns}CBFormula {
@ -461,6 +478,7 @@
width: 20px; width: 20px;
text-align: center; text-align: center;
display: inline-block; display: inline-block;
align-self: center;
user-select: none; user-select: none;
} }

View File

@ -2,6 +2,7 @@
overflow: visible; overflow: visible;
max-width: 100%; max-width: 100%;
box-sizing: content-box; box-sizing: content-box;
min-height: px2rem(450px);
@mixin scrollbar { @mixin scrollbar {
&::-webkit-scrollbar { &::-webkit-scrollbar {
@ -31,10 +32,59 @@
flex: 1; flex: 1;
height: 100%; height: 100%;
max-width: #{px2rem(530px)}; max-width: #{px2rem(530px)};
min-width: 0;
border-top: var(--Form-input-borderWidth) solid border-top: var(--Form-input-borderWidth) solid
var(--Form-input-borderColor); var(--Form-input-borderColor);
border-bottom: var(--Form-input-borderWidth) solid border-bottom: var(--Form-input-borderWidth) solid
var(--Form-input-borderColor); 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 { &-header {
@ -51,8 +101,8 @@
&-editor { &-editor {
@include scrollbar(); @include scrollbar();
min-height: px2rem(200px); flex: 1;
height: calc(100% - 35px); min-height: 0;
padding: #{px2rem(5px)}; padding: #{px2rem(5px)};
padding-right: 0; padding-right: 0;
@ -418,27 +468,6 @@
overflow: hidden; overflow: hidden;
background: var(--InputFormula-code-bgColor); 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 { .#{$ns}FormulaPicker {
@ -459,8 +488,9 @@
&-input { &-input {
flex: 1; flex: 1;
min-width: 0;
margin-right: #{px2rem(10px)}; margin-right: #{px2rem(10px)};
padding-right: 0; padding-right: px2rem(4px);
max-width: calc(100% - #{px2rem(42px)}); max-width: calc(100% - #{px2rem(42px)});
} }
@ -515,7 +545,8 @@
height: var(--Form-input-height); height: var(--Form-input-height);
&.#{$ns}FormulaPicker--text { &.#{$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 { .#{$ns}FormulaPicker-input {
@ -534,8 +565,8 @@
box-sizing: none; box-sizing: none;
} }
&-number,
&-select, &-select,
&-number,
&-boolean, &-boolean,
&-date, &-date,
&-time, &-time,
@ -548,6 +579,20 @@
.#{$ns}Number-handler-wrap { .#{$ns}Number-handler-wrap {
height: unset; /* 避免调节器超出Input框 */ height: unset; /* 避免调节器超出Input框 */
} }
padding-left: 0;
}
&-custom {
border: 0;
flex: 1;
min-width: 0;
> div {
border: 0;
padding: 0 0 0 var(--Form-input-paddingX);
align-items: center;
height: var(--InputFormula-input-schema-height);
}
} }
&-variable { &-variable {
@ -575,3 +620,68 @@
} }
} }
} }
.#{$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;
border-right-width: 0 !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;
}

View File

@ -1,13 +1,18 @@
import React from 'react'; import React from 'react';
// import 'codemirror/lib/codemirror.css'; // import 'codemirror/lib/codemirror.css';
import type CodeMirror from 'codemirror'; import type CodeMirror from 'codemirror';
import {autobind} from 'amis-core'; import {autobind, changedEffect} from 'amis-core';
import {resizeSensor} from 'amis-core'; import {resizeSensor} from 'amis-core';
import 'codemirror/theme/idea.css';
import 'codemirror/theme/base16-dark.css';
// import 'codemirror/theme/base16-light.css';
export interface CodeMirrorEditorProps { export interface CodeMirrorEditorProps {
className?: string; className?: string;
style?: any; style?: any;
value?: string; value?: string;
readOnly?: boolean;
onChange?: (value: string) => void; onChange?: (value: string) => void;
onFocus?: (e: any) => void; onFocus?: (e: any) => void;
onBlur?: (e: any) => void; onBlur?: (e: any) => void;
@ -37,14 +42,17 @@ export class CodeMirrorEditor extends React.Component<CodeMirrorEditorProps> {
await import('codemirror/mode/htmlmixed/htmlmixed'); await import('codemirror/mode/htmlmixed/htmlmixed');
await import('codemirror/addon/mode/simple'); await import('codemirror/addon/mode/simple');
await import('codemirror/addon/mode/multiplex'); await import('codemirror/addon/mode/multiplex');
await import('codemirror/addon/display/placeholder');
if (this.unmounted) { if (this.unmounted) {
return; return;
} }
this.dom.current!.innerHTML = '';
this.editor = this.editor =
this.props.editorFactory?.(this.dom.current!, cm, this.props) ?? this.props.editorFactory?.(this.dom.current!, cm, this.props) ??
cm(this.dom.current!, { cm(this.dom.current!, {
value: this.props.value || '' value: this.props.value || '',
readOnly: this.props.readOnly ? 'nocursor' : false
}); });
this.props.editorDidMount?.(cm, this.editor); this.props.editorDidMount?.(cm, this.editor);
@ -52,6 +60,8 @@ export class CodeMirrorEditor extends React.Component<CodeMirrorEditorProps> {
this.editor.on('blur', this.handleBlur); this.editor.on('blur', this.handleBlur);
this.editor.on('focus', this.handleFocus); this.editor.on('focus', this.handleFocus);
this.setValue(this.props.value);
this.toDispose.push( this.toDispose.push(
resizeSensor(this.dom.current as HTMLElement, () => resizeSensor(this.dom.current as HTMLElement, () =>
this.editor?.refresh() this.editor?.refresh()
@ -70,6 +80,10 @@ export class CodeMirrorEditor extends React.Component<CodeMirrorEditorProps> {
if (props.value !== prevProps.value) { if (props.value !== prevProps.value) {
this.editor && this.setValue(props.value); this.editor && this.setValue(props.value);
} }
changedEffect(['readOnly'], prevProps, this.props, (changes: any) => {
this.editor?.setOption('readOnly', changes.readOnly ? 'nocursor' : false);
});
} }
componentWillUnmount() { componentWillUnmount() {
@ -97,9 +111,9 @@ export class CodeMirrorEditor extends React.Component<CodeMirrorEditorProps> {
setValue(value?: string) { setValue(value?: string) {
const doc = this.editor!.getDoc(); const doc = this.editor!.getDoc();
if (value && value !== doc.getValue()) { if (value !== doc.getValue()) {
const cursor = doc.getCursor(); const cursor = doc.getCursor();
doc.setValue(value); doc.setValue(value || '');
doc.setCursor(cursor); doc.setCursor(cursor);
} }
} }

View File

@ -19,6 +19,7 @@ export interface InputBoxProps
children?: React.ReactNode | Array<React.ReactNode>; children?: React.ReactNode | Array<React.ReactNode>;
borderMode?: 'full' | 'half' | 'none'; borderMode?: 'full' | 'half' | 'none';
testid?: string; testid?: string;
inputRender?: (props: any, ref?: any) => JSX.Element;
} }
export interface InputBoxState { export interface InputBoxState {
@ -49,7 +50,7 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
@autobind @autobind
handleChange(e: React.ChangeEvent<HTMLInputElement>) { handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const onChange = this.props.onChange; const onChange = this.props.onChange;
onChange && onChange(e.currentTarget.value); onChange && onChange(e.currentTarget ? e.currentTarget.value : (e as any));
} }
@autobind @autobind
@ -86,6 +87,7 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
onClick, onClick,
mobileUI, mobileUI,
testid, testid,
inputRender,
...rest ...rest
} = this.props; } = this.props;
const isFocused = this.state.isFocused; const isFocused = this.state.isFocused;
@ -104,17 +106,30 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
> >
{result} {result}
<Input {typeof inputRender === 'function' ? (
{...rest} inputRender({
value={value ?? ''} ...rest,
onChange={this.handleChange} value: value ?? '',
placeholder={placeholder} onChange: this.handleChange as any,
onFocus={this.handleFocus} placeholder,
onBlur={this.handleBlur} onFocus: this.handleFocus,
size={12} onBlur: this.handleBlur,
disabled={disabled} disabled,
{...buildTestId(testid)} ...buildTestId(testid)
/> })
) : (
<Input
{...rest}
value={value ?? ''}
onChange={this.handleChange}
placeholder={placeholder}
onFocus={this.handleFocus}
onBlur={this.handleBlur}
size={12}
disabled={disabled}
{...buildTestId(testid)}
/>
)}
{children} {children}

View File

@ -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; tooltip?: string | TooltipObject;
classPrefix: string; classPrefix: string;
classnames: ClassNamesFn; classnames: ClassNamesFn;
placement: 'top' | 'right' | 'bottom' | 'left'; placement: 'top' | 'right' | 'bottom' | 'left' | 'auto';
container?: HTMLElement | (() => HTMLElement | null | undefined); container?: HTMLElement | (() => HTMLElement | null | undefined);
trigger: Trigger | Array<Trigger>; trigger: Trigger | Array<Trigger>;
rootClose: boolean; rootClose: boolean;

View File

@ -1,6 +1,12 @@
import React from 'react'; import React from 'react';
import {FieldSimple} from './types'; import {FieldSimple} from './types';
import {ThemeProps, themeable, localeable, LocaleProps} from 'amis-core'; import {
ThemeProps,
themeable,
localeable,
LocaleProps,
autobind
} from 'amis-core';
import InputBox from '../InputBox'; import InputBox from '../InputBox';
import NumberInput from '../NumberInput'; import NumberInput from '../NumberInput';
import DatePicker from '../DatePicker'; import DatePicker from '../DatePicker';
@ -22,6 +28,25 @@ export interface ValueProps extends ThemeProps, LocaleProps {
} }
export class Value extends React.Component<ValueProps> { export class Value extends React.Component<ValueProps> {
@autobind
renderCustomValue(props: any) {
const {renderEtrValue, data, classnames: cx} = this.props;
const field = props.inputSettings;
return renderEtrValue
? renderEtrValue(
{...field.value, name: 'TMP_WHATEVER_NAME'}, // name 随便输入,应该是 value 传入的为主,目前表单项内部逻辑还有问题先传一个 name
{
data,
onChange: props.onChange,
value: props.value,
inputClassName: cx(field.className, props.className)
}
)
: null;
}
render() { render() {
let { let {
classnames: cx, classnames: cx,
@ -34,7 +59,6 @@ export class Value extends React.Component<ValueProps> {
disabled, disabled,
formula, formula,
popOverContainer, popOverContainer,
renderEtrValue,
mobileUI mobileUI
} = this.props; } = this.props;
let input: JSX.Element | undefined = undefined; let input: JSX.Element | undefined = undefined;
@ -50,19 +74,24 @@ export class Value extends React.Component<ValueProps> {
disabled disabled
}; };
const inputSettings = const inputSettings = formula?.inputSettings
field.type !== 'custom' && formula?.inputSettings ? {
? { ...formula?.inputSettings,
...formula?.inputSettings, ...field,
...field, multiple:
multiple: field.type === 'select' &&
field.type === 'select' && op &&
op && typeof op === 'string' &&
typeof op === 'string' && ['select_any_in', 'select_not_any_in'].includes(op)
['select_any_in', 'select_not_any_in'].includes(op) }
} : undefined;
: undefined; input = (
input = <FormulaPicker {...formula} inputSettings={inputSettings} />; <FormulaPicker
{...formula}
inputSettings={inputSettings}
customInputRender={this.renderCustomValue}
/>
);
} else if (field.type === 'text') { } else if (field.type === 'text') {
input = ( input = (
<InputBox <InputBox
@ -162,17 +191,11 @@ export class Value extends React.Component<ValueProps> {
/> />
); );
} else if (field.type === 'custom') { } else if (field.type === 'custom') {
input = renderEtrValue input = this.renderCustomValue({
? renderEtrValue( value: value ?? field.defaultValue,
{...field.value, name: 'TMP_WHATEVER_NAME'}, // name 随便输入,应该是 value 传入的为主,目前表单项内部逻辑还有问题先传一个 name onChange,
inputSettings: field
{ });
data,
onChange,
value: value ?? field.defaultValue
}
)
: null;
} }
return <div className={cx('CBValue')}>{input}</div>; return <div className={cx('CBValue')}>{input}</div>;

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

View File

@ -2,50 +2,43 @@
* @file * @file
*/ */
import React from 'react'; import React from 'react';
import {mapTree, uncontrollable} from 'amis-core'; import {
eachTree,
resolveVariableAndFilterForAsync,
uncontrollable
} from 'amis-core';
import { import {
parse, parse,
autobind, autobind,
utils,
themeable, themeable,
ThemeProps, ThemeProps,
localeable, localeable,
LocaleProps, LocaleProps
eachTree
} from 'amis-core'; } from 'amis-core';
import {functionDocs} from 'amis-formula';
import type {FunctionDocMap} from 'amis-formula/lib/types'; import type {FunctionDocMap} from 'amis-formula/lib/types';
import {FormulaPlugin, editorFactory} from './plugin'; import {editorFactory} from './plugin';
import FuncList from './FuncList'; import FuncList from './FuncList';
import VariableList from './VariableList'; import VariableList from './VariableList';
import CodeMirrorEditor from '../CodeMirror';
import {toast} from '../Toast'; import {toast} from '../Toast';
import Switch from '../Switch'; 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 { const collapseStyles: {
label: string; [propName: string]: string;
value?: string; } = {
path?: string; // 路径label [EXITED]: 'out',
children?: Array<VariableItem>; [EXITING]: 'out',
type?: string; [ENTERING]: 'in'
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 FormulaEditorProps extends ThemeProps, LocaleProps { export interface FormulaEditorProps extends ThemeProps, LocaleProps {
onChange?: (value: string) => void; onChange?: (value: string) => void;
@ -106,11 +99,14 @@ export interface FunctionProps {
} }
export interface FormulaState { export interface FormulaState {
functions: FuncGroup[];
focused: boolean; focused: boolean;
isCodeMode: boolean; isCodeMode: boolean;
showRunPanel: boolean;
expandTree: boolean; expandTree: boolean;
normalizeVariables?: Array<VariableItem>; functions?: Array<FuncGroup>;
runContext: string;
runResult: string;
runValid: boolean;
} }
export class FormulaEditor extends React.Component< export class FormulaEditor extends React.Component<
@ -120,12 +116,15 @@ export class FormulaEditor extends React.Component<
state: FormulaState = { state: FormulaState = {
focused: false, focused: false,
isCodeMode: false, isCodeMode: false,
showRunPanel: false,
expandTree: false, expandTree: false,
normalizeVariables: [], functions: this.props.functions,
functions: [] runContext: '{\n}',
runResult: '',
runValid: false
}; };
editorPlugin?: FormulaPlugin;
unmounted: boolean = false; unmounted: boolean = false;
editor = React.createRef<any>();
static buildDefaultFunctions( static buildDefaultFunctions(
doc: Array<{ 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'> = { static defaultProps: Pick<FormulaEditorProps, 'variables' | 'evalMode'> = {
variables: [], variables: [],
evalMode: true evalMode: true
@ -182,6 +200,15 @@ export class FormulaEditor extends React.Component<
return new RegExp(reg); return new RegExp(reg);
} }
/**
* 使
* codemirror
* @deprecated
* @param value
* @param variables
* @param evalMode
* @returns
*/
static highlightValue( static highlightValue(
value: string, value: string,
variables: Array<VariableItem>, variables: Array<VariableItem>,
@ -252,78 +279,38 @@ export class FormulaEditor extends React.Component<
return {html}; return {html};
} }
componentDidMount(): void { constructor(props: FormulaEditorProps) {
const {variables} = this.props; super(props);
this.normalizeVariables(variables as VariableItem[]); this.runCode = debounce(this.runCode.bind(this), 250, {
this.buildFunctions(); leading: false,
trailing: true
});
} }
componentDidUpdate( async componentDidMount() {
prevProps: Readonly<FormulaEditorProps>, if (!this.state.functions) {
prevState: Readonly<FormulaState>, const functionList = await FormulaEditor.buildFunctions();
snapshot?: any if (this.unmounted) {
): void { return;
if (prevProps.variables !== this.props.variables) { }
this.normalizeVariables(this.props.variables as VariableItem[]);
}
this.setState({
functions: functionList
});
}
}
componentDidUpdate(prevProps: FormulaEditorProps): void {
if (prevProps.functions !== this.props.functions) { if (prevProps.functions !== this.props.functions) {
this.buildFunctions(); this.setState({
functions: this.props.functions
});
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.editorPlugin?.dispose();
this.unmounted = true; this.unmounted = true;
} (this.runCode as any).cancel();
async buildFunctions() {
const {doc} = await import('amis-formula/lib/doc');
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
}
: {})
};
}
);
this.setState({normalizeVariables: list});
} }
@autobind @autobind
@ -340,17 +327,17 @@ export class FormulaEditor extends React.Component<
}); });
} }
@autobind getEditor() {
insertValue(value: any, type: 'variable' | 'func') { let ref = this.editor.current;
this.editorPlugin?.insertContent(value, type); while (ref?.getWrappedInstance) {
ref = ref.getWrappedInstance();
}
return ref;
} }
@autobind @autobind
handleEditorMounted(cm: any, editor: any) { insertValue(value: any, type: 'variable' | 'func') {
this.editorPlugin = new FormulaPlugin(editor, cm, () => ({ this.getEditor()?.insertContent(value, type);
...this.props,
variables: this.state.normalizeVariables
}));
} }
@autobind @autobind
@ -372,12 +359,12 @@ export class FormulaEditor extends React.Component<
@autobind @autobind
handleFunctionSelect(item: FuncItem) { handleFunctionSelect(item: FuncItem) {
this.editorPlugin?.insertContent(`${item.name}`, 'func'); this.getEditor()?.insertContent(`${item.name}`, 'func');
} }
@autobind @autobind
handleVariableSelect(item: VariableItem) { handleVariableSelect(item: VariableItem) {
const {evalMode, selfVariableName} = this.props; const {selfVariableName} = this.props;
if ( if (
item && item &&
@ -393,7 +380,7 @@ export class FormulaEditor extends React.Component<
return; return;
} }
this.editorPlugin?.insertContent( this.getEditor()?.insertContent(
item.isMember item.isMember
? item.value ? item.value
: { : {
@ -412,23 +399,72 @@ export class FormulaEditor extends React.Component<
handleOnChange(value: any) { handleOnChange(value: any) {
const onChange = this.props.onChange; const onChange = this.props.onChange;
onChange?.(value); onChange?.(value);
this.runCode();
} }
@autobind @autobind
editorFactory(dom: HTMLElement, cm: any) { editorFactory(dom: HTMLElement, cm: any) {
const {editorOptions, ...rest} = this.props; const {editorOptions, ...rest} = this.props;
return editorFactory(dom, cm, rest, { return editorFactory(dom, cm, rest, {
lineWrapping: true // 自动换行 lineWrapping: true, // 自动换行
autoFocus: true
}); });
} }
@autobind @autobind
handleIsCodeModeChange(showCode: boolean) { handleIsCodeModeChange(showCode: boolean) {
// 重置一下value // 重置一下value
this.editorPlugin?.setValue(this.editorPlugin?.getValue()); // this.getEditor()?.setValue(this.getEditor()?.getValue());
// 非源码模式则mark一下 // 非源码模式则mark一下
!showCode && this.editorPlugin?.autoMarkText(); // !showCode && this.getEditor()?.autoMarkText();
this.setState({isCodeMode: showCode}); 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 @autobind
@ -440,21 +476,25 @@ export class FormulaEditor extends React.Component<
const { const {
header, header,
value, value,
functions, variables,
variableMode, variableMode,
translate: __, translate: __,
classnames: cx, classnames: cx,
variableClassName, variableClassName,
functionClassName, functionClassName,
classPrefix, classPrefix,
selfVariableName selfVariableName,
evalMode
} = this.props; } = this.props;
const { const {
focused, focused,
isCodeMode, isCodeMode,
showRunPanel,
expandTree, expandTree,
normalizeVariables, functions,
functions: functionList runContext,
runResult,
runValid
} = this.state; } = this.state;
return ( return (
@ -467,15 +507,19 @@ export class FormulaEditor extends React.Component<
<FuncList <FuncList
className={functionClassName} className={functionClassName}
title={__('FormulaEditor.function')} title={__('FormulaEditor.function')}
data={functionList} data={functions || []}
onSelect={this.handleFunctionSelect} onSelect={this.handleFunctionSelect}
/> />
<div className={cx(`FormulaEditor-content`)}> <div className={cx(`FormulaEditor-content`)}>
<header className={cx(`FormulaEditor-header`)}> <header className={cx(`FormulaEditor-header`)}>
{__(header || 'FormulaEditor.title')} {__(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`)}> <div className={cx(`FormulaEditor-header-toolbar`)}>
<span></span> <span>{__('FormulaEditor.sourceMode')}</span>
<Switch <Switch
value={isCodeMode} value={isCodeMode}
onChange={this.handleIsCodeModeChange} onChange={this.handleIsCodeModeChange}
@ -483,15 +527,61 @@ export class FormulaEditor extends React.Component<
</div> </div>
</header> </header>
<CodeMirrorEditor <CodeEditor
evalMode={evalMode}
functions={functions}
variables={variables}
className={cx('FormulaEditor-editor')} className={cx('FormulaEditor-editor')}
value={value} value={value}
onChange={this.handleOnChange} onChange={this.handleOnChange}
editorFactory={this.editorFactory} ref={this.editor}
editorDidMount={this.handleEditorMounted}
onFocus={this.handleFocus} onFocus={this.handleFocus}
onBlur={this.handleBlur} 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>
<div className={cx('FormulaEditor-panel', 'right')}> <div className={cx('FormulaEditor-panel', 'right')}>
{variableMode !== 'tabs' ? ( {variableMode !== 'tabs' ? (
@ -499,7 +589,7 @@ export class FormulaEditor extends React.Component<
{__('FormulaEditor.variable')} {__('FormulaEditor.variable')}
{variableMode === 'tree' ? ( {variableMode === 'tree' ? (
<div className={cx(`FormulaEditor-header-toolbar`)}> <div className={cx(`FormulaEditor-header-toolbar`)}>
<span></span> <span>{__('FormulaEditor.toggleAll')}</span>
<Switch <Switch
value={expandTree} value={expandTree}
onChange={this.handleExpandTreeChange} onChange={this.handleExpandTreeChange}
@ -523,7 +613,7 @@ export class FormulaEditor extends React.Component<
)} )}
expandTree={expandTree} expandTree={expandTree}
selectMode={variableMode} selectMode={variableMode}
data={normalizeVariables!} data={variables!}
onSelect={this.handleVariableSelect} onSelect={this.handleVariableSelect}
selfVariableName={selfVariableName} selfVariableName={selfVariableName}
/> />

View File

@ -1,10 +1,10 @@
import React from 'react'; import React from 'react';
import {themeable, ThemeProps} from 'amis-core'; import {mapTree, themeable, ThemeProps} from 'amis-core';
import Collapse from '../Collapse'; import Collapse from '../Collapse';
import CollapseGroup from '../CollapseGroup'; import CollapseGroup from '../CollapseGroup';
import SearchBox from '../SearchBox'; import SearchBox from '../SearchBox';
import type {FuncGroup, FuncItem} from './Editor'; import type {FuncGroup, FuncItem} from './CodeEditor';
import TooltipWrapper from '../TooltipWrapper'; import TooltipWrapper from '../TooltipWrapper';
import {Icon} from '../icons'; import {Icon} from '../icons';
@ -25,26 +25,32 @@ export function FuncList(props: FuncListProps) {
descClassName, descClassName,
mobileUI mobileUI
} = props; } = props;
const [term, setTerm] = React.useState('');
const [filteredFuncs, setFiteredFuncs] = React.useState(props.data); const [filteredFuncs, setFiteredFuncs] = React.useState(props.data);
const [activeFunc, setActiveFunc] = React.useState<any>(null); const [activeFunc, setActiveFunc] = React.useState<any>(null);
React.useEffect(() => { const onSearch = React.useCallback(
setFiteredFuncs(props.data); (term: string) => {
}, [props.data]); const filtered = props.data
.map(item => {
return {
...item,
items: term
? item.items.filter(
(item: any) => ~item.name.indexOf(term.toUpperCase())
)
: item.items
};
})
.filter(item => item.items.length);
setFiteredFuncs(filtered);
},
[props.data]
);
function onSearch(term: string) { React.useEffect(() => {
const filtered = props.data onSearch(term);
.map(item => { }, [props.data]);
return {
...item,
items: term
? item.items.filter(item => ~item.name.indexOf(term.toUpperCase()))
: item.items
};
})
.filter(item => item.items.length);
setFiteredFuncs(filtered);
}
return ( return (
<div className={cx('FormulaEditor-panel', 'left', className)}> <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-header')}>{title}</div>
<div className={cx('FormulaEditor-panel-body')}> <div className={cx('FormulaEditor-panel-body')}>
<div className={cx('FormulaEditor-FuncList-searchBox')}> <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>
<div className={cx('FormulaEditor-FuncList-body', bodyClassName)}> <div className={cx('FormulaEditor-FuncList-body', bodyClassName)}>
<CollapseGroup <CollapseGroup
@ -81,7 +93,7 @@ export function FuncList(props: FuncListProps) {
header={item.groupName} header={item.groupName}
key={item.groupName} key={item.groupName}
> >
{item.items.map(item => ( {item.items.map((item: any) => (
<div <div
className={cx('FormulaEditor-FuncList-item', { className={cx('FormulaEditor-FuncList-item', {
'is-active': item.name === activeFunc?.name 'is-active': item.name === activeFunc?.name

View File

@ -13,14 +13,16 @@ import {
isObject isObject
} from 'amis-core'; } from 'amis-core';
import {FormulaEditor, VariableItem} from './Editor'; import {FormulaEditor} from './Editor';
import ResultBox from '../ResultBox'; import ResultBox from '../ResultBox';
import Select from '../Select'; import {SelectWithRemoteOptions as Select} from '../Select';
import NumberInput from '../NumberInput'; import NumberInput from '../NumberInput';
import DatePicker from '../DatePicker'; import DatePicker from '../DatePicker';
import Tag from '../Tag'; import Tag from '../Tag';
import type {FormulaPickerProps} from './Picker'; import type {FormulaPickerInputSettings, FormulaPickerProps} from './Picker';
import CodeEditor, {FuncGroup, VariableItem} from './CodeEditor';
import InputBox from '../InputBox';
export interface FormulaInputProps export interface FormulaInputProps
extends Pick< extends Pick<
@ -28,7 +30,6 @@ export interface FormulaInputProps
| 'className' | 'className'
| 'disabled' | 'disabled'
| 'evalMode' | 'evalMode'
| 'allowInput'
| 'placeholder' | 'placeholder'
| 'clearable' | 'clearable'
| 'borderMode' | 'borderMode'
@ -42,9 +43,19 @@ export interface FormulaInputProps
*/ */
value?: string; value?: string;
/**
* evalMode
*
*
* ${}
*
*/
mixedMode?: boolean; mixedMode?: boolean;
autoFoucs?: boolean;
variables?: VariableItem[]; variables?: VariableItem[];
functions?: Array<FuncGroup>;
popOverContainer?: any; popOverContainer?: any;
@ -54,36 +65,48 @@ export interface FormulaInputProps
onChange?: (value: string | any[]) => void; onChange?: (value: string | any[]) => void;
/** /**
* *
*/ */
itemRender?: (value: any) => JSX.Element | string; customInputRender?: (props: {
value: any;
onChange: (value: any) => void;
className?: string;
inputSettings: FormulaPickerInputSettings;
}) => JSX.Element;
} }
const FormulaInput: React.FC<FormulaInputProps> = props => { const FormulaInput = (props: FormulaInputProps, ref: any) => {
const { const {
translate: __, translate: __,
className, className,
classnames: cx, classnames: cx,
allowInput,
placeholder, placeholder,
borderMode, borderMode,
evalMode, evalMode,
mixedMode, mixedMode,
value, value,
variables, variables,
functions,
inputSettings = {type: 'text'}, inputSettings = {type: 'text'},
popOverContainer, popOverContainer,
onChange, onChange,
itemRender customInputRender
} = props; } = props;
const schemaType = inputSettings.type; const schemaType = inputSettings.type;
/** 自上层共享的属性 */ /** 自上层共享的属性 */
const sharedProps = pick(props, ['disabled', 'clearable']); const sharedProps = pick(props, ['disabled', 'clearable', 'data']);
const pipInValue = useCallback( const pipInValue = useCallback(
(value?: any) => { (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; return value;
}, },
['value'] [schemaType]
); );
const pipOutValue = useCallback( const pipOutValue = useCallback(
(origin: any) => { (origin: any) => {
@ -98,7 +121,7 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
result = origin.value; result = origin.value;
} else if (schemaType === 'select') { } else if (schemaType === 'select') {
const { const {
joinValues, joinValues = true,
extractValue, extractValue,
delimiter, delimiter,
multiple, multiple,
@ -129,110 +152,13 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
} }
onChange?.(result); onChange?.(result);
}, },
['onChange'] [schemaType, onChange, inputSettings]
); );
let cmptValue = pipInValue(value ?? inputSettings.defaultValue); let cmptValue = pipInValue(value ?? inputSettings.defaultValue);
const isExpr = isExpression(cmptValue);
/** 数据来源可能是从 query中下发的CRUD查询表头导致数字或者布尔值被转为 string 格式,这里预处理一下 */ if (!isExpr && schemaType === 'number') {
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') {
return ( return (
<NumberInput <NumberInput
{...sharedProps} {...sharedProps}
@ -247,9 +173,7 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
onChange={pipOutValue} onChange={pipOutValue}
/> />
); );
} else if (schemaType === 'date') { } else if (!isExpr && schemaType === 'date') {
const cmptValue = pipInValue(value ?? inputSettings.defaultValue);
return ( return (
<DatePicker <DatePicker
{...sharedProps} {...sharedProps}
@ -265,7 +189,7 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
onChange={pipOutValue} onChange={pipOutValue}
/> />
); );
} else if (schemaType === 'time') { } else if (!isExpr && schemaType === 'time') {
return ( return (
<DatePicker <DatePicker
{...sharedProps} {...sharedProps}
@ -279,11 +203,11 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
dateFormat="" dateFormat=""
timeFormat={inputSettings.format || 'HH:mm'} timeFormat={inputSettings.format || 'HH:mm'}
popOverContainer={popOverContainer} popOverContainer={popOverContainer}
value={pipInValue(value ?? inputSettings.defaultValue)} value={cmptValue}
onChange={pipOutValue} onChange={pipOutValue}
/> />
); );
} else if (schemaType === 'datetime') { } else if (!isExpr && schemaType === 'datetime') {
return ( return (
<DatePicker <DatePicker
{...sharedProps} {...sharedProps}
@ -295,14 +219,14 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
inputFormat={inputSettings.inputFormat || 'YYYY-MM-DD HH:mm'} inputFormat={inputSettings.inputFormat || 'YYYY-MM-DD HH:mm'}
timeFormat={inputSettings.timeFormat || 'HH:mm'} timeFormat={inputSettings.timeFormat || 'HH:mm'}
popOverContainer={popOverContainer} popOverContainer={popOverContainer}
value={pipInValue(value ?? inputSettings.defaultValue)} value={cmptValue}
onChange={pipOutValue} onChange={pipOutValue}
/> />
); );
} else if (schemaType === 'select' || schemaType === 'boolean') { } else if (!isExpr && (schemaType === 'select' || schemaType === 'boolean')) {
return ( return (
<Select <Select
{...sharedProps} {...(sharedProps as any)}
className={cx(className, `FormulaPicker-input-${schemaType}`)} className={cx(className, `FormulaPicker-input-${schemaType}`)}
borderMode="none" borderMode="none"
multiple={schemaType === 'boolean' ? false : inputSettings.multiple} multiple={schemaType === 'boolean' ? false : inputSettings.multiple}
@ -320,6 +244,7 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
] ]
: inputSettings.options ?? [] : inputSettings.options ?? []
} }
source={inputSettings.source}
value={pipInValue(value)} value={pipInValue(value)}
renderValueLabel={option => { renderValueLabel={option => {
const label = option.label?.toString() ?? ''; const label = option.label?.toString() ?? '';
@ -333,23 +258,34 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
onChange={pipOutValue} onChange={pipOutValue}
/> />
); );
} else if (!isExpr && schemaType === 'custom' && customInputRender) {
return customInputRender({
value: cmptValue,
onChange: pipOutValue,
inputSettings,
className: `FormulaPicker-input-custom`
});
} else { } else {
return ( return (
<ResultBox <InputBox
{...sharedProps} className={cx('FormulaPicker-input')}
className={cx(className)} inputRender={({value, onChange, onFocus, onBlur, placeholder}: any) => (
allowInput={allowInput} <CodeEditor
singleLine
value={value}
onChange={onChange}
onFocus={onFocus}
onBlur={onBlur}
functions={functions}
variables={variables}
evalMode={evalMode}
placeholder={placeholder}
/>
)}
borderMode={borderMode} borderMode={borderMode}
placeholder={placeholder} value={cmptValue}
value={pipInValue(value)}
result={
allowInput || !value
? void 0
: FormulaEditor.highlightValue(value, variables!, evalMode)
}
itemRender={itemRender}
onResultChange={noop}
onChange={pipOutValue} onChange={pipOutValue}
placeholder={__(placeholder ?? 'placeholder.enter')}
/> />
); );
} }
@ -357,7 +293,7 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
export default themeable( export default themeable(
localeable( localeable(
uncontrollable(FormulaInput, { uncontrollable(React.forwardRef(FormulaInput), {
value: 'onChange' value: 'onChange'
}) })
) )

View File

@ -5,12 +5,7 @@ import {
uncontrollable uncontrollable
} from 'amis-core'; } from 'amis-core';
import React from 'react'; import React from 'react';
import { import {FormulaEditor, FormulaEditorProps} from './Editor';
FormulaEditor,
FormulaEditorProps,
FuncGroup,
VariableItem
} from './Editor';
import { import {
autobind, autobind,
noop, noop,
@ -28,6 +23,8 @@ import {Icon} from '../icons';
import Modal from '../Modal'; import Modal from '../Modal';
import PopUp from '../PopUp'; import PopUp from '../PopUp';
import FormulaInput from './Input'; import FormulaInput from './Input';
import {FuncGroup, VariableItem} from './CodeEditor';
import {functionDocs} from 'amis-formula';
export const InputSchemaType = [ export const InputSchemaType = [
'text', 'text',
@ -36,7 +33,8 @@ export const InputSchemaType = [
'date', 'date',
'time', 'time',
'datetime', 'datetime',
'select' 'select',
'custom'
] as const; ] as const;
export type FormulaPickerInputSettingType = (typeof InputSchemaType)[number]; export type FormulaPickerInputSettingType = (typeof InputSchemaType)[number];
@ -107,11 +105,6 @@ export interface FormulaPickerProps
*/ */
disabled?: boolean; disabled?: boolean;
/**
* fx在弹窗中输入
*/
allowInput?: boolean;
/** /**
* *
*/ */
@ -137,11 +130,23 @@ export interface FormulaPickerProps
*/ */
inputSettings?: FormulaPickerInputSettings; inputSettings?: FormulaPickerInputSettings;
/**
*
*/
customInputRender?: (props: {
value: any;
onChange: (value: any) => void;
className?: string;
inputSettings: FormulaPickerInputSettings;
}) => JSX.Element;
/** /**
* variables functions * variables functions
*/ */
onPickerOpen?: (props: FormulaPickerProps) => any; onPickerOpen?: (props: FormulaPickerProps) => any;
functionsFilter?: (functions: Array<FuncGroup>) => Array<FuncGroup>;
children?: (props: { children?: (props: {
onClick: (e: React.MouseEvent) => void; onClick: (e: React.MouseEvent) => void;
setState: (state: any) => void; setState: (state: any) => void;
@ -180,6 +185,7 @@ export class FormulaPicker extends React.Component<
evalMode: true evalMode: true
}; };
unmounted = false;
constructor(props: FormulaPickerProps) { constructor(props: FormulaPickerProps) {
super(props); super(props);
this.props.onRef && this.props.onRef(this); this.props.onRef && this.props.onRef(this);
@ -205,6 +211,7 @@ export class FormulaPicker extends React.Component<
); );
this.setState({variables: result}); this.setState({variables: result});
} }
this.buildFunctions();
} }
async componentDidUpdate(prevProps: FormulaPickerProps) { async componentDidUpdate(prevProps: FormulaPickerProps) {
@ -231,12 +238,47 @@ export class FormulaPicker extends React.Component<
this.setState({variables: result}); 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) { 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 = ''; let editorValue = '';
try { try {
@ -244,20 +286,15 @@ export class FormulaPicker extends React.Component<
} catch (error) {} } catch (error) {}
return editorValue; return editorValue;
} else {
return value
? mixedMode
? isExpression(value)
? `\`${value.replace(/`/g, '\\`')}\``
: JSON.stringify(value)
: value
: '';
} }
if (props.mixedMode) {
if (
typeof props.value === 'string' &&
/^\s*\$\{(.+?)\}\s*$/.test(props.value)
) {
return RegExp.$1;
} else {
return '';
}
}
return String(props.value || '');
} }
isTextInput() { isTextInput() {
@ -281,22 +318,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 @autobind
handleInputChange(value: string) { handleInputChange(value: string) {
this.setState({value}, () => this.handleConfirm()); this.setState({value}, () => this.handleConfirm());
@ -321,45 +342,52 @@ export class FormulaPicker extends React.Component<
const {translate: __, inputSettings} = this.props; const {translate: __, inputSettings} = this.props;
const {editorValue} = this.state; const {editorValue} = this.state;
if (this.isTextInput()) { let ast: any;
return this.confirm(editorValue); try {
} else if (inputSettings) { ast = parse(editorValue, {evalMode: true, allowFilter: false});
} catch (error) {
this.setState({isError: error?.message ?? true});
return;
}
if (
inputSettings?.type &&
['boolean', 'number'].includes(inputSettings?.type)
) {
let result = editorValue; let result = editorValue;
const schemaType = inputSettings?.type; // const schemaType = inputSettings?.type;
try { if (ast.type === 'literal' || ast.type === 'string') {
const ast = parse(editorValue, {evalMode: true, allowFilter: false}); result = ast.value ?? '';
if (
schemaType === 'select' &&
inputSettings.multiple &&
ast.type === 'array'
) {
result = ast.members.map((i: any) => i.value);
} else if (ast.type === 'literal' || ast.type === 'string') {
result = ast.value ?? '';
}
} catch (error) {
this.setState({isError: error?.message ?? true});
return;
} }
this.setState({isError: false}); this.setState({isError: false});
return this.confirm(result); return this.confirm(result);
} }
return this.confirm(editorValue, ast);
} }
confirm(value: string) { confirm(value: any, ast?: any) {
const {mixedMode} = this.props; const {mixedMode} = this.props;
const validate = this.validate(value); const validate = this.validate(value);
if (validate === true) { if (validate === true) {
this.setState( let result = value;
{value: mixedMode && value ? `\${${value}}` : value},
() => { if (mixedMode && typeof value === 'string') {
this.close(undefined, () => this.handleConfirm()); result =
} ast?.type === 'string'
); ? ast.value
: ast?.type === 'template' &&
ast.body.length === 1 &&
ast.body[0].type === 'template_raw'
? ast.body[0].value
: `\${${value}}`;
}
this.setState({value: result}, () => {
this.close(undefined, () => this.handleConfirm());
});
} else { } else {
this.setState({isError: validate}); this.setState({isError: validate});
} }
@ -375,6 +403,9 @@ export class FormulaPicker extends React.Component<
isOpened: true isOpened: true
}; };
if (state.functions) {
state.functions = await this.buildFunctions(state.functions, false);
}
this.setState(state); this.setState(state);
} }
@ -431,7 +462,7 @@ export class FormulaPicker extends React.Component<
try { try {
value && value &&
parse(value, { parse(value, {
evalMode: this.props.mixedMode ? true : this.props.evalMode, evalMode: this.props.mixedMode ? false : this.props.evalMode,
allowFilter: false allowFilter: false
}); });
@ -450,7 +481,6 @@ export class FormulaPicker extends React.Component<
classnames: cx, classnames: cx,
translate: __, translate: __,
disabled, disabled,
allowInput = true,
className, className,
style, style,
onChange, onChange,
@ -472,6 +502,7 @@ export class FormulaPicker extends React.Component<
popOverContainer, popOverContainer,
mobileUI, mobileUI,
inputSettings, inputSettings,
customInputRender,
...rest ...rest
} = this.props; } = this.props;
const {isOpened, value, editorValue, isError} = this.state; const {isOpened, value, editorValue, isError} = this.state;
@ -502,6 +533,7 @@ export class FormulaPicker extends React.Component<
className={cx('FormulaPicker-action', 'w-full')} className={cx('FormulaPicker-action', 'w-full')}
level={level} level={level}
size={btnSize} size={btnSize}
active={!!value}
onClick={this.handleClick} onClick={this.handleClick}
> >
{iconElement ? ( {iconElement ? (
@ -529,31 +561,25 @@ export class FormulaPicker extends React.Component<
)} )}
{mode === 'input-button' && ( {mode === 'input-button' && (
<> <>
<ResultBox <FormulaInput
className={cx( className={cx(
'FormulaPicker-input', 'FormulaPicker-input',
isOpened ? 'is-active' : '', isOpened ? 'is-active' : '',
!!isError ? 'is-error' : '' !!isError ? 'is-error' : ''
)} )}
allowInput={allowInput} inputSettings={inputSettings}
customInputRender={customInputRender}
clearable={clearable} clearable={clearable}
evalMode={mixedMode ? false : evalMode}
variables={this.state.variables!}
functions={this.state.functions ?? functions}
value={value} value={value}
result={
allowInput
? void 0
: FormulaEditor.highlightValue(
value,
this.state.variables!,
this.props.evalMode
)
}
itemRender={this.renderFormulaValue}
onResultChange={noop}
onChange={this.handleInputChange} onChange={this.handleInputChange}
disabled={disabled} disabled={disabled}
borderMode={borderMode} borderMode={borderMode}
placeholder={placeholder} placeholder={placeholder}
/> />
<Button <Button
className={cx('FormulaPicker-action')} className={cx('FormulaPicker-action')}
onClick={this.handleClick} onClick={this.handleClick}
@ -576,13 +602,12 @@ export class FormulaPicker extends React.Component<
!!isError ? 'is-error' : '' !!isError ? 'is-error' : ''
)} )}
inputSettings={inputSettings} inputSettings={inputSettings}
allowInput={allowInput} customInputRender={customInputRender}
clearable={clearable} clearable={clearable}
evalMode={evalMode} evalMode={mixedMode ? false : evalMode}
mixedMode={mixedMode}
variables={this.state.variables!} variables={this.state.variables!}
functions={this.state.functions ?? functions}
value={value} value={value}
itemRender={this.renderFormulaValue}
onChange={this.handleInputChange} onChange={this.handleInputChange}
disabled={disabled} disabled={disabled}
borderMode={borderMode} borderMode={borderMode}

View File

@ -1,12 +1,12 @@
import React, {useEffect} from 'react'; 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 GroupedSelection from '../GroupedSelection';
import Tabs, {Tab} from '../Tabs'; import Tabs, {Tab} from '../Tabs';
import TreeSelection from '../TreeSelection'; import TreeSelection from '../TreeSelection';
import SearchBox from '../SearchBox'; import SearchBox from '../SearchBox';
import type {VariableItem} from './Editor'; import type {VariableItem} from './CodeEditor';
import type {ItemRenderStates} from '../Selection'; import type {ItemRenderStates} from '../Selection';
import type {Option} from '../Select'; import type {Option} from '../Select';
import type {TabsMode} from '../Tabs'; import type {TabsMode} from '../Tabs';
@ -85,7 +85,6 @@ export interface VariableListProps extends ThemeProps, SpinnerExtraProps {
function VariableList(props: VariableListProps) { function VariableList(props: VariableListProps) {
const variableListRef = React.useRef<HTMLDivElement>(null); const variableListRef = React.useRef<HTMLDivElement>(null);
const { const {
data: list,
className, className,
classnames: cx, classnames: cx,
tabsMode = 'line', tabsMode = 'line',
@ -97,14 +96,37 @@ function VariableList(props: VariableListProps) {
selfVariableName, selfVariableName,
expandTree expandTree
} = props; } = 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`; const classPrefix = `${themePrefix}FormulaEditor-VariableList`;
useEffect(() => { React.useEffect(() => {
const {data} = props; // 追加path用于分级高亮
if (data) { const list = mapTree(
setFilterVars(data); 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]); }, [props.data]);
const itemRender = const itemRender =
@ -217,7 +239,7 @@ function VariableList(props: VariableListProps) {
function onSearch(term: string) { function onSearch(term: string) {
const tree = filterTree( const tree = filterTree(
list, variables,
(i: any, key: number, level: number, paths: any[]) => { (i: any, key: number, level: number, paths: any[]) => {
return !!( return !!(
(Array.isArray(i.children) && i.children.length) || (Array.isArray(i.children) && i.children.length) ||
@ -231,7 +253,7 @@ function VariableList(props: VariableListProps) {
true true
); );
setFilterVars(!term ? list : tree); setFilterVars(!term ? variables : tree);
} }
function renderSearchBox() { function renderSearchBox() {

View File

@ -3,8 +3,10 @@
*/ */
import type CodeMirror from 'codemirror'; import type CodeMirror from 'codemirror';
import {eachTree} from 'amis-core'; import {findTree} from 'amis-core';
import {FormulaEditorProps, VariableItem, FormulaEditor} from './Editor'; import {FuncGroup, VariableItem} from './CodeEditor';
import {parse} from 'amis-formula';
import debounce from 'lodash/debounce';
export function editorFactory( export function editorFactory(
dom: HTMLElement, dom: HTMLElement,
@ -16,30 +18,88 @@ export function editorFactory(
return cm(dom, { return cm(dom, {
value: props.value || '', value: props.value || '',
autofocus: true, autofocus: false,
mode: props.evalMode ? 'text/formula' : 'text/formula-template', mode: props.evalMode ? 'text/formula' : 'text/formula-template',
readOnly: props.readOnly ? 'nocursor' : false,
...options ...options
}); });
} }
export class FormulaPlugin { function traverseAst(ast: any, iterator: (ast: any) => void | false) {
constructor( if (!ast || !ast.type) {
readonly editor: CodeMirror.Editor, return;
readonly cm: typeof CodeMirror,
readonly getProps: () => FormulaEditorProps
) {
// editor.on('change', this.autoMarkText);
this.autoMarkText();
} }
autoMarkText() { const ret = iterator(ast);
const {functions, variables, value} = this.getProps(); if (ret === false) {
return;
}
if (value) { Object.keys(ast).forEach(key => {
// todo functions 也需要自动替换 const value = ast[key];
this.autoMark(variables!);
this.focus(value); 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
) {
// this.autoMarkText();
this.autoMarkText = debounce(this.autoMarkText.bind(this), 250, {
leading: false,
trailing: true
});
editor.on('blur', () => this.autoMarkText());
}
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 } ] // 计算 `${`、`}` 括号的位置,如 ${a}+${b}, 结果是 [ { from: 0, to: 3 }, { from: 5, to: 8 } ]
@ -108,50 +168,19 @@ export class FormulaPlugin {
} }
} }
insertContent( insertContent(value: any, type?: 'variable' | 'func') {
value: any,
type?: 'variable' | 'func',
className: string = 'cm-field',
toMark: boolean = true
) {
let from = this.editor.getCursor(); let from = this.editor.getCursor();
const {evalMode} = this.getProps(); const evalMode = this.evalMode;
if (type === 'variable') { if (type === 'variable') {
this.editor.replaceSelection(value.key); this.editor.replaceSelection(value.key);
const to = this.editor.getCursor(); 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); !evalMode && this.insertBraces(from, to);
} else if (type === 'func') { } else if (type === 'func') {
this.editor.replaceSelection(`${value}()`); this.editor.replaceSelection(`${value}()`);
const to = this.editor.getCursor(); const to = this.editor.getCursor();
toMark &&
this.markText(
from,
{
line: to.line,
ch: to.ch - 2
},
value,
'cm-func'
);
this.editor.setCursor({ this.editor.setCursor({
line: to.line, line: to.line,
ch: to.ch - 1 ch: to.ch - 1
@ -167,7 +196,6 @@ export class FormulaPlugin {
} else if (typeof value === 'string') { } else if (typeof value === 'string') {
this.editor.replaceSelection(value); this.editor.replaceSelection(value);
// 非变量、非函数,可能是组合模式,也需要标记 // 非变量、非函数,可能是组合模式,也需要标记
toMark && setTimeout(() => this.autoMarkText(), 0);
} }
this.editor.focus(); this.editor.focus();
@ -185,93 +213,190 @@ export class FormulaPlugin {
from: CodeMirror.Position, from: CodeMirror.Position,
to: CodeMirror.Position, to: CodeMirror.Position,
label: string, label: string,
className = 'cm-func' className = 'cm-func',
rawString?: string
) { ) {
const text = document.createElement('span'); const text = document.createElement('span');
text.className = className; text.className = className;
text.innerText = label; text.innerText = label;
this.editor.markText(from, to, {
if (rawString) {
text.setAttribute('data-tooltip', rawString);
text.setAttribute('data-position', 'bottom');
}
return this.editor.markText(from, to, {
atomic: true, atomic: true,
replacedWith: text replacedWith: text
}); });
} }
autoMark(variables: Array<VariableItem>) { widgets: any[] = [];
if (!Array.isArray(variables) || !variables.length) { marks: any[] = [];
return; autoMark() {
}
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);
const editor = this.editor; const editor = this.editor;
const lines = editor.lineCount(); const value = editor.getValue();
const {evalMode = true} = this.getProps(); const functions = this.functions;
for (let line = 0; line < lines; line++) { const variables = this.variables;
const content = editor.getLine(line);
// 标记方法调用 // 把旧的清掉
content.replace(/([A-Z]+)\s*\(/g, (_, func, pos) => { this.widgets.forEach(widget => editor.removeLineWidget(widget));
this.markText( this.widgets = [];
{
line: line, this.marks.forEach(mark => mark.clear());
ch: pos this.marks = [];
},
{ try {
line: line, const ast = parse(value, {
ch: pos + func.length evalMode: this.evalMode,
}, variableMode: false
func,
'cm-func'
);
return _;
}); });
traverseAst(ast, (ast: any): any => {
const REPLACE_KEY = 'AMIS_FORMULA_REPLACE_KEY'; if (ast.type === 'func_call') {
// 标记变量 const funName = ast.identifier;
vars.forEach(v => { const exists = functions.some(item =>
let from = 0; item.items.some(i => i.name === funName)
let idx = -1;
while (~(idx = content.indexOf(v, from))) {
const encode = FormulaEditor.replaceStrByIndex(
content,
idx,
v,
REPLACE_KEY
); );
const reg = FormulaEditor.getRegExpByMode(evalMode, REPLACE_KEY); if (exists) {
this.markText(
if (reg.test(encode)) { {
let markFrom = idx; line: ast.start.line - 1,
v.split('.').forEach((val: string, index: number) => { ch: ast.start.column - 1
},
{
line: ast.start.line - 1,
ch: ast.start.column + funName.length - 1
},
funName,
'cm-func'
);
}
} 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
);
if (variable) {
// 先标记顶层对象
this.markText( this.markText(
{ {
line: line, line: host.start.line - 1,
ch: markFrom ch: host.start.column - 1
}, },
{ {
line: line, line: host.end.line - 1,
ch: markFrom + val.length ch: host.end.column - 1
}, },
varMap[v].split('.')[index], variable.label,
'cm-field' 'cm-field',
host.name
); );
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',
item.name
);
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',
ast.name
);
}
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 +408,9 @@ export class FormulaPlugin {
}); });
} }
dispose() {} dispose() {
(this.autoMarkText as any).cancel();
}
validate() {} validate() {}
} }
@ -295,6 +422,8 @@ function registerLaunguageMode(cm: typeof CodeMirror) {
} }
modeRegisted = true; modeRegisted = true;
// TODO 自定义语言规则
// 对应 evalMode // 对应 evalMode
cm.defineMode('formula', (config: any, parserConfig: any) => { cm.defineMode('formula', (config: any, parserConfig: any) => {
var formula = cm.getMode(config, 'javascript'); var formula = cm.getMode(config, 'javascript');
@ -306,7 +435,6 @@ function registerLaunguageMode(cm: typeof CodeMirror) {
mode: formula mode: formula
}); });
}); });
cm.defineMIME('text/formula', {name: 'formula'}); cm.defineMIME('text/formula', {name: 'formula'});
cm.defineMIME('text/formula-template', {name: 'formula', base: 'htmlmixed'}); cm.defineMIME('text/formula-template', {name: 'formula', base: 'htmlmixed'});
} }

View File

@ -74,7 +74,8 @@ import SchemaVariableList from './schema-editor/SchemaVariableList';
import VariableList from './formula/VariableList'; import VariableList from './formula/VariableList';
import FormulaPicker from './formula/Picker'; import FormulaPicker from './formula/Picker';
import {FormulaEditor} from './formula/Editor'; 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 PickerContainer from './PickerContainer';
import InputJSONSchema from './json-schema'; import InputJSONSchema from './json-schema';
import {Badge, withBadge} from './Badge'; import {Badge, withBadge} from './Badge';
@ -202,6 +203,7 @@ export {
PickerContainer, PickerContainer,
ConfirmBox, ConfirmBox,
FormulaPicker, FormulaPicker,
FormulaCodeEditor,
VariableItem, VariableItem,
FormulaEditor, FormulaEditor,
InputJSONSchema, InputJSONSchema,

View File

@ -8,7 +8,7 @@ import {
themeable, themeable,
ThemeProps ThemeProps
} from 'amis-core'; } from 'amis-core';
import {VariableItem} from '../formula/Editor'; import {VariableItem} from '../formula/CodeEditor';
import VariableList from '../formula/VariableList'; import VariableList from '../formula/VariableList';
import TooltipWrapper from '../TooltipWrapper'; import TooltipWrapper from '../TooltipWrapper';

View File

@ -393,6 +393,11 @@ register('de-DE', {
'expand': 'Entfalten', 'expand': 'Entfalten',
'FormulaEditor.btnLabel': 'Formel Bearbeiten', 'FormulaEditor.btnLabel': 'Formel Bearbeiten',
'FormulaEditor.title': 'Formel Editor', '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.variable': 'Variable',
'FormulaEditor.function': 'Funktion', 'FormulaEditor.function': 'Funktion',
'FormulaEditor.invalidData': 'FormulaEditor.invalidData':

View File

@ -377,6 +377,11 @@ register('en-US', {
'expand': 'Expand', 'expand': 'Expand',
'FormulaEditor.btnLabel': 'Formula Edit', 'FormulaEditor.btnLabel': 'Formula Edit',
'FormulaEditor.title': 'Formula Editor', '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.variable': 'Variable',
'FormulaEditor.function': 'Function', 'FormulaEditor.function': 'Function',
'FormulaEditor.invalidData': 'invalid data, position or reason is {{err}}', 'FormulaEditor.invalidData': 'invalid data, position or reason is {{err}}',

View File

@ -370,6 +370,11 @@ register('zh-CN', {
'expand': '展开', 'expand': '展开',
'FormulaEditor.btnLabel': '公式编辑', 'FormulaEditor.btnLabel': '公式编辑',
'FormulaEditor.title': '公式编辑器', 'FormulaEditor.title': '公式编辑器',
'FormulaEditor.run': '运行',
'FormulaEditor.sourceMode': '源码模式',
'FormulaEditor.runContext': '上下文数据',
'FormulaEditor.runResult': '运行结果',
'FormulaEditor.toggleAll': '展开全部',
'FormulaEditor.variable': '变量', 'FormulaEditor.variable': '变量',
'FormulaEditor.function': '函数', 'FormulaEditor.function': '函数',
'FormulaEditor.invalidData': '公式值校验错误,错误的位置/原因是 {{err}}', 'FormulaEditor.invalidData': '公式值校验错误,错误的位置/原因是 {{err}}',

View File

@ -131,3 +131,13 @@ Object.defineProperty(global, 'IntersectionObserver', {
configurable: true, configurable: true,
value: IntersectionObserver value: IntersectionObserver
}); });
(global as any).document.createRange = () => ({
selectNodeContents: jest.fn(),
getBoundingClientRect: jest.fn(() => ({
width: 500
})),
getClientRects: jest.fn(() => []),
setStart: jest.fn(),
setEnd: jest.fn()
});

View File

@ -197,7 +197,7 @@ exports[`Renderer:input-formula button 1`] = `
class="cxd-FormulaPicker cxd-FormulaPicker--text cxd-Form-control" class="cxd-FormulaPicker cxd-FormulaPicker--text cxd-Form-control"
> >
<button <button
class="cxd-Button cxd-Button--default cxd-Button--size-default cxd-FormulaPicker-action w-full" class="cxd-Button cxd-Button--default cxd-Button--size-default is-active cxd-FormulaPicker-action w-full"
type="button" type="button"
> >
<span <span
@ -431,23 +431,143 @@ exports[`Renderer:input-formula input-group 1`] = `
class="cxd-FormulaPicker is-input-group cxd-FormulaPicker--text cxd-Form-control" class="cxd-FormulaPicker is-input-group cxd-FormulaPicker--text cxd-Form-control"
> >
<div <div
class="cxd-ResultBox cxd-FormulaPicker-input cxd-ResultBox--borderFull" class="cxd-InputBox cxd-FormulaPicker-input cxd-InputBox--borderFull"
tabindex="-1"
> >
<div <div
class="cxd-ResultBox-value-wrap" class="cxd-FormulaCodeEditor cxd-FormulaCodeEditor--singleLine"
style="position: relative;"
> >
<input <div
class="cxd-ResultBox-value-input" class="CodeMirror cm-s-idea"
placeholder="暂无数据" translate="no"
theme="cxd" >
type="text" <div
value="SUM(1 + 2)" 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="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> </div>
<div <a
class="cxd-ResultBox-actions" class="cxd-InputBox-clear"
/> >
<icon-mock
classname="icon icon-input-clear"
icon="input-clear"
/>
</a>
</div> </div>
<a <a
class="cxd-FormulaPicker-toggler" class="cxd-FormulaPicker-toggler"

View File

@ -14,11 +14,17 @@
*/ */
import React from 'react'; 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 '../../../src';
import {render as amisRender, clearStoresCache} from '../../../src'; import {render as amisRender, clearStoresCache} from '../../../src';
import {makeEnv, replaceReactAriaIds, wait} from '../../helper'; 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(() => { afterEach(() => {
cleanup(); cleanup();
@ -818,218 +824,248 @@ test('Renderer:condition-builder with not embed', async () => {
describe('Renderer: condition-builder with formula', () => { describe('Renderer: condition-builder with formula', () => {
const onSubmit = jest.fn(); const onSubmit = jest.fn();
test('condition-builder with different fields', async () => { test('condition-builder with different fields', async () => {
const {container} = render(amisRender({ const {container} = render(
"type": "form", amisRender(
"data": {
"conditions": {
"id": "68bddc1495e9",
"conjunction": "and",
"children": [
{
"id": "b9cc34dae93a",
"left": {
"type": "field",
"field": "text"
},
"op": "equal"
},
{
"id": "4c718986c321",
"left": {
"type": "field",
"field": "number"
},
"op": "equal"
},
{
"id": "7ee79c416422",
"left": {
"type": "field",
"field": "boolean"
},
"op": "equal"
},
{
"id": "9cd76d8a6522",
"left": {
"type": "field",
"field": "select"
},
"op": "select_equals"
},
{
"id": "20a65e9df546",
"left": {
"type": "field",
"field": "date"
},
"op": "equal"
},
{
"id": "e729b32ea9e8",
"left": {
"type": "field",
"field": "time"
},
"op": "equal"
},
{
"id": "a5f48e000557",
"left": {
"type": "field",
"field": "datetime"
},
"op": "equal"
}
]
}
},
"body": [
{ {
"type": "condition-builder", type: 'form',
"label": "条件组件", data: {
"name": "conditions", conditions: {
"searchable": true, id: '68bddc1495e9',
"formula": { conjunction: 'and',
"mode": "input-group", children: [
"inputSettings": {},
"allowInput": true,
"mixedMode": true,
"variables": []
},
"fields": [
{
"label": "文本",
"type": "text",
"name": "text"
},
{
"label": "数字",
"type": "number",
"name": "number"
},
{
"label": "布尔",
"type": "boolean",
"name": "boolean"
},
{
"label": "选项",
"type": "select",
"name": "select",
"options": [
{ {
"label": "A", id: 'b9cc34dae93a',
"value": "a" left: {
type: 'field',
field: 'text'
},
op: 'equal'
}, },
{ {
"label": "B", id: '4c718986c321',
"value": "b" left: {
type: 'field',
field: 'number'
},
op: 'equal'
}, },
{ {
"label": "C", id: '7ee79c416422',
"value": "c" left: {
type: 'field',
field: 'boolean'
},
op: 'equal'
},
{
id: '9cd76d8a6522',
left: {
type: 'field',
field: 'select'
},
op: 'select_equals'
},
{
id: '20a65e9df546',
left: {
type: 'field',
field: 'date'
},
op: 'equal'
},
{
id: 'e729b32ea9e8',
left: {
type: 'field',
field: 'time'
},
op: 'equal'
},
{
id: 'a5f48e000557',
left: {
type: 'field',
field: 'datetime'
},
op: 'equal'
} }
] ]
}, }
},
body: [
{ {
"label": "日期", type: 'condition-builder',
"children": [ label: '条件组件',
name: 'conditions',
searchable: true,
formula: {
mode: 'input-group',
inputSettings: {},
allowInput: true,
mixedMode: true,
variables: []
},
fields: [
{ {
"label": "日期", label: '文本',
"type": "date", type: 'text',
"name": "date" name: 'text'
}, },
{ {
"label": "时间", label: '数字',
"type": "time", type: 'number',
"name": "time" name: 'number'
}, },
{ {
"label": "日期时间", label: '布尔',
"type": "datetime", type: 'boolean',
"name": "datetime" name: 'boolean'
},
{
label: '选项',
type: 'select',
name: 'select',
options: [
{
label: 'A',
value: 'a'
},
{
label: 'B',
value: 'b'
},
{
label: 'C',
value: 'c'
}
]
},
{
label: '日期',
children: [
{
label: '日期',
type: 'date',
name: 'date'
},
{
label: '时间',
type: 'time',
name: 'time'
},
{
label: '日期时间',
type: 'datetime',
name: 'datetime'
}
]
} }
] ]
} }
] ]
} },
] {onSubmit},
}, {onSubmit}, makeEnv({}))); makeEnv({})
)
);
replaceReactAriaIds(container); replaceReactAriaIds(container);
// 7种类型都存在 // 7种类型都存在
expect(container.querySelectorAll('.cxd-FormulaPicker-input')?.length).toEqual(7); expect(
expect(container.querySelector('.cxd-FormulaPicker--text')).toBeInTheDocument(); container.querySelectorAll('.cxd-FormulaPicker-input')?.length
expect(container.querySelector('.cxd-FormulaPicker-input-number')).toBeInTheDocument(); ).toEqual(7);
expect(container.querySelector('.cxd-FormulaPicker-input-boolean')).toBeInTheDocument(); expect(
expect(container.querySelector('.cxd-FormulaPicker-input-select')).toBeInTheDocument(); container.querySelector('.cxd-FormulaPicker--text')
expect(container.querySelector('.cxd-FormulaPicker-input-date')).toBeInTheDocument(); ).toBeInTheDocument();
expect(container.querySelector('.cxd-FormulaPicker-input-time')).toBeInTheDocument(); expect(
expect(container.querySelector('.cxd-FormulaPicker-input-datetime')).toBeInTheDocument(); 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 () => { test('condition-builder with select field and change operator', async () => {
const {container, findByText} = render(amisRender({ const {container, findByText} = render(
"type": "form", amisRender(
"data": {
"conditions": {
"id": "68bddc1495e9",
"conjunction": "and",
"children": [
{
"id": "9cd76d8a6522",
"left": {
"type": "field",
"field": "select"
},
"op": "select_equals"
}
]
}
},
"body": [
{ {
"type": "condition-builder", type: 'form',
"label": "条件组件", data: {
"name": "conditions", conditions: {
"searchable": true, id: '68bddc1495e9',
"formula": { conjunction: 'and',
"mode": "input-group", children: [
"inputSettings": {}, {
"allowInput": true, id: '9cd76d8a6522',
"mixedMode": true, left: {
"variables": [] type: 'field',
field: 'select'
},
op: 'select_equals'
}
]
}
}, },
"fields": [ body: [
{ {
"label": "选项", type: 'condition-builder',
"type": "select", label: '条件组件',
"name": "select", name: 'conditions',
"options": [ searchable: true,
formula: {
mode: 'input-group',
inputSettings: {},
allowInput: true,
mixedMode: true,
variables: []
},
fields: [
{ {
"label": "A", label: '选项',
"value": "a" type: 'select',
}, name: 'select',
{ options: [
"label": "B", {
"value": "b" label: 'A',
}, value: 'a'
{ },
"label": "C", {
"value": "c" label: 'B',
value: 'b'
},
{
label: 'C',
value: 'c'
}
]
} }
] ]
} }
] ]
} },
] {},
}, {}, makeEnv({}))); makeEnv({})
)
);
replaceReactAriaIds(container); replaceReactAriaIds(container);
// 选中第一个选项Form中默认值是等于操作 // 选中第一个选项Form中默认值是等于操作
let fieldValueControl = container.querySelector('.cxd-FormulaPicker-input-select')!; let fieldValueControl = container.querySelector(
'.cxd-FormulaPicker-input-select'
)!;
fireEvent.click(fieldValueControl); fireEvent.click(fieldValueControl);
await wait(100); await wait(100);
fireEvent.click(await findByText('A')); fireEvent.click(await findByText('A'));
@ -1041,85 +1077,101 @@ describe('Renderer: condition-builder with formula', () => {
await wait(100); await wait(100);
fireEvent.click(await findByText('包含')); fireEvent.click(await findByText('包含'));
await wait(100); await wait(100);
expect(container.querySelector('.cxd-Select-placeholder')).toBeInTheDocument(); expect(
fieldValueControl = container.querySelector('.cxd-FormulaPicker-input-select')!; container.querySelector('.cxd-Select-placeholder')
).toBeInTheDocument();
fieldValueControl = container.querySelector(
'.cxd-FormulaPicker-input-select'
)!;
fireEvent.click(fieldValueControl); fireEvent.click(fieldValueControl);
await wait(100); 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 () => { test('condition-builder with field type change', async () => {
const onSubmit = jest.fn(); const onSubmit = jest.fn();
const {container, findByText, findByPlaceholderText} = render(amisRender({ const {container, findByText, findByPlaceholderText} = render(
"type": "form", amisRender(
"data": {
"conditions": {
"id": "68bddc1495e9",
"conjunction": "and",
"children": [
{
"id": "b9cc34dae93a",
"left": {
"type": "field",
"field": "text"
},
"op": "equal"
}
]
}
},
"body": [
{ {
"type": "condition-builder", type: 'form',
"label": "条件组件", data: {
"name": "conditions", conditions: {
"searchable": true, id: '68bddc1495e9',
"formula": { conjunction: 'and',
"mode": "input-group", children: [
"inputSettings": {}, {
"allowInput": true, id: 'b9cc34dae93a',
"mixedMode": true, left: {
"variables": [] type: 'field',
field: 'text'
},
op: 'equal'
}
]
}
}, },
"fields": [ body: [
{ {
"label": "文本", type: 'condition-builder',
"type": "text", label: '条件组件',
"name": "text" name: 'conditions',
}, searchable: true,
{ formula: {
"label": "选项", mode: 'input-group',
"type": "select", inputSettings: {},
"name": "select", allowInput: true,
"options": [ mixedMode: true,
variables: []
},
fields: [
{ {
"label": "A", label: '文本',
"value": "a" type: 'text',
name: 'text'
}, },
{ {
"label": "B", label: '选项',
"value": "b" type: 'select',
}, name: 'select',
{ options: [
"label": "C", {
"value": "c" label: 'A',
value: 'a'
},
{
label: 'B',
value: 'b'
},
{
label: 'C',
value: 'c'
}
]
} }
] ]
} }
] ]
} },
] {onSubmit},
}, {onSubmit}, makeEnv({}))); makeEnv({})
)
);
replaceReactAriaIds(container); replaceReactAriaIds(container);
// 切换字段类型,对应字段值控件更新 // 切换字段类型,对应字段值控件更新
const fieldControl = container.querySelector('.cxd-DropDownSelection-input')!; const fieldControl = container.querySelector(
'.cxd-DropDownSelection-input'
)!;
fireEvent.click(fieldControl); fireEvent.click(fieldControl);
await wait(100); await wait(100);
fireEvent.click(await findByText('选项')); fireEvent.click(await findByText('选项'));
await wait(100); await wait(100);
let selectValueControl = container.querySelector('.cxd-FormulaPicker-input-select')!; let selectValueControl = container.querySelector(
'.cxd-FormulaPicker-input-select'
)!;
expect(selectValueControl).toBeInTheDocument(); expect(selectValueControl).toBeInTheDocument();
// 切换操作符,下拉选项变成多选 // 切换操作符,下拉选项变成多选
@ -1128,11 +1180,17 @@ describe('Renderer: condition-builder with formula', () => {
await wait(100); await wait(100);
fireEvent.click(await findByText('包含')); fireEvent.click(await findByText('包含'));
await wait(100); await wait(100);
expect(container.querySelector('.cxd-Select-placeholder')).toBeInTheDocument(); expect(
selectValueControl = container.querySelector('.cxd-FormulaPicker-input-select')!; container.querySelector('.cxd-Select-placeholder')
).toBeInTheDocument();
selectValueControl = container.querySelector(
'.cxd-FormulaPicker-input-select'
)!;
fireEvent.click(selectValueControl); fireEvent.click(selectValueControl);
await wait(100); await wait(100);
expect(container.querySelectorAll('.cxd-Select-option-checkbox').length).toEqual(3); expect(
container.querySelectorAll('.cxd-Select-option-checkbox').length
).toEqual(3);
// 选择2个选项绑定值变化 // 选择2个选项绑定值变化
fireEvent.click(await findByText('A')); fireEvent.click(await findByText('A'));
@ -1145,4 +1203,4 @@ describe('Renderer: condition-builder with formula', () => {
expect(selectedValues.length).toEqual(2); expect(selectedValues.length).toEqual(2);
expect(selectedValues.join(',')).toEqual('A,C'); expect(selectedValues.join(',')).toEqual('A,C');
}); });
}) });

View File

@ -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 后不支持 // TODO: 不知道为啥切换到 @swc/jest 后不支持
// expect(container).toMatchSnapshot(); // expect(container).toMatchSnapshot();

View File

@ -398,6 +398,7 @@ export default class Dialog extends React.Component<DialogProps> {
statusStore && isAlive(statusStore) && statusStore.resetAll(); statusStore && isAlive(statusStore) && statusStore.resetAll();
if (isAlive(store)) { if (isAlive(store)) {
store.reset(); store.reset();
store.clearMessage();
store.setEntered(false); store.setEntered(false);
if (typeof lazySchema === 'function') { if (typeof lazySchema === 'function') {
store.setSchema(''); store.setSchema('');
@ -938,15 +939,16 @@ export class DialogRenderer extends Dialog {
store.updateMessage(reason.message, true); store.updateMessage(reason.message, true);
store.markBusying(false); store.markBusying(false);
if (reason.constructor?.name === ValidateError.name) { // 通常都是数据错误,过 3 秒自动清理错误信息
clearTimeout(this.clearErrorTimer); // if (reason.constructor?.name === ValidateError.name) {
this.clearErrorTimer = setTimeout(() => { clearTimeout(this.clearErrorTimer);
if (this.isDead) { this.clearErrorTimer = setTimeout(() => {
return; if (this.isDead) {
} return;
store.updateMessage(''); }
}, 3000); store.updateMessage('');
} }, 3000);
// }
}); });
return true; return true;

View File

@ -448,6 +448,7 @@ export default class Drawer extends React.Component<DrawerProps> {
statusStore && isAlive(statusStore) && statusStore.resetAll(); statusStore && isAlive(statusStore) && statusStore.resetAll();
if (isAlive(store)) { if (isAlive(store)) {
store.reset(); store.reset();
store.clearMessage();
store.setEntered(false); store.setEntered(false);
if (typeof lazySchema === 'function') { if (typeof lazySchema === 'function') {
store.setSchema(''); store.setSchema('');
@ -891,12 +892,13 @@ export class DrawerRenderer extends Drawer {
store.updateMessage(reason.message, true); store.updateMessage(reason.message, true);
store.markBusying(false); store.markBusying(false);
if (reason.constructor?.name === ValidateError.name) { // 通常都是数据错误,过 3 秒自动清理错误信息
clearTimeout(this.clearErrorTimer); // if (reason.constructor?.name === ValidateError.name) {
this.clearErrorTimer = setTimeout(() => { clearTimeout(this.clearErrorTimer);
store.updateMessage(''); this.clearErrorTimer = setTimeout(() => {
}, 3000); store.updateMessage('');
} }, 3000);
// }
}); });
return true; return true;

View File

@ -8,7 +8,7 @@ import {isPureVariable, resolveVariableAndFilter} from 'amis-core';
import type { import type {
FuncGroup, FuncGroup,
VariableItem 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'; import type {FormulaPickerInputSettings} from 'amis-ui/lib/components/formula/Picker';
/** /**
@ -227,7 +227,6 @@ export class InputFormulaRenderer extends React.Component<InputFormulaProps> {
className={className} className={className}
value={value} value={value}
disabled={disabled} disabled={disabled}
allowInput={allowInput}
onChange={onChange} onChange={onChange}
evalMode={evalMode} evalMode={evalMode}
variables={variables} variables={variables}