feat: (plugin-workflow) dynamic expression (#1560)
* feat(plugin-formula): add dynamic expression field * feat(plugin-workflow): add dynamic expression for calculation * refactor(client): allow select part of paths in variable component * fix(client): fix types * feat(plugin-formula): add dynamic expression config * feat(plugin-workflow): add dynamic calculation * refactor(plugin-formula): move expression field type to workflow plugin * fix(plugin-workflow): fix types * fix(plugin-workflow): fix register field in client * fix(plugin-workflow): fix expression result value default * fix(plugin-workflow): fix dynamic expression field error when switch collection * fix(plugin-workflow): test component value change * test(plugin-workflow): test component linkages * refactor(plugin-workflow): change to expression collection template * fix(client): fix hooks of Variable.TextArea * fix(client): fix to import evaluators in client * fix(evaluators): move renderReference method to plugin * fix(plugin-workflow): fix missed component * fix(plugin-workflow): fix dynamic expression test case * refactor(client): change popover to double click to choose entire object * refactor(plugin-workflow): make variable options and filter more sensible * fix(plugin-workflow): fix form effect * fix(plugin-workflow): fix variable filtering in collection trigger * fix(plugin-workflow): fix types --------- Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
b8776fe2d0
commit
52329df140
@ -13,6 +13,7 @@
|
||||
"@formily/antd": "2.0.20",
|
||||
"@formily/core": "2.0.20",
|
||||
"@formily/react": "2.0.20",
|
||||
"@nocobase/evaluators": "0.9.1-alpha.2",
|
||||
"@nocobase/sdk": "0.9.1-alpha.2",
|
||||
"@nocobase/utils": "0.9.1-alpha.2",
|
||||
"ahooks": "^3.7.2",
|
||||
|
@ -3,12 +3,13 @@ import { action } from '@formily/reactive';
|
||||
import { useCollectionManager } from ".";
|
||||
import { useCompile } from "../../schema-component";
|
||||
|
||||
export function useCollectionDataSource() {
|
||||
export function useCollectionDataSource(filter?: Function) {
|
||||
return (field: any) => {
|
||||
const compile = useCompile();
|
||||
const { collections = [] } = useCollectionManager();
|
||||
action.bound((data: any) => {
|
||||
field.dataSource = data.map(item => ({
|
||||
const filtered = typeof filter === 'function' ? data.filter(filter) : data;
|
||||
field.dataSource = filtered.map(item => ({
|
||||
label: compile(item.title),
|
||||
value: item.name
|
||||
}));
|
||||
|
@ -21,7 +21,7 @@ export const id: IField = {
|
||||
'x-read-pretty': true,
|
||||
},
|
||||
},
|
||||
availableTypes:['bigInt','integer'],
|
||||
availableTypes:['bigInt','integer','string'],
|
||||
properties: {
|
||||
'uiSchema.title': {
|
||||
type: 'string',
|
||||
|
@ -0,0 +1,58 @@
|
||||
import { getOptions } from '@nocobase/evaluators/client';
|
||||
import { getConfigurableProperties } from './properties';
|
||||
import { ICollectionTemplate } from './types';
|
||||
|
||||
export const expression: ICollectionTemplate = {
|
||||
name: 'expression',
|
||||
title: '{{t("Expression collection")}}',
|
||||
order: 4,
|
||||
color: 'orange',
|
||||
default: {
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
sortable: true,
|
||||
fields: [
|
||||
{
|
||||
name: 'engine',
|
||||
type: 'string',
|
||||
interface: 'radioGroup',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: '{{t("Calculation engine")}}',
|
||||
'x-component': 'Radio.Group',
|
||||
enum: getOptions(),
|
||||
default: 'formula.js',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'sourceCollection',
|
||||
type: 'string',
|
||||
interface: 'select',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: '{{t("Collection")}}',
|
||||
'x-component': 'CollectionSelect',
|
||||
'x-component-props': {
|
||||
multiple: true
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'expression',
|
||||
type: 'text',
|
||||
interface: 'expression',
|
||||
uiSchema: {
|
||||
type: 'string',
|
||||
title: '{{t("Expression")}}',
|
||||
'x-component': 'DynamicExpression',
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
availableFieldInterfaces: {
|
||||
include: [],
|
||||
},
|
||||
configurableProperties: getConfigurableProperties('title', 'name', 'inherits','category'),
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
export * from './calendar';
|
||||
export * from './general';
|
||||
export * from './tree';
|
||||
export * from './expression';
|
||||
export * from './view';
|
||||
|
||||
|
@ -78,7 +78,9 @@ export default {
|
||||
"Properties":"属性",
|
||||
"Add linkage rule":"添加联动规则",
|
||||
"Add property":"添加属性",
|
||||
"Calculation engine": "计算引擎",
|
||||
"Expression":"表达式",
|
||||
"Expression collection": "表达式表",
|
||||
"Sort":"排序",
|
||||
"Categories":"数据表类别",
|
||||
"Category name":"分类名称",
|
||||
@ -164,7 +166,6 @@ export default {
|
||||
"Connect data blocks": "连接数据区块",
|
||||
"Action type": "操作类型",
|
||||
"Actions": "操作",
|
||||
"Insert": "新增",
|
||||
"Update": "更新",
|
||||
"View": "查看",
|
||||
"View record": "查看数据",
|
||||
@ -712,9 +713,11 @@ export default {
|
||||
'Column width': '列宽',
|
||||
'Sortable': '可排序的',
|
||||
'Constant': '常量',
|
||||
'Select a variable': '选择变量',
|
||||
"Insert": "插入",
|
||||
'System variables': '系统变量',
|
||||
'Date variables': '日期变量',
|
||||
'Use variable': '使用变量',
|
||||
'Double click to choose entire object': '双击选择整个对象',
|
||||
'True': '真',
|
||||
'False': '假',
|
||||
'Prettify': '格式化',
|
||||
|
@ -39,12 +39,12 @@ export const PinnedPluginList = () => {
|
||||
`}
|
||||
style={{ display: 'inline-block' }}
|
||||
>
|
||||
{Object.values<any>(ctx.items)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.filter((v) => getSnippetsAllow(v.snippet))
|
||||
.map((item) => {
|
||||
const Action = get(components, item.component);
|
||||
return Action ? <Action /> : null;
|
||||
{Object.keys(ctx.items)
|
||||
.sort((a, b) => ctx.items[a].order - ctx.items[b].order)
|
||||
.filter((key) => getSnippetsAllow(ctx.items[key].snippet))
|
||||
.map((key) => {
|
||||
const Action = get(components, ctx.items[key].component);
|
||||
return Action ? <Action key={key} /> : null;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
@ -0,0 +1,54 @@
|
||||
import { connect, mapReadPretty, observer } from "@formily/react";
|
||||
import { Select, SelectProps, Tag } from "antd";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useCollectionManager } from "../../../collection-manager/hooks";
|
||||
import { useCompile } from "../../hooks";
|
||||
|
||||
export type CollectionSelectProps = SelectProps<any, any> & {
|
||||
filter?: (item: any, index: number, array: any[]) => boolean
|
||||
};
|
||||
|
||||
function useOptions({ filter }: CollectionSelectProps) {
|
||||
const compile = useCompile();
|
||||
const { collections = [] } = useCollectionManager();
|
||||
const filtered = typeof filter === 'function' ? collections.filter(filter) : collections;
|
||||
return filtered.map(item => ({
|
||||
label: compile(item.title),
|
||||
value: item.name,
|
||||
color: item.category?.color,
|
||||
}));
|
||||
}
|
||||
|
||||
export const CollectionSelect = connect((props: CollectionSelectProps) => {
|
||||
const { filter, ...others } = props;
|
||||
const options = useOptions(props);
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Select
|
||||
placeholder={t('Select collection')}
|
||||
{...others}
|
||||
options={options}
|
||||
/>
|
||||
);
|
||||
}, mapReadPretty(observer((props: CollectionSelectProps) => {
|
||||
const { mode } = props;
|
||||
const compile = useCompile();
|
||||
const options = useOptions(props).filter(option => {
|
||||
if (mode === 'multiple') {
|
||||
return (props.value ?? []).includes(option.value);
|
||||
}
|
||||
return props.value === option.value;
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
{options.map((option) => (
|
||||
<Tag key={option.value} color={option.color}>
|
||||
{compile(option.label)}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
})));
|
@ -0,0 +1 @@
|
||||
export * from './CollectionSelect';
|
@ -5,6 +5,7 @@ export * from './calendar';
|
||||
export * from './card-item';
|
||||
export * from './cascader';
|
||||
export * from './checkbox';
|
||||
export * from './collection-select';
|
||||
export * from './color-select';
|
||||
export * from './cron';
|
||||
export * from './date-picker';
|
||||
|
@ -7,12 +7,12 @@ import { ReadPretty } from './ReadPretty';
|
||||
import { Json, JSONTextAreaProps } from './Json';
|
||||
|
||||
type ComposedInput = React.FC<InputProps> & {
|
||||
TextArea?: React.FC<TextAreaProps>;
|
||||
URL?: React.FC<InputProps>;
|
||||
JSON?: React.FC<JSONTextAreaProps>;
|
||||
TextArea: React.FC<TextAreaProps>;
|
||||
URL: React.FC<InputProps>;
|
||||
JSON: React.FC<JSONTextAreaProps>;
|
||||
};
|
||||
|
||||
export const Input: ComposedInput = connect(
|
||||
export const Input: ComposedInput = Object.assign(connect(
|
||||
AntdInput,
|
||||
mapProps((props, field) => {
|
||||
return {
|
||||
@ -21,9 +21,8 @@ export const Input: ComposedInput = connect(
|
||||
};
|
||||
}),
|
||||
mapReadPretty(ReadPretty.Input),
|
||||
);
|
||||
|
||||
Input.TextArea = connect(
|
||||
), {
|
||||
TextArea: connect(
|
||||
AntdInput.TextArea,
|
||||
mapProps((props, field) => {
|
||||
return {
|
||||
@ -35,13 +34,9 @@ Input.TextArea = connect(
|
||||
};
|
||||
}),
|
||||
mapReadPretty(ReadPretty.TextArea),
|
||||
);
|
||||
|
||||
Input.URL = connect(AntdInput, mapReadPretty(ReadPretty.URL));
|
||||
|
||||
Input.JSON = connect(
|
||||
Json,
|
||||
mapReadPretty(ReadPretty.JSON),
|
||||
);
|
||||
),
|
||||
URL: connect(AntdInput, mapReadPretty(ReadPretty.URL)),
|
||||
JSON: connect(Json, mapReadPretty(ReadPretty.JSON))
|
||||
});
|
||||
|
||||
export default Input;
|
||||
|
@ -4,9 +4,9 @@ import { useField } from '@formily/react';
|
||||
import { Input } from 'antd';
|
||||
import { TextAreaProps } from 'antd/lib/input';
|
||||
|
||||
export type JSONTextAreaProps = TextAreaProps & { ref: Ref<any>, value: any, space: number };
|
||||
export type JSONTextAreaProps = TextAreaProps & { value?: string, space?: number };
|
||||
|
||||
export const Json = React.forwardRef<Ref<any>>(({ value, onChange, space = 2, ...props }: JSONTextAreaProps, ref: Ref<any>) => {
|
||||
export const Json = React.forwardRef<typeof Input.TextArea, JSONTextAreaProps>(({ value, onChange, space = 2, ...props }: JSONTextAreaProps, ref: Ref<any>) => {
|
||||
const field = useField<Field>();
|
||||
return (
|
||||
<Input.TextArea
|
||||
@ -17,7 +17,7 @@ export const Json = React.forwardRef<Ref<any>>(({ value, onChange, space = 2, ..
|
||||
try {
|
||||
const v = ev.target.value.trim() !== '' ? JSON.parse(ev.target.value) : null;
|
||||
field.setFeedback({});
|
||||
onChange(v);
|
||||
onChange?.(v);
|
||||
} catch (err) {
|
||||
field.setFeedback({
|
||||
type: 'error',
|
||||
|
@ -13,8 +13,8 @@ type Composed = {
|
||||
TextArea: React.FC<
|
||||
TextAreaProps & { ellipsis?: any; text?: any; addonBefore?: any; suffix?: any; addonAfter?: any; autop?: boolean }
|
||||
>;
|
||||
Html?: any;
|
||||
JSON?: React.FC<TextAreaProps & { space: number }>;
|
||||
Html: any;
|
||||
JSON: React.FC<TextAreaProps & { space: number }>;
|
||||
};
|
||||
|
||||
export const ReadPretty: Composed = () => null;
|
||||
@ -63,10 +63,9 @@ ReadPretty.TextArea = (props) => {
|
||||
};
|
||||
|
||||
function convertToText(html: string) {
|
||||
let temp = document.createElement('div');
|
||||
const temp = document.createElement('div');
|
||||
temp.innerHTML = html;
|
||||
const text = temp.innerText;
|
||||
temp = null;
|
||||
return text.replace(/[\n\r]/g, '');
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useCompile } from '../..';
|
||||
import { XButton } from './XButton';
|
||||
|
||||
const JT_VALUE_RE = /^\s*{{\s*([^{}]+)\s*}}\s*$/;
|
||||
|
||||
@ -95,6 +96,19 @@ const ConstantTypes = {
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
function getTypedConstantOption(type) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return {
|
||||
value: '',
|
||||
label: t('Constant'),
|
||||
children: Object.values(ConstantTypes),
|
||||
component: ConstantTypes[type]?.component
|
||||
};
|
||||
}
|
||||
|
||||
type VariableOptions = {
|
||||
value: string;
|
||||
label?: string;
|
||||
@ -102,17 +116,28 @@ type VariableOptions = {
|
||||
};
|
||||
|
||||
export function Input(props) {
|
||||
const { value = '', scope, onChange, children, button } = props;
|
||||
const { value = '', scope, onChange, children, button, useTypedConstant } = props;
|
||||
const parsed = parseValue(value);
|
||||
const isConstant = typeof 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 { component: ConstantComponent, ...constantOption }: VariableOptions & { component: React.FC<any> } = children
|
||||
? {
|
||||
value: '',
|
||||
label: '{{t("Constant")}}',
|
||||
component: () => children
|
||||
}
|
||||
: (useTypedConstant
|
||||
? getTypedConstantOption(type)
|
||||
: {
|
||||
value: '',
|
||||
label: '{{t("Null")}}',
|
||||
component: ConstantTypes.null.component
|
||||
}
|
||||
);
|
||||
const options: VariableOptions[] = compile([
|
||||
{ value: '', label: t('Constant'), children: children ? null : constantOptions },
|
||||
constantOption,
|
||||
...(typeof scope === 'function' ? scope() : scope ?? []),
|
||||
]);
|
||||
|
||||
@ -223,20 +248,17 @@ export function Input(props) {
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
children ?? <ConstantComponent value={value} onChange={onChange} />
|
||||
<ConstantComponent value={value} onChange={onChange} />
|
||||
)}
|
||||
{options.length > 1 ? (
|
||||
<Cascader value={variable ?? ['', ...(children ? [] : [type])]} options={options} onChange={onSwitch}>
|
||||
{button ?? (
|
||||
<Button
|
||||
type={variable ? 'primary' : 'default'}
|
||||
className={css`
|
||||
font-style: italic;
|
||||
font-family: 'New York', 'Times New Roman', Times, serif;
|
||||
`}
|
||||
<Cascader
|
||||
options={options}
|
||||
value={variable ?? ['', ...(children || !constantOption.children?.length ? [] : [type])]}
|
||||
onChange={onSwitch}
|
||||
changeOnSelect
|
||||
>
|
||||
x
|
||||
</Button>
|
||||
{button ?? (
|
||||
<XButton type={variable ? 'primary' : 'default'} />
|
||||
)}
|
||||
</Cascader>
|
||||
) : null}
|
||||
|
@ -1,25 +1,27 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { Button, Cascader } from 'antd';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { Button, Cascader, Popover, Input as AntInput } from 'antd';
|
||||
import { css } from "@emotion/css";
|
||||
|
||||
import { Input } from "../input";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { XButton } from './XButton';
|
||||
|
||||
|
||||
|
||||
// NOTE: https://stackoverflow.com/questions/23892547/what-is-the-best-way-to-trigger-onchange-event-in-react-js/46012210#46012210
|
||||
function setNativeInputValue(input, value) {
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(input.constructor.prototype, 'value').set;
|
||||
nativeInputValueSetter.call(input, value);
|
||||
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(input.constructor.prototype, 'value')?.set;
|
||||
nativeInputValueSetter?.call(input, value);
|
||||
input.dispatchEvent(new Event('input', {
|
||||
bubbles: true,
|
||||
}));
|
||||
}
|
||||
|
||||
export function JSONInput(props) {
|
||||
const inputRef = useRef(null);
|
||||
const inputRef = useRef<any>(null);
|
||||
const { value, space = 2, scope } = props;
|
||||
const { t } = useTranslation()
|
||||
const { t } = useTranslation();
|
||||
const [selectedVar, setSelectedVar] = useState<string[]>([]);
|
||||
const options = typeof scope === 'function' ? scope() : (scope ?? []);
|
||||
|
||||
function onFormat() {
|
||||
@ -70,20 +72,24 @@ export function JSONInput(props) {
|
||||
`}
|
||||
>
|
||||
<Button onClick={onFormat}>{t('Prettify')}</Button>
|
||||
<Popover
|
||||
content={(
|
||||
<AntInput.Group compact>
|
||||
<Cascader
|
||||
value={[]}
|
||||
placeholder={t('Select a variable')}
|
||||
value={selectedVar}
|
||||
options={options}
|
||||
onChange={onInsert}
|
||||
onChange={(keyPaths) => setSelectedVar(keyPaths as string[])}
|
||||
changeOnSelect
|
||||
/>
|
||||
<Button onClick={onInsert}>{t('Insert')}</Button>
|
||||
</AntInput.Group>
|
||||
)}
|
||||
trigger="click"
|
||||
placement="topRight"
|
||||
>
|
||||
<Button
|
||||
className={css`
|
||||
font-style: italic;
|
||||
font-family: "New York", "Times New Roman", Times, serif;
|
||||
`}
|
||||
>
|
||||
x
|
||||
</Button>
|
||||
</Cascader>
|
||||
<XButton />
|
||||
</Popover>
|
||||
</Button.Group>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Input, Cascader, Tooltip, Button } from 'antd';
|
||||
import { Input, Cascader, Button } from 'antd';
|
||||
import { useForm } from '@formily/react';
|
||||
import { cx, css } from '@emotion/css';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -24,6 +24,8 @@ function pasteHTML(container: HTMLElement, html: string, { selectPastedContent =
|
||||
if (indexes[0] === -1) {
|
||||
if (indexes[1]) {
|
||||
range.setStartAfter(children[indexes[1] - 1]);
|
||||
} else {
|
||||
range.setStart(container, 0);
|
||||
}
|
||||
} else {
|
||||
range.setStart(children[indexes[0]], indexes[1]);
|
||||
@ -32,6 +34,8 @@ function pasteHTML(container: HTMLElement, html: string, { selectPastedContent =
|
||||
if (indexes[2] === -1) {
|
||||
if (indexes[3]) {
|
||||
range.setEndAfter(children[indexes[3] - 1]);
|
||||
} else {
|
||||
range.setEnd(container, 0);
|
||||
}
|
||||
} else {
|
||||
range.setEnd(children[indexes[2]], indexes[3]);
|
||||
@ -173,7 +177,7 @@ function getCurrentRange(element: HTMLElement): RangeIndexes {
|
||||
}
|
||||
|
||||
export function TextArea(props) {
|
||||
const { value = '', scope, onChange, multiline = true, button } = props;
|
||||
const { value = '', scope, onChange, multiline = true } = props;
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
@ -185,9 +189,17 @@ export function TextArea(props) {
|
||||
const [html, setHtml] = useState(() => renderHTML(value ?? '', keyLabelMap));
|
||||
// NOTE: e.g. [startElementIndex, startOffset, endElementIndex, endOffset]
|
||||
const [range, setRange] = useState<[number, number, number, number]>([-1, 0, -1, 0]);
|
||||
const [selectedVar, setSelectedVar] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedVar([]);
|
||||
}, [scope]);
|
||||
|
||||
useEffect(() => {
|
||||
setHtml(renderHTML(value ?? '', keyLabelMap));
|
||||
if (!changed) {
|
||||
setRange([-1, 0, -1, 0]);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -197,6 +209,7 @@ export function TextArea(props) {
|
||||
}
|
||||
const nextRange = new Range();
|
||||
if (changed) {
|
||||
setChanged(false);
|
||||
if (range.join() === '-1,0,-1,0') {
|
||||
return;
|
||||
}
|
||||
@ -234,8 +247,8 @@ export function TextArea(props) {
|
||||
}
|
||||
}, [html]);
|
||||
|
||||
function onInsert(keyPath) {
|
||||
const variable: string[] = keyPath.filter((key) => Boolean(key.trim()));
|
||||
function onInsert(paths: string[]) {
|
||||
const variable: string[] = paths.filter((key) => Boolean(key.trim()));
|
||||
const { current } = inputRef;
|
||||
if (!current || !variable) {
|
||||
return;
|
||||
@ -243,7 +256,8 @@ export function TextArea(props) {
|
||||
|
||||
current.focus();
|
||||
|
||||
pasteHTML(current, createVariableTagHTML(variable.join('.'), keyLabelMap), {
|
||||
const content = createVariableTagHTML(variable.join('.'), keyLabelMap);
|
||||
pasteHTML(current, content, {
|
||||
range,
|
||||
});
|
||||
|
||||
@ -326,6 +340,19 @@ export function TextArea(props) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.x-button{
|
||||
.ant-select.ant-cascader{
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
min-width: auto;
|
||||
width: calc(100% + 2px);
|
||||
height: calc(100% + 2px);
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
@ -353,20 +380,61 @@ export function TextArea(props) {
|
||||
contentEditable={!disabled}
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
<Tooltip title={t('Use variable')}>
|
||||
<Cascader value={[]} options={options} onChange={onInsert}>
|
||||
{button ?? (
|
||||
<Button
|
||||
<Button className={cx('x-button', css`
|
||||
position: relative;
|
||||
`)}>
|
||||
<span
|
||||
className={css`
|
||||
font-style: italic;
|
||||
font-family: 'New York', 'Times New Roman', Times, serif;
|
||||
font-family: "New York", "Times New Roman", Times, serif;
|
||||
`}
|
||||
>x</span>
|
||||
<Cascader
|
||||
placeholder={t('Select a variable')}
|
||||
value={[]}
|
||||
options={options}
|
||||
onChange={(keyPaths = [], selectedOptions = []) => {
|
||||
setSelectedVar(keyPaths as string[]);
|
||||
if (!keyPaths.length) {
|
||||
return;
|
||||
}
|
||||
const option = selectedOptions[selectedOptions.length - 1];
|
||||
if (!option?.children?.length) {
|
||||
onInsert(keyPaths);
|
||||
}
|
||||
}}
|
||||
changeOnSelect
|
||||
onClick={(e: any) => {
|
||||
if (e.detail !== 2) {
|
||||
return;
|
||||
}
|
||||
for (let n = e.target; n && n !== e.currentTarget; n = n.parentNode) {
|
||||
if (Array.from(n.classList ?? []).includes('ant-cascader-menu-item')) {
|
||||
onInsert(selectedVar);
|
||||
}
|
||||
}
|
||||
}}
|
||||
dropdownClassName={css`
|
||||
.ant-cascader-menu{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`}
|
||||
dropdownRender={(menu) => (
|
||||
<>
|
||||
{menu}
|
||||
<div
|
||||
className={css`
|
||||
padding: .5em;
|
||||
border-top: 1px solid rgba(0, 0, 0, .06);
|
||||
color: rgba(0, 0, 0, .45);
|
||||
`}
|
||||
>
|
||||
x
|
||||
</Button>
|
||||
{t('Double click to choose entire object')}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Cascader>
|
||||
</Tooltip>
|
||||
/>
|
||||
</Button>
|
||||
</Input.Group>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { Button, ButtonProps } from "antd";
|
||||
import { css } from "@emotion/css";
|
||||
|
||||
|
||||
|
||||
export const XButton = forwardRef((props: ButtonProps, ref: any) => (
|
||||
<Button
|
||||
ref={ref}
|
||||
className={css`
|
||||
font-style: italic;
|
||||
font-family: "New York", "Times New Roman", Times, serif;
|
||||
`}
|
||||
{...props}
|
||||
>
|
||||
x
|
||||
</Button>
|
||||
));
|
@ -1,7 +1,3 @@
|
||||
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';
|
||||
@ -19,29 +15,8 @@ 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 function getOptions() {
|
||||
return Array.from((evaluators as Registry<Evaluator>).getEntities()).reduce((result: any[], [value, options]) => result.concat({ value, ...options }), []);
|
||||
}
|
||||
|
||||
export default evaluators;
|
||||
|
@ -157,7 +157,7 @@ export const createSchema = () => {
|
||||
'x-component': 'Select',
|
||||
'x-read-pretty': true,
|
||||
enum: [
|
||||
{ label: "{{ t('Insert') }}", value: 'create', color: 'green' },
|
||||
{ label: "{{ t('Add new') }}", value: 'create', color: 'green' },
|
||||
{ label: "{{ t('Update') }}", value: 'update', color: 'blue' },
|
||||
{ label: "{{ t('Delete') }}", value: 'destroy', color: 'red' },
|
||||
],
|
||||
|
@ -3,11 +3,11 @@ import { onFormValuesChange } from '@formily/core';
|
||||
import { useFieldSchema, useFormEffects } from '@formily/react';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
|
||||
import { evaluators, Evaluator } from '@nocobase/evaluators/client';
|
||||
import evaluators, { Evaluator } from '@nocobase/evaluators/client';
|
||||
import { Registry, toFixedByStep } from '@nocobase/utils/client';
|
||||
import { Checkbox, DatePicker, Input as InputString, InputNumber, useCollection } from '@nocobase/client';
|
||||
|
||||
import { toDbType } from '../../utils';
|
||||
import { toDbType } from '../../../utils';
|
||||
|
||||
const TypedComponents = {
|
||||
boolean: Checkbox,
|
@ -1,4 +1,4 @@
|
||||
import { connect, mapReadPretty } from '@formily/react';
|
||||
import { connect } from '@formily/react';
|
||||
|
||||
import Expression from './Expression';
|
||||
import Result from './Result';
|
@ -0,0 +1 @@
|
||||
export { Formula } from './Formula';
|
@ -5,14 +5,14 @@ import { css } from '@emotion/css';
|
||||
import { CollectionManagerContext, registerField, SchemaComponentOptions } from '@nocobase/client';
|
||||
import { evaluators, Evaluator } from '@nocobase/evaluators/client';
|
||||
|
||||
import { Formula } from './formula';
|
||||
import field from './field';
|
||||
import { Formula } from './components';
|
||||
import formulaField from './interfaces/formula';
|
||||
import { NAMESPACE } from './locale';
|
||||
import { Registry } from '@nocobase/utils/client';
|
||||
|
||||
|
||||
|
||||
registerField(field.group, 'formula', field);
|
||||
registerField(formulaField.group, 'formula', formulaField);
|
||||
|
||||
function renderExpressionDescription(key: string) {
|
||||
const engine = (evaluators as Registry<Evaluator>).get(key);
|
||||
@ -36,20 +36,28 @@ function renderExpressionDescription(key: string) {
|
||||
: null
|
||||
}
|
||||
|
||||
export { Formula } from './formula';
|
||||
|
||||
export default React.memo((props) => {
|
||||
const ctx = useContext(CollectionManagerContext);
|
||||
return (
|
||||
<SchemaComponentOptions
|
||||
components={{
|
||||
Formula
|
||||
Formula,
|
||||
// DynamicExpression
|
||||
}}
|
||||
scope={{
|
||||
renderExpressionDescription
|
||||
}}
|
||||
>
|
||||
<CollectionManagerContext.Provider value={{ ...ctx, interfaces: { ...ctx.interfaces, formula: field } }}>
|
||||
<CollectionManagerContext.Provider
|
||||
value={{
|
||||
...ctx,
|
||||
interfaces: {
|
||||
...ctx.interfaces,
|
||||
formula: formulaField,
|
||||
// expression: expressionField
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</CollectionManagerContext.Provider>
|
||||
</SchemaComponentOptions>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { i18n, IField, interfacesProperties } from '@nocobase/client';
|
||||
import { evaluators, Evaluator } from '@nocobase/evaluators/client';
|
||||
import evaluators, { Evaluator } from '@nocobase/evaluators/client';
|
||||
import { Registry } from '@nocobase/utils/client';
|
||||
|
||||
import { NAMESPACE } from './locale';
|
||||
import { NAMESPACE } from '../locale';
|
||||
|
||||
|
||||
|
||||
@ -125,7 +125,7 @@ export default {
|
||||
...datetimeProperties,
|
||||
engine: {
|
||||
type: 'string',
|
||||
title: `{{t("Formula engine", { ns: "${NAMESPACE}" })}}`,
|
||||
title: `{{t("Calculation engine", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
enum: Array.from((evaluators as Registry<Evaluator>).getEntities()).reduce((result: any[], [value, options]) => result.concat({ value, ...options }), []),
|
@ -1,8 +1,8 @@
|
||||
export default {
|
||||
'Formula': '公式',
|
||||
'Formula engine': '公式引擎',
|
||||
'Calculation engine': '计算引擎',
|
||||
'Expression': '表达式',
|
||||
'Expression syntax error': '表达式语法错误',
|
||||
'Syntax references': '语法参考',
|
||||
'Compute a value based on the other fields': '基于其他字段进行计算'
|
||||
'Compute a value based on the other fields': '基于其他字段进行计算',
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import { InstallOptions, Plugin } from '@nocobase/server';
|
||||
import { resolve } from 'path';
|
||||
import { FormulaField } from './formula-field';
|
||||
|
||||
|
||||
|
||||
export class FormulaFieldPlugin extends Plugin {
|
||||
afterAdd() {}
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
"react-js-cron": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nocobase/plugin-formula-field": "0.9.1-alpha.2",
|
||||
"@nocobase/test": "0.9.1-alpha.2",
|
||||
"@types/ejs": "^3.1.1"
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Card } from 'antd';
|
||||
import { PluginManagerContext, RouteSwitchContext, SchemaComponent, SchemaComponentOptions, SettingsCenterProvider } from '@nocobase/client';
|
||||
import { CollectionManagerContext, PluginManagerContext, RouteSwitchContext, SchemaComponent, SchemaComponentOptions, SettingsCenterProvider, registerField, useCollectionDataSource } from '@nocobase/client';
|
||||
|
||||
import { WorkflowPage } from './WorkflowPage';
|
||||
import { ExecutionPage } from './ExecutionPage';
|
||||
import { triggers } from './triggers';
|
||||
@ -13,6 +14,12 @@ import { ExecutionLink } from './ExecutionLink';
|
||||
import OpenDrawer from './components/OpenDrawer';
|
||||
import { WorkflowTodo } from './nodes/manual/WorkflowTodo';
|
||||
import { WorkflowTodoBlockInitializer } from './nodes/manual/WorkflowTodoBlockInitializer';
|
||||
import { DynamicExpression } from './components/DynamicExpression';
|
||||
import expressionField from './interfaces/expression';
|
||||
|
||||
|
||||
|
||||
// registerField(expressionField.group, 'expression', expressionField);
|
||||
|
||||
export const WorkflowContext = React.createContext({});
|
||||
|
||||
@ -37,7 +44,8 @@ function WorkflowPane() {
|
||||
};
|
||||
|
||||
export const WorkflowProvider = (props) => {
|
||||
const ctx = useContext(PluginManagerContext);
|
||||
const pmCtx = useContext(PluginManagerContext);
|
||||
const cmCtx = useContext(CollectionManagerContext);
|
||||
const { routes, components, ...others } = useContext(RouteSwitchContext);
|
||||
routes[1].routes.unshift(
|
||||
{
|
||||
@ -71,16 +79,35 @@ export const WorkflowProvider = (props) => {
|
||||
<PluginManagerContext.Provider
|
||||
value={{
|
||||
components: {
|
||||
...ctx?.components,
|
||||
...pmCtx?.components,
|
||||
// WorkflowShortcut,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RouteSwitchContext.Provider value={{ components: { ...components, WorkflowPage, ExecutionPage }, ...others, routes }}>
|
||||
<SchemaComponentOptions components={{ WorkflowTodo, WorkflowTodoBlockInitializer }}>
|
||||
<SchemaComponentOptions
|
||||
components={{
|
||||
WorkflowTodo,
|
||||
WorkflowTodoBlockInitializer,
|
||||
DynamicExpression
|
||||
}}
|
||||
scope={{
|
||||
useCollectionDataSource
|
||||
}}
|
||||
>
|
||||
<CollectionManagerContext.Provider
|
||||
value={{
|
||||
...cmCtx,
|
||||
interfaces: {
|
||||
...cmCtx.interfaces,
|
||||
expression: expressionField
|
||||
}
|
||||
}}
|
||||
>
|
||||
<WorkflowContext.Provider value={{ triggers, instructions }}>
|
||||
{props.children}
|
||||
</WorkflowContext.Provider>
|
||||
</CollectionManagerContext.Provider>
|
||||
</SchemaComponentOptions>
|
||||
</RouteSwitchContext.Provider>
|
||||
</PluginManagerContext.Provider>
|
||||
|
@ -0,0 +1,43 @@
|
||||
import React, { useState } from "react";
|
||||
import { onFieldInputValueChange } from '@formily/core';
|
||||
import { observer, connect, mapReadPretty, useFormEffects } from '@formily/react';
|
||||
import { Tag } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Variable } from "@nocobase/client";
|
||||
|
||||
import { NAMESPACE } from "../locale";
|
||||
import { useCollectionFieldOptions } from "../variable";
|
||||
|
||||
|
||||
|
||||
const InternalExpression = observer((props: any) => {
|
||||
const { value, onChange } = props;
|
||||
const [collection, setCollection] = useState(null);
|
||||
|
||||
useFormEffects(() => {
|
||||
onFieldInputValueChange('sourceCollection', (f) => {
|
||||
setCollection(f.value);
|
||||
onChange(null);
|
||||
});
|
||||
});
|
||||
|
||||
const options = useCollectionFieldOptions({ collection });
|
||||
|
||||
return (
|
||||
<Variable.TextArea
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
scope={options}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
function Result({ value }) {
|
||||
const { t } = useTranslation();
|
||||
return value
|
||||
? <Tag color="purple">{t('Expression')}</Tag>
|
||||
: <Tag>{t('Unconfigured', { ns: NAMESPACE })}</Tag>;
|
||||
}
|
||||
|
||||
export const DynamicExpression = connect(InternalExpression, mapReadPretty(Result));
|
@ -15,6 +15,7 @@ export function FilterDynamicComponent({ value, onChange, renderSchemaComponent
|
||||
onChange={onChange}
|
||||
scope={scope}
|
||||
>
|
||||
{renderSchemaComponent()}
|
||||
</Variable.Input>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
import React from "react";
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { i18n } from "@nocobase/client";
|
||||
import evaluators from "@nocobase/evaluators/client";
|
||||
|
||||
|
||||
|
||||
export const renderEngineReference = (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
|
||||
};
|
@ -0,0 +1,24 @@
|
||||
import { IField, interfacesProperties } from "@nocobase/client";
|
||||
|
||||
import { NAMESPACE } from "../locale";
|
||||
|
||||
const { defaultProps } = interfacesProperties;
|
||||
|
||||
export default {
|
||||
name: 'expression',
|
||||
type: 'string',
|
||||
group: 'advanced',
|
||||
order: 1,
|
||||
title: `{{t("Expression", { ns: "${NAMESPACE}" })}}`,
|
||||
description: `{{t("An expression for calculation in each rows", { ns: "${NAMESPACE}" })}}`,
|
||||
sortable: true,
|
||||
default: {
|
||||
type: 'text',
|
||||
uiSchema: {
|
||||
'x-component': 'DynamicExpression',
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
...defaultProps,
|
||||
},
|
||||
} as IField;
|
@ -60,8 +60,8 @@ export default {
|
||||
'Arithmetic calculation': '算术运算',
|
||||
'String operation': '字符串',
|
||||
'System variables': '系统变量',
|
||||
'System time': '系统时间',
|
||||
'Date variables': '日期变量',
|
||||
'Current time': '当前时间',
|
||||
|
||||
'Executed at': '执行于',
|
||||
'Queueing': '队列中',
|
||||
@ -86,6 +86,11 @@ export default {
|
||||
'Extended types': '扩展类型',
|
||||
'Node type': '节点类型',
|
||||
'Calculation': '运算',
|
||||
'Expression type': '表达式类型',
|
||||
'Static': '静态',
|
||||
'Dynamic': '动态',
|
||||
'Select dynamic expression': '选择动态表达式',
|
||||
'Variable datasource': '变量数据源',
|
||||
'Calculation engine': '运算引擎',
|
||||
'Basic': '基础',
|
||||
'Calculation expression': '运算表达式',
|
||||
@ -165,4 +170,8 @@ export default {
|
||||
|
||||
'Workflow todos': '工作流待办',
|
||||
'Task': '任务',
|
||||
|
||||
'Dynamic expression': '动态表达式',
|
||||
'An expression for calculation in each rows': '每行数据计算规则不同时使用',
|
||||
'Unconfigured': '未配置'
|
||||
};
|
||||
|
@ -1,32 +1,90 @@
|
||||
import React from 'react';
|
||||
import { observer } from '@formily/react';
|
||||
import { FormLayout, FormItem } from '@formily/antd';
|
||||
import { css } from '@emotion/css';
|
||||
import parse from 'json-templates';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Radio } from 'antd';
|
||||
|
||||
import { SchemaInitializer, SchemaInitializerItemOptions } from '@nocobase/client';
|
||||
import { evaluators, renderReference, Evaluator } from '@nocobase/evaluators/client';
|
||||
import { SchemaInitializer, SchemaInitializerItemOptions, useCollectionManager, Variable } from '@nocobase/client';
|
||||
import { evaluators, Evaluator, getOptions } from '@nocobase/evaluators/client';
|
||||
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
import { lang, NAMESPACE } from '../locale';
|
||||
import { TypeSets, useWorkflowVariableOptions } from '../variable';
|
||||
import { BaseTypeSets, useWorkflowVariableOptions } from '../variable';
|
||||
import { RadioWithTooltip } from '../components/RadioWithTooltip';
|
||||
import { renderEngineReference } from '../components/renderEngineReference';
|
||||
|
||||
|
||||
|
||||
function matchDynamicExpressionCollectionField(field): boolean {
|
||||
const { getCollectionFields, getCollection } = useCollectionManager();
|
||||
if (field.type !== 'belongsTo') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fields = getCollectionFields(field.target);
|
||||
return fields.some(f => f.interface === 'expression');
|
||||
}
|
||||
|
||||
const DynamicConfig = ({ value, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const scope = useWorkflowVariableOptions([
|
||||
matchDynamicExpressionCollectionField
|
||||
]);
|
||||
|
||||
return (
|
||||
<FormLayout layout="vertical">
|
||||
<FormItem label={t('Expression type', { ns: NAMESPACE })}>
|
||||
<Radio.Group value={value === false ? false : (value || null)} onChange={(ev) => {
|
||||
onChange(ev.target.value);
|
||||
}}>
|
||||
<Radio value={false}>{t("Static", { ns: NAMESPACE })}</Radio>
|
||||
<Radio value={value || null}>{t("Dynamic", { ns: NAMESPACE })}</Radio>
|
||||
</Radio.Group>
|
||||
</FormItem>
|
||||
{value !== false ? (
|
||||
<FormItem label={t('Select dynamic expression', { ns: NAMESPACE })}>
|
||||
<Variable.Input value={value || null} onChange={(v) => onChange(v)} scope={scope} />
|
||||
</FormItem>
|
||||
) : null}
|
||||
</FormLayout>
|
||||
)
|
||||
};
|
||||
|
||||
function useWorkflowVariableEntityOptions() {
|
||||
return useWorkflowVariableOptions([{ type: "reference", options: { collection: "*", entity: true } }]);
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
title: `{{t("Calculation", { ns: "${NAMESPACE}" })}}`,
|
||||
type: 'calculation',
|
||||
group: 'control',
|
||||
fieldset: {
|
||||
dynamic: {
|
||||
type: 'string',
|
||||
'x-component': 'DynamicConfig',
|
||||
default: false,
|
||||
},
|
||||
engine: {
|
||||
type: 'string',
|
||||
title: `{{t("Calculation engine", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'RadioWithTooltip',
|
||||
'x-component-props': {
|
||||
options: Array.from(evaluators.getEntities()).reduce((result: any[], [value, options]) => result.concat({ value, ...options }), [])
|
||||
options: getOptions()
|
||||
},
|
||||
required: true,
|
||||
default: 'math.js'
|
||||
default: 'math.js',
|
||||
'x-reactions': {
|
||||
dependencies: ['dynamic'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{$deps[0] === false}}',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
expression: {
|
||||
type: 'string',
|
||||
@ -47,15 +105,42 @@ export default {
|
||||
return lang('Expression syntax error');
|
||||
}
|
||||
},
|
||||
'x-reactions': {
|
||||
dependencies: ['engine'],
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['dynamic'],
|
||||
fulfill: {
|
||||
schema: {
|
||||
description: '{{renderReference($deps[0])}}',
|
||||
state: {
|
||||
visible: '{{$deps[0] === false}}',
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
dependencies: ['engine'],
|
||||
fulfill: {
|
||||
schema: {
|
||||
description: '{{renderEngineReference($deps[0])}}',
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
required: true
|
||||
},
|
||||
scope: {
|
||||
type: 'string',
|
||||
title: `{{t("Variable datasource", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Variable.Input',
|
||||
'x-component-props': {
|
||||
scope: '{{useWorkflowVariableEntityOptions}}'
|
||||
},
|
||||
'x-reactions': {
|
||||
dependencies: ['dynamic'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{$deps[0] !== false}}',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
view: {
|
||||
@ -63,7 +148,8 @@ export default {
|
||||
},
|
||||
scope: {
|
||||
useWorkflowVariableOptions,
|
||||
renderReference
|
||||
useWorkflowVariableEntityOptions,
|
||||
renderEngineReference
|
||||
},
|
||||
components: {
|
||||
CalculationResult({ dataSource }) {
|
||||
@ -83,10 +169,11 @@ export default {
|
||||
</pre>
|
||||
);
|
||||
},
|
||||
RadioWithTooltip
|
||||
RadioWithTooltip,
|
||||
DynamicConfig
|
||||
},
|
||||
getOptions(config, types) {
|
||||
if (types && !types.some(type => type in TypeSets || Object.values(TypeSets).some(set => set.has(type)))) {
|
||||
if (types && !types.some(type => type in BaseTypeSets || Object.values(BaseTypeSets).some(set => set.has(type)))) {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
|
@ -6,7 +6,7 @@ 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 { evaluators } from "@nocobase/evaluators/client";
|
||||
|
||||
import { NodeDefaultView } from ".";
|
||||
import { Branch } from "../Branch";
|
||||
@ -15,6 +15,7 @@ import { branchBlockClass, nodeSubtreeClass } from "../style";
|
||||
import { lang, NAMESPACE } from "../locale";
|
||||
import { useWorkflowVariableOptions } from "../variable";
|
||||
import { RadioWithTooltip, RadioWithTooltipOption } from "../components/RadioWithTooltip";
|
||||
import { renderEngineReference } from "../components/renderEngineReference";
|
||||
|
||||
interface Calculator {
|
||||
name: string;
|
||||
@ -154,6 +155,7 @@ export function Calculation({ calculator, operands = [], onChange }) {
|
||||
value={operands[0]}
|
||||
onChange={(v => onChange({ calculator, operands: [v, operands[1]] }))}
|
||||
scope={options}
|
||||
useTypedConstant
|
||||
/>
|
||||
<Select
|
||||
value={calculator}
|
||||
@ -172,6 +174,7 @@ export function Calculation({ calculator, operands = [], onChange }) {
|
||||
value={operands[1]}
|
||||
onChange={(v => onChange({ calculator, operands: [operands[0], v] }))}
|
||||
scope={options}
|
||||
useTypedConstant
|
||||
/>
|
||||
</fieldset>
|
||||
);
|
||||
@ -378,7 +381,7 @@ export default {
|
||||
visible: '{{$deps[0] !== "basic"}}'
|
||||
},
|
||||
schema: {
|
||||
description: '{{renderReference($deps[0])}}',
|
||||
description: '{{renderEngineReference($deps[0])}}',
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -439,7 +442,7 @@ export default {
|
||||
)
|
||||
},
|
||||
scope: {
|
||||
renderReference,
|
||||
renderEngineReference,
|
||||
useWorkflowVariableOptions
|
||||
},
|
||||
components: {
|
||||
|
@ -45,7 +45,7 @@ export default {
|
||||
FieldsSelect
|
||||
},
|
||||
getOptions(config, types) {
|
||||
return useCollectionFieldOptions({ collection: config.collection, types });
|
||||
return useCollectionFieldOptions({ collection: config.collection, types, depth: config.params?.appends?.length ? 1 : 0 });
|
||||
},
|
||||
useInitializers(node): SchemaInitializerItemOptions | null {
|
||||
if (!node.config.collection) {
|
||||
|
@ -30,7 +30,7 @@ import destroy from './destroy';
|
||||
import { JobStatusOptions, JobStatusOptionsMap } from '../constants';
|
||||
import { lang, NAMESPACE } from '../locale';
|
||||
import request from "./request";
|
||||
import { VariableOption } from '../variable';
|
||||
import { VariableOptions } from '../variable';
|
||||
|
||||
export interface Instruction {
|
||||
title: string;
|
||||
@ -43,7 +43,7 @@ export interface Instruction {
|
||||
components?: { [key: string]: any };
|
||||
render?(props): React.ReactNode;
|
||||
endding?: boolean;
|
||||
getOptions?(config, types?): VariableOption[] | null;
|
||||
getOptions?(config, types?): VariableOptions;
|
||||
useInitializers?(node): SchemaInitializerItemOptions | null;
|
||||
initializers?: { [key: string]: any };
|
||||
};
|
||||
|
@ -6,12 +6,11 @@ import { useWorkflowVariableOptions } from '../../variable';
|
||||
|
||||
|
||||
export function AssigneesSelect({ multiple = false, value = [], onChange }) {
|
||||
const scope = useWorkflowVariableOptions();
|
||||
const scope = useWorkflowVariableOptions([{ type: 'reference', options: { collection: 'users' } }]);
|
||||
|
||||
return (
|
||||
<Variable.Input
|
||||
scope={scope}
|
||||
types={[{ type: 'reference', options: { collection: 'users' } }]}
|
||||
value={value[0]}
|
||||
onChange={(next) => {
|
||||
onChange([next]);
|
||||
|
@ -50,7 +50,7 @@ export default {
|
||||
FieldsSelect
|
||||
},
|
||||
getOptions(config, types) {
|
||||
return useCollectionFieldOptions({ collection: config.collection, types });
|
||||
return useCollectionFieldOptions({ collection: config.collection, types, depth: config.params?.appends?.length ? 1 : 0 });
|
||||
},
|
||||
useInitializers(node): SchemaInitializerItemOptions | null {
|
||||
if (!node.config.collection) {
|
||||
|
@ -96,7 +96,7 @@ export default {
|
||||
dependencies: ['collection', 'mode'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: `{{$deps[0] && $deps[1] & ${COLLECTION_TRIGGER_MODE.UPDATED}}}`,
|
||||
visible: `{{!!$deps[0] && ($deps[1] & ${COLLECTION_TRIGGER_MODE.UPDATED})}}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -139,10 +139,18 @@ export default {
|
||||
},
|
||||
getOptions(config, types) {
|
||||
const { t } = useWorkflowTranslation();
|
||||
const fieldOptions = useCollectionFieldOptions({ collection: config.collection, types });
|
||||
const options: any[] = [
|
||||
...(fieldOptions?.length ? [{ label: t('Trigger data'), key: 'data', value: 'data', children: fieldOptions }] : []),
|
||||
const rootFields = [
|
||||
{
|
||||
collectionName: config.collection,
|
||||
name: 'data',
|
||||
type: 'hasOne',
|
||||
target: config.collection,
|
||||
uiSchema: {
|
||||
title: t('Trigger data')
|
||||
}
|
||||
}
|
||||
];
|
||||
const options = useCollectionFieldOptions({ fields: rootFields, types, depth: config.appends?.length ? 2 : 1 });
|
||||
return options;
|
||||
},
|
||||
useInitializers(config): SchemaInitializerItemOptions | null {
|
||||
|
@ -13,6 +13,7 @@ import { useFlowContext } from "../FlowContext";
|
||||
import collection from './collection';
|
||||
import schedule from "./schedule/";
|
||||
import { lang, NAMESPACE } from "../locale";
|
||||
import { VariableOptions } from "../variable";
|
||||
|
||||
|
||||
function useUpdateConfigAction() {
|
||||
@ -44,7 +45,7 @@ export interface Trigger {
|
||||
title: string;
|
||||
type: string;
|
||||
// group: string;
|
||||
getOptions?(config: any, types: any[]): { label: string; value: any; key: string }[];
|
||||
getOptions?(config: any, types: any[]): VariableOptions;
|
||||
fieldset: { [key: string]: ISchema };
|
||||
view?: ISchema;
|
||||
scope?: { [key: string]: any };
|
||||
|
@ -34,7 +34,7 @@ export function OnField({ value, onChange }) {
|
||||
{dir
|
||||
? (
|
||||
<>
|
||||
<InputNumber value={Math.abs(value.offset)} onChange={(v) => onChange({ ...value, offset: v * dir })}/>
|
||||
<InputNumber value={Math.abs(value.offset)} onChange={(v) => onChange({ ...value, offset: (v ?? 0) * dir })}/>
|
||||
<Select value={value.unit || 86400000} onChange={unit => onChange({ ...value, unit })}>
|
||||
<Select.Option value={86400000}>{localT('Days')}</Select.Option>
|
||||
<Select.Option value={3600000}>{localT('Hours')}</Select.Option>
|
||||
|
@ -5,12 +5,14 @@ import { instructions, useAvailableUpstreams, useNodeContext } from './nodes';
|
||||
import { triggers } from './triggers';
|
||||
|
||||
export type VariableOption = {
|
||||
key: string;
|
||||
key?: string;
|
||||
value: string;
|
||||
label: string;
|
||||
children?: VariableOption[] | null;
|
||||
children?: VariableOptions;
|
||||
};
|
||||
|
||||
export type VariableOptions = VariableOption[] | null;
|
||||
|
||||
const VariableTypes = [
|
||||
{
|
||||
title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
|
||||
@ -46,85 +48,162 @@ const VariableTypes = [
|
||||
{
|
||||
title: `{{t("System variables", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$system',
|
||||
options: [
|
||||
options(types) {
|
||||
return [
|
||||
...(!types || types.includes('date') ? [
|
||||
{
|
||||
key: 'now',
|
||||
value: 'now',
|
||||
label: `{{t("Now")}}`,
|
||||
},
|
||||
],
|
||||
label: `{{t("System time")}}`,
|
||||
}
|
||||
] : [])
|
||||
];
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export const TypeSets = {
|
||||
boolean: new Set(['boolean']),
|
||||
number: new Set(['integer', 'bigInt', 'float', 'double', 'real', 'decimal']),
|
||||
string: new Set(['string', 'text', 'password']),
|
||||
date: new Set(['date', 'time']),
|
||||
export const BaseTypeSets = {
|
||||
boolean: new Set(['checkbox']),
|
||||
number: new Set(['number', 'percent']),
|
||||
string: new Set(['input', 'password', 'email', 'phone', 'select', 'radioGroup', 'text', 'markdown', 'richText', 'expression', 'time']),
|
||||
date: new Set(['date', 'createdAt', 'updatedAt']),
|
||||
};
|
||||
|
||||
// { type: 'reference', options: { collection: 'users', multiple: false } }
|
||||
// { type: 'reference', options: { collection: 'attachments', multiple: false } }
|
||||
// { type: 'reference', options: { collection: 'myExpressions', entity: false } }
|
||||
|
||||
function matchFieldType(field, type): Boolean {
|
||||
if (typeof type === 'string') {
|
||||
return Boolean(TypeSets[type]?.has(field.type));
|
||||
const inputType = typeof type;
|
||||
if (inputType === 'string') {
|
||||
return Boolean(BaseTypeSets[type]?.has(field.interface));
|
||||
}
|
||||
|
||||
if (typeof type === 'object' && type.type === 'reference') {
|
||||
if (inputType === 'object' && type.type === 'reference') {
|
||||
if (isAssociationField(field)) {
|
||||
return type.options?.entity && (field.collectionName === type.options?.collection || type.options?.collection === '*');
|
||||
} else if (field.isForeignKey) {
|
||||
return (
|
||||
(field.collectionName === type.options?.collection && field.name === 'id') ||
|
||||
(field.type === 'belongsTo' && field.target === type.options?.collection)
|
||||
(field.target === type.options?.collection)
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (inputType === 'function') {
|
||||
return type(field);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function filterTypedFields(fields, types) {
|
||||
return types ? fields.filter((field) => types.some((type) => matchFieldType(field, type))) : fields;
|
||||
function isAssociationField(field): boolean {
|
||||
return ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type);
|
||||
}
|
||||
|
||||
export function useWorkflowVariableOptions() {
|
||||
export function filterTypedFields(fields, types, depth = 1) {
|
||||
if (!types) {
|
||||
return fields;
|
||||
}
|
||||
return fields.filter((field) => {
|
||||
if (isAssociationField(field) && depth && filterTypedFields(useNormallizedFields(field.target), types, depth - 1).length) {
|
||||
return true;
|
||||
}
|
||||
return types.some((type) => matchFieldType(field, type));
|
||||
});
|
||||
}
|
||||
|
||||
export function useWorkflowVariableOptions(types?) {
|
||||
const compile = useCompile();
|
||||
const options = VariableTypes.map((item: any) => {
|
||||
const options = typeof item.options === 'function' ? item.options().filter(Boolean) : item.options;
|
||||
const opts = typeof item.options === 'function' ? item.options(types).filter(Boolean) : item.options;
|
||||
return {
|
||||
label: compile(item.title),
|
||||
value: item.value,
|
||||
key: item.value,
|
||||
children: compile(options),
|
||||
disabled: options && !options.length,
|
||||
children: compile(opts),
|
||||
disabled: opts && !opts.length,
|
||||
};
|
||||
});
|
||||
return options;
|
||||
}
|
||||
|
||||
function useCollectionNormalFields(collection) {
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const fields = getCollectionFields(collection);
|
||||
return fields.filter(field => field.interface);
|
||||
}
|
||||
|
||||
export function useCollectionFieldOptions(options, depth = 1): VariableOption[] {
|
||||
const { fields, collection, types } = options;
|
||||
function useNormallizedFields(collection) {
|
||||
const compile = useCompile();
|
||||
const result: VariableOption[] = [];
|
||||
filterTypedFields((fields ?? useCollectionNormalFields(collection)), types)
|
||||
.forEach(field => {
|
||||
const label = compile(field.uiSchema?.title || field.name);
|
||||
const { getCollection } = useCollectionManager();
|
||||
if (!collection) {
|
||||
return [];
|
||||
}
|
||||
const { fields } = getCollection(collection);
|
||||
const foreignKeyFields: any[] = [];
|
||||
const otherFields: any[] = [];
|
||||
fields.forEach(field => {
|
||||
if (field.isForeignKey) {
|
||||
foreignKeyFields.push(field);
|
||||
} else {
|
||||
otherFields.push(field);
|
||||
}
|
||||
});
|
||||
for (let i = otherFields.length - 1; i >= 0; i--) {
|
||||
const field = otherFields[i];
|
||||
if (field.type === 'belongsTo') {
|
||||
result.push({
|
||||
label: `${label} ID`,
|
||||
key: field.foreignKey,
|
||||
value: field.foreignKey,
|
||||
const foreignKeyField = foreignKeyFields.find(f => f.name === field.foreignKey);
|
||||
if (foreignKeyField) {
|
||||
otherFields.splice(i, 0, {
|
||||
...foreignKeyField,
|
||||
uiSchema: {
|
||||
...field.uiSchema,
|
||||
title: field.uiSchema?.title ? `${compile(field.uiSchema?.title)} ID` : foreignKeyField.name,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
otherFields.splice(i, 0, {
|
||||
...field,
|
||||
name: field.foreignKey,
|
||||
type: 'bigInt',
|
||||
isForeignKey: true,
|
||||
interface: field.interface,
|
||||
uiSchema: {
|
||||
...field.uiSchema,
|
||||
title: field.uiSchema?.title ? `${compile(field.uiSchema?.title)} ID` : field.name,
|
||||
}
|
||||
});
|
||||
}
|
||||
result.push({
|
||||
} else if (field.type === 'context' && field.collectionName === 'users') {
|
||||
const belongsToField = otherFields.find(f => f.type === 'belongsTo' && f.target === 'users' && f.foreignKey === field.name) ?? {};
|
||||
otherFields.splice(i, 0, {
|
||||
...field,
|
||||
type: field.dataType,
|
||||
interface: belongsToField.interface,
|
||||
uiSchema: {
|
||||
...belongsToField.uiSchema,
|
||||
title: belongsToField.uiSchema?.title ? `${compile(belongsToField.uiSchema?.title)} ID` : field.name
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return otherFields.filter(field => field.interface && !field.hidden);
|
||||
}
|
||||
|
||||
export function useCollectionFieldOptions(options): VariableOption[] {
|
||||
const { fields, collection, types, depth = 1 } = options;
|
||||
const compile = useCompile();
|
||||
const normalizedFields = fields ?? useNormallizedFields(collection);
|
||||
const result: VariableOption[] = filterTypedFields(normalizedFields, types, depth)
|
||||
.filter(field => !isAssociationField(field) || depth)
|
||||
.map(field => {
|
||||
const label = compile(field.uiSchema?.title || field.name);
|
||||
return {
|
||||
label,
|
||||
key: field.name,
|
||||
value: field.name,
|
||||
children: ['linkTo', 'belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type) && depth > 0
|
||||
? useCollectionFieldOptions({ collection: field.target, types }, depth - 1)
|
||||
children: isAssociationField(field) && depth
|
||||
? useCollectionFieldOptions({ collection: field.target, types, depth: depth - 1 })
|
||||
: null
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
return result;
|
||||
|
@ -4,6 +4,7 @@ import { Op } from '@nocobase/database';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { Registry } from '@nocobase/utils';
|
||||
|
||||
import initFields from './fields';
|
||||
import initActions from './actions';
|
||||
import { EXECUTION_STATUS } from './constants';
|
||||
import initInstructions, { Instruction } from './instructions';
|
||||
@ -72,12 +73,12 @@ export default class WorkflowPlugin extends Plugin {
|
||||
async load() {
|
||||
const { db, options } = this;
|
||||
|
||||
initFields(this);
|
||||
initActions(this);
|
||||
initTriggers(this, options.triggers);
|
||||
initInstructions(this, options.instructions);
|
||||
initFunctions(this, options.functions);
|
||||
|
||||
|
||||
this.app.acl.registerSnippet({
|
||||
name: `pm.${this.name}.workflows`,
|
||||
actions: [
|
||||
|
@ -6,6 +6,18 @@ export default {
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'engine'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'collection'
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'expression'
|
||||
}
|
||||
],
|
||||
} as CollectionOptions;
|
||||
|
@ -28,6 +28,15 @@ export default {
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
name: 'tags'
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'read',
|
||||
defaultValue: 0
|
||||
},
|
||||
{
|
||||
type: 'expression',
|
||||
name: 'dexp'
|
||||
}
|
||||
]
|
||||
} as CollectionOptions;
|
||||
|
@ -9,6 +9,7 @@ describe('workflow > instructions > calculation', () => {
|
||||
let app: Application;
|
||||
let db: Database;
|
||||
let PostRepo;
|
||||
let CategoryRepo;
|
||||
let WorkflowModel;
|
||||
let workflow;
|
||||
|
||||
@ -18,6 +19,7 @@ describe('workflow > instructions > calculation', () => {
|
||||
db = app.db;
|
||||
WorkflowModel = db.getCollection('workflows').model;
|
||||
PostRepo = db.getCollection('posts').repository;
|
||||
CategoryRepo = db.getCollection('categories').repository;
|
||||
|
||||
workflow = await WorkflowModel.create({
|
||||
title: 'test workflow',
|
||||
@ -169,4 +171,81 @@ describe('workflow > instructions > calculation', () => {
|
||||
expect(job.result).toBe('at1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dynamic expression', () => {
|
||||
it('dynamic expression field in current table', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'calculation',
|
||||
config: {
|
||||
dynamic: '{{$context.data.category}}',
|
||||
scope: '{{$context.data}}',
|
||||
}
|
||||
});
|
||||
|
||||
const post = await PostRepo.create({
|
||||
values: {
|
||||
title: 't1',
|
||||
category: {
|
||||
engine: 'math.js',
|
||||
expression: '1 + {{read}}',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result).toBe(1);
|
||||
});
|
||||
|
||||
it('dynamic expression field in association table', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'query',
|
||||
config: {
|
||||
collection: 'categories',
|
||||
params: {
|
||||
filter: {
|
||||
$and: [
|
||||
{ id: '{{$context.data.categoryId}}' },
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const n2 = await workflow.createNode({
|
||||
type: 'calculation',
|
||||
config: {
|
||||
dynamic: `{{$jobsMapByNodeId.${n1.id}}}`,
|
||||
scope: '{{$context.data}}',
|
||||
},
|
||||
upstreamId: n1.id
|
||||
});
|
||||
|
||||
await n1.setDownstream(n2);
|
||||
|
||||
const category = await CategoryRepo.create({
|
||||
values: {
|
||||
title: 'c1',
|
||||
engine: 'math.js',
|
||||
expression: '1 + {{read}}',
|
||||
}
|
||||
});
|
||||
|
||||
const post = await PostRepo.create({
|
||||
values: {
|
||||
title: 't1',
|
||||
categoryId: category.id
|
||||
}
|
||||
});
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
const jobs = await execution.getJobs({ order: [['id', 'ASC']] });
|
||||
expect(jobs.length).toBe(2);
|
||||
expect(jobs[1].result).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
import { BaseFieldOptions, Field } from '@nocobase/database';
|
||||
|
||||
export interface ExpressionFieldOptions extends BaseFieldOptions {
|
||||
type: 'expression',
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class ExpressionField extends Field {
|
||||
get dataType() {
|
||||
return DataTypes.TEXT;
|
||||
}
|
||||
}
|
7
packages/plugins/workflow/src/server/fields/index.ts
Normal file
7
packages/plugins/workflow/src/server/fields/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { ExpressionField } from "./expression-field";
|
||||
|
||||
export default function (plugin) {
|
||||
plugin.db.registerFieldTypes({
|
||||
expression: ExpressionField
|
||||
});
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import parse from 'json-templates';
|
||||
|
||||
import evaluators, { Evaluator } from '@nocobase/evaluators';
|
||||
|
||||
import { Processor } from '..';
|
||||
@ -8,15 +10,24 @@ import { Instruction } from ".";
|
||||
|
||||
|
||||
interface CalculationConfig {
|
||||
dynamic?: boolean | string;
|
||||
engine?: string;
|
||||
expression?: string;
|
||||
}
|
||||
|
||||
export default {
|
||||
async run(node: FlowNodeModel, prevJob, processor: Processor) {
|
||||
const { engine = 'math.js', expression = '' } = <CalculationConfig>node.config || {};
|
||||
const { dynamic = false } = <CalculationConfig>node.config || {};
|
||||
let { engine = 'math.js', expression = '' } = node.config;
|
||||
let scope = processor.getScope();
|
||||
if (dynamic) {
|
||||
const parsed = parse(dynamic)(scope) ?? {};
|
||||
engine = parsed.engine;
|
||||
expression = parsed.expression;
|
||||
scope = parse(node.config.scope ?? '')(scope) ?? {};
|
||||
}
|
||||
|
||||
const evaluator = <Evaluator | undefined>evaluators.get(engine);
|
||||
const scope = processor.getScope();
|
||||
|
||||
try {
|
||||
const result = evaluator && expression
|
||||
|
Loading…
Reference in New Issue
Block a user