mirror of
https://gitee.com/baidu/amis.git
synced 2024-12-02 03:58:07 +08:00
chore: 公式编辑器 input 换成 codemirror 提升交互体验 (#9821)
This commit is contained in:
parent
95787b98ef
commit
90ced059b1
@ -102,8 +102,8 @@ order: 21
|
|||||||
"name": "formula",
|
"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 时则为模板模式,意思是说默认不当做表达式,只有 `${`和`}`包裹的部分才是表达式。
|
||||||
|
@ -59,7 +59,10 @@ export function StatusScoped<
|
|||||||
} = {
|
} = {
|
||||||
statusStore: this.store!
|
statusStore: this.store!
|
||||||
};
|
};
|
||||||
const refConfig = ComposedComponent.prototype?.isReactComponent
|
const refConfig =
|
||||||
|
ComposedComponent.prototype?.isReactComponent ||
|
||||||
|
(ComposedComponent as any).$$typeof ===
|
||||||
|
Symbol.for('react.forward_ref')
|
||||||
? {ref: this.childRef}
|
? {ref: this.childRef}
|
||||||
: {forwardedRef: this.childRef};
|
: {forwardedRef: this.childRef};
|
||||||
|
|
||||||
|
@ -162,7 +162,10 @@ export function localeable<
|
|||||||
translate: translate!
|
translate: translate!
|
||||||
};
|
};
|
||||||
moment.locale(momentLocaleMap?.[locale] ?? locale);
|
moment.locale(momentLocaleMap?.[locale] ?? locale);
|
||||||
const refConfig = ComposedComponent.prototype?.isReactComponent
|
const refConfig =
|
||||||
|
ComposedComponent.prototype?.isReactComponent ||
|
||||||
|
(ComposedComponent as any).$$typeof ===
|
||||||
|
Symbol.for('react.forward_ref')
|
||||||
? {ref: this.childRef}
|
? {ref: this.childRef}
|
||||||
: {forwardedRef: this.childRef};
|
: {forwardedRef: this.childRef};
|
||||||
|
|
||||||
|
@ -182,7 +182,10 @@ export function themeable<
|
|||||||
classnames: config.classnames,
|
classnames: config.classnames,
|
||||||
theme
|
theme
|
||||||
};
|
};
|
||||||
const refConfig = ComposedComponent.prototype?.isReactComponent
|
const refConfig =
|
||||||
|
ComposedComponent.prototype?.isReactComponent ||
|
||||||
|
(ComposedComponent as any).$$typeof ===
|
||||||
|
Symbol.for('react.forward_ref')
|
||||||
? {ref: this.childRef}
|
? {ref: this.childRef}
|
||||||
: {forwardedRef: this.childRef};
|
: {forwardedRef: this.childRef};
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用于判断是否需要执行表达式:
|
// 用于判断是否需要执行表达式:
|
||||||
|
@ -38,4 +38,6 @@
|
|||||||
}
|
}
|
||||||
.btn-configured-tooltip {
|
.btn-configured-tooltip {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 320px;
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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(
|
||||||
'停止条件',
|
'停止条件',
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
<div className="ae-editor-FormulaControl-tooltipBox">
|
readOnly
|
||||||
<div
|
|
||||||
className="ae-editor-FormulaControl-ResultBox-wrapper"
|
|
||||||
onClick={this.handleFormulaClick}
|
|
||||||
>
|
|
||||||
<ResultBox
|
|
||||||
className={cx(
|
|
||||||
'ae-editor-FormulaControl-ResultBox',
|
|
||||||
isError ? 'is-error' : ''
|
|
||||||
)}
|
|
||||||
allowInput={false}
|
|
||||||
value={value}
|
value={value}
|
||||||
result={{
|
variables={variables}
|
||||||
html: this.hasDateShortcutkey(value)
|
evalMode={false}
|
||||||
? value
|
editorTheme="dark"
|
||||||
: highlightValue?.html
|
|
||||||
}}
|
|
||||||
itemRender={renderFormulaValue}
|
|
||||||
onChange={this.handleInputChange}
|
|
||||||
onResultChange={() => {
|
|
||||||
this.handleInputChange(undefined);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
)
|
||||||
{value && (
|
}}
|
||||||
<Icon
|
>
|
||||||
icon="input-clear"
|
<div className={cx('ae-editor-FormulaControl-tooltipBox')}>
|
||||||
className="input-clear-icon"
|
<InputBox
|
||||||
onClick={() => this.handleInputChange('')}
|
onClick={this.handleFormulaClick}
|
||||||
|
hasError={isError}
|
||||||
|
inputRender={({value, onChange, onFocus, onBlur}: any) => (
|
||||||
|
<FormulaCodeEditor
|
||||||
|
singleLine
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
functions={[]}
|
||||||
|
variables={variables}
|
||||||
|
evalMode={false}
|
||||||
|
readOnly
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
value={value}
|
||||||
|
onChange={this.handleInputChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TooltipWrapper>
|
</TooltipWrapper>
|
||||||
)}
|
)}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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';
|
||||||
|
@ -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 {
|
||||||
|
@ -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,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.#{$ns}CBItem {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
> * {
|
||||||
|
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: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.#{$ns}CBFormula {
|
.#{$ns}CBFormula {
|
||||||
@ -461,6 +477,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,7 @@
|
|||||||
.#{$ns}Number-handler-wrap {
|
.#{$ns}Number-handler-wrap {
|
||||||
height: unset; /* 避免调节器超出Input框 */
|
height: unset; /* 避免调节器超出Input框 */
|
||||||
}
|
}
|
||||||
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-variable {
|
&-variable {
|
||||||
@ -575,3 +607,67 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.#{$ns}FormulaCodeEditor {
|
||||||
|
.cm-field,
|
||||||
|
.cm-func {
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-field {
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cm-field {
|
||||||
|
background: #007bff;
|
||||||
|
}
|
||||||
|
.cm-func {
|
||||||
|
color: #ae4597;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 14px;
|
||||||
|
}
|
||||||
|
.cm-error-token {
|
||||||
|
background-position: left bottom;
|
||||||
|
background-repeat: repeat-x;
|
||||||
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==);
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-placeholder {
|
||||||
|
color: var(--Form-input-placeholderColor) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lint-error {
|
||||||
|
color: var(--Form-input-onError-borderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--singleLine {
|
||||||
|
max-width: 100%;
|
||||||
|
> .CodeMirror {
|
||||||
|
height: 21px;
|
||||||
|
|
||||||
|
.CodeMirror-hscrollbar,
|
||||||
|
.CodeMirror-vscrollbar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.CodeMirror-sizer {
|
||||||
|
min-height: 21px !important;
|
||||||
|
}
|
||||||
|
.CodeMirror-scroll {
|
||||||
|
height: 21px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
.CodeMirror-lines {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$ns}InputBox > .#{$ns}FormulaCodeEditor {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import React from 'react';
|
import 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/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 +41,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 +59,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 +79,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 +110,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,6 +106,18 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
|
|||||||
>
|
>
|
||||||
{result}
|
{result}
|
||||||
|
|
||||||
|
{typeof inputRender === 'function' ? (
|
||||||
|
inputRender({
|
||||||
|
...rest,
|
||||||
|
value: value ?? '',
|
||||||
|
onChange: this.handleChange as any,
|
||||||
|
placeholder,
|
||||||
|
onFocus: this.handleFocus,
|
||||||
|
onBlur: this.handleBlur,
|
||||||
|
disabled,
|
||||||
|
...buildTestId(testid)
|
||||||
|
})
|
||||||
|
) : (
|
||||||
<Input
|
<Input
|
||||||
{...rest}
|
{...rest}
|
||||||
value={value ?? ''}
|
value={value ?? ''}
|
||||||
@ -115,6 +129,7 @@ export class InputBox extends React.Component<InputBoxProps, InputBoxState> {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
{...buildTestId(testid)}
|
{...buildTestId(testid)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ export interface TooltipObject {
|
|||||||
/**
|
/**
|
||||||
* 浮层出现位置
|
* 浮层出现位置
|
||||||
*/
|
*/
|
||||||
placement?: 'top' | 'right' | 'bottom' | 'left';
|
placement?: 'top' | 'right' | 'bottom' | 'left' | 'auto';
|
||||||
/**
|
/**
|
||||||
* 主题样式
|
* 主题样式
|
||||||
*/
|
*/
|
||||||
@ -95,7 +95,7 @@ export interface TooltipWrapperProps {
|
|||||||
tooltip?: string | TooltipObject;
|
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;
|
||||||
|
235
packages/amis-ui/src/components/formula/CodeEditor.tsx
Normal file
235
packages/amis-ui/src/components/formula/CodeEditor.tsx
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
import {ThemeProps, themeable} from 'amis-core';
|
||||||
|
import React from 'react';
|
||||||
|
import CodeMirrorEditor, {CodeMirrorEditorProps} from '../CodeMirror';
|
||||||
|
import {FormulaPlugin, editorFactory as createEditor} from './plugin';
|
||||||
|
import type CodeMirror from 'codemirror';
|
||||||
|
|
||||||
|
export interface VariableItem {
|
||||||
|
label: string;
|
||||||
|
value?: string;
|
||||||
|
path?: string; // 路径(label)
|
||||||
|
children?: Array<VariableItem>;
|
||||||
|
type?: string;
|
||||||
|
tag?: string;
|
||||||
|
selectMode?: 'tree' | 'tabs';
|
||||||
|
isMember?: boolean; // 是否是数组成员
|
||||||
|
// chunks?: string[]; // 内容块,作为一个整体进行高亮标记
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FuncGroup {
|
||||||
|
groupName: string;
|
||||||
|
items: Array<FuncItem>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FuncItem {
|
||||||
|
name: string; // 函数名
|
||||||
|
example?: string; // 示例
|
||||||
|
description?: string; // 描述
|
||||||
|
[propName: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeEditorProps
|
||||||
|
extends ThemeProps,
|
||||||
|
Omit<CodeMirrorEditorProps, 'style' | 'editorFactory' | 'editorDidMount'> {
|
||||||
|
readOnly?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否为单行模式,默认为 false
|
||||||
|
*/
|
||||||
|
singleLine?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* evalMode 即直接就是表达式,否则
|
||||||
|
* 需要 ${这里面才是表达式}
|
||||||
|
* 默认为 true
|
||||||
|
*/
|
||||||
|
evalMode?: boolean;
|
||||||
|
|
||||||
|
autoFocus?: boolean;
|
||||||
|
|
||||||
|
editorTheme?: 'dark' | 'light';
|
||||||
|
|
||||||
|
editorOptions?: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于提示的变量集合,默认为空
|
||||||
|
*/
|
||||||
|
variables?: Array<VariableItem>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数集合,默认不需要传,即 amis-formula 里面那个函数
|
||||||
|
* 如果有扩充,则需要传。
|
||||||
|
*/
|
||||||
|
functions?: Array<FuncGroup>;
|
||||||
|
|
||||||
|
placeholder?: string;
|
||||||
|
|
||||||
|
editorDidMount?: (
|
||||||
|
cm: typeof CodeMirror,
|
||||||
|
editor: CodeMirror.Editor,
|
||||||
|
plugin: FormulaPlugin
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CodeEditor(props: CodeEditorProps, ref: any) {
|
||||||
|
const {
|
||||||
|
classnames: cx,
|
||||||
|
className,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
editorDidMount,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
functions,
|
||||||
|
variables,
|
||||||
|
evalMode,
|
||||||
|
singleLine,
|
||||||
|
autoFocus,
|
||||||
|
editorTheme,
|
||||||
|
theme: defaultTheme,
|
||||||
|
editorOptions,
|
||||||
|
placeholder
|
||||||
|
} = props;
|
||||||
|
const pluginRef = React.useRef<FormulaPlugin>();
|
||||||
|
|
||||||
|
const editorFactory = React.useCallback((dom: HTMLElement, cm: any) => {
|
||||||
|
let theme =
|
||||||
|
(editorTheme ??
|
||||||
|
((defaultTheme || '').includes('dark') ? 'dark' : 'light')) === 'dark'
|
||||||
|
? 'base16-dark'
|
||||||
|
: 'default';
|
||||||
|
let options: any = {
|
||||||
|
autoFocus,
|
||||||
|
indentUnit: 2,
|
||||||
|
lineNumbers: true,
|
||||||
|
lineWrapping: true, // 自动换行
|
||||||
|
theme,
|
||||||
|
placeholder,
|
||||||
|
...editorOptions
|
||||||
|
};
|
||||||
|
if (singleLine) {
|
||||||
|
options = {
|
||||||
|
lineNumbers: false,
|
||||||
|
indentWithTabs: false,
|
||||||
|
indentUnit: 4,
|
||||||
|
lineWrapping: false,
|
||||||
|
scrollbarStyle: null,
|
||||||
|
theme,
|
||||||
|
placeholder,
|
||||||
|
...editorOptions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return createEditor(dom, cm, props, options);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [readOnly, setReadOnly] = React.useState(props.readOnly);
|
||||||
|
|
||||||
|
React.useEffect(() => setReadOnly(props.readOnly), [props.readOnly]);
|
||||||
|
React.useEffect(
|
||||||
|
() => pluginRef.current?.editor?.setOption('placeholder', placeholder),
|
||||||
|
[placeholder]
|
||||||
|
);
|
||||||
|
|
||||||
|
// singleLine 模式下,禁止输入换行符
|
||||||
|
const onEditorBeforeChange = React.useCallback((cm: any, event: any) => {
|
||||||
|
// Identify typing events that add a newline to the buffer.
|
||||||
|
const hasTypedNewline =
|
||||||
|
event.origin === '+input' &&
|
||||||
|
typeof event.text === 'object' &&
|
||||||
|
event.text.join('') === '';
|
||||||
|
|
||||||
|
// Prevent newline characters from being added to the buffer.
|
||||||
|
if (hasTypedNewline) {
|
||||||
|
return event.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identify paste events.
|
||||||
|
const hasPastedNewline =
|
||||||
|
event.origin === 'paste' &&
|
||||||
|
typeof event.text === 'object' &&
|
||||||
|
event.text.length > 1;
|
||||||
|
|
||||||
|
// Format pasted text to replace newlines with spaces.
|
||||||
|
if (hasPastedNewline) {
|
||||||
|
const newText = event.text.join(' ');
|
||||||
|
return event.update(null, null, [newText]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onEditorMount = React.useCallback(
|
||||||
|
(cm: any, editor: any) => {
|
||||||
|
const plugin = (pluginRef.current = new FormulaPlugin(editor, cm));
|
||||||
|
plugin.setEvalMode(!!evalMode);
|
||||||
|
plugin.setFunctions(functions || []);
|
||||||
|
plugin.setVariables(variables || []);
|
||||||
|
editorDidMount?.(cm, editor, plugin);
|
||||||
|
plugin.autoMarkText();
|
||||||
|
|
||||||
|
// 单行模式,不允许输入换行,同时原来的换行符也要去掉
|
||||||
|
if (singleLine) {
|
||||||
|
editor.on('beforeChange', onEditorBeforeChange);
|
||||||
|
|
||||||
|
const value = editor.getValue();
|
||||||
|
if (value && /[\n\r]/.test(value)) {
|
||||||
|
// 初始数据有换行,不允许直接编辑
|
||||||
|
// 只能弹窗弹出非单行模式编辑
|
||||||
|
setReadOnly(true);
|
||||||
|
editor.setValue(value.replace(/[\n\r]+/g, ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[evalMode, functions, variables]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
pluginRef.current?.editor.off('beforeChange', onEditorBeforeChange);
|
||||||
|
pluginRef.current?.dispose();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => {
|
||||||
|
return {
|
||||||
|
insertContent: (value: any, type: 'variable' | 'func') =>
|
||||||
|
pluginRef.current?.insertContent(value, type),
|
||||||
|
setValue: (value: any) => pluginRef.current?.setValue(value),
|
||||||
|
getValue: () => pluginRef.current?.getValue(),
|
||||||
|
setDisableAutoMark: (value: boolean) =>
|
||||||
|
pluginRef.current?.setDisableAutoMark(value)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const plugin = pluginRef.current;
|
||||||
|
if (!plugin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.setEvalMode(!!evalMode);
|
||||||
|
plugin.setFunctions(functions || []);
|
||||||
|
plugin.setVariables(variables || []);
|
||||||
|
plugin.autoMarkText();
|
||||||
|
}, [evalMode, functions, variables, value]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CodeMirrorEditor
|
||||||
|
className={cx(
|
||||||
|
'FormulaCodeEditor',
|
||||||
|
className,
|
||||||
|
singleLine ? 'FormulaCodeEditor--singleLine' : ''
|
||||||
|
)}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
editorFactory={editorFactory}
|
||||||
|
editorDidMount={onEditorMount}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
readOnly={readOnly}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default themeable(React.forwardRef(CodeEditor));
|
@ -2,50 +2,43 @@
|
|||||||
* @file 公式编辑器
|
* @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
|
|
||||||
): void {
|
|
||||||
if (prevProps.variables !== this.props.variables) {
|
|
||||||
this.normalizeVariables(this.props.variables as VariableItem[]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.functions !== this.props.functions) {
|
|
||||||
this.buildFunctions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.editorPlugin?.dispose();
|
|
||||||
this.unmounted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async buildFunctions() {
|
|
||||||
const {doc} = await import('amis-formula/lib/doc');
|
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const customFunctions = Array.isArray(this.props.functions)
|
|
||||||
? this.props.functions
|
|
||||||
: [];
|
|
||||||
const functionList = [
|
|
||||||
...FormulaEditor.buildDefaultFunctions(doc),
|
|
||||||
...FormulaEditor.buildCustomFunctions(functionDocs),
|
|
||||||
...customFunctions
|
|
||||||
];
|
|
||||||
this.setState({
|
this.setState({
|
||||||
functions: functionList
|
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 {
|
componentDidUpdate(prevProps: FormulaEditorProps): void {
|
||||||
...item,
|
if (prevProps.functions !== this.props.functions) {
|
||||||
path: `${path}${path ? '.' : ''}${item.label}`,
|
this.setState({
|
||||||
// 自己是数组成员或者父级有数组成员
|
functions: this.props.functions
|
||||||
...(item.isMember || paths.some(item => item.isMember)
|
});
|
||||||
? {
|
|
||||||
memberDepth: paths?.filter((item: any) => item.type === 'array')
|
|
||||||
?.length
|
|
||||||
}
|
}
|
||||||
: {})
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
this.setState({normalizeVariables: list});
|
componentWillUnmount() {
|
||||||
|
this.unmounted = true;
|
||||||
|
(this.runCode as any).cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@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}
|
||||||
/>
|
/>
|
||||||
|
@ -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]);
|
|
||||||
|
|
||||||
function onSearch(term: string) {
|
|
||||||
const filtered = props.data
|
const filtered = props.data
|
||||||
.map(item => {
|
.map(item => {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
items: term
|
items: term
|
||||||
? item.items.filter(item => ~item.name.indexOf(term.toUpperCase()))
|
? item.items.filter(
|
||||||
|
(item: any) => ~item.name.indexOf(term.toUpperCase())
|
||||||
|
)
|
||||||
: item.items
|
: item.items
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter(item => item.items.length);
|
.filter(item => item.items.length);
|
||||||
setFiteredFuncs(filtered);
|
setFiteredFuncs(filtered);
|
||||||
}
|
},
|
||||||
|
[props.data]
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
onSearch(term);
|
||||||
|
}, [props.data]);
|
||||||
|
|
||||||
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
|
||||||
|
@ -13,7 +13,7 @@ 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 Select from '../Select';
|
||||||
import NumberInput from '../NumberInput';
|
import NumberInput from '../NumberInput';
|
||||||
@ -21,6 +21,8 @@ import DatePicker from '../DatePicker';
|
|||||||
import Tag from '../Tag';
|
import Tag from '../Tag';
|
||||||
|
|
||||||
import type {FormulaPickerProps} from './Picker';
|
import type {FormulaPickerProps} from './Picker';
|
||||||
|
import CodeEditor, {FuncGroup, VariableItem} from './CodeEditor';
|
||||||
|
import InputBox from '../InputBox';
|
||||||
|
|
||||||
export interface FormulaInputProps
|
export interface FormulaInputProps
|
||||||
extends Pick<
|
extends Pick<
|
||||||
@ -42,9 +44,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;
|
||||||
|
|
||||||
@ -59,7 +71,7 @@ export interface FormulaInputProps
|
|||||||
itemRender?: (value: any) => JSX.Element | string;
|
itemRender?: (value: any) => JSX.Element | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FormulaInput: React.FC<FormulaInputProps> = props => {
|
const FormulaInput = (props: FormulaInputProps, ref: any) => {
|
||||||
const {
|
const {
|
||||||
translate: __,
|
translate: __,
|
||||||
className,
|
className,
|
||||||
@ -71,6 +83,7 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
|||||||
mixedMode,
|
mixedMode,
|
||||||
value,
|
value,
|
||||||
variables,
|
variables,
|
||||||
|
functions,
|
||||||
inputSettings = {type: 'text'},
|
inputSettings = {type: 'text'},
|
||||||
popOverContainer,
|
popOverContainer,
|
||||||
onChange,
|
onChange,
|
||||||
@ -81,9 +94,16 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
|||||||
const sharedProps = pick(props, ['disabled', 'clearable']);
|
const sharedProps = pick(props, ['disabled', 'clearable']);
|
||||||
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) => {
|
||||||
@ -133,106 +153,9 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
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 +170,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 +186,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 +200,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,11 +216,11 @@ 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}
|
||||||
@ -335,21 +256,25 @@ const FormulaInput: React.FC<FormulaInputProps> = props => {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<ResultBox
|
<InputBox
|
||||||
{...sharedProps}
|
className={cx('FormulaPicker-input')}
|
||||||
className={cx(className)}
|
inputRender={({value, onChange, onFocus, onBlur, placeholder}: any) => (
|
||||||
allowInput={allowInput}
|
<CodeEditor
|
||||||
borderMode={borderMode}
|
singleLine
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
functions={functions}
|
||||||
|
variables={variables}
|
||||||
|
evalMode={evalMode}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
value={pipInValue(value)}
|
/>
|
||||||
result={
|
)}
|
||||||
allowInput || !value
|
borderMode={borderMode}
|
||||||
? void 0
|
value={cmptValue}
|
||||||
: FormulaEditor.highlightValue(value, variables!, evalMode)
|
|
||||||
}
|
|
||||||
itemRender={itemRender}
|
|
||||||
onResultChange={noop}
|
|
||||||
onChange={pipOutValue}
|
onChange={pipOutValue}
|
||||||
|
placeholder={__(placeholder ?? 'placeholder.enter')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -357,7 +282,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'
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -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',
|
||||||
@ -142,6 +139,8 @@ export interface FormulaPickerProps
|
|||||||
*/
|
*/
|
||||||
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 +179,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 +205,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 +232,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,22 +280,11 @@ export class FormulaPicker extends React.Component<
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
||||||
return editorValue;
|
return editorValue;
|
||||||
}
|
|
||||||
|
|
||||||
if (props.mixedMode) {
|
|
||||||
if (
|
|
||||||
typeof props.value === 'string' &&
|
|
||||||
/^\s*\$\{(.+?)\}\s*$/.test(props.value)
|
|
||||||
) {
|
|
||||||
return RegExp.$1;
|
|
||||||
} else {
|
} else {
|
||||||
return '';
|
return value ? (mixedMode ? `\`${value}\`` : value) : '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(props.value || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
isTextInput() {
|
isTextInput() {
|
||||||
const {inputSettings} = this.props;
|
const {inputSettings} = this.props;
|
||||||
|
|
||||||
@ -281,22 +306,6 @@ export class FormulaPicker extends React.Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind
|
|
||||||
renderFormulaValue(item: any) {
|
|
||||||
const {allowInput, classnames: cx} = this.props;
|
|
||||||
const html = {__html: item.html};
|
|
||||||
if (allowInput) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cx('FormulaPicker-ResultBox')}
|
|
||||||
dangerouslySetInnerHTML={html}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@autobind
|
@autobind
|
||||||
handleInputChange(value: string) {
|
handleInputChange(value: string) {
|
||||||
this.setState({value}, () => this.handleConfirm());
|
this.setState({value}, () => this.handleConfirm());
|
||||||
@ -321,22 +330,17 @@ 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()) {
|
if (
|
||||||
return this.confirm(editorValue);
|
inputSettings?.type &&
|
||||||
} else if (inputSettings) {
|
['boolean', 'number'].includes(inputSettings?.type)
|
||||||
|
) {
|
||||||
let result = editorValue;
|
let result = editorValue;
|
||||||
const schemaType = inputSettings?.type;
|
// const schemaType = inputSettings?.type;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const ast = parse(editorValue, {evalMode: true, allowFilter: false});
|
const ast = parse(editorValue, {evalMode: true, allowFilter: false});
|
||||||
|
|
||||||
if (
|
if (ast.type === 'literal' || ast.type === 'string') {
|
||||||
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 ?? '';
|
result = ast.value ?? '';
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -347,19 +351,28 @@ export class FormulaPicker extends React.Component<
|
|||||||
this.setState({isError: false});
|
this.setState({isError: false});
|
||||||
return this.confirm(result);
|
return this.confirm(result);
|
||||||
}
|
}
|
||||||
|
return this.confirm(editorValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
confirm(value: string) {
|
confirm(value: 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 =
|
||||||
|
!value.includes('$') &&
|
||||||
|
value[0] === '`' &&
|
||||||
|
value[value.length - 1] === '`'
|
||||||
|
? value.substring(1, value.length - 1)
|
||||||
|
: `\${${value}}`;
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
this.setState({value: result}, () => {
|
||||||
|
this.close(undefined, () => this.handleConfirm());
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({isError: validate});
|
this.setState({isError: validate});
|
||||||
}
|
}
|
||||||
@ -375,6 +388,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 +447,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
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -529,31 +545,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' : ''
|
||||||
)}
|
)}
|
||||||
|
inputSettings={inputSettings}
|
||||||
allowInput={allowInput}
|
allowInput={allowInput}
|
||||||
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}
|
||||||
@ -578,11 +588,10 @@ export class FormulaPicker extends React.Component<
|
|||||||
inputSettings={inputSettings}
|
inputSettings={inputSettings}
|
||||||
allowInput={allowInput}
|
allowInput={allowInput}
|
||||||
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}
|
||||||
|
@ -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() {
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function traverseAst(ast: any, iterator: (ast: any) => void | false) {
|
||||||
|
if (!ast || !ast.type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ret = iterator(ast);
|
||||||
|
if (ret === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.keys(ast).forEach(key => {
|
||||||
|
const value = ast[key];
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(child => traverseAst(child, iterator));
|
||||||
|
} else {
|
||||||
|
traverseAst(value, iterator);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export class FormulaPlugin {
|
export class FormulaPlugin {
|
||||||
|
/**
|
||||||
|
* 用于提示的变量集合,默认为空
|
||||||
|
*/
|
||||||
|
variables: Array<VariableItem> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 函数集合,默认不需要传,即 amis-formula 里面那个函数
|
||||||
|
* 如果有扩充,则需要传。
|
||||||
|
*/
|
||||||
|
functions: Array<FuncGroup> = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* evalMode 即直接就是表达式,否则就是混合模式
|
||||||
|
*/
|
||||||
|
evalMode: boolean = true;
|
||||||
|
|
||||||
|
disableAutoMark = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
readonly editor: CodeMirror.Editor,
|
readonly editor: CodeMirror.Editor,
|
||||||
readonly cm: typeof CodeMirror,
|
readonly cm: typeof CodeMirror
|
||||||
readonly getProps: () => FormulaEditorProps
|
|
||||||
) {
|
) {
|
||||||
// editor.on('change', this.autoMarkText);
|
// this.autoMarkText();
|
||||||
this.autoMarkText();
|
this.autoMarkText = debounce(this.autoMarkText.bind(this), 250, {
|
||||||
|
leading: false,
|
||||||
|
trailing: true
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.on('blur', () => this.autoMarkText());
|
||||||
}
|
}
|
||||||
|
|
||||||
autoMarkText() {
|
setVariables(variables: Array<VariableItem>) {
|
||||||
const {functions, variables, value} = this.getProps();
|
this.variables = Array.isArray(variables) ? variables : [];
|
||||||
|
|
||||||
if (value) {
|
|
||||||
// todo functions 也需要自动替换
|
|
||||||
this.autoMark(variables!);
|
|
||||||
this.focus(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
@ -190,88 +218,175 @@ export class FormulaPlugin {
|
|||||||
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, {
|
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.widgets = [];
|
||||||
|
|
||||||
|
this.marks.forEach(mark => mark.clear());
|
||||||
|
this.marks = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ast = parse(value, {
|
||||||
|
evalMode: this.evalMode,
|
||||||
|
variableMode: false
|
||||||
|
});
|
||||||
|
traverseAst(ast, (ast: any): any => {
|
||||||
|
if (ast.type === 'func_call') {
|
||||||
|
const funName = ast.identifier;
|
||||||
|
const exists = functions.some(item =>
|
||||||
|
item.items.some(i => i.name === funName)
|
||||||
|
);
|
||||||
|
if (exists) {
|
||||||
this.markText(
|
this.markText(
|
||||||
{
|
{
|
||||||
line: line,
|
line: ast.start.line - 1,
|
||||||
ch: pos
|
ch: ast.start.column - 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
line: line,
|
line: ast.start.line - 1,
|
||||||
ch: pos + func.length
|
ch: ast.start.column + funName.length - 1
|
||||||
},
|
},
|
||||||
func,
|
funName,
|
||||||
'cm-func'
|
'cm-func'
|
||||||
);
|
);
|
||||||
return _;
|
}
|
||||||
});
|
} else if (ast.type === 'getter') {
|
||||||
|
// 获取对象中的变量
|
||||||
const REPLACE_KEY = 'AMIS_FORMULA_REPLACE_KEY';
|
const list = [ast];
|
||||||
// 标记变量
|
let current = ast;
|
||||||
vars.forEach(v => {
|
while (current?.type === 'getter') {
|
||||||
let from = 0;
|
current = current.host;
|
||||||
let idx = -1;
|
list.unshift(current);
|
||||||
|
}
|
||||||
while (~(idx = content.indexOf(v, from))) {
|
const host = list.shift();
|
||||||
const encode = FormulaEditor.replaceStrByIndex(
|
if (host?.type === 'variable') {
|
||||||
content,
|
const variable = findTree(
|
||||||
idx,
|
variables,
|
||||||
v,
|
item => item.value === host.name
|
||||||
REPLACE_KEY
|
|
||||||
);
|
);
|
||||||
const reg = FormulaEditor.getRegExpByMode(evalMode, REPLACE_KEY);
|
if (variable) {
|
||||||
|
// 先标记顶层对象
|
||||||
if (reg.test(encode)) {
|
|
||||||
let markFrom = idx;
|
|
||||||
v.split('.').forEach((val: string, index: number) => {
|
|
||||||
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'
|
||||||
);
|
);
|
||||||
markFrom += 1 + val.length;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
from = idx + v.length;
|
// 再标记子对象
|
||||||
|
let path = host.name + '.';
|
||||||
|
let vars = variable.children || [];
|
||||||
|
for (let i = 0, len = list.length; i < len; i++) {
|
||||||
|
const item = list[i]?.key;
|
||||||
|
|
||||||
|
// 只能识别这种固定下标的情况
|
||||||
|
if (item?.type === 'identifier') {
|
||||||
|
const variable =
|
||||||
|
findTree(vars, v => v.value === path + item.name) ??
|
||||||
|
findTree(
|
||||||
|
vars,
|
||||||
|
v => v.value === item.name // 兼容不带路径的情况
|
||||||
|
);
|
||||||
|
if (variable) {
|
||||||
|
this.markText(
|
||||||
|
{
|
||||||
|
line: item.start.line - 1,
|
||||||
|
ch: item.start.column - 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
line: item.end.line - 1,
|
||||||
|
ch: item.end.column - 1
|
||||||
|
},
|
||||||
|
variable.label,
|
||||||
|
'cm-field'
|
||||||
|
);
|
||||||
|
path += item.name + '.';
|
||||||
|
vars = variable.children || [];
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else if (ast.type === 'variable') {
|
||||||
|
// 直接就是变量
|
||||||
|
const variable = findTree(variables, item => item.value === ast.name);
|
||||||
|
if (variable) {
|
||||||
|
this.markText(
|
||||||
|
{
|
||||||
|
line: ast.start.line - 1,
|
||||||
|
ch: ast.start.column - 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
line: ast.end.line - 1,
|
||||||
|
ch: ast.end.column - 1
|
||||||
|
},
|
||||||
|
variable.label,
|
||||||
|
'cm-field'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (e) {
|
||||||
|
const reg = /^Unexpected\stoken\s(.+)\sin\s(\d+):(\d+)$/.exec(e.message);
|
||||||
|
if (reg) {
|
||||||
|
const token = reg[1];
|
||||||
|
const line = parseInt(reg[2], 10);
|
||||||
|
const column = parseInt(reg[3], 10);
|
||||||
|
const msg = document.createElement('div');
|
||||||
|
const icon = msg.appendChild(document.createElement('span'));
|
||||||
|
icon.innerText = '!!';
|
||||||
|
icon.className = 'lint-error-icon';
|
||||||
|
msg.appendChild(
|
||||||
|
document.createTextNode(`Unexpected token \`${token}\``)
|
||||||
|
);
|
||||||
|
msg.className = 'lint-error';
|
||||||
|
this.widgets.push(
|
||||||
|
editor.addLineWidget(line - 1, msg, {
|
||||||
|
coverGutter: false,
|
||||||
|
noHScroll: true
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
this.marks.push(
|
||||||
|
this.markText(
|
||||||
|
{
|
||||||
|
line: line - 1,
|
||||||
|
ch: column - 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
line: line - 1,
|
||||||
|
ch: column + token.length - 1
|
||||||
|
},
|
||||||
|
token,
|
||||||
|
'cm-error-token'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.warn('synax error, ignore it');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -283,7 +398,9 @@ export class FormulaPlugin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispose() {}
|
dispose() {
|
||||||
|
(this.autoMarkText as any).cancel();
|
||||||
|
}
|
||||||
|
|
||||||
validate() {}
|
validate() {}
|
||||||
}
|
}
|
||||||
@ -295,6 +412,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 +425,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'});
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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';
|
||||||
|
|
||||||
|
@ -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':
|
||||||
|
@ -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}}',
|
||||||
|
@ -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}}',
|
||||||
|
@ -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()
|
||||||
|
});
|
||||||
|
@ -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-default"
|
||||||
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>
|
||||||
<div
|
<div
|
||||||
class="cxd-ResultBox-actions"
|
class="CodeMirror-scrollbar-filler"
|
||||||
|
cm-not-content="true"
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
class="CodeMirror-gutter-filler"
|
||||||
|
cm-not-content="true"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="CodeMirror-scroll"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="CodeMirror-sizer"
|
||||||
|
style="margin-left: 0px; min-width: 3px;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="position: relative;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="CodeMirror-lines"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
role="presentation"
|
||||||
|
style="position: relative; outline: none;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="CodeMirror-measure"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="CodeMirror-measure"
|
||||||
|
>
|
||||||
|
<pre
|
||||||
|
class="CodeMirror-line"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
role="presentation"
|
||||||
|
style="padding-right: .1px;"
|
||||||
|
>
|
||||||
|
SUM(1 + 2)
|
||||||
|
</span>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="position: relative; z-index: 1;"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="CodeMirror-cursors"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="CodeMirror-code"
|
||||||
|
role="presentation"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="position: absolute; height: 50px; width: 1px;"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="CodeMirror-gutters"
|
||||||
|
style="display: none;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="resize-sensor"
|
||||||
|
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="resize-sensor-expand"
|
||||||
|
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="position: absolute; left: 0px; top: 0px; width: 10px; height: 10px;"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="resize-sensor-shrink"
|
||||||
|
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
style="position: absolute; left: 0; top: 0; width: 200%; height: 200%"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="resize-sensor-appear"
|
||||||
|
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;animation-name: apearSensor; animation-duration: 0.2s;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
class="cxd-InputBox-clear"
|
||||||
|
>
|
||||||
|
<icon-mock
|
||||||
|
classname="icon icon-input-clear"
|
||||||
|
icon="input-clear"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
class="cxd-FormulaPicker-toggler"
|
class="cxd-FormulaPicker-toggler"
|
||||||
|
@ -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",
|
type: 'form',
|
||||||
"left": {
|
data: {
|
||||||
"type": "field",
|
conditions: {
|
||||||
"field": "text"
|
id: '68bddc1495e9',
|
||||||
|
conjunction: 'and',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'b9cc34dae93a',
|
||||||
|
left: {
|
||||||
|
type: 'field',
|
||||||
|
field: 'text'
|
||||||
},
|
},
|
||||||
"op": "equal"
|
op: 'equal'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "4c718986c321",
|
id: '4c718986c321',
|
||||||
"left": {
|
left: {
|
||||||
"type": "field",
|
type: 'field',
|
||||||
"field": "number"
|
field: 'number'
|
||||||
},
|
},
|
||||||
"op": "equal"
|
op: 'equal'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "7ee79c416422",
|
id: '7ee79c416422',
|
||||||
"left": {
|
left: {
|
||||||
"type": "field",
|
type: 'field',
|
||||||
"field": "boolean"
|
field: 'boolean'
|
||||||
},
|
},
|
||||||
"op": "equal"
|
op: 'equal'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "9cd76d8a6522",
|
id: '9cd76d8a6522',
|
||||||
"left": {
|
left: {
|
||||||
"type": "field",
|
type: 'field',
|
||||||
"field": "select"
|
field: 'select'
|
||||||
},
|
},
|
||||||
"op": "select_equals"
|
op: 'select_equals'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "20a65e9df546",
|
id: '20a65e9df546',
|
||||||
"left": {
|
left: {
|
||||||
"type": "field",
|
type: 'field',
|
||||||
"field": "date"
|
field: 'date'
|
||||||
},
|
},
|
||||||
"op": "equal"
|
op: 'equal'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "e729b32ea9e8",
|
id: 'e729b32ea9e8',
|
||||||
"left": {
|
left: {
|
||||||
"type": "field",
|
type: 'field',
|
||||||
"field": "time"
|
field: 'time'
|
||||||
},
|
},
|
||||||
"op": "equal"
|
op: 'equal'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "a5f48e000557",
|
id: 'a5f48e000557',
|
||||||
"left": {
|
left: {
|
||||||
"type": "field",
|
type: 'field',
|
||||||
"field": "datetime"
|
field: 'datetime'
|
||||||
},
|
},
|
||||||
"op": "equal"
|
op: 'equal'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": [
|
body: [
|
||||||
{
|
{
|
||||||
"type": "condition-builder",
|
type: 'condition-builder',
|
||||||
"label": "条件组件",
|
label: '条件组件',
|
||||||
"name": "conditions",
|
name: 'conditions',
|
||||||
"searchable": true,
|
searchable: true,
|
||||||
"formula": {
|
formula: {
|
||||||
"mode": "input-group",
|
mode: 'input-group',
|
||||||
"inputSettings": {},
|
inputSettings: {},
|
||||||
"allowInput": true,
|
allowInput: true,
|
||||||
"mixedMode": true,
|
mixedMode: true,
|
||||||
"variables": []
|
variables: []
|
||||||
},
|
},
|
||||||
"fields": [
|
fields: [
|
||||||
{
|
{
|
||||||
"label": "文本",
|
label: '文本',
|
||||||
"type": "text",
|
type: 'text',
|
||||||
"name": "text"
|
name: 'text'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "数字",
|
label: '数字',
|
||||||
"type": "number",
|
type: 'number',
|
||||||
"name": "number"
|
name: 'number'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "布尔",
|
label: '布尔',
|
||||||
"type": "boolean",
|
type: 'boolean',
|
||||||
"name": "boolean"
|
name: 'boolean'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "选项",
|
label: '选项',
|
||||||
"type": "select",
|
type: 'select',
|
||||||
"name": "select",
|
name: 'select',
|
||||||
"options": [
|
options: [
|
||||||
{
|
{
|
||||||
"label": "A",
|
label: 'A',
|
||||||
"value": "a"
|
value: 'a'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "B",
|
label: 'B',
|
||||||
"value": "b"
|
value: 'b'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "C",
|
label: 'C',
|
||||||
"value": "c"
|
value: 'c'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "日期",
|
label: '日期',
|
||||||
"children": [
|
children: [
|
||||||
{
|
{
|
||||||
"label": "日期",
|
label: '日期',
|
||||||
"type": "date",
|
type: 'date',
|
||||||
"name": "date"
|
name: 'date'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "时间",
|
label: '时间',
|
||||||
"type": "time",
|
type: 'time',
|
||||||
"name": "time"
|
name: 'time'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "日期时间",
|
label: '日期时间',
|
||||||
"type": "datetime",
|
type: 'datetime',
|
||||||
"name": "datetime"
|
name: 'datetime'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}, {onSubmit}, makeEnv({})));
|
},
|
||||||
|
{onSubmit},
|
||||||
|
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",
|
type: 'form',
|
||||||
"left": {
|
data: {
|
||||||
"type": "field",
|
conditions: {
|
||||||
"field": "select"
|
id: '68bddc1495e9',
|
||||||
|
conjunction: 'and',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: '9cd76d8a6522',
|
||||||
|
left: {
|
||||||
|
type: 'field',
|
||||||
|
field: 'select'
|
||||||
},
|
},
|
||||||
"op": "select_equals"
|
op: 'select_equals'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": [
|
body: [
|
||||||
{
|
{
|
||||||
"type": "condition-builder",
|
type: 'condition-builder',
|
||||||
"label": "条件组件",
|
label: '条件组件',
|
||||||
"name": "conditions",
|
name: 'conditions',
|
||||||
"searchable": true,
|
searchable: true,
|
||||||
"formula": {
|
formula: {
|
||||||
"mode": "input-group",
|
mode: 'input-group',
|
||||||
"inputSettings": {},
|
inputSettings: {},
|
||||||
"allowInput": true,
|
allowInput: true,
|
||||||
"mixedMode": true,
|
mixedMode: true,
|
||||||
"variables": []
|
variables: []
|
||||||
},
|
},
|
||||||
"fields": [
|
fields: [
|
||||||
{
|
{
|
||||||
"label": "选项",
|
label: '选项',
|
||||||
"type": "select",
|
type: 'select',
|
||||||
"name": "select",
|
name: 'select',
|
||||||
"options": [
|
options: [
|
||||||
{
|
{
|
||||||
"label": "A",
|
label: 'A',
|
||||||
"value": "a"
|
value: 'a'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "B",
|
label: 'B',
|
||||||
"value": "b"
|
value: 'b'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "C",
|
label: 'C',
|
||||||
"value": "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",
|
type: 'form',
|
||||||
"left": {
|
data: {
|
||||||
"type": "field",
|
conditions: {
|
||||||
"field": "text"
|
id: '68bddc1495e9',
|
||||||
|
conjunction: 'and',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: 'b9cc34dae93a',
|
||||||
|
left: {
|
||||||
|
type: 'field',
|
||||||
|
field: 'text'
|
||||||
},
|
},
|
||||||
"op": "equal"
|
op: 'equal'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"body": [
|
body: [
|
||||||
{
|
{
|
||||||
"type": "condition-builder",
|
type: 'condition-builder',
|
||||||
"label": "条件组件",
|
label: '条件组件',
|
||||||
"name": "conditions",
|
name: 'conditions',
|
||||||
"searchable": true,
|
searchable: true,
|
||||||
"formula": {
|
formula: {
|
||||||
"mode": "input-group",
|
mode: 'input-group',
|
||||||
"inputSettings": {},
|
inputSettings: {},
|
||||||
"allowInput": true,
|
allowInput: true,
|
||||||
"mixedMode": true,
|
mixedMode: true,
|
||||||
"variables": []
|
variables: []
|
||||||
},
|
},
|
||||||
"fields": [
|
fields: [
|
||||||
{
|
{
|
||||||
"label": "文本",
|
label: '文本',
|
||||||
"type": "text",
|
type: 'text',
|
||||||
"name": "text"
|
name: 'text'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "选项",
|
label: '选项',
|
||||||
"type": "select",
|
type: 'select',
|
||||||
"name": "select",
|
name: 'select',
|
||||||
"options": [
|
options: [
|
||||||
{
|
{
|
||||||
"label": "A",
|
label: 'A',
|
||||||
"value": "a"
|
value: 'a'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "B",
|
label: 'B',
|
||||||
"value": "b"
|
value: 'b'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "C",
|
label: 'C',
|
||||||
"value": "c"
|
value: 'c'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}, {onSubmit}, makeEnv({})));
|
},
|
||||||
|
{onSubmit},
|
||||||
|
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');
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
@ -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();
|
||||||
|
@ -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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user