refactor(plugin-workflow): migrate evaluators (#1485)
* fix(plugin-formula): fix locale * refactor(client): migrate variable component * refactor(plugin-workflow): use core evaluators * refactor(plugin-workflow): migrate calculation engines to evaluators
This commit is contained in:
parent
85ff054db4
commit
86d9eaf2fb
@ -158,6 +158,7 @@ export default {
|
|||||||
"Long text": "多行文本",
|
"Long text": "多行文本",
|
||||||
"Phone": "手机号码",
|
"Phone": "手机号码",
|
||||||
"Email": "电子邮箱",
|
"Email": "电子邮箱",
|
||||||
|
'Null': '空值',
|
||||||
"Boolean": "逻辑值",
|
"Boolean": "逻辑值",
|
||||||
"Number": "数字",
|
"Number": "数字",
|
||||||
"Integer": "整数",
|
"Integer": "整数",
|
||||||
@ -640,4 +641,10 @@ export default {
|
|||||||
'Display page title': '显示页面标题',
|
'Display page title': '显示页面标题',
|
||||||
'Edit page title': '编辑页面标题',
|
'Edit page title': '编辑页面标题',
|
||||||
'Enable page tabs': '启用页面选项卡',
|
'Enable page tabs': '启用页面选项卡',
|
||||||
|
|
||||||
|
'Constant': '常量',
|
||||||
|
'Use variable': '使用变量',
|
||||||
|
'True': '真',
|
||||||
|
'False': '假',
|
||||||
|
'Prettify': '格式化'
|
||||||
}
|
}
|
||||||
|
@ -36,4 +36,5 @@ export * from './tabs';
|
|||||||
export * from './time-picker';
|
export * from './time-picker';
|
||||||
export * from './tree-select';
|
export * from './tree-select';
|
||||||
export * from './upload';
|
export * from './upload';
|
||||||
|
export * from './variable';
|
||||||
import './index.less';
|
import './index.less';
|
||||||
|
@ -1,20 +1,18 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { useForm } from '@formily/react';
|
import { useForm } from '@formily/react';
|
||||||
import { Cascader, Input, Button, Tag, InputNumber, Select, DatePicker } from "antd";
|
import { Cascader, Input as AntInput, Button, Tag, InputNumber, Select, DatePicker } from "antd";
|
||||||
import { CloseCircleFilled } from "@ant-design/icons";
|
import { CloseCircleFilled } from "@ant-design/icons";
|
||||||
import { cx, css } from "@emotion/css";
|
import { cx, css } from "@emotion/css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
|
||||||
import { useCompile } from "@nocobase/client";
|
import { useCompile } from '../../hooks/useCompile';
|
||||||
|
|
||||||
import { lang, NAMESPACE } from "../locale";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const JT_VALUE_RE = /^\s*\{\{([\s\S]*)\}\}\s*$/;
|
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
||||||
|
|
||||||
export function parseValue(value: any): string | string[] {
|
function parseValue(value: any): string | string[] {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return 'null';
|
return 'null';
|
||||||
}
|
}
|
||||||
@ -36,11 +34,11 @@ export function parseValue(value: any): string | string[] {
|
|||||||
|
|
||||||
const ConstantTypes = {
|
const ConstantTypes = {
|
||||||
string: {
|
string: {
|
||||||
label: `{{t("String", { ns: "${NAMESPACE}" })}}`,
|
label: `{{t("String")}}`,
|
||||||
value: 'string',
|
value: 'string',
|
||||||
component({ onChange, value }) {
|
component({ onChange, value }) {
|
||||||
return (
|
return (
|
||||||
<Input
|
<AntInput
|
||||||
value={value}
|
value={value}
|
||||||
onChange={ev => onChange(ev.target.value)}
|
onChange={ev => onChange(ev.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -62,7 +60,7 @@ const ConstantTypes = {
|
|||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
boolean: {
|
boolean: {
|
||||||
label: `{{t("Boolean", { ns: "${NAMESPACE}" })}}`,
|
label: `{{t("Boolean")}}`,
|
||||||
value: 'boolean',
|
value: 'boolean',
|
||||||
component({ onChange, value }) {
|
component({ onChange, value }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -72,8 +70,8 @@ const ConstantTypes = {
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder={t('Select')}
|
placeholder={t('Select')}
|
||||||
options={[
|
options={[
|
||||||
{ value: true, label: lang('True') },
|
{ value: true, label: t('True') },
|
||||||
{ value: false, label: lang('False') },
|
{ value: false, label: t('False') },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -81,7 +79,7 @@ const ConstantTypes = {
|
|||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
date: {
|
date: {
|
||||||
label: '日期',
|
label: '{{t("Date")}}',
|
||||||
value: 'date',
|
value: 'date',
|
||||||
component({ onChange, value }) {
|
component({ onChange, value }) {
|
||||||
return (
|
return (
|
||||||
@ -99,11 +97,12 @@ const ConstantTypes = {
|
|||||||
})(),
|
})(),
|
||||||
},
|
},
|
||||||
null: {
|
null: {
|
||||||
label: `{{t("Null", { ns: "${NAMESPACE}" })}}`,
|
label: `{{t("Null")}}`,
|
||||||
value: 'null',
|
value: 'null',
|
||||||
component() {
|
component() {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Input readOnly placeholder={lang('Null')} />
|
<AntInput readOnly placeholder={t('Null')} className="null-value" />
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
default: null,
|
default: null,
|
||||||
@ -116,19 +115,21 @@ type VariableOptions = {
|
|||||||
children?: VariableOptions[];
|
children?: VariableOptions[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VariableInput(props) {
|
export function Input(props) {
|
||||||
const { value = '', scope, onChange, children, button } = props;
|
const { value = '', scope, onChange, children, button } = props;
|
||||||
const parsed = parseValue(value);
|
const parsed = parseValue(value);
|
||||||
const isConstant = typeof parsed === 'string';
|
const isConstant = typeof parsed === 'string';
|
||||||
const type = isConstant ? parsed : 'string';
|
const type = isConstant ? parsed : '';
|
||||||
const variable = isConstant ? null : parsed;
|
const variable = isConstant ? null : parsed;
|
||||||
const ConstantComponent = ConstantTypes[type]?.component;
|
const ConstantComponent = ConstantTypes[type]?.component;
|
||||||
const constantOptions = Object.values(ConstantTypes);
|
const constantOptions = Object.values(ConstantTypes);
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
const { t } = useTranslation();
|
||||||
const options: VariableOptions[] = compile([
|
const options: VariableOptions[] = compile([
|
||||||
{ value: '', label: lang('Constant'), children: children ? null : constantOptions },
|
{ value: '', label: t('Constant'), children: children ? null : constantOptions },
|
||||||
...(typeof scope === 'function' ? scope() : (scope ?? []))
|
...(typeof scope === 'function' ? scope() : (scope ?? []))
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
|
|
||||||
function onSwitch(next) {
|
function onSwitch(next) {
|
||||||
@ -155,7 +156,7 @@ export function VariableInput(props) {
|
|||||||
const disabled = props.disabled || form.disabled;
|
const disabled = props.disabled || form.disabled;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input.Group compact className={css`
|
<AntInput.Group compact className={css`
|
||||||
width: auto;
|
width: auto;
|
||||||
.ant-input-disabled{
|
.ant-input-disabled{
|
||||||
.ant-tag{
|
.ant-tag{
|
||||||
@ -163,6 +164,10 @@ export function VariableInput(props) {
|
|||||||
border-color: #d9d9d9;
|
border-color: #d9d9d9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.ant-input.null-value{
|
||||||
|
width: 4em;
|
||||||
|
min-width: 4em;
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{variable
|
{variable
|
||||||
@ -202,6 +207,7 @@ export function VariableInput(props) {
|
|||||||
}}
|
}}
|
||||||
className={cx('ant-input', { 'ant-input-disabled': disabled })}
|
className={cx('ant-input', { 'ant-input-disabled': disabled })}
|
||||||
contentEditable={!disabled}
|
contentEditable={!disabled}
|
||||||
|
suppressContentEditableWarning
|
||||||
>
|
>
|
||||||
<Tag contentEditable={false} color="blue">{variableText}</Tag>
|
<Tag contentEditable={false} color="blue">{variableText}</Tag>
|
||||||
</div>
|
</div>
|
||||||
@ -247,6 +253,6 @@ export function VariableInput(props) {
|
|||||||
)
|
)
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
</Input.Group>
|
</AntInput.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -2,9 +2,8 @@ import React, { useRef } from 'react';
|
|||||||
import { Button, Cascader } from 'antd';
|
import { Button, Cascader } from 'antd';
|
||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
|
|
||||||
import { Input } from "@nocobase/client";
|
import { Input } from "../input";
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { lang } from '../locale';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -17,9 +16,10 @@ function setNativeInputValue(input, value) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VariableJSONInput(props) {
|
export function JSONInput(props) {
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
const { value, space = 2, scope } = props;
|
const { value, space = 2, scope } = props;
|
||||||
|
const { t } = useTranslation()
|
||||||
const options = typeof scope === 'function' ? scope() : (scope ?? []);
|
const options = typeof scope === 'function' ? scope() : (scope ?? []);
|
||||||
|
|
||||||
function onFormat() {
|
function onFormat() {
|
||||||
@ -69,7 +69,7 @@ export function VariableJSONInput(props) {
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<Button onClick={onFormat}>{lang('Format')}</Button>
|
<Button onClick={onFormat}>{t('Prettify')}</Button>
|
||||||
<Cascader
|
<Cascader
|
||||||
value={[]}
|
value={[]}
|
||||||
options={options}
|
options={options}
|
@ -2,11 +2,11 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
|
|||||||
import { Input, Cascader, Tooltip, Button } from 'antd';
|
import { Input, Cascader, Tooltip, Button } from 'antd';
|
||||||
import { useForm } from '@formily/react';
|
import { useForm } from '@formily/react';
|
||||||
import { cx, css } from "@emotion/css";
|
import { cx, css } from "@emotion/css";
|
||||||
import { lang } from '../locale';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const VARIABLE_RE = /\{\{\s*([^{}]+)\s*\}\}/g;
|
const VARIABLE_RE = /{{\s*([^{}]+)\s*}}/g;
|
||||||
|
|
||||||
function pasteHtml(container, html, { selectPastedContent = false, range: indexes }) {
|
function pasteHtml(container, html, { selectPastedContent = false, range: indexes }) {
|
||||||
// IE9 and non-IE
|
// IE9 and non-IE
|
||||||
@ -85,13 +85,13 @@ function renderHTML(exp: string, keyLabelMap) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function createOptionsKeyLabelMap(options: any[]) {
|
function createOptionsValueLabelMap(options: any[]) {
|
||||||
const map = new Map<string, string[]>();
|
const map = new Map<string, string[]>();
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
map.set(option.key, [option.label]);
|
map.set(option.value, [option.label]);
|
||||||
if (option.children) {
|
if (option.children) {
|
||||||
for (const [key, labels] of createOptionsKeyLabelMap(option.children)) {
|
for (const [value, labels] of createOptionsValueLabelMap(option.children)) {
|
||||||
map.set(`${option.key}.${key}`, [option.label, ...labels]);
|
map.set(`${option.value}.${value}`, [option.label, ...labels]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -103,12 +103,13 @@ function createVariableTagHTML(variable, keyLabelMap) {
|
|||||||
return `<span class="ant-tag ant-tag-blue" contentEditable="false" data-key="${variable}">${labels?.join(' / ')}</span>`;
|
return `<span class="ant-tag ant-tag-blue" contentEditable="false" data-key="${variable}">${labels?.join(' / ')}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VariableTextArea(props) {
|
export function TextArea(props) {
|
||||||
const { value = '', scope, onChange, multiline = true, button } = props;
|
const { value = '', scope, onChange, multiline = true, button } = props;
|
||||||
|
const { t } = useTranslation()
|
||||||
const inputRef = useRef<HTMLDivElement>(null);
|
const inputRef = useRef<HTMLDivElement>(null);
|
||||||
const options = (typeof scope === 'function' ? scope() : scope) ?? [];
|
const options = (typeof scope === 'function' ? scope() : scope) ?? [];
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const keyLabelMap = useMemo(() => createOptionsKeyLabelMap(options), [scope]);
|
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [scope]);
|
||||||
const [changed, setChanged] = useState(false);
|
const [changed, setChanged] = useState(false);
|
||||||
const [html, setHtml] = useState(() => renderHTML(value ?? '', keyLabelMap));
|
const [html, setHtml] = useState(() => renderHTML(value ?? '', keyLabelMap));
|
||||||
// [startElementIndex, startOffset, endElementIndex, endOffset]
|
// [startElementIndex, startOffset, endElementIndex, endOffset]
|
||||||
@ -213,7 +214,7 @@ export function VariableTextArea(props) {
|
|||||||
contentEditable={!disabled}
|
contentEditable={!disabled}
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
/>
|
/>
|
||||||
<Tooltip title={lang('Use variable')}>
|
<Tooltip title={t('Use variable')}>
|
||||||
<Cascader
|
<Cascader
|
||||||
value={[]}
|
value={[]}
|
||||||
options={options}
|
options={options}
|
@ -0,0 +1,19 @@
|
|||||||
|
import { connect } from '@formily/react';
|
||||||
|
|
||||||
|
import { Input } from "./Input";
|
||||||
|
import { TextArea } from "./TextArea";
|
||||||
|
import { JSONInput } from "./JSONInput";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function Variable() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Variable.Input = connect(Input);
|
||||||
|
|
||||||
|
Variable.TextArea = connect(TextArea);
|
||||||
|
|
||||||
|
Variable.JSON = connect(JSONInput);
|
||||||
|
|
||||||
|
export default Variable;
|
@ -0,0 +1 @@
|
|||||||
|
export * from './Variable';
|
1
packages/core/evaluators/client.d.ts
vendored
1
packages/core/evaluators/client.d.ts
vendored
@ -1,2 +1,3 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
export * from './lib/client';
|
export * from './lib/client';
|
||||||
|
export { default } from './lib/client';
|
||||||
|
1
packages/core/evaluators/server.d.ts
vendored
1
packages/core/evaluators/server.d.ts
vendored
@ -1,2 +1,3 @@
|
|||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
export * from './lib/server';
|
export * from './lib/server';
|
||||||
|
export { default } from './lib/server';
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import evaluate from '../../utils/formulajs';
|
import { evaluate } from '../../utils';
|
||||||
|
import formulajs from '../../utils/formulajs';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -6,5 +7,5 @@ export default {
|
|||||||
label: 'Formula.js',
|
label: 'Formula.js',
|
||||||
tooltip: '{{t("Formula.js supports most Microsoft Excel formula functions.")}}',
|
tooltip: '{{t("Formula.js supports most Microsoft Excel formula functions.")}}',
|
||||||
link: 'https://formulajs.info/functions/',
|
link: 'https://formulajs.info/functions/',
|
||||||
evaluate
|
evaluate: evaluate.bind(formulajs)
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import evaluate from "../../utils/mathjs";
|
import { evaluate } from "../../utils";
|
||||||
|
import mathjs from "../../utils/mathjs";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -6,5 +7,5 @@ export default {
|
|||||||
label: 'Math.js',
|
label: 'Math.js',
|
||||||
tooltip: `{{t('Math.js comes with a large set of built-in functions and constants, and offers an integrated solution to work with different data types')}}`,
|
tooltip: `{{t('Math.js comes with a large set of built-in functions and constants, and offers an integrated solution to work with different data types')}}`,
|
||||||
link: "https://mathjs.org/",
|
link: "https://mathjs.org/",
|
||||||
evaluate
|
evaluate: evaluate.bind(mathjs)
|
||||||
};
|
};
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
|
import { i18n } from '@nocobase/client';
|
||||||
import { Registry } from '@nocobase/utils/client';
|
import { Registry } from '@nocobase/utils/client';
|
||||||
|
|
||||||
import mathjs from './engines/mathjs';
|
import mathjs from './engines/mathjs';
|
||||||
@ -10,9 +14,34 @@ export interface Evaluator {
|
|||||||
evaluate(exp: string, scope?: { [key: string]: any }): any;
|
evaluate(exp: string, scope?: { [key: string]: any }): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
const evaluators = new Registry<Evaluator>();
|
export const evaluators = new Registry<Evaluator>();
|
||||||
|
|
||||||
evaluators.register('math.js', mathjs);
|
evaluators.register('math.js', mathjs);
|
||||||
evaluators.register('formula.js', formulajs);
|
evaluators.register('formula.js', formulajs);
|
||||||
|
|
||||||
|
export const renderReference = (key: string) => {
|
||||||
|
const engine = evaluators.get(key);
|
||||||
|
if (!engine) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine.link
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
<span className={css`
|
||||||
|
&:after {
|
||||||
|
content: ':';
|
||||||
|
}
|
||||||
|
& + a {
|
||||||
|
margin-left: .25em;
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
{i18n.t('Syntax references')}
|
||||||
|
</span>
|
||||||
|
<a href={engine.link} target="_blank">{engine.label}</a>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
|
||||||
export default evaluators;
|
export default evaluators;
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export { default } from './server';
|
export { default } from './server';
|
||||||
|
export * from './server';
|
||||||
|
@ -1,24 +1,16 @@
|
|||||||
import { get } from "lodash";
|
import { Registry } from "@nocobase/utils";
|
||||||
|
|
||||||
import { Registry, RegistryOptions } from "@nocobase/utils";
|
|
||||||
|
|
||||||
|
import { evaluate, Evaluator } from '../utils';
|
||||||
import mathjs from "../utils/mathjs";
|
import mathjs from "../utils/mathjs";
|
||||||
import formulajs from "../utils/formulajs";
|
import formulajs from "../utils/formulajs";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export interface Evaluator {
|
export { Evaluator } from '../utils';
|
||||||
(expression: string, scope?: { [key: string]: any }): any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EvaluatorsOptions extends RegistryOptions {
|
export const evaluators = new Registry<Evaluator>();
|
||||||
empty?: boolean;
|
|
||||||
evaluators?: { [key: string]: Evaluator };
|
|
||||||
}
|
|
||||||
|
|
||||||
const evaluators = new Registry<Evaluator>();
|
evaluators.register('math.js', evaluate.bind(mathjs));
|
||||||
|
evaluators.register('formula.js', evaluate.bind(formulajs));
|
||||||
evaluators.register('math.js', mathjs);
|
|
||||||
evaluators.register('formula.js', formulajs);
|
|
||||||
|
|
||||||
export default evaluators;
|
export default evaluators;
|
||||||
|
@ -5,8 +5,7 @@ import * as functions from '@formulajs/formulajs';
|
|||||||
const fnNames = Object.keys(functions).filter(key => key !== 'default');
|
const fnNames = Object.keys(functions).filter(key => key !== 'default');
|
||||||
const fns = fnNames.map(key => functions[key]);
|
const fns = fnNames.map(key => functions[key]);
|
||||||
|
|
||||||
export default function(exp: string, scope = {}) {
|
export default function(expression: string, scope = {}) {
|
||||||
const expression = exp.replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => v);
|
|
||||||
const fn = new Function(...fnNames, ...Object.keys(scope), `return ${expression}`);
|
const fn = new Function(...fnNames, ...Object.keys(scope), `return ${expression}`);
|
||||||
const result = fn(...fns, ...Object.values(scope));
|
const result = fn(...fns, ...Object.values(scope));
|
||||||
if (typeof result === 'number') {
|
if (typeof result === 'number') {
|
||||||
|
16
packages/core/evaluators/src/utils/index.ts
Normal file
16
packages/core/evaluators/src/utils/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { get } from "lodash";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export type Scope = { [key: string]: any };
|
||||||
|
|
||||||
|
export type Evaluator = (expression: string, scope?: Scope) => any;
|
||||||
|
|
||||||
|
export function evaluate(this: Evaluator, expression: string, scope: Scope = {}) {
|
||||||
|
const exp = expression.trim().replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => {
|
||||||
|
const item = get(scope, v);
|
||||||
|
const key = v.replace(/\.(\d+)/g, '["$1"]');
|
||||||
|
return ` ${typeof item === 'function' ? item() : key} `;
|
||||||
|
});
|
||||||
|
return this(exp, scope);
|
||||||
|
}
|
@ -1,8 +1,7 @@
|
|||||||
import * as math from 'mathjs';
|
import * as math from 'mathjs';
|
||||||
|
|
||||||
export default function (expression: string, scope = {}) {
|
export default function (expression: string, scope = {}) {
|
||||||
const exp = expression.trim().replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => v);
|
const result = math.evaluate(expression, scope);
|
||||||
const result = math.evaluate(exp, scope);
|
|
||||||
if (typeof result === 'number') {
|
if (typeof result === 'number') {
|
||||||
if (Number.isNaN(result) || !Number.isFinite(result)) {
|
if (Number.isNaN(result) || !Number.isFinite(result)) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -61,7 +61,7 @@ export default {
|
|||||||
group: 'advanced',
|
group: 'advanced',
|
||||||
order: 1,
|
order: 1,
|
||||||
title: `{{t("Formula", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Formula", { ns: "${NAMESPACE}" })}}`,
|
||||||
description: '{{t("Compute a value based on the other fields using mathjs")}}',
|
description: `{{t("Compute a value based on the other fields", { ns: "${NAMESPACE}" })}}`,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
default: {
|
default: {
|
||||||
type: 'formula',
|
type: 'formula',
|
||||||
|
@ -1,242 +1,25 @@
|
|||||||
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
import React from 'react';
|
||||||
import { css, cx } from '@emotion/css';
|
import { useCompile, Variable } from '@nocobase/client';
|
||||||
import { useForm } from '@formily/react';
|
|
||||||
import { Button, Cascader, Input, Tooltip } from 'antd';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { useCompile } from '@nocobase/client';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const VARIABLE_RE = /\{\{\s*([^{}]+)\s*\}\}/g;
|
|
||||||
|
|
||||||
function pasteHtml(container, html, { selectPastedContent = false, range: indexes }) {
|
|
||||||
// IE9 and non-IE
|
|
||||||
const sel = window.getSelection?.();
|
|
||||||
if (!sel?.getRangeAt || !sel.rangeCount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const range = sel.getRangeAt(0);
|
|
||||||
if (!range) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const children = Array.from(container.childNodes) as HTMLElement[];
|
|
||||||
if (indexes[0] === -1) {
|
|
||||||
if (indexes[1]) {
|
|
||||||
range.setStartAfter(children[indexes[1] - 1]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
range.setStart(children[indexes[0]], indexes[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (indexes[2] === -1) {
|
|
||||||
if (indexes[3]) {
|
|
||||||
range.setEndAfter(children[indexes[3] - 1]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
range.setEnd(children[indexes[2]], indexes[3]);
|
|
||||||
}
|
|
||||||
range.deleteContents();
|
|
||||||
|
|
||||||
// Range.createContextualFragment() would be useful here but is
|
|
||||||
// only relatively recently standardized and is not supported in
|
|
||||||
// some browsers (IE9, for one)
|
|
||||||
const el = document.createElement('div');
|
|
||||||
el.innerHTML = html;
|
|
||||||
|
|
||||||
const frag = document.createDocumentFragment();
|
|
||||||
let lastNode;
|
|
||||||
while (el.firstChild) {
|
|
||||||
lastNode = frag.appendChild(el.firstChild);
|
|
||||||
}
|
|
||||||
const { firstChild } = frag;
|
|
||||||
range.insertNode(frag);
|
|
||||||
|
|
||||||
// Preserve the selection
|
|
||||||
if (lastNode) {
|
|
||||||
const next = range.cloneRange();
|
|
||||||
next.setStartAfter(lastNode);
|
|
||||||
if (selectPastedContent) {
|
|
||||||
if (firstChild) {
|
|
||||||
next.setStartBefore(firstChild);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
next.collapse(true);
|
|
||||||
}
|
|
||||||
sel.removeAllRanges();
|
|
||||||
sel.addRange(next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getValue(el) {
|
|
||||||
const values: any[] = [];
|
|
||||||
for (const node of el.childNodes) {
|
|
||||||
if (node.nodeName === 'SPAN') {
|
|
||||||
values.push(`{{${node['dataset']['key']}}}`);
|
|
||||||
} else {
|
|
||||||
values.push(node.textContent?.trim?.());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return values.join(' ').replace(/\s+/g, ' ').trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderHTML(exp: string, keyLabelMap) {
|
|
||||||
return exp.replace(VARIABLE_RE, (_, i) => {
|
|
||||||
const key = i.trim();
|
|
||||||
return createVariableTagHTML(key, keyLabelMap) ?? '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createOptionsKeyLabelMap(options: any[]) {
|
|
||||||
const map = new Map<string, string[]>();
|
|
||||||
for (const option of options) {
|
|
||||||
map.set(option.value, [option.label]);
|
|
||||||
if (option.children) {
|
|
||||||
for (const [value, labels] of createOptionsKeyLabelMap(option.children)) {
|
|
||||||
map.set(`${option.value}.${value}`, [option.label, ...labels]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createVariableTagHTML(variable, keyLabelMap) {
|
|
||||||
const labels = keyLabelMap.get(variable);
|
|
||||||
return `<span class="ant-tag ant-tag-blue" contentEditable="false" data-key="${variable}">${labels?.join(' / ')}</span>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Expression = (props) => {
|
export const Expression = (props) => {
|
||||||
const { value = '', supports, useCurrentFields, onChange } = props;
|
const { value = '', supports, useCurrentFields, onChange } = props;
|
||||||
const { t } = useTranslation();
|
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
|
|
||||||
const fields = useCurrentFields().filter(field => supports.includes(field.interface));
|
const fields = useCurrentFields().filter(field => supports.includes(field.interface));
|
||||||
|
|
||||||
const inputRef = useRef<HTMLDivElement>(null);
|
|
||||||
const options = fields.map(field => ({
|
const options = fields.map(field => ({
|
||||||
label: compile(field.uiSchema.title),
|
label: compile(field.uiSchema.title),
|
||||||
value: field.name
|
value: field.name
|
||||||
}));
|
}));
|
||||||
const form = useForm();
|
|
||||||
const keyLabelMap = useMemo(() => createOptionsKeyLabelMap(options), []);
|
|
||||||
const [changed, setChanged] = useState(false);
|
|
||||||
const [html, setHtml] = useState(() => renderHTML(value ?? '', keyLabelMap));
|
|
||||||
// [startElementIndex, startOffset, endElementIndex, endOffset]
|
|
||||||
const [range, setRange] = useState<[number, number, number, number]>([-1, 0, -1, 0]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (changed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setHtml(renderHTML(value ?? '', keyLabelMap));
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const { current } = inputRef;
|
|
||||||
if (!current || changed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const nextRange = new Range();
|
|
||||||
const { lastChild } = current;
|
|
||||||
if (lastChild) {
|
|
||||||
nextRange.setStartAfter(lastChild);
|
|
||||||
nextRange.setEndAfter(lastChild);
|
|
||||||
const nodes = Array.from(current.childNodes);
|
|
||||||
const startElementIndex = nextRange.startContainer === current ? -1 : nodes.indexOf(lastChild);
|
|
||||||
const endElementIndex = nextRange.startContainer === current ? -1 : nodes.indexOf(lastChild);
|
|
||||||
setRange([startElementIndex, nextRange.startOffset, endElementIndex, nextRange.endOffset]);
|
|
||||||
}
|
|
||||||
}, [html]);
|
|
||||||
|
|
||||||
function onInsert(keyPath) {
|
|
||||||
const variable: string[] = keyPath.filter(key => Boolean(key.trim()));
|
|
||||||
const { current } = inputRef;
|
|
||||||
if (!current || !variable) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
current.focus();
|
|
||||||
|
|
||||||
pasteHtml(current, createVariableTagHTML(variable.join('.'), keyLabelMap), {
|
|
||||||
range,
|
|
||||||
});
|
|
||||||
|
|
||||||
setChanged(true);
|
|
||||||
onChange(getValue(current));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onInput({ currentTarget }) {
|
|
||||||
setChanged(true);
|
|
||||||
onChange(getValue(currentTarget));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBlur({ currentTarget }) {
|
|
||||||
const sel = window.getSelection?.();
|
|
||||||
if (!sel?.getRangeAt || !sel.rangeCount) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const r = sel.getRangeAt(0);
|
|
||||||
const nodes = Array.from(currentTarget.childNodes);
|
|
||||||
const startElementIndex = nodes.indexOf(r.startContainer);
|
|
||||||
const endElementIndex = nodes.indexOf(r.endContainer);
|
|
||||||
setRange([startElementIndex, r.startOffset, endElementIndex, r.endOffset]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const disabled = props.disabled || form.disabled;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input.Group compact className={css`
|
<Variable.TextArea
|
||||||
&.ant-input-group.ant-input-group-compact{
|
value={value}
|
||||||
display: flex;
|
onChange={onChange}
|
||||||
.ant-input{
|
scope={options}
|
||||||
flex-grow: 1;
|
/>
|
||||||
}
|
|
||||||
.ant-input-disabled{
|
|
||||||
.ant-tag{
|
|
||||||
color: #bfbfbf;
|
|
||||||
border-color: #d9d9d9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
<div
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onBlur={onBlur}
|
|
||||||
onInput={onInput}
|
|
||||||
className={cx('ant-input', { 'ant-input-disabled': disabled }, css`
|
|
||||||
overflow: auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
.ant-tag{
|
|
||||||
display: inline;
|
|
||||||
line-height: 19px;
|
|
||||||
margin: 0 .5em;
|
|
||||||
padding: 2px 7px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
`)}
|
|
||||||
ref={inputRef as any}
|
|
||||||
contentEditable={!disabled}
|
|
||||||
dangerouslySetInnerHTML={{ __html: html }}
|
|
||||||
/>
|
|
||||||
<Tooltip title={t('Use variable')}>
|
|
||||||
<Cascader
|
|
||||||
value={[]}
|
|
||||||
options={options}
|
|
||||||
onChange={onInsert}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
className={css`
|
|
||||||
font-style: italic;
|
|
||||||
font-family: "New York", "Times New Roman", Times, serif;
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
x
|
|
||||||
</Button>
|
|
||||||
</Cascader>
|
|
||||||
</Tooltip>
|
|
||||||
</Input.Group>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { css } from '@emotion/css';
|
|||||||
import { CollectionManagerContext, registerField, SchemaComponentOptions } from '@nocobase/client';
|
import { CollectionManagerContext, registerField, SchemaComponentOptions } from '@nocobase/client';
|
||||||
import evaluators, { Evaluator } from '@nocobase/evaluators/client';
|
import evaluators, { Evaluator } from '@nocobase/evaluators/client';
|
||||||
|
|
||||||
import Formula from './formula';
|
import { Formula } from './formula';
|
||||||
import field from './field';
|
import field from './field';
|
||||||
import { NAMESPACE } from './locale';
|
import { NAMESPACE } from './locale';
|
||||||
import { Registry } from '@nocobase/utils/client';
|
import { Registry } from '@nocobase/utils/client';
|
||||||
@ -36,6 +36,8 @@ function renderExpressionDescription(key: string) {
|
|||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { Formula } from './formula';
|
||||||
|
|
||||||
export default React.memo((props) => {
|
export default React.memo((props) => {
|
||||||
const ctx = useContext(CollectionManagerContext);
|
const ctx = useContext(CollectionManagerContext);
|
||||||
return (
|
return (
|
||||||
|
@ -3,5 +3,6 @@ export default {
|
|||||||
'Formula engine': '公式引擎',
|
'Formula engine': '公式引擎',
|
||||||
'Expression': '表达式',
|
'Expression': '表达式',
|
||||||
'Expression syntax error': '表达式语法错误',
|
'Expression syntax error': '表达式语法错误',
|
||||||
'Syntax references': '语法参考'
|
'Syntax references': '语法参考',
|
||||||
|
'Compute a value based on the other fields': '基于其他字段进行计算'
|
||||||
}
|
}
|
||||||
|
@ -89,6 +89,25 @@ describe('formula field', () => {
|
|||||||
|
|
||||||
expect(test.get('sum')).toEqual(3.22);
|
expect(test.get('sum')).toEqual(3.22);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('scope with number key', async () => {
|
||||||
|
const expression = '{{a.1}}+1';
|
||||||
|
const Test = db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [
|
||||||
|
{ type: 'json', name: 'a' },
|
||||||
|
{ name: 'sum', type: 'formula', expression, engine: 'math.js' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await db.sync();
|
||||||
|
|
||||||
|
const test = await Test.model.create<any>({
|
||||||
|
a: { '1': 1 },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(test.get('sum')).toEqual(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('formula.js', () => {
|
describe('formula.js', () => {
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
"@nocobase/actions": "0.9.0-alpha.2",
|
"@nocobase/actions": "0.9.0-alpha.2",
|
||||||
"@nocobase/client": "0.9.0-alpha.2",
|
"@nocobase/client": "0.9.0-alpha.2",
|
||||||
"@nocobase/database": "0.9.0-alpha.2",
|
"@nocobase/database": "0.9.0-alpha.2",
|
||||||
|
"@nocobase/evaluators": "0.9.0-alpha.2",
|
||||||
"@nocobase/resourcer": "0.9.0-alpha.2",
|
"@nocobase/resourcer": "0.9.0-alpha.2",
|
||||||
"@nocobase/server": "0.9.0-alpha.2",
|
"@nocobase/server": "0.9.0-alpha.2",
|
||||||
"@nocobase/utils": "0.9.0-alpha.2",
|
"@nocobase/utils": "0.9.0-alpha.2",
|
||||||
@ -16,9 +17,7 @@
|
|||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
"cron-parser": "4.4.0",
|
"cron-parser": "4.4.0",
|
||||||
"@formulajs/formulajs": "4.2.0",
|
|
||||||
"json-templates": "^4.2.0",
|
"json-templates": "^4.2.0",
|
||||||
"mathjs": "^10.6.0",
|
|
||||||
"moment": "^2.29.2",
|
"moment": "^2.29.2",
|
||||||
"react-js-cron": "^1.4.0"
|
"react-js-cron": "^1.4.0"
|
||||||
},
|
},
|
||||||
|
@ -5,8 +5,7 @@ import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { css } from "@emotion/css";
|
import { css } from "@emotion/css";
|
||||||
|
|
||||||
import { CollectionField, CollectionProvider, SchemaComponent, useCollectionManager, useCompile } from "@nocobase/client";
|
import { CollectionField, CollectionProvider, SchemaComponent, Variable, useCollectionManager, useCompile } from "@nocobase/client";
|
||||||
import { VariableInput } from "./VariableInput";
|
|
||||||
import { lang } from "../locale";
|
import { lang } from "../locale";
|
||||||
import { useWorkflowVariableOptions } from "../variable";
|
import { useWorkflowVariableOptions } from "../variable";
|
||||||
|
|
||||||
@ -40,7 +39,8 @@ export default observer(({ value, disabled, onChange }: any) => {
|
|||||||
.filter(field => (
|
.filter(field => (
|
||||||
!field.hidden
|
!field.hidden
|
||||||
&& (field.uiSchema ? !field.uiSchema['x-read-pretty'] : false)
|
&& (field.uiSchema ? !field.uiSchema['x-read-pretty'] : false)
|
||||||
// && (!['linkTo', 'hasMany', 'hasOne', 'belongsToMany'].includes(field.type))
|
// TODO: should use some field option but not type to control this
|
||||||
|
&& (!['formula'].includes(field.type))
|
||||||
));
|
));
|
||||||
|
|
||||||
const unassignedFields = fields.filter(field => !(field.name in value));
|
const unassignedFields = fields.filter(field => !(field.name in value));
|
||||||
@ -77,7 +77,7 @@ export default observer(({ value, disabled, onChange }: any) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
`}>
|
`}>
|
||||||
<VariableInput
|
<Variable.Input
|
||||||
scope={['hasMany', 'belongsToMany'].includes(field.type) ? [] : scope}
|
scope={['hasMany', 'belongsToMany'].includes(field.type) ? [] : scope}
|
||||||
value={value[field.name]}
|
value={value[field.name]}
|
||||||
onChange={(next) => {
|
onChange={(next) => {
|
||||||
@ -94,7 +94,7 @@ export default observer(({ value, disabled, onChange }: any) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</VariableInput>
|
</Variable.Input>
|
||||||
{!mergedDisabled
|
{!mergedDisabled
|
||||||
? (
|
? (
|
||||||
<Button
|
<Button
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
|
import { Variable } from "@nocobase/client";
|
||||||
|
|
||||||
import { useWorkflowVariableOptions } from "../variable";
|
import { useWorkflowVariableOptions } from "../variable";
|
||||||
import { VariableInput } from "./VariableInput";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -9,12 +10,11 @@ export function FilterDynamicComponent({ value, onChange, renderSchemaComponent
|
|||||||
const scope = useWorkflowVariableOptions();
|
const scope = useWorkflowVariableOptions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VariableInput
|
<Variable.Input
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
scope={scope}
|
scope={scope}
|
||||||
>
|
>
|
||||||
{renderSchemaComponent()}
|
</Variable.Input>
|
||||||
</VariableInput>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -54,11 +54,6 @@ export default {
|
|||||||
'Advanced': '高级模式',
|
'Advanced': '高级模式',
|
||||||
'End': '结束',
|
'End': '结束',
|
||||||
'Node result': '节点数据',
|
'Node result': '节点数据',
|
||||||
'Constant': '常量',
|
|
||||||
'Null': '空值',
|
|
||||||
'Boolean': '逻辑值',
|
|
||||||
'String': '字符串',
|
|
||||||
'Date': '日期',
|
|
||||||
'Calculator': '运算',
|
'Calculator': '运算',
|
||||||
'Arithmetic calculation': '算术运算',
|
'Arithmetic calculation': '算术运算',
|
||||||
'String operation': '字符串',
|
'String operation': '字符串',
|
||||||
|
@ -3,13 +3,12 @@ import { css } from '@emotion/css';
|
|||||||
import parse from 'json-templates';
|
import parse from 'json-templates';
|
||||||
|
|
||||||
import { SchemaInitializer, SchemaInitializerItemOptions } from '@nocobase/client';
|
import { SchemaInitializer, SchemaInitializerItemOptions } from '@nocobase/client';
|
||||||
|
import evaluators, { renderReference, Evaluator } from '@nocobase/evaluators/client';
|
||||||
|
|
||||||
import { useFlowContext } from '../../FlowContext';
|
import { useFlowContext } from '../FlowContext';
|
||||||
import { lang, NAMESPACE } from '../../locale';
|
import { lang, NAMESPACE } from '../locale';
|
||||||
import { VariableTextArea } from '../../components/VariableTextArea';
|
import { TypeSets, useWorkflowVariableOptions } from '../variable';
|
||||||
import { TypeSets, useWorkflowVariableOptions } from '../../variable';
|
import { RadioWithTooltip } from '../components/RadioWithTooltip';
|
||||||
import { calculationEngines, renderReference } from './engines';
|
|
||||||
import { RadioWithTooltip } from '../../components/RadioWithTooltip';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -25,7 +24,7 @@ export default {
|
|||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'RadioWithTooltip',
|
'x-component': 'RadioWithTooltip',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
options: Array.from(calculationEngines.getEntities()).reduce((result: any[], [value, options]) => result.concat({ value, ...options }), [])
|
options: Array.from(evaluators.getEntities()).reduce((result: any[], [value, options]) => result.concat({ value, ...options }), [])
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
default: 'math.js'
|
default: 'math.js'
|
||||||
@ -35,14 +34,14 @@ export default {
|
|||||||
title: `{{t("Calculation expression", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Calculation expression", { ns: "${NAMESPACE}" })}}`,
|
||||||
name: 'config.expression',
|
name: 'config.expression',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'VariableTextArea',
|
'x-component': 'Variable.TextArea',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
scope: '{{useWorkflowVariableOptions}}'
|
scope: '{{useWorkflowVariableOptions}}'
|
||||||
},
|
},
|
||||||
['x-validator'](value, rules, { form }) {
|
['x-validator'](value, rules, { form }) {
|
||||||
const { values } = form;
|
const { values } = form;
|
||||||
const { evaluate } = calculationEngines.get(values.config.engine);
|
const { evaluate } = evaluators.get(values.config.engine) as Evaluator;
|
||||||
const exp = value.trim().replace(/\{\{([^{}]+)\}\}/g, '1');
|
const exp = value.trim().replace(/{{([^{}]+)}}/g, '1');
|
||||||
try {
|
try {
|
||||||
evaluate(exp);
|
evaluate(exp);
|
||||||
return '';
|
return '';
|
||||||
@ -86,8 +85,7 @@ export default {
|
|||||||
</pre>
|
</pre>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
RadioWithTooltip,
|
RadioWithTooltip
|
||||||
VariableTextArea
|
|
||||||
},
|
},
|
||||||
getOptions(config, types) {
|
getOptions(config, types) {
|
||||||
if (types && !types.some(type => type in TypeSets || Object.values(TypeSets).some(set => set.has(type)))) {
|
if (types && !types.some(type => type in TypeSets || Object.values(TypeSets).some(set => set.has(type)))) {
|
@ -1,13 +0,0 @@
|
|||||||
import * as formulajs from '@formulajs/formulajs';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default {
|
|
||||||
label: 'Formula.js',
|
|
||||||
tooltip: '{{t("Formula.js supports most Microsoft Excel formula functions.")}}',
|
|
||||||
link: 'https://formulajs.info/functions/',
|
|
||||||
evaluate(exp: string) {
|
|
||||||
const fn = new Function(...Object.keys(formulajs), `return ${exp}`);
|
|
||||||
return fn(...Object.values(formulajs));
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,45 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { css } from '@emotion/css';
|
|
||||||
|
|
||||||
import { Registry } from '@nocobase/utils/client';
|
|
||||||
import { i18n } from '@nocobase/client';
|
|
||||||
|
|
||||||
import mathjs from './mathjs';
|
|
||||||
import formulajs from './formulajs';
|
|
||||||
|
|
||||||
export interface CalculationEngine {
|
|
||||||
label: string;
|
|
||||||
tooltip?: string;
|
|
||||||
link?: string;
|
|
||||||
evaluate(exp: string): any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const calculationEngines = new Registry<CalculationEngine>();
|
|
||||||
|
|
||||||
calculationEngines.register('math.js', mathjs);
|
|
||||||
calculationEngines.register('formula.js', formulajs);
|
|
||||||
|
|
||||||
export const renderReference = (key: string) => {
|
|
||||||
const engine = calculationEngines.get(key);
|
|
||||||
if (!engine) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return engine.link
|
|
||||||
? (
|
|
||||||
<>
|
|
||||||
<span className={css`
|
|
||||||
&:after {
|
|
||||||
content: ':';
|
|
||||||
}
|
|
||||||
& + a {
|
|
||||||
margin-left: .25em;
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
{i18n.t('Syntax references')}
|
|
||||||
</span>
|
|
||||||
<a href={engine.link} target="_blank">{engine.label}</a>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
: null
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
import { evaluate } from "mathjs";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default {
|
|
||||||
label: 'Math.js',
|
|
||||||
tooltip: `{{t('Math.js comes with a large set of built-in functions and constants, and offers an integrated solution to work with different data types')}}`,
|
|
||||||
link: "https://mathjs.org/",
|
|
||||||
evaluate
|
|
||||||
};
|
|
@ -5,6 +5,8 @@ import { CloseCircleOutlined } from '@ant-design/icons';
|
|||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Registry } from "@nocobase/utils/client";
|
import { Registry } from "@nocobase/utils/client";
|
||||||
|
import { Variable, useCompile } from "@nocobase/client";
|
||||||
|
import evaluators, { renderReference } from "@nocobase/evaluators/client";
|
||||||
|
|
||||||
import { NodeDefaultView } from ".";
|
import { NodeDefaultView } from ".";
|
||||||
import { Branch } from "../Branch";
|
import { Branch } from "../Branch";
|
||||||
@ -12,11 +14,7 @@ import { useFlowContext } from '../FlowContext';
|
|||||||
import { branchBlockClass, nodeSubtreeClass } from "../style";
|
import { branchBlockClass, nodeSubtreeClass } from "../style";
|
||||||
import { lang, NAMESPACE } from "../locale";
|
import { lang, NAMESPACE } from "../locale";
|
||||||
import { useWorkflowVariableOptions } from "../variable";
|
import { useWorkflowVariableOptions } from "../variable";
|
||||||
import { VariableTextArea } from "../components/VariableTextArea";
|
|
||||||
import { VariableInput } from "../components/VariableInput";
|
|
||||||
import { RadioWithTooltip, RadioWithTooltipOption } from "../components/RadioWithTooltip";
|
import { RadioWithTooltip, RadioWithTooltipOption } from "../components/RadioWithTooltip";
|
||||||
import { calculationEngines, renderReference } from "./calculation/engines";
|
|
||||||
import { useCompile } from "@nocobase/client";
|
|
||||||
|
|
||||||
interface Calculator {
|
interface Calculator {
|
||||||
name: string;
|
name: string;
|
||||||
@ -152,7 +150,7 @@ export function Calculation({ calculator, operands = [], onChange }) {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
`}>
|
`}>
|
||||||
<VariableInput
|
<Variable.Input
|
||||||
value={operands[0]}
|
value={operands[0]}
|
||||||
onChange={(v => onChange({ calculator, operands: [v, operands[1]] }))}
|
onChange={(v => onChange({ calculator, operands: [v, operands[1]] }))}
|
||||||
scope={options}
|
scope={options}
|
||||||
@ -170,7 +168,7 @@ export function Calculation({ calculator, operands = [], onChange }) {
|
|||||||
</Select.OptGroup>
|
</Select.OptGroup>
|
||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
<VariableInput
|
<Variable.Input
|
||||||
value={operands[1]}
|
value={operands[1]}
|
||||||
onChange={(v => onChange({ calculator, operands: [operands[0], v] }))}
|
onChange={(v => onChange({ calculator, operands: [operands[0], v] }))}
|
||||||
scope={options}
|
scope={options}
|
||||||
@ -335,7 +333,7 @@ export default {
|
|||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
options: [
|
options: [
|
||||||
['basic', { label: `{{t("Basic", { ns: "${NAMESPACE}" })}}` }],
|
['basic', { label: `{{t("Basic", { ns: "${NAMESPACE}" })}}` }],
|
||||||
...Array.from(calculationEngines.getEntities())
|
...Array.from(evaluators.getEntities())
|
||||||
].reduce((result: RadioWithTooltipOption[], [value, options]: any) => result.concat({ value, ...options }), []),
|
].reduce((result: RadioWithTooltipOption[], [value, options]: any) => result.concat({ value, ...options }), []),
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
@ -362,14 +360,14 @@ export default {
|
|||||||
title: `{{t("Condition expression", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Condition expression", { ns: "${NAMESPACE}" })}}`,
|
||||||
name: 'config.expression',
|
name: 'config.expression',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'VariableTextArea',
|
'x-component': 'Variable.TextArea',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
scope: '{{useWorkflowVariableOptions}}'
|
scope: '{{useWorkflowVariableOptions}}'
|
||||||
},
|
},
|
||||||
['x-validator'](value, rules, { form }) {
|
['x-validator'](value, rules, { form }) {
|
||||||
const { values } = form;
|
const { values } = form;
|
||||||
const { evaluate } = calculationEngines.get(values.config.engine);
|
const { evaluate } = evaluators.get(values.config.engine);
|
||||||
const exp = value.trim().replace(/\{\{([^{}]+)\}\}/g, '1');
|
const exp = value.trim().replace(/{{([^{}]+)}}/g, '1');
|
||||||
try {
|
try {
|
||||||
evaluate(exp);
|
evaluate(exp);
|
||||||
return '';
|
return '';
|
||||||
@ -450,7 +448,6 @@ export default {
|
|||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
CalculationConfig,
|
CalculationConfig,
|
||||||
VariableTextArea,
|
|
||||||
RadioWithTooltip
|
RadioWithTooltip
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import { RemoteSelect } from '@nocobase/client';
|
import { RemoteSelect } from '@nocobase/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { VariableInput } from '../../components/VariableInput';
|
import { Variable } from '@nocobase/client';
|
||||||
import { useWorkflowVariableOptions } from '../../variable';
|
import { useWorkflowVariableOptions } from '../../variable';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function AssigneesSelect({ multiple = false, value = [], onChange }) {
|
export function AssigneesSelect({ multiple = false, value = [], onChange }) {
|
||||||
const scope = useWorkflowVariableOptions();
|
const scope = useWorkflowVariableOptions();
|
||||||
|
console.log(value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VariableInput
|
<Variable.Input
|
||||||
scope={scope}
|
scope={scope}
|
||||||
types={[{ type: 'reference', options: { collection: 'users' } }]}
|
types={[{ type: 'reference', options: { collection: 'users' } }]}
|
||||||
value={value[0]}
|
value={value[0]}
|
||||||
@ -30,6 +31,6 @@ export function AssigneesSelect({ multiple = false, value = [], onChange }) {
|
|||||||
onChange([v]);
|
onChange([v]);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</VariableInput>
|
</Variable.Input>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,6 @@ import { css } from '@emotion/css';
|
|||||||
|
|
||||||
import { NAMESPACE } from '../locale';
|
import { NAMESPACE } from '../locale';
|
||||||
import { useWorkflowVariableOptions } from '../variable';
|
import { useWorkflowVariableOptions } from '../variable';
|
||||||
import { VariableJSONInput } from '../components/VariableJSONInput';
|
|
||||||
import { VariableInput } from '../components/VariableInput';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -79,7 +77,7 @@ export default {
|
|||||||
value: {
|
value: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'VariableInput',
|
'x-component': 'Variable.Input',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
scope: useWorkflowVariableOptions
|
scope: useWorkflowVariableOptions
|
||||||
}
|
}
|
||||||
@ -125,7 +123,7 @@ export default {
|
|||||||
value: {
|
value: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'VariableInput',
|
'x-component': 'Variable.Input',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
scope: useWorkflowVariableOptions
|
scope: useWorkflowVariableOptions
|
||||||
}
|
}
|
||||||
@ -153,7 +151,7 @@ export default {
|
|||||||
title: `{{t("Body", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Body", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-decorator-props': {},
|
'x-decorator-props': {},
|
||||||
'x-component': 'VariableJSONInput',
|
'x-component': 'Variable.JSON',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
scope: useWorkflowVariableOptions,
|
scope: useWorkflowVariableOptions,
|
||||||
autoSize: {
|
autoSize: {
|
||||||
@ -193,7 +191,5 @@ export default {
|
|||||||
scope: {},
|
scope: {},
|
||||||
components: {
|
components: {
|
||||||
ArrayItems,
|
ArrayItems,
|
||||||
VariableInput,
|
|
||||||
VariableJSONInput
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -5,8 +5,6 @@ import { Plugin } from '@nocobase/server';
|
|||||||
import { Registry } from '@nocobase/utils';
|
import { Registry } from '@nocobase/utils';
|
||||||
|
|
||||||
import initActions from './actions';
|
import initActions from './actions';
|
||||||
import initCalculationEngines from './calculators';
|
|
||||||
import type { Evaluator } from './calculators';
|
|
||||||
import { EXECUTION_STATUS } from './constants';
|
import { EXECUTION_STATUS } from './constants';
|
||||||
import initInstructions, { Instruction } from './instructions';
|
import initInstructions, { Instruction } from './instructions';
|
||||||
import ExecutionModel from './models/Execution';
|
import ExecutionModel from './models/Execution';
|
||||||
@ -20,7 +18,6 @@ type Pending = [ExecutionModel, JobModel?];
|
|||||||
export default class WorkflowPlugin extends Plugin {
|
export default class WorkflowPlugin extends Plugin {
|
||||||
instructions: Registry<Instruction> = new Registry();
|
instructions: Registry<Instruction> = new Registry();
|
||||||
triggers: Registry<Trigger> = new Registry();
|
triggers: Registry<Trigger> = new Registry();
|
||||||
calculators: Registry<Evaluator> = new Registry();
|
|
||||||
functions: Registry<Function> = new Registry();
|
functions: Registry<Function> = new Registry();
|
||||||
executing: ExecutionModel | null = null;
|
executing: ExecutionModel | null = null;
|
||||||
pending: Pending[] = [];
|
pending: Pending[] = [];
|
||||||
@ -78,7 +75,6 @@ export default class WorkflowPlugin extends Plugin {
|
|||||||
initActions(this);
|
initActions(this);
|
||||||
initTriggers(this, options.triggers);
|
initTriggers(this, options.triggers);
|
||||||
initInstructions(this, options.instructions);
|
initInstructions(this, options.instructions);
|
||||||
initCalculationEngines(this);
|
|
||||||
initFunctions(this, options.functions);
|
initFunctions(this, options.functions);
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { MockServer, mockServer } from '@nocobase/test';
|
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { ApplicationOptions } from '@nocobase/server';
|
import { ApplicationOptions } from '@nocobase/server';
|
||||||
|
import { MockServer, mockServer } from '@nocobase/test';
|
||||||
|
|
||||||
import Plugin from '..';
|
import Plugin from '..';
|
||||||
import calculators from '../calculators';
|
|
||||||
import { JOB_STATUS } from '../constants';
|
import { JOB_STATUS } from '../constants';
|
||||||
|
|
||||||
export function sleep(ms: number) {
|
export function sleep(ms: number) {
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
import { Registry } from "@nocobase/utils";
|
|
||||||
|
|
||||||
export const calculators = new Registry<Function>();
|
|
||||||
|
|
||||||
|
|
||||||
// built-in functions
|
|
||||||
function equal(a, b) {
|
|
||||||
return a === b;
|
|
||||||
}
|
|
||||||
|
|
||||||
function notEqual(a, b) {
|
|
||||||
return a !== b;
|
|
||||||
}
|
|
||||||
|
|
||||||
function gt(a, b) {
|
|
||||||
return a > b;
|
|
||||||
}
|
|
||||||
|
|
||||||
function gte(a, b) {
|
|
||||||
return a >= b;
|
|
||||||
}
|
|
||||||
|
|
||||||
function lt(a, b) {
|
|
||||||
return a < b;
|
|
||||||
}
|
|
||||||
|
|
||||||
function lte(a, b) {
|
|
||||||
return a <= b;
|
|
||||||
}
|
|
||||||
|
|
||||||
calculators.register('equal', equal);
|
|
||||||
calculators.register('notEqual', notEqual);
|
|
||||||
calculators.register('gt', gt);
|
|
||||||
calculators.register('gte', gte);
|
|
||||||
calculators.register('lt', lt);
|
|
||||||
calculators.register('lte', lte);
|
|
||||||
|
|
||||||
calculators.register('===', equal);
|
|
||||||
calculators.register('!==', notEqual);
|
|
||||||
calculators.register('>', gt);
|
|
||||||
calculators.register('>=', gte);
|
|
||||||
calculators.register('<', lt);
|
|
||||||
calculators.register('<=', lte);
|
|
||||||
|
|
||||||
function includes(a, b) {
|
|
||||||
return a.includes(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function notIncludes(a, b) {
|
|
||||||
return !a.includes(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function startsWith(a: string, b: string) {
|
|
||||||
return a.startsWith(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function notStartsWith(a: string, b: string) {
|
|
||||||
return !a.startsWith(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function endsWith(a: string, b: string) {
|
|
||||||
return a.endsWith(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
function notEndsWith(a: string, b: string) {
|
|
||||||
return !a.endsWith(b);
|
|
||||||
}
|
|
||||||
|
|
||||||
calculators.register('includes', includes);
|
|
||||||
calculators.register('notIncludes', notIncludes);
|
|
||||||
calculators.register('startsWith', startsWith);
|
|
||||||
calculators.register('notStartsWith', notStartsWith);
|
|
||||||
calculators.register('endsWith', endsWith);
|
|
||||||
calculators.register('notEndsWith', notEndsWith);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function(calculation, scope) {
|
|
||||||
const fn = calculators.get(calculation.calculator);
|
|
||||||
if (!fn) {
|
|
||||||
throw new Error(`no calculator function registered for "${calculation.calculator}"`);
|
|
||||||
}
|
|
||||||
return Boolean(fn(...calculation.operands));
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
import { default as fns } from '@formulajs/formulajs';
|
|
||||||
import { parseExpression, Scope } from '..';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function(expression: string, scope?: Scope) {
|
|
||||||
const exp = parseExpression(expression, scope);
|
|
||||||
const fn = new Function(...Object.keys(fns), ...Object.keys(scope), `return ${exp}`);
|
|
||||||
const result = fn(...Object.values(fns), ...Object.values(scope));
|
|
||||||
if (typeof result === 'number') {
|
|
||||||
if (Number.isNaN(result) || !Number.isFinite(result)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return fns.ROUND(result, 9);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
import { get } from 'lodash';
|
|
||||||
|
|
||||||
import Plugin from "..";
|
|
||||||
import basic from './basic';
|
|
||||||
import mathjs from "./mathjs";
|
|
||||||
import formulajs from "./formulajs";
|
|
||||||
|
|
||||||
export type Scope = { [key: string]: any };
|
|
||||||
|
|
||||||
export type Evaluator = (expression: string, scope?: Scope) => any;
|
|
||||||
|
|
||||||
export function parseExpression(exp: string, scope: Scope = {}) {
|
|
||||||
return exp.trim().replace(/\s*{{\s*([^{}]+)\s*}}\s*/g, (_, v) => {
|
|
||||||
const item = get(scope, v);
|
|
||||||
const key = v.replace(/\.(\d+)/g, '["$1"]');
|
|
||||||
return ` ${typeof item === 'function' ? item() : key} `;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function(plugin: Plugin) {
|
|
||||||
plugin.calculators.register('basic', basic);
|
|
||||||
plugin.calculators.register('math.js', mathjs);
|
|
||||||
plugin.calculators.register('formula.js', formulajs);
|
|
||||||
};
|
|
@ -1,16 +0,0 @@
|
|||||||
import * as math from 'mathjs';
|
|
||||||
import { parseExpression, Scope } from '..';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default function (expression: string, scope: Scope = {}) {
|
|
||||||
const exp = parseExpression(expression, scope);
|
|
||||||
const result = math.evaluate(exp, scope);
|
|
||||||
if (typeof result === 'number') {
|
|
||||||
if (Number.isNaN(result) || !Number.isFinite(result)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return math.round(result, 9);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
@ -1,5 +1,4 @@
|
|||||||
export * from './constants';
|
export * from './constants';
|
||||||
export * from './calculators';
|
|
||||||
// export * from './instructions';
|
// export * from './instructions';
|
||||||
export { Trigger } from './triggers';
|
export { Trigger } from './triggers';
|
||||||
export { default as Processor } from './Processor';
|
export { default as Processor } from './Processor';
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { get } from "lodash";
|
import evaluators, { Evaluator } from '@nocobase/evaluators';
|
||||||
|
|
||||||
import { Evaluator, Processor } from '..';
|
import { Processor } from '..';
|
||||||
import { JOB_STATUS } from "../constants";
|
import { JOB_STATUS } from "../constants";
|
||||||
import FlowNodeModel from "../models/FlowNode";
|
import FlowNodeModel from "../models/FlowNode";
|
||||||
import { Instruction } from ".";
|
import { Instruction } from ".";
|
||||||
@ -8,14 +8,14 @@ import { Instruction } from ".";
|
|||||||
|
|
||||||
|
|
||||||
interface CalculationConfig {
|
interface CalculationConfig {
|
||||||
engine: string;
|
engine?: string;
|
||||||
expression: string;
|
expression?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async run(node: FlowNodeModel, prevJob, processor: Processor) {
|
async run(node: FlowNodeModel, prevJob, processor: Processor) {
|
||||||
const { engine, expression = '' } = <CalculationConfig>node.config || {};
|
const { engine = 'math.js', expression = '' } = <CalculationConfig>node.config || {};
|
||||||
const evaluator = <Evaluator | undefined>processor.options.plugin.calculators.get(engine);
|
const evaluator = <Evaluator | undefined>evaluators.get(engine);
|
||||||
const scope = processor.getScope();
|
const scope = processor.getScope();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -1,40 +1,132 @@
|
|||||||
import { Evaluator, Processor } from '..';
|
import { Registry } from "@nocobase/utils";
|
||||||
|
import evaluators from '@nocobase/evaluators';
|
||||||
|
|
||||||
|
import { Processor } from '..';
|
||||||
import { JOB_STATUS } from "../constants";
|
import { JOB_STATUS } from "../constants";
|
||||||
import FlowNodeModel from "../models/FlowNode";
|
import FlowNodeModel from "../models/FlowNode";
|
||||||
import { Instruction } from ".";
|
import { Instruction } from ".";
|
||||||
|
|
||||||
function logicCalculate(calculation, evaluator, scope) {
|
export const calculators = new Registry<Function>();
|
||||||
|
|
||||||
|
// built-in functions
|
||||||
|
function equal(a, b) {
|
||||||
|
return a === b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notEqual(a, b) {
|
||||||
|
return a !== b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gt(a, b) {
|
||||||
|
return a > b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gte(a, b) {
|
||||||
|
return a >= b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lt(a, b) {
|
||||||
|
return a < b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function lte(a, b) {
|
||||||
|
return a <= b;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculators.register('equal', equal);
|
||||||
|
calculators.register('notEqual', notEqual);
|
||||||
|
calculators.register('gt', gt);
|
||||||
|
calculators.register('gte', gte);
|
||||||
|
calculators.register('lt', lt);
|
||||||
|
calculators.register('lte', lte);
|
||||||
|
|
||||||
|
calculators.register('===', equal);
|
||||||
|
calculators.register('!==', notEqual);
|
||||||
|
calculators.register('>', gt);
|
||||||
|
calculators.register('>=', gte);
|
||||||
|
calculators.register('<', lt);
|
||||||
|
calculators.register('<=', lte);
|
||||||
|
|
||||||
|
function includes(a, b) {
|
||||||
|
return a.includes(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notIncludes(a, b) {
|
||||||
|
return !a.includes(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function startsWith(a: string, b: string) {
|
||||||
|
return a.startsWith(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notStartsWith(a: string, b: string) {
|
||||||
|
return !a.startsWith(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function endsWith(a: string, b: string) {
|
||||||
|
return a.endsWith(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
function notEndsWith(a: string, b: string) {
|
||||||
|
return !a.endsWith(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
calculators.register('includes', includes);
|
||||||
|
calculators.register('notIncludes', notIncludes);
|
||||||
|
calculators.register('startsWith', startsWith);
|
||||||
|
calculators.register('notStartsWith', notStartsWith);
|
||||||
|
calculators.register('endsWith', endsWith);
|
||||||
|
calculators.register('notEndsWith', notEndsWith);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type CalculationItem = {
|
||||||
|
calculator?: string;
|
||||||
|
operands?: [any?, any?];
|
||||||
|
};
|
||||||
|
|
||||||
|
type CalculationGroup = {
|
||||||
|
group: {
|
||||||
|
type: 'and' | 'or';
|
||||||
|
calculations?: Calculation[];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type Calculation = CalculationItem | CalculationGroup;
|
||||||
|
|
||||||
|
function calculate(calculation: CalculationItem = {}) {
|
||||||
|
let fn: Function;
|
||||||
|
if (!(calculation.calculator && (fn = calculators.get(calculation.calculator)))) {
|
||||||
|
throw new Error(`no calculator function registered for "${calculation.calculator}"`);
|
||||||
|
}
|
||||||
|
return Boolean(fn(...(calculation.operands ?? [])));
|
||||||
|
}
|
||||||
|
|
||||||
|
function logicCalculate(calculation?: Calculation) {
|
||||||
if (!calculation) {
|
if (!calculation) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof calculation === 'object' && calculation.group) {
|
if (typeof calculation['group'] === 'object') {
|
||||||
const method = calculation.group.type === 'and' ? 'every' : 'some';
|
const method = calculation['group'].type === 'and' ? 'every' : 'some';
|
||||||
return calculation.group.calculations[method](item => logicCalculate(item, evaluator, scope));
|
return (calculation['group'].calculations ?? [])[method]((item: Calculation) => logicCalculate(item));
|
||||||
}
|
}
|
||||||
|
|
||||||
return evaluator(calculation, scope);
|
return calculate(calculation as CalculationItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async run(node: FlowNodeModel, prevJob, processor: Processor) {
|
async run(node: FlowNodeModel, prevJob, processor: Processor) {
|
||||||
// TODO(optimize): loading of jobs could be reduced and turned into incrementally in processor
|
const { engine, calculation, expression, rejectOnFalse } = node.config || {};
|
||||||
// const jobs = await processor.getJobs();
|
const evaluator = evaluators.get(engine);
|
||||||
const { engine = 'basic', calculation, expression, rejectOnFalse } = node.config || {};
|
|
||||||
const evaluator = <Evaluator | undefined>processor.options.plugin.calculators.get(engine);
|
|
||||||
if (!evaluator) {
|
|
||||||
return {
|
|
||||||
status: JOB_STATUS.ERROR,
|
|
||||||
result: new Error('no calculator engine configured')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const scope = processor.getScope();
|
|
||||||
let result = true;
|
let result = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
result = logicCalculate(engine === 'basic' ? processor.getParsedValue(calculation) : expression, evaluator, scope);
|
result = evaluator
|
||||||
|
? evaluator(expression, processor.getScope())
|
||||||
|
: logicCalculate(processor.getParsedValue(calculation));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return {
|
return {
|
||||||
result: e.toString(),
|
result: e.toString(),
|
||||||
|
@ -4236,7 +4236,7 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@formily/shared" "2.0.20"
|
"@formily/shared" "2.0.20"
|
||||||
|
|
||||||
"@formulajs/formulajs@^4.2.0":
|
"@formulajs/formulajs@4.2.0", "@formulajs/formulajs@^4.2.0":
|
||||||
version "4.2.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@formulajs/formulajs/-/formulajs-4.2.0.tgz#e5c6a98fa5863442cb68f93b8b9b28d75070abc4"
|
resolved "https://registry.yarnpkg.com/@formulajs/formulajs/-/formulajs-4.2.0.tgz#e5c6a98fa5863442cb68f93b8b9b28d75070abc4"
|
||||||
integrity sha512-egxyvwj08iwOznFgxv7dvjgHUC7C8jdtznAs+15uThIti7TwDGhB3wsbJt1dlfhSHKvlRAiW4MDYxNkvgmyjyg==
|
integrity sha512-egxyvwj08iwOznFgxv7dvjgHUC7C8jdtznAs+15uThIti7TwDGhB3wsbJt1dlfhSHKvlRAiW4MDYxNkvgmyjyg==
|
||||||
|
Loading…
Reference in New Issue
Block a user