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": "多行文本",
|
||||
"Phone": "手机号码",
|
||||
"Email": "电子邮箱",
|
||||
'Null': '空值',
|
||||
"Boolean": "逻辑值",
|
||||
"Number": "数字",
|
||||
"Integer": "整数",
|
||||
@ -640,4 +641,10 @@ export default {
|
||||
'Display page title': '显示页面标题',
|
||||
'Edit page title': '编辑页面标题',
|
||||
'Enable page tabs': '启用页面选项卡',
|
||||
|
||||
'Constant': '常量',
|
||||
'Use variable': '使用变量',
|
||||
'True': '真',
|
||||
'False': '假',
|
||||
'Prettify': '格式化'
|
||||
}
|
||||
|
@ -36,4 +36,5 @@ export * from './tabs';
|
||||
export * from './time-picker';
|
||||
export * from './tree-select';
|
||||
export * from './upload';
|
||||
export * from './variable';
|
||||
import './index.less';
|
||||
|
@ -1,20 +1,18 @@
|
||||
import React from "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 { cx, css } from "@emotion/css";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import moment from "moment";
|
||||
|
||||
import { useCompile } from "@nocobase/client";
|
||||
|
||||
import { lang, NAMESPACE } from "../locale";
|
||||
import { useCompile } from '../../hooks/useCompile';
|
||||
|
||||
|
||||
|
||||
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) {
|
||||
return 'null';
|
||||
}
|
||||
@ -36,11 +34,11 @@ export function parseValue(value: any): string | string[] {
|
||||
|
||||
const ConstantTypes = {
|
||||
string: {
|
||||
label: `{{t("String", { ns: "${NAMESPACE}" })}}`,
|
||||
label: `{{t("String")}}`,
|
||||
value: 'string',
|
||||
component({ onChange, value }) {
|
||||
return (
|
||||
<Input
|
||||
<AntInput
|
||||
value={value}
|
||||
onChange={ev => onChange(ev.target.value)}
|
||||
/>
|
||||
@ -62,7 +60,7 @@ const ConstantTypes = {
|
||||
default: 0
|
||||
},
|
||||
boolean: {
|
||||
label: `{{t("Boolean", { ns: "${NAMESPACE}" })}}`,
|
||||
label: `{{t("Boolean")}}`,
|
||||
value: 'boolean',
|
||||
component({ onChange, value }) {
|
||||
const { t } = useTranslation();
|
||||
@ -72,8 +70,8 @@ const ConstantTypes = {
|
||||
onChange={onChange}
|
||||
placeholder={t('Select')}
|
||||
options={[
|
||||
{ value: true, label: lang('True') },
|
||||
{ value: false, label: lang('False') },
|
||||
{ value: true, label: t('True') },
|
||||
{ value: false, label: t('False') },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
@ -81,7 +79,7 @@ const ConstantTypes = {
|
||||
default: false
|
||||
},
|
||||
date: {
|
||||
label: '日期',
|
||||
label: '{{t("Date")}}',
|
||||
value: 'date',
|
||||
component({ onChange, value }) {
|
||||
return (
|
||||
@ -99,11 +97,12 @@ const ConstantTypes = {
|
||||
})(),
|
||||
},
|
||||
null: {
|
||||
label: `{{t("Null", { ns: "${NAMESPACE}" })}}`,
|
||||
label: `{{t("Null")}}`,
|
||||
value: 'null',
|
||||
component() {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Input readOnly placeholder={lang('Null')} />
|
||||
<AntInput readOnly placeholder={t('Null')} className="null-value" />
|
||||
);
|
||||
},
|
||||
default: null,
|
||||
@ -116,19 +115,21 @@ type VariableOptions = {
|
||||
children?: VariableOptions[];
|
||||
}
|
||||
|
||||
export function VariableInput(props) {
|
||||
export function Input(props) {
|
||||
const { value = '', scope, onChange, children, button } = props;
|
||||
const parsed = parseValue(value);
|
||||
const isConstant = typeof parsed === 'string';
|
||||
const type = isConstant ? parsed : 'string';
|
||||
const type = isConstant ? parsed : '';
|
||||
const variable = isConstant ? null : parsed;
|
||||
const ConstantComponent = ConstantTypes[type]?.component;
|
||||
const constantOptions = Object.values(ConstantTypes);
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
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 ?? []))
|
||||
]);
|
||||
|
||||
const form = useForm();
|
||||
|
||||
function onSwitch(next) {
|
||||
@ -155,7 +156,7 @@ export function VariableInput(props) {
|
||||
const disabled = props.disabled || form.disabled;
|
||||
|
||||
return (
|
||||
<Input.Group compact className={css`
|
||||
<AntInput.Group compact className={css`
|
||||
width: auto;
|
||||
.ant-input-disabled{
|
||||
.ant-tag{
|
||||
@ -163,6 +164,10 @@ export function VariableInput(props) {
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
}
|
||||
.ant-input.null-value{
|
||||
width: 4em;
|
||||
min-width: 4em;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{variable
|
||||
@ -202,6 +207,7 @@ export function VariableInput(props) {
|
||||
}}
|
||||
className={cx('ant-input', { 'ant-input-disabled': disabled })}
|
||||
contentEditable={!disabled}
|
||||
suppressContentEditableWarning
|
||||
>
|
||||
<Tag contentEditable={false} color="blue">{variableText}</Tag>
|
||||
</div>
|
||||
@ -247,6 +253,6 @@ export function VariableInput(props) {
|
||||
)
|
||||
: null
|
||||
}
|
||||
</Input.Group>
|
||||
</AntInput.Group>
|
||||
);
|
||||
}
|
@ -2,9 +2,8 @@ import React, { useRef } from 'react';
|
||||
import { Button, Cascader } from 'antd';
|
||||
import { css } from "@emotion/css";
|
||||
|
||||
import { Input } from "@nocobase/client";
|
||||
|
||||
import { lang } from '../locale';
|
||||
import { Input } from "../input";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
|
||||
|
||||
@ -17,9 +16,10 @@ function setNativeInputValue(input, value) {
|
||||
}));
|
||||
}
|
||||
|
||||
export function VariableJSONInput(props) {
|
||||
export function JSONInput(props) {
|
||||
const inputRef = useRef(null);
|
||||
const { value, space = 2, scope } = props;
|
||||
const { t } = useTranslation()
|
||||
const options = typeof scope === 'function' ? scope() : (scope ?? []);
|
||||
|
||||
function onFormat() {
|
||||
@ -69,7 +69,7 @@ export function VariableJSONInput(props) {
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Button onClick={onFormat}>{lang('Format')}</Button>
|
||||
<Button onClick={onFormat}>{t('Prettify')}</Button>
|
||||
<Cascader
|
||||
value={[]}
|
||||
options={options}
|
@ -2,11 +2,11 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Input, Cascader, Tooltip, Button } from 'antd';
|
||||
import { useForm } from '@formily/react';
|
||||
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 }) {
|
||||
// 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[]>();
|
||||
for (const option of options) {
|
||||
map.set(option.key, [option.label]);
|
||||
map.set(option.value, [option.label]);
|
||||
if (option.children) {
|
||||
for (const [key, labels] of createOptionsKeyLabelMap(option.children)) {
|
||||
map.set(`${option.key}.${key}`, [option.label, ...labels]);
|
||||
for (const [value, labels] of createOptionsValueLabelMap(option.children)) {
|
||||
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>`;
|
||||
}
|
||||
|
||||
export function VariableTextArea(props) {
|
||||
export function TextArea(props) {
|
||||
const { value = '', scope, onChange, multiline = true, button } = props;
|
||||
const { t } = useTranslation()
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
const options = (typeof scope === 'function' ? scope() : scope) ?? [];
|
||||
const form = useForm();
|
||||
const keyLabelMap = useMemo(() => createOptionsKeyLabelMap(options), [scope]);
|
||||
const keyLabelMap = useMemo(() => createOptionsValueLabelMap(options), [scope]);
|
||||
const [changed, setChanged] = useState(false);
|
||||
const [html, setHtml] = useState(() => renderHTML(value ?? '', keyLabelMap));
|
||||
// [startElementIndex, startOffset, endElementIndex, endOffset]
|
||||
@ -213,7 +214,7 @@ export function VariableTextArea(props) {
|
||||
contentEditable={!disabled}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
<Tooltip title={lang('Use variable')}>
|
||||
<Tooltip title={t('Use variable')}>
|
||||
<Cascader
|
||||
value={[]}
|
||||
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
|
||||
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
|
||||
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',
|
||||
tooltip: '{{t("Formula.js supports most Microsoft Excel formula 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',
|
||||
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
|
||||
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 mathjs from './engines/mathjs';
|
||||
@ -10,9 +14,34 @@ export interface Evaluator {
|
||||
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('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;
|
||||
|
@ -1 +1,2 @@
|
||||
export { default } from './server';
|
||||
export * from './server';
|
||||
|
@ -1,24 +1,16 @@
|
||||
import { get } from "lodash";
|
||||
|
||||
import { Registry, RegistryOptions } from "@nocobase/utils";
|
||||
import { Registry } from "@nocobase/utils";
|
||||
|
||||
import { evaluate, Evaluator } from '../utils';
|
||||
import mathjs from "../utils/mathjs";
|
||||
import formulajs from "../utils/formulajs";
|
||||
|
||||
|
||||
|
||||
export interface Evaluator {
|
||||
(expression: string, scope?: { [key: string]: any }): any;
|
||||
}
|
||||
export { Evaluator } from '../utils';
|
||||
|
||||
export interface EvaluatorsOptions extends RegistryOptions {
|
||||
empty?: boolean;
|
||||
evaluators?: { [key: string]: Evaluator };
|
||||
}
|
||||
export const evaluators = new Registry<Evaluator>();
|
||||
|
||||
const evaluators = new Registry<Evaluator>();
|
||||
|
||||
evaluators.register('math.js', mathjs);
|
||||
evaluators.register('formula.js', formulajs);
|
||||
evaluators.register('math.js', evaluate.bind(mathjs));
|
||||
evaluators.register('formula.js', evaluate.bind(formulajs));
|
||||
|
||||
export default evaluators;
|
||||
|
@ -5,8 +5,7 @@ import * as functions from '@formulajs/formulajs';
|
||||
const fnNames = Object.keys(functions).filter(key => key !== 'default');
|
||||
const fns = fnNames.map(key => functions[key]);
|
||||
|
||||
export default function(exp: string, scope = {}) {
|
||||
const expression = exp.replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => v);
|
||||
export default function(expression: string, scope = {}) {
|
||||
const fn = new Function(...fnNames, ...Object.keys(scope), `return ${expression}`);
|
||||
const result = fn(...fns, ...Object.values(scope));
|
||||
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';
|
||||
|
||||
export default function (expression: string, scope = {}) {
|
||||
const exp = expression.trim().replace(/{{\s*([^{}]+)\s*}}/g, (_, v) => v);
|
||||
const result = math.evaluate(exp, scope);
|
||||
const result = math.evaluate(expression, scope);
|
||||
if (typeof result === 'number') {
|
||||
if (Number.isNaN(result) || !Number.isFinite(result)) {
|
||||
return null;
|
||||
|
@ -61,7 +61,7 @@ export default {
|
||||
group: 'advanced',
|
||||
order: 1,
|
||||
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,
|
||||
default: {
|
||||
type: 'formula',
|
||||
|
@ -1,242 +1,25 @@
|
||||
import React, { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { useForm } from '@formily/react';
|
||||
import { Button, Cascader, Input, Tooltip } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCompile } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { useCompile, Variable } 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) => {
|
||||
const { value = '', supports, useCurrentFields, onChange } = props;
|
||||
const { t } = useTranslation();
|
||||
const compile = useCompile();
|
||||
|
||||
const fields = useCurrentFields().filter(field => supports.includes(field.interface));
|
||||
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
const options = fields.map(field => ({
|
||||
label: compile(field.uiSchema.title),
|
||||
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 (
|
||||
<Input.Group compact className={css`
|
||||
&.ant-input-group.ant-input-group-compact{
|
||||
display: flex;
|
||||
.ant-input{
|
||||
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>
|
||||
<Variable.TextArea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
scope={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { css } from '@emotion/css';
|
||||
import { CollectionManagerContext, registerField, SchemaComponentOptions } from '@nocobase/client';
|
||||
import evaluators, { Evaluator } from '@nocobase/evaluators/client';
|
||||
|
||||
import Formula from './formula';
|
||||
import { Formula } from './formula';
|
||||
import field from './field';
|
||||
import { NAMESPACE } from './locale';
|
||||
import { Registry } from '@nocobase/utils/client';
|
||||
@ -36,6 +36,8 @@ function renderExpressionDescription(key: string) {
|
||||
: null
|
||||
}
|
||||
|
||||
export { Formula } from './formula';
|
||||
|
||||
export default React.memo((props) => {
|
||||
const ctx = useContext(CollectionManagerContext);
|
||||
return (
|
||||
|
@ -3,5 +3,6 @@ export default {
|
||||
'Formula engine': '公式引擎',
|
||||
'Expression': '表达式',
|
||||
'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);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
@ -9,6 +9,7 @@
|
||||
"@nocobase/actions": "0.9.0-alpha.2",
|
||||
"@nocobase/client": "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/server": "0.9.0-alpha.2",
|
||||
"@nocobase/utils": "0.9.0-alpha.2",
|
||||
@ -16,9 +17,7 @@
|
||||
"axios": "^0.27.2",
|
||||
"classnames": "^2.3.1",
|
||||
"cron-parser": "4.4.0",
|
||||
"@formulajs/formulajs": "4.2.0",
|
||||
"json-templates": "^4.2.0",
|
||||
"mathjs": "^10.6.0",
|
||||
"moment": "^2.29.2",
|
||||
"react-js-cron": "^1.4.0"
|
||||
},
|
||||
|
@ -5,8 +5,7 @@ import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { css } from "@emotion/css";
|
||||
|
||||
import { CollectionField, CollectionProvider, SchemaComponent, useCollectionManager, useCompile } from "@nocobase/client";
|
||||
import { VariableInput } from "./VariableInput";
|
||||
import { CollectionField, CollectionProvider, SchemaComponent, Variable, useCollectionManager, useCompile } from "@nocobase/client";
|
||||
import { lang } from "../locale";
|
||||
import { useWorkflowVariableOptions } from "../variable";
|
||||
|
||||
@ -40,7 +39,8 @@ export default observer(({ value, disabled, onChange }: any) => {
|
||||
.filter(field => (
|
||||
!field.hidden
|
||||
&& (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));
|
||||
@ -77,7 +77,7 @@ export default observer(({ value, disabled, onChange }: any) => {
|
||||
display: flex;
|
||||
}
|
||||
`}>
|
||||
<VariableInput
|
||||
<Variable.Input
|
||||
scope={['hasMany', 'belongsToMany'].includes(field.type) ? [] : scope}
|
||||
value={value[field.name]}
|
||||
onChange={(next) => {
|
||||
@ -94,7 +94,7 @@ export default observer(({ value, disabled, onChange }: any) => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</VariableInput>
|
||||
</Variable.Input>
|
||||
{!mergedDisabled
|
||||
? (
|
||||
<Button
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React from "react";
|
||||
|
||||
import { Variable } from "@nocobase/client";
|
||||
|
||||
import { useWorkflowVariableOptions } from "../variable";
|
||||
import { VariableInput } from "./VariableInput";
|
||||
|
||||
|
||||
|
||||
@ -9,12 +10,11 @@ export function FilterDynamicComponent({ value, onChange, renderSchemaComponent
|
||||
const scope = useWorkflowVariableOptions();
|
||||
|
||||
return (
|
||||
<VariableInput
|
||||
<Variable.Input
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
scope={scope}
|
||||
>
|
||||
{renderSchemaComponent()}
|
||||
</VariableInput>
|
||||
</Variable.Input>
|
||||
);
|
||||
}
|
||||
|
@ -54,11 +54,6 @@ export default {
|
||||
'Advanced': '高级模式',
|
||||
'End': '结束',
|
||||
'Node result': '节点数据',
|
||||
'Constant': '常量',
|
||||
'Null': '空值',
|
||||
'Boolean': '逻辑值',
|
||||
'String': '字符串',
|
||||
'Date': '日期',
|
||||
'Calculator': '运算',
|
||||
'Arithmetic calculation': '算术运算',
|
||||
'String operation': '字符串',
|
||||
|
@ -3,13 +3,12 @@ import { css } from '@emotion/css';
|
||||
import parse from 'json-templates';
|
||||
|
||||
import { SchemaInitializer, SchemaInitializerItemOptions } from '@nocobase/client';
|
||||
import evaluators, { renderReference, Evaluator } from '@nocobase/evaluators/client';
|
||||
|
||||
import { useFlowContext } from '../../FlowContext';
|
||||
import { lang, NAMESPACE } from '../../locale';
|
||||
import { VariableTextArea } from '../../components/VariableTextArea';
|
||||
import { TypeSets, useWorkflowVariableOptions } from '../../variable';
|
||||
import { calculationEngines, renderReference } from './engines';
|
||||
import { RadioWithTooltip } from '../../components/RadioWithTooltip';
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
import { lang, NAMESPACE } from '../locale';
|
||||
import { TypeSets, useWorkflowVariableOptions } from '../variable';
|
||||
import { RadioWithTooltip } from '../components/RadioWithTooltip';
|
||||
|
||||
|
||||
|
||||
@ -25,7 +24,7 @@ export default {
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'RadioWithTooltip',
|
||||
'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,
|
||||
default: 'math.js'
|
||||
@ -35,14 +34,14 @@ export default {
|
||||
title: `{{t("Calculation expression", { ns: "${NAMESPACE}" })}}`,
|
||||
name: 'config.expression',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'VariableTextArea',
|
||||
'x-component': 'Variable.TextArea',
|
||||
'x-component-props': {
|
||||
scope: '{{useWorkflowVariableOptions}}'
|
||||
},
|
||||
['x-validator'](value, rules, { form }) {
|
||||
const { values } = form;
|
||||
const { evaluate } = calculationEngines.get(values.config.engine);
|
||||
const exp = value.trim().replace(/\{\{([^{}]+)\}\}/g, '1');
|
||||
const { evaluate } = evaluators.get(values.config.engine) as Evaluator;
|
||||
const exp = value.trim().replace(/{{([^{}]+)}}/g, '1');
|
||||
try {
|
||||
evaluate(exp);
|
||||
return '';
|
||||
@ -86,8 +85,7 @@ export default {
|
||||
</pre>
|
||||
);
|
||||
},
|
||||
RadioWithTooltip,
|
||||
VariableTextArea
|
||||
RadioWithTooltip
|
||||
},
|
||||
getOptions(config, types) {
|
||||
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 { Registry } from "@nocobase/utils/client";
|
||||
import { Variable, useCompile } from "@nocobase/client";
|
||||
import evaluators, { renderReference } from "@nocobase/evaluators/client";
|
||||
|
||||
import { NodeDefaultView } from ".";
|
||||
import { Branch } from "../Branch";
|
||||
@ -12,11 +14,7 @@ import { useFlowContext } from '../FlowContext';
|
||||
import { branchBlockClass, nodeSubtreeClass } from "../style";
|
||||
import { lang, NAMESPACE } from "../locale";
|
||||
import { useWorkflowVariableOptions } from "../variable";
|
||||
import { VariableTextArea } from "../components/VariableTextArea";
|
||||
import { VariableInput } from "../components/VariableInput";
|
||||
import { RadioWithTooltip, RadioWithTooltipOption } from "../components/RadioWithTooltip";
|
||||
import { calculationEngines, renderReference } from "./calculation/engines";
|
||||
import { useCompile } from "@nocobase/client";
|
||||
|
||||
interface Calculator {
|
||||
name: string;
|
||||
@ -152,7 +150,7 @@ export function Calculation({ calculator, operands = [], onChange }) {
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
`}>
|
||||
<VariableInput
|
||||
<Variable.Input
|
||||
value={operands[0]}
|
||||
onChange={(v => onChange({ calculator, operands: [v, operands[1]] }))}
|
||||
scope={options}
|
||||
@ -170,7 +168,7 @@ export function Calculation({ calculator, operands = [], onChange }) {
|
||||
</Select.OptGroup>
|
||||
))}
|
||||
</Select>
|
||||
<VariableInput
|
||||
<Variable.Input
|
||||
value={operands[1]}
|
||||
onChange={(v => onChange({ calculator, operands: [operands[0], v] }))}
|
||||
scope={options}
|
||||
@ -335,7 +333,7 @@ export default {
|
||||
'x-component-props': {
|
||||
options: [
|
||||
['basic', { label: `{{t("Basic", { ns: "${NAMESPACE}" })}}` }],
|
||||
...Array.from(calculationEngines.getEntities())
|
||||
...Array.from(evaluators.getEntities())
|
||||
].reduce((result: RadioWithTooltipOption[], [value, options]: any) => result.concat({ value, ...options }), []),
|
||||
},
|
||||
required: true,
|
||||
@ -362,14 +360,14 @@ export default {
|
||||
title: `{{t("Condition expression", { ns: "${NAMESPACE}" })}}`,
|
||||
name: 'config.expression',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'VariableTextArea',
|
||||
'x-component': 'Variable.TextArea',
|
||||
'x-component-props': {
|
||||
scope: '{{useWorkflowVariableOptions}}'
|
||||
},
|
||||
['x-validator'](value, rules, { form }) {
|
||||
const { values } = form;
|
||||
const { evaluate } = calculationEngines.get(values.config.engine);
|
||||
const exp = value.trim().replace(/\{\{([^{}]+)\}\}/g, '1');
|
||||
const { evaluate } = evaluators.get(values.config.engine);
|
||||
const exp = value.trim().replace(/{{([^{}]+)}}/g, '1');
|
||||
try {
|
||||
evaluate(exp);
|
||||
return '';
|
||||
@ -450,7 +448,6 @@ export default {
|
||||
},
|
||||
components: {
|
||||
CalculationConfig,
|
||||
VariableTextArea,
|
||||
RadioWithTooltip
|
||||
}
|
||||
};
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { RemoteSelect } from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { VariableInput } from '../../components/VariableInput';
|
||||
import { Variable } from '@nocobase/client';
|
||||
import { useWorkflowVariableOptions } from '../../variable';
|
||||
|
||||
|
||||
|
||||
export function AssigneesSelect({ multiple = false, value = [], onChange }) {
|
||||
const scope = useWorkflowVariableOptions();
|
||||
console.log(value);
|
||||
|
||||
return (
|
||||
<VariableInput
|
||||
<Variable.Input
|
||||
scope={scope}
|
||||
types={[{ type: 'reference', options: { collection: 'users' } }]}
|
||||
value={value[0]}
|
||||
@ -30,6 +31,6 @@ export function AssigneesSelect({ multiple = false, value = [], onChange }) {
|
||||
onChange([v]);
|
||||
}}
|
||||
/>
|
||||
</VariableInput>
|
||||
</Variable.Input>
|
||||
);
|
||||
}
|
||||
|
@ -3,8 +3,6 @@ import { css } from '@emotion/css';
|
||||
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { useWorkflowVariableOptions } from '../variable';
|
||||
import { VariableJSONInput } from '../components/VariableJSONInput';
|
||||
import { VariableInput } from '../components/VariableInput';
|
||||
|
||||
|
||||
|
||||
@ -79,7 +77,7 @@ export default {
|
||||
value: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'VariableInput',
|
||||
'x-component': 'Variable.Input',
|
||||
'x-component-props': {
|
||||
scope: useWorkflowVariableOptions
|
||||
}
|
||||
@ -125,7 +123,7 @@ export default {
|
||||
value: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'VariableInput',
|
||||
'x-component': 'Variable.Input',
|
||||
'x-component-props': {
|
||||
scope: useWorkflowVariableOptions
|
||||
}
|
||||
@ -153,7 +151,7 @@ export default {
|
||||
title: `{{t("Body", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {},
|
||||
'x-component': 'VariableJSONInput',
|
||||
'x-component': 'Variable.JSON',
|
||||
'x-component-props': {
|
||||
scope: useWorkflowVariableOptions,
|
||||
autoSize: {
|
||||
@ -193,7 +191,5 @@ export default {
|
||||
scope: {},
|
||||
components: {
|
||||
ArrayItems,
|
||||
VariableInput,
|
||||
VariableJSONInput
|
||||
},
|
||||
};
|
||||
|
@ -5,8 +5,6 @@ import { Plugin } from '@nocobase/server';
|
||||
import { Registry } from '@nocobase/utils';
|
||||
|
||||
import initActions from './actions';
|
||||
import initCalculationEngines from './calculators';
|
||||
import type { Evaluator } from './calculators';
|
||||
import { EXECUTION_STATUS } from './constants';
|
||||
import initInstructions, { Instruction } from './instructions';
|
||||
import ExecutionModel from './models/Execution';
|
||||
@ -20,7 +18,6 @@ type Pending = [ExecutionModel, JobModel?];
|
||||
export default class WorkflowPlugin extends Plugin {
|
||||
instructions: Registry<Instruction> = new Registry();
|
||||
triggers: Registry<Trigger> = new Registry();
|
||||
calculators: Registry<Evaluator> = new Registry();
|
||||
functions: Registry<Function> = new Registry();
|
||||
executing: ExecutionModel | null = null;
|
||||
pending: Pending[] = [];
|
||||
@ -78,7 +75,6 @@ export default class WorkflowPlugin extends Plugin {
|
||||
initActions(this);
|
||||
initTriggers(this, options.triggers);
|
||||
initInstructions(this, options.instructions);
|
||||
initCalculationEngines(this);
|
||||
initFunctions(this, options.functions);
|
||||
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { MockServer, mockServer } from '@nocobase/test';
|
||||
import path from 'path';
|
||||
|
||||
import { ApplicationOptions } from '@nocobase/server';
|
||||
import { MockServer, mockServer } from '@nocobase/test';
|
||||
|
||||
import Plugin from '..';
|
||||
import calculators from '../calculators';
|
||||
import { JOB_STATUS } from '../constants';
|
||||
|
||||
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 './calculators';
|
||||
// export * from './instructions';
|
||||
export { Trigger } from './triggers';
|
||||
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 FlowNodeModel from "../models/FlowNode";
|
||||
import { Instruction } from ".";
|
||||
@ -8,14 +8,14 @@ import { Instruction } from ".";
|
||||
|
||||
|
||||
interface CalculationConfig {
|
||||
engine: string;
|
||||
expression: string;
|
||||
engine?: string;
|
||||
expression?: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
async run(node: FlowNodeModel, prevJob, processor: Processor) {
|
||||
const { engine, expression = '' } = <CalculationConfig>node.config || {};
|
||||
const evaluator = <Evaluator | undefined>processor.options.plugin.calculators.get(engine);
|
||||
const { engine = 'math.js', expression = '' } = <CalculationConfig>node.config || {};
|
||||
const evaluator = <Evaluator | undefined>evaluators.get(engine);
|
||||
const scope = processor.getScope();
|
||||
|
||||
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 FlowNodeModel from "../models/FlowNode";
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof calculation === 'object' && calculation.group) {
|
||||
const method = calculation.group.type === 'and' ? 'every' : 'some';
|
||||
return calculation.group.calculations[method](item => logicCalculate(item, evaluator, scope));
|
||||
if (typeof calculation['group'] === 'object') {
|
||||
const method = calculation['group'].type === 'and' ? 'every' : 'some';
|
||||
return (calculation['group'].calculations ?? [])[method]((item: Calculation) => logicCalculate(item));
|
||||
}
|
||||
|
||||
return evaluator(calculation, scope);
|
||||
return calculate(calculation as CalculationItem);
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
async run(node: FlowNodeModel, prevJob, processor: Processor) {
|
||||
// TODO(optimize): loading of jobs could be reduced and turned into incrementally in processor
|
||||
// const jobs = await processor.getJobs();
|
||||
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 { engine, calculation, expression, rejectOnFalse } = node.config || {};
|
||||
const evaluator = evaluators.get(engine);
|
||||
|
||||
const scope = processor.getScope();
|
||||
let result = true;
|
||||
|
||||
try {
|
||||
result = logicCalculate(engine === 'basic' ? processor.getParsedValue(calculation) : expression, evaluator, scope);
|
||||
result = evaluator
|
||||
? evaluator(expression, processor.getScope())
|
||||
: logicCalculate(processor.getParsedValue(calculation));
|
||||
} catch (e) {
|
||||
return {
|
||||
result: e.toString(),
|
||||
|
@ -4236,7 +4236,7 @@
|
||||
dependencies:
|
||||
"@formily/shared" "2.0.20"
|
||||
|
||||
"@formulajs/formulajs@^4.2.0":
|
||||
"@formulajs/formulajs@4.2.0", "@formulajs/formulajs@^4.2.0":
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@formulajs/formulajs/-/formulajs-4.2.0.tgz#e5c6a98fa5863442cb68f93b8b9b28d75070abc4"
|
||||
integrity sha512-egxyvwj08iwOznFgxv7dvjgHUC7C8jdtznAs+15uThIti7TwDGhB3wsbJt1dlfhSHKvlRAiW4MDYxNkvgmyjyg==
|
||||
|
Loading…
Reference in New Issue
Block a user