公式编辑器提交增加校验 (#4100)

* 公式编辑器提交增加校验

* feat: 公式编辑器不同模式变量/函数选择优化
This commit is contained in:
Allen 2022-04-22 15:09:33 +08:00 committed by GitHub
parent b74112ac30
commit 01d9f4d0c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 255 additions and 115 deletions

View File

@ -360,6 +360,8 @@
&-input {
flex: 1;
margin-right: #{px2rem(10px)};
padding-right: 0;
max-width: calc(100% - #{px2rem(42px)});
}
&-action {
@ -384,6 +386,9 @@
&-ResultBox {
padding-right: 10px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
span.c-field {
background: #007bff;
padding: 3px 5px;

View File

@ -129,7 +129,7 @@ export class FormulaEditor extends React.Component<
static highlightValue(
value: string,
variables: Array<VariableItem>,
functions: Array<FuncGroup>
evalMode: boolean = true
) {
if (!Array.isArray(variables) || !variables.length || !value) {
return;
@ -139,10 +139,12 @@ export class FormulaEditor extends React.Component<
[propname: string]: string;
} = {};
eachTree(
variables,
item => item.value && (varMap[item.value] = item.label)
);
eachTree(variables, item => {
if (item.value) {
const key = evalMode ? item.value : '${' + item.value + '}';
varMap[key] = item.label;
}
});
const vars = Object.keys(varMap)
.filter(item => item)
.sort((a, b) => b.length - a.length);
@ -159,7 +161,7 @@ export class FormulaEditor extends React.Component<
let from = 0;
let idx = -1;
while (~(idx = content.indexOf(v, from))) {
html = content.replace(v, `<span class="c-field">${varMap[v]}</span>`);
html = html.replace(v, `<span class="c-field">${varMap[v]}</span>`);
from = idx + v.length;
}
});

View File

@ -3,19 +3,20 @@ import React from 'react';
import {FormulaEditor, FormulaEditorProps} from './Editor';
import {autobind, noop} from '../../utils/helper';
import {generateIcon} from '../../utils/icon';
import PickerContainer from '../PickerContainer';
import Editor from './Editor';
import ResultBox from '../ResultBox';
import Button from '../Button';
import {Icon} from '../icons';
import Modal from '../Modal';
import {themeable} from '../../theme';
import {localeable} from '../../locale';
import type {SchemaIcon} from '../../Schema';
import {
resolveVariableAndFilter,
isPureVariable
} from '../../utils/tpl-builtin';
import {toast} from '../../components';
import {parse, Evaluator} from 'amis-formula';
export interface FormulaPickerProps extends FormulaEditorProps {
// 新的属性?
@ -95,9 +96,31 @@ export interface FormulaPickerProps extends FormulaEditorProps {
data?: any;
}
export class FormulaPicker extends React.Component<FormulaPickerProps> {
export interface FormulaPickerState {
isOpened: boolean;
value: string;
editorValue: string;
isError: boolean | string;
}
export class FormulaPicker extends React.Component<
FormulaPickerProps,
FormulaPickerState
> {
static defaultProps = {
evalMode: true
};
state: FormulaPickerState = {
isOpened: false,
value: this.props.value,
editorValue: this.props.value,
isError: false
};
@autobind
handleConfirm(value: any) {
handleConfirm() {
const value = this.state.value;
this.props.onChange?.(value);
}
@ -116,10 +139,94 @@ export class FormulaPicker extends React.Component<FormulaPickerProps> {
);
}
@autobind
handleInputChange(value: string) {
const validate = this.validate(value);
if (validate === true) {
this.setState(
{
value,
isError: false
},
() => this.handleConfirm()
);
} else {
this.setState({isError: validate});
}
}
@autobind
handleEditorChange(value: string) {
this.setState({
editorValue: value
});
}
@autobind
handleEditorConfirm() {
const {translate: __} = this.props;
const value = this.state.editorValue;
const validate = this.validate(value, true);
if (validate === true) {
this.setState({value}, () => {
this.close(undefined, () => this.handleConfirm());
});
} else {
this.setState({isError: validate});
}
}
@autobind
handleClick() {
this.setState({
editorValue: this.props.value,
isOpened: true
});
}
@autobind
close(e?: any, callback?: () => void) {
this.setState(
{
isOpened: false,
isError: false
},
() => {
if (callback) {
callback();
return;
}
}
);
}
@autobind
validate(value: string, remind?: boolean) {
const {translate: __} = this.props;
try {
const ast = parse(value, {
evalMode: this.props.evalMode,
allowFilter: false
});
new Evaluator({}).evalute(ast);
return true;
} catch (e) {
const [, position] = /\s(\d+:\d+)$/.exec(e.message) || [];
remind &&
toast.error(
__('FormulaEditor.invalidData', {position: position || '-'})
);
return position;
}
}
render() {
let {
classnames: cx,
value,
translate: __,
disabled,
allowInput,
@ -139,6 +246,8 @@ export class FormulaPicker extends React.Component<FormulaPickerProps> {
functions,
...rest
} = this.props;
const {isOpened, value, editorValue, isError} = this.state;
if (isPureVariable(variables)) {
// 如果 variables 是 ${xxx} 这种形式,将其处理成实际的值
variables = resolveVariableAndFilter(variables, this.props.data, '| raw');
@ -151,98 +260,110 @@ export class FormulaPicker extends React.Component<FormulaPickerProps> {
const iconElement = generateIcon(cx, icon, 'Icon');
return (
<PickerContainer
title={__(title || 'FormulaEditor.title')}
headerClassName="font-bold"
bodyRender={({onClose, value, onChange}) => {
return (
<>
<div className={cx('FormulaPicker', className)}>
{mode === 'button' ? (
<Button
className={cx('FormulaPicker-action', 'w-full')}
level={level}
size={btnSize}
onClick={this.handleClick}
>
{iconElement ? (
React.cloneElement(iconElement, {
className: cx(
iconElement?.props?.className ?? '',
'FormulaPicker-icon',
{
['is-filled']: !!value
}
)
})
) : (
<Icon
icon="function"
className={cx('FormulaPicker-icon', 'icon', {
['is-filled']: !!value
})}
/>
)}
<span className={cx('FormulaPicker-label')}>
{__(btnLabel || 'FormulaEditor.btnLabel')}
</span>
</Button>
) : (
<>
<ResultBox
className={cx(
'FormulaPicker-input',
isOpened ? 'is-active' : '',
!!isError ? 'is-error' : ''
)}
allowInput={allowInput}
clearable={clearable}
value={value}
result={
allowInput
? void 0
: FormulaEditor.highlightValue(
value,
variables,
this.props.evalMode
)
}
itemRender={this.renderFormulaValue}
onResultChange={noop}
onChange={this.handleInputChange}
disabled={disabled}
borderMode={borderMode}
placeholder={placeholder}
/>
<Button
className={cx('FormulaPicker-action')}
onClick={this.handleClick}
>
<Icon
icon="function"
className={cx('FormulaPicker-icon', 'icon', {
['is-filled']: !!value
})}
/>
</Button>
</>
)}
</div>
{!!isError ? (
<ul className={cx('Form-feedback')}>
<li>{__('FormulaEditor.invalidData', {position: isError})}</li>
</ul>
) : null}
<Modal
size="md"
closeOnEsc
show={this.state.isOpened}
onHide={this.close}
>
<Modal.Header onClose={this.close} className="font-bold">
{__(title || 'FormulaEditor.title')}
</Modal.Header>
<Modal.Body>
<Editor
{...rest}
variables={variables}
functions={functions}
value={value}
onChange={onChange}
value={editorValue}
onChange={this.handleEditorChange}
/>
);
}}
value={value}
onConfirm={this.handleConfirm}
size={'md'}
>
{({onClick, isOpened}) => (
<div className={cx('FormulaPicker', className)}>
{mode === 'button' ? (
<Button
className={cx('FormulaPicker-action', 'w-full')}
level={level}
size={btnSize}
onClick={onClick}
>
{iconElement ? (
React.cloneElement(iconElement, {
className: cx(
iconElement?.props?.className ?? '',
'FormulaPicker-icon',
{
['is-filled']: !!value
}
)
})
) : (
<Icon
icon="function"
className={cx('FormulaPicker-icon', 'icon', {
['is-filled']: !!value
})}
/>
)}
<span className={cx('FormulaPicker-label')}>
{__(btnLabel || 'FormulaEditor.btnLabel')}
</span>
</Button>
) : (
<>
<ResultBox
className={cx(
'FormulaPicker-input',
isOpened ? 'is-active' : ''
)}
allowInput={allowInput}
clearable={clearable}
value={value}
result={
allowInput
? void 0
: FormulaEditor.highlightValue(
value,
variables,
functions
)
}
itemRender={this.renderFormulaValue}
onResultChange={noop}
onChange={this.handleConfirm}
disabled={disabled}
borderMode={borderMode}
placeholder={placeholder}
/>
<Button
className={cx('FormulaPicker-action')}
onClick={onClick}
>
<Icon
icon="function"
className={cx('FormulaPicker-icon', 'icon', {
['is-filled']: !!value
})}
/>
</Button>
</>
)}
</div>
)}
</PickerContainer>
</Modal.Body>
<Modal.Footer>
<Button onClick={this.close}>{__('cancel')}</Button>
<Button onClick={this.handleEditorConfirm} level="primary">
{__('confirm')}
</Button>
</Modal.Footer>
</Modal>
</>
);
}
}

View File

@ -39,30 +39,37 @@ export class FormulaPlugin {
}
insertContent(value: any, type: 'variable' | 'func') {
const {evalMode} = this.getProps();
const from = this.editor.getCursor();
if (type === 'variable') {
this.editor.replaceSelection(value.key);
const key = evalMode ? value.key : '${' + value.key + '}';
this.editor.replaceSelection(key);
var to = this.editor.getCursor();
this.markText(from, to, value.name, 'cm-field');
} else if (type === 'func') {
// todo 支持 snippet目前是不支持的
this.editor.replaceSelection(`${value}()`);
const key = evalMode ? `${value}()` : '${' + value + '()}';
this.editor.replaceSelection(key);
var to = this.editor.getCursor();
this.markText(
from,
{
line: to.line,
ch: to.ch - 2
},
value,
'cm-func'
);
// todo 模板模式下 ${XXX()} 高亮处理
evalMode &&
this.markText(
from,
{
line: to.line,
ch: to.ch - 2
},
value,
'cm-func'
);
this.editor.setCursor({
line: to.line,
ch: to.ch - 1
ch: evalMode ? to.ch - 1 : to.ch - 2
});
} else if (typeof value === 'string') {
this.editor.replaceSelection(value);
@ -90,15 +97,17 @@ export class FormulaPlugin {
if (!Array.isArray(variables) || !variables.length) {
return;
}
const {evalMode} = this.getProps();
const varMap: {
[propname: string]: string;
} = {};
eachTree(
variables,
item => item.value && (varMap[item.value] = item.label)
);
eachTree(variables, item => {
if (item.value) {
const key = evalMode ? item.value : '${' + item.value + '}';
varMap[key] = item.label;
}
});
const vars = Object.keys(varMap).sort((a, b) => b.length - a.length);
const editor = this.editor;

View File

@ -301,6 +301,7 @@ register('de-DE', {
'FormulaEditor.title': 'Formel Editor',
'FormulaEditor.variable': 'Variable',
'FormulaEditor.function': 'Funktion',
'FormulaEditor.invalidData': 'Überprüfungsfehler, position in {{position}}',
'pullRefresh.pullingText': 'Zum Aktualisieren nach unten ziehen...',
'pullRefresh.loosingText': 'Zum Aktualisieren freigeben...',
'pullRefresh.loadingText': 'Laden...',

View File

@ -303,6 +303,7 @@ register('en-US', {
'FormulaEditor.title': 'Formula Editor',
'FormulaEditor.variable': 'Variable',
'FormulaEditor.function': 'Function',
'FormulaEditor.invalidData': 'invalid data, position in {{position}}',
'pullRefresh.pullingText': 'Pull down to refresh...',
'pullRefresh.loosingText': 'Release to refresh...',
'pullRefresh.loadingText': 'Loading...',

View File

@ -310,6 +310,7 @@ register('zh-CN', {
'FormulaEditor.title': '公式编辑器',
'FormulaEditor.variable': '变量',
'FormulaEditor.function': '函数',
'FormulaEditor.invalidData': '公式值校验错误,错误的位置是 {{position}}',
'pullRefresh.pullingText': '下拉即可刷新...',
'pullRefresh.loosingText': '释放即可刷新...',
'pullRefresh.loadingText': '加载中...',