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 }}
 | 
			
		||||
    <Variable.TextArea
 | 
			
		||||
      value={value}
 | 
			
		||||
      onChange={onChange}
 | 
			
		||||
      scope={options}
 | 
			
		||||
    />
 | 
			
		||||
      <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 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