feat(plugin-workflow): aggregate (#1852)
* feat(plugin-workflow): add aggregate instruction * test(plugin-workflow): add test cases * fix(plugin-workflow): fix types * fix(plugin-workflow): fix double result type * test(plugin-workflow): fix test cases in mysql * refactor(plugin-workflow): consolidate variables api * fix(plugin-workflow): fix create node variable * fix(plugin-workflow): fix aggregate association name * fix(plugin-workflow): fix test cases * fix(plugin-workflow): fix aggregate node config for duplication * fix(plugin-workflow): fix variable api * fix(plugin-workflow): fix variable api caller * fix(plugin-workflow): fix job button style
This commit is contained in:
parent
770f53ec4e
commit
f4064767c6
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { parse } from '@nocobase/utils/client';
|
||||
import { SchemaInitializer } from '@nocobase/client';
|
||||
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
|
||||
export const ValueBlock: (() => JSX.Element) & {
|
||||
Initializer: (props) => JSX.Element;
|
||||
Result: (props) => JSX.Element;
|
||||
} = () => {
|
||||
return null;
|
||||
};
|
||||
|
||||
function Initializer({ node, resultTitle, insert, ...props }) {
|
||||
return (
|
||||
<SchemaInitializer.Item
|
||||
{...props}
|
||||
onClick={() => {
|
||||
insert({
|
||||
type: 'void',
|
||||
name: node.id,
|
||||
title: node.title,
|
||||
'x-component': 'CardItem',
|
||||
'x-component-props': {
|
||||
title: node.title ?? `#${node.id}`,
|
||||
},
|
||||
'x-designer': 'SimpleDesigner',
|
||||
properties: {
|
||||
result: {
|
||||
type: 'void',
|
||||
title: resultTitle,
|
||||
'x-component': 'ValueBlock.Result',
|
||||
'x-component-props': {
|
||||
// NOTE: as same format as other reference for migration of revision
|
||||
dataSource: `{{$jobsMapByNodeId.${node.id}}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Result({ dataSource }) {
|
||||
const field = useFieldSchema();
|
||||
const { execution } = useFlowContext();
|
||||
if (!execution) {
|
||||
return field.title;
|
||||
}
|
||||
const result = parse(dataSource)({
|
||||
$jobsMapByNodeId: (execution.jobs ?? []).reduce((map, job) => Object.assign(map, { [job.nodeId]: job.result }), {}),
|
||||
});
|
||||
|
||||
return (
|
||||
<pre
|
||||
className={css`
|
||||
margin: 0;
|
||||
`}
|
||||
>
|
||||
{JSON.stringify(result, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
ValueBlock.Initializer = Initializer;
|
||||
ValueBlock.Result = Result;
|
@ -160,6 +160,16 @@ export default {
|
||||
'Please add at least one condition': '请添加至少一个条件',
|
||||
'Fields that are not assigned a value will be set to the default value, and those that do not have a default value are set to null.':
|
||||
'未被赋值的字段将被设置为默认值,没有默认值的设置为空值。',
|
||||
|
||||
Aggregate: '聚合查询',
|
||||
'Aggregator function': '聚合函数',
|
||||
'Target type': '目标类型',
|
||||
'Data of collection': '数据表数据',
|
||||
'Data of associated collection': '关联数据表数据',
|
||||
'Field to aggregate': '聚合字段',
|
||||
Distinct: '去重',
|
||||
'Query result': '查询结果',
|
||||
|
||||
'Trigger in executed workflow cannot be modified': '已经执行过工作流的触发器不能被修改',
|
||||
'Node in executed workflow cannot be modified': '已经执行过工作流中的节点不能被修改',
|
||||
'Can not delete': '无法删除',
|
||||
|
322
packages/plugins/workflow/src/client/nodes/aggregate.tsx
Normal file
322
packages/plugins/workflow/src/client/nodes/aggregate.tsx
Normal file
@ -0,0 +1,322 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Cascader } from 'antd';
|
||||
import { useForm } from '@formily/react';
|
||||
|
||||
import {
|
||||
SchemaInitializerItemOptions,
|
||||
useCollectionDataSource,
|
||||
useCompile,
|
||||
SchemaComponentContext,
|
||||
useCollectionManager,
|
||||
} from '@nocobase/client';
|
||||
|
||||
import { collection, filter } from '../schemas/collection';
|
||||
import { NAMESPACE, lang } from '../locale';
|
||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||
import { BaseTypeSets, nodesOptions, triggerOptions, useWorkflowVariableOptions } from '../variable';
|
||||
import { FieldsSelect } from '../components/FieldsSelect';
|
||||
import { ValueBlock } from '../components/ValueBlock';
|
||||
import { useNodeContext } from '.';
|
||||
|
||||
function matchToManyField(field, depth): boolean {
|
||||
return ['hasMany', 'belongsToMany'].includes(field.type) && depth;
|
||||
}
|
||||
|
||||
function AssociatedConfig({ value, onChange, ...props }): JSX.Element {
|
||||
const { setValuesIn } = useForm();
|
||||
const compile = useCompile();
|
||||
const { getCollection } = useCollectionManager();
|
||||
const current = useNodeContext();
|
||||
const options = [nodesOptions, triggerOptions].map((item) => {
|
||||
const children = item.useOptions(current, { types: [matchToManyField] })?.filter(Boolean);
|
||||
return {
|
||||
label: compile(item.label),
|
||||
value: item.value,
|
||||
key: item.value,
|
||||
children: compile(children),
|
||||
disabled: children && !children.length,
|
||||
};
|
||||
});
|
||||
|
||||
const { associatedKey = '', name: fieldName } = value ?? {};
|
||||
let p = [];
|
||||
const matched = associatedKey.match(/^{{(.*)}}$/);
|
||||
if (matched) {
|
||||
p = [...matched[1].trim().split('.').slice(0, -1), fieldName];
|
||||
}
|
||||
|
||||
const onSelectChange = useCallback(
|
||||
(path, option) => {
|
||||
if (!path?.length) {
|
||||
setValuesIn('collection', null);
|
||||
onChange({});
|
||||
return;
|
||||
}
|
||||
|
||||
// const associationFieldName = path.pop();
|
||||
const { field } = option.pop();
|
||||
// need to get:
|
||||
// * source collection (from node.config)
|
||||
// * target collection (from field name)
|
||||
const { collectionName, target, name } = field;
|
||||
|
||||
const collection = getCollection(collectionName);
|
||||
const primaryKeyField = collection.fields.find((f) => f.primaryKey);
|
||||
|
||||
setValuesIn('collection', target);
|
||||
|
||||
onChange({
|
||||
name,
|
||||
// primary key data path
|
||||
associatedKey: `{{${path.slice(0, -1).join('.')}.${primaryKeyField.name}}}`,
|
||||
// data associated collection name
|
||||
associatedCollection: collectionName,
|
||||
});
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
return <Cascader {...props} value={p} options={options} onChange={onSelectChange} />;
|
||||
}
|
||||
|
||||
// based on collection:
|
||||
// { collection, field }
|
||||
|
||||
// based on data associated collection
|
||||
// { key: '{{$context.data.id}}', collection: "collection.association", field }
|
||||
// select data based
|
||||
|
||||
export default {
|
||||
title: `{{t("Aggregate", { ns: "${NAMESPACE}" })}}`,
|
||||
type: 'aggregate',
|
||||
group: 'collection',
|
||||
fieldset: {
|
||||
aggregator: {
|
||||
type: 'string',
|
||||
title: `{{t("Aggregator function", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
enum: [
|
||||
{ label: 'COUNT', value: 'count' },
|
||||
{ label: 'SUM', value: 'sum' },
|
||||
{ label: 'AVG', value: 'avg' },
|
||||
{ label: 'MIN', value: 'min' },
|
||||
{ label: 'MAX', value: 'max' },
|
||||
],
|
||||
required: true,
|
||||
default: 'count',
|
||||
},
|
||||
associated: {
|
||||
type: 'boolean',
|
||||
title: `{{t("Target type", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Radio.Group',
|
||||
enum: [
|
||||
{ label: `{{t("Data of collection", { ns: "${NAMESPACE}" })}}`, value: false },
|
||||
{ label: `{{t("Data of associated collection", { ns: "${NAMESPACE}" })}}`, value: true },
|
||||
],
|
||||
required: true,
|
||||
default: false,
|
||||
'x-reactions': [
|
||||
{
|
||||
target: 'collection',
|
||||
effects: ['onFieldValueChange'],
|
||||
fulfill: {
|
||||
state: {
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'association',
|
||||
effects: ['onFieldValueChange'],
|
||||
fulfill: {
|
||||
state: {
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
collectionField: {
|
||||
type: 'void',
|
||||
'x-decorator': 'SchemaComponentContext.Provider',
|
||||
'x-decorator-props': {
|
||||
value: { designable: false },
|
||||
},
|
||||
'x-component': 'Grid',
|
||||
properties: {
|
||||
row: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Row',
|
||||
properties: {
|
||||
target: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
collection: {
|
||||
...collection,
|
||||
title: `{{t("Data of collection", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-component-props': {
|
||||
...collection['x-component-props'],
|
||||
className: 'full-width',
|
||||
},
|
||||
'x-reactions': [
|
||||
...collection['x-reactions'],
|
||||
{
|
||||
dependencies: ['associated'],
|
||||
fulfill: {
|
||||
state: {
|
||||
display: '{{$deps[0] ? "hidden" : "visible"}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'params.field',
|
||||
effects: ['onFieldValueChange'],
|
||||
fulfill: {
|
||||
state: {
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'params.filter',
|
||||
effects: ['onFieldValueChange'],
|
||||
fulfill: {
|
||||
state: {
|
||||
value: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
association: {
|
||||
type: 'object',
|
||||
title: `{{t("Data of associated collection", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'AssociatedConfig',
|
||||
'x-component-props': {
|
||||
className: 'full-width',
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['associated'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$deps[0]}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
field: {
|
||||
type: 'void',
|
||||
'x-component': 'Grid.Col',
|
||||
properties: {
|
||||
'params.field': {
|
||||
type: 'string',
|
||||
title: `{{t("Field to aggregate", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'FieldsSelect',
|
||||
'x-component-props': {
|
||||
filter(field) {
|
||||
return (
|
||||
!field.hidden &&
|
||||
field.interface &&
|
||||
!['belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type)
|
||||
);
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['collection'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$deps[0]}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
distinct: {
|
||||
type: 'boolean',
|
||||
title: `{{t("Distinct", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Checkbox',
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['collection', 'aggregator'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$deps[0] && ["count"].includes($deps[1])}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
filter: {
|
||||
...filter,
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['collection'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$deps[0]}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
view: {},
|
||||
scope: {
|
||||
useCollectionDataSource,
|
||||
},
|
||||
components: {
|
||||
SchemaComponentContext,
|
||||
FilterDynamicComponent,
|
||||
FieldsSelect,
|
||||
ValueBlock,
|
||||
AssociatedConfig,
|
||||
},
|
||||
useVariables(current, { types }) {
|
||||
if (
|
||||
types &&
|
||||
!types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type)))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return [
|
||||
// { key: '', value: '', label: lang('Calculation result') }
|
||||
];
|
||||
},
|
||||
useInitializers(node): SchemaInitializerItemOptions | null {
|
||||
if (!node.config.collection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'item',
|
||||
title: node.title ?? `#${node.id}`,
|
||||
component: ValueBlock.Initializer,
|
||||
node,
|
||||
resultTitle: lang('Query result'),
|
||||
};
|
||||
},
|
||||
};
|
@ -11,9 +11,10 @@ import { RadioWithTooltip } from '../components/RadioWithTooltip';
|
||||
import { renderEngineReference } from '../components/renderEngineReference';
|
||||
import { NAMESPACE, lang } from '../locale';
|
||||
import { BaseTypeSets, useWorkflowVariableOptions } from '../variable';
|
||||
import { ValueBlock } from '../components/ValueBlock';
|
||||
|
||||
function matchDynamicExpressionCollectionField(field): boolean {
|
||||
const { getCollectionFields, getCollection } = useCollectionManager();
|
||||
function useDynamicExpressionCollectionFieldMatcher(field): boolean {
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
if (field.type !== 'belongsTo') {
|
||||
return false;
|
||||
}
|
||||
@ -24,7 +25,7 @@ function matchDynamicExpressionCollectionField(field): boolean {
|
||||
|
||||
const DynamicConfig = ({ value, onChange }) => {
|
||||
const { t } = useTranslation();
|
||||
const scope = useWorkflowVariableOptions([matchDynamicExpressionCollectionField]);
|
||||
const scope = useWorkflowVariableOptions({ types: [useDynamicExpressionCollectionFieldMatcher] });
|
||||
|
||||
return (
|
||||
<FormLayout layout="vertical">
|
||||
@ -49,7 +50,7 @@ const DynamicConfig = ({ value, onChange }) => {
|
||||
};
|
||||
|
||||
function useWorkflowVariableEntityOptions() {
|
||||
return useWorkflowVariableOptions([{ type: 'reference', options: { collection: '*', entity: true } }]);
|
||||
return useWorkflowVariableOptions({ types: [{ type: 'reference', options: { collection: '*', entity: true } }] });
|
||||
}
|
||||
|
||||
export default {
|
||||
@ -170,7 +171,8 @@ export default {
|
||||
RadioWithTooltip,
|
||||
DynamicConfig,
|
||||
},
|
||||
useVariables(current, types) {
|
||||
useVariables(current, options) {
|
||||
const { types } = options ?? {};
|
||||
if (
|
||||
types &&
|
||||
!types.some((type) => type in BaseTypeSets || Object.values(BaseTypeSets).some((set) => set.has(type)))
|
||||
@ -185,38 +187,9 @@ export default {
|
||||
return {
|
||||
type: 'item',
|
||||
title: node.title ?? `#${node.id}`,
|
||||
component: CalculationInitializer,
|
||||
component: ValueBlock.Initializer,
|
||||
node,
|
||||
resultTitle: lang('Calculation result'),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function CalculationInitializer({ node, insert, ...props }) {
|
||||
return (
|
||||
<SchemaInitializer.Item
|
||||
{...props}
|
||||
onClick={() => {
|
||||
insert({
|
||||
type: 'void',
|
||||
name: node.id,
|
||||
title: node.title,
|
||||
'x-component': 'CardItem',
|
||||
'x-component-props': {
|
||||
title: node.title ?? `#${node.id}`,
|
||||
},
|
||||
'x-designer': 'SimpleDesigner',
|
||||
properties: {
|
||||
result: {
|
||||
type: 'void',
|
||||
'x-component': 'CalculationResult',
|
||||
'x-component-props': {
|
||||
// NOTE: as same format as other reference for migration of revision
|
||||
dataSource: `{{$jobsMapByNodeId.${node.id}}}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -139,7 +139,7 @@ function getGroupCalculators(group) {
|
||||
return Array.from(calculators.getEntities()).filter(([key, value]) => value.group === group);
|
||||
}
|
||||
|
||||
export function Calculation({ calculator, operands = [], onChange }) {
|
||||
function Calculation({ calculator, operands = [], onChange }) {
|
||||
const compile = useCompile();
|
||||
const options = useWorkflowVariableOptions();
|
||||
return (
|
||||
|
@ -40,12 +40,14 @@ export default {
|
||||
CollectionFieldset,
|
||||
FieldsSelect,
|
||||
},
|
||||
useVariables({ config }, types) {
|
||||
return useCollectionFieldOptions({
|
||||
collection: config.collection,
|
||||
types,
|
||||
depth: config.params?.appends?.length ? 1 : 0,
|
||||
useVariables({ config }, options) {
|
||||
const result = useCollectionFieldOptions({
|
||||
collection: config?.collection,
|
||||
...options,
|
||||
depth: options?.depth ?? config?.params?.appends?.length ? 1 : 0,
|
||||
});
|
||||
|
||||
return result?.length ? result : null;
|
||||
},
|
||||
useInitializers(node): SchemaInitializerItemOptions | null {
|
||||
if (!node.config.collection) {
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
useResourceActionContext,
|
||||
} from '@nocobase/client';
|
||||
|
||||
import { nodeBlockClass, nodeCardClass, nodeClass, nodeJobButtonClass, nodeMetaClass, nodeTitleClass } from '../style';
|
||||
import { nodeBlockClass, nodeCardClass, nodeClass, nodeJobButtonClass, nodeMetaClass } from '../style';
|
||||
import { AddButton } from '../AddButton';
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
|
||||
@ -33,6 +33,8 @@ import query from './query';
|
||||
import create from './create';
|
||||
import update from './update';
|
||||
import destroy from './destroy';
|
||||
import aggregate from './aggregate';
|
||||
|
||||
import { JobStatusOptionsMap } from '../constants';
|
||||
import { NAMESPACE, lang } from '../locale';
|
||||
import request from './request';
|
||||
@ -49,8 +51,8 @@ export interface Instruction {
|
||||
components?: { [key: string]: any };
|
||||
render?(props): React.ReactNode;
|
||||
endding?: boolean;
|
||||
useVariables?(node, types?): VariableOptions;
|
||||
useScopeVariables?(node, types?): VariableOptions;
|
||||
useVariables?(node, options?): VariableOptions;
|
||||
useScopeVariables?(node, options?): VariableOptions;
|
||||
useInitializers?(node): SchemaInitializerItemOptions | null;
|
||||
initializers?: { [key: string]: any };
|
||||
}
|
||||
@ -69,6 +71,8 @@ instructions.register('query', query);
|
||||
instructions.register('create', create);
|
||||
instructions.register('update', update);
|
||||
instructions.register('destroy', destroy);
|
||||
instructions.register('aggregate', aggregate);
|
||||
|
||||
instructions.register('request', request);
|
||||
|
||||
function useUpdateAction() {
|
||||
@ -234,7 +238,7 @@ function InnerJobButton({ job, ...props }) {
|
||||
const { icon, color } = JobStatusOptionsMap[job.status];
|
||||
|
||||
return (
|
||||
<Button {...props} shape="circle" className={cx(nodeJobButtonClass, 'workflow-node-job-button')}>
|
||||
<Button {...props} shape="circle" className={nodeJobButtonClass}>
|
||||
<Tag color={color}>{icon}</Tag>
|
||||
</Button>
|
||||
);
|
||||
@ -252,7 +256,6 @@ export function JobButton() {
|
||||
<span
|
||||
className={cx(
|
||||
nodeJobButtonClass,
|
||||
'workflow-node-job-button',
|
||||
css`
|
||||
border: 2px solid #d9d9d9;
|
||||
border-radius: 50%;
|
||||
@ -287,7 +290,7 @@ export function JobButton() {
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span className={cx(nodeJobButtonClass, 'workflow-node-job-button')}>
|
||||
<span className={nodeJobButtonClass}>
|
||||
<Tag color={color}>{icon}</Tag>
|
||||
</span>
|
||||
<time>{str2moment(job.updatedAt).format('YYYY-MM-DD HH:mm:ss')}</time>
|
||||
|
@ -8,7 +8,7 @@ import { useCompile } from '@nocobase/client';
|
||||
import { NodeDefaultView } from '.';
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
import { lang, NAMESPACE } from '../locale';
|
||||
import { useWorkflowVariableOptions, VariableOption, VariableTypes } from '../variable';
|
||||
import { useWorkflowVariableOptions, VariableOption, nodesOptions, triggerOptions } from '../variable';
|
||||
import { addButtonClass, branchBlockClass, branchClass, nodeSubtreeClass } from '../style';
|
||||
import { Branch } from '../Branch';
|
||||
|
||||
@ -137,18 +137,16 @@ export default {
|
||||
.split('.')
|
||||
.map((path) => path.trim());
|
||||
|
||||
const options = VariableTypes.filter((item) => ['$context', '$jobsMapByNodeId'].includes(item.value)).map(
|
||||
(item: any) => {
|
||||
const opts = typeof item.useOptions === 'function' ? item.useOptions(node, types).filter(Boolean) : null;
|
||||
return {
|
||||
label: compile(item.title),
|
||||
value: item.value,
|
||||
key: item.value,
|
||||
children: compile(opts),
|
||||
disabled: opts && !opts.length,
|
||||
};
|
||||
},
|
||||
);
|
||||
const options = [nodesOptions, triggerOptions].map((item: any) => {
|
||||
const opts = typeof item.useOptions === 'function' ? item.useOptions(node, { types }).filter(Boolean) : null;
|
||||
return {
|
||||
label: compile(item.title),
|
||||
value: item.value,
|
||||
key: item.value,
|
||||
children: compile(opts),
|
||||
disabled: opts && !opts.length,
|
||||
};
|
||||
});
|
||||
|
||||
targetOption.children = findOption(options, paths);
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { Variable } from '@nocobase/client';
|
||||
import { useWorkflowVariableOptions } from '../../variable';
|
||||
|
||||
export function AssigneesSelect({ multiple = false, value = [], onChange }) {
|
||||
const scope = useWorkflowVariableOptions([{ type: 'reference', options: { collection: 'users' } }]);
|
||||
const scope = useWorkflowVariableOptions({ types: [{ type: 'reference', options: { collection: 'users' } }] });
|
||||
|
||||
return (
|
||||
<Variable.Input
|
||||
|
@ -86,7 +86,7 @@ export default {
|
||||
ModeConfig,
|
||||
AssigneesSelect,
|
||||
},
|
||||
useVariables({ config }, types) {
|
||||
useVariables({ config }, { types }) {
|
||||
const formKeys = Object.keys(config.forms ?? {});
|
||||
if (!formKeys.length) {
|
||||
return null;
|
||||
|
@ -42,12 +42,14 @@ export default {
|
||||
FilterDynamicComponent,
|
||||
FieldsSelect,
|
||||
},
|
||||
useVariables({ config }, types) {
|
||||
return useCollectionFieldOptions({
|
||||
collection: config.collection,
|
||||
types,
|
||||
depth: config.params?.appends?.length ? 1 : 0,
|
||||
useVariables({ config }, options) {
|
||||
const result = useCollectionFieldOptions({
|
||||
collection: config?.collection,
|
||||
...options,
|
||||
depth: options?.depth ?? config?.params?.appends?.length ? 1 : 0,
|
||||
});
|
||||
|
||||
return result?.length ? result : null;
|
||||
},
|
||||
useInitializers(node): SchemaInitializerItemOptions | null {
|
||||
if (!node.config.collection) {
|
||||
|
@ -206,12 +206,6 @@ export const nodeCardClass = css`
|
||||
}
|
||||
}
|
||||
|
||||
> .workflow-node-job-button {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.ant-input {
|
||||
font-weight: bold;
|
||||
|
||||
@ -246,6 +240,9 @@ export const nodeCardClass = css`
|
||||
|
||||
export const nodeJobButtonClass = css`
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 1.25em;
|
||||
right: 1.25em;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
min-width: 1.25rem;
|
||||
|
@ -134,7 +134,7 @@ export default {
|
||||
components: {
|
||||
FieldsSelect,
|
||||
},
|
||||
getOptions(config, types) {
|
||||
useVariables(config, options) {
|
||||
const { t } = useWorkflowTranslation();
|
||||
const rootFields = [
|
||||
{
|
||||
@ -147,8 +147,12 @@ export default {
|
||||
},
|
||||
},
|
||||
];
|
||||
const options = useCollectionFieldOptions({ fields: rootFields, types, depth: config.appends?.length ? 2 : 1 });
|
||||
return options;
|
||||
const result = useCollectionFieldOptions({
|
||||
...options,
|
||||
fields: rootFields,
|
||||
depth: options?.depth ?? (config.appends?.length ? 2 : 1),
|
||||
});
|
||||
return result;
|
||||
},
|
||||
useInitializers(config): SchemaInitializerItemOptions | null {
|
||||
if (!config.collection) {
|
||||
|
@ -53,7 +53,7 @@ export interface Trigger {
|
||||
title: string;
|
||||
type: string;
|
||||
// group: string;
|
||||
getOptions?(config: any, types: any[]): VariableOptions;
|
||||
useVariables?(config: any, options?): VariableOptions;
|
||||
fieldset: { [key: string]: ISchema };
|
||||
view?: ISchema;
|
||||
scope?: { [key: string]: any };
|
||||
|
@ -39,15 +39,15 @@ export default {
|
||||
ScheduleConfig,
|
||||
FieldsSelect,
|
||||
},
|
||||
getOptions(config, types) {
|
||||
useVariables(config, { types }) {
|
||||
const { t } = useWorkflowTranslation();
|
||||
const options: any[] = [];
|
||||
if (!types || types.includes('date')) {
|
||||
options.push({ key: 'date', value: 'date', label: t('Trigger time') });
|
||||
}
|
||||
if (config.mode === SCHEDULE_MODE.COLLECTION_FIELD) {
|
||||
const fieldOptions = useCollectionFieldOptions({ collection: config.collection });
|
||||
|
||||
const fieldOptions = useCollectionFieldOptions({ collection: config.collection });
|
||||
if (config.mode === SCHEDULE_MODE.COLLECTION_FIELD) {
|
||||
if (fieldOptions.length) {
|
||||
options.push({
|
||||
key: 'data',
|
||||
|
@ -13,76 +13,77 @@ export type VariableOption = {
|
||||
|
||||
export type VariableOptions = VariableOption[] | null;
|
||||
|
||||
export const VariableTypes = [
|
||||
{
|
||||
title: `{{t("Scope variables", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$scopes',
|
||||
useOptions(current, types) {
|
||||
const scopes = useUpstreamScopes(current);
|
||||
const options: VariableOption[] = [];
|
||||
scopes.forEach((node) => {
|
||||
const instruction = instructions.get(node.type);
|
||||
const subOptions = instruction.useScopeVariables?.(node, types);
|
||||
if (subOptions) {
|
||||
options.push({
|
||||
key: node.id.toString(),
|
||||
value: node.id.toString(),
|
||||
label: node.title ?? `#${node.id}`,
|
||||
children: subOptions,
|
||||
});
|
||||
}
|
||||
});
|
||||
return options;
|
||||
},
|
||||
export const nodesOptions = {
|
||||
label: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$jobsMapByNodeId',
|
||||
useOptions(current, options) {
|
||||
const upstreams = useAvailableUpstreams(current);
|
||||
const result: VariableOption[] = [];
|
||||
upstreams.forEach((node) => {
|
||||
const instruction = instructions.get(node.type);
|
||||
const subOptions = instruction.useVariables?.(node, options);
|
||||
if (subOptions) {
|
||||
result.push({
|
||||
key: node.id.toString(),
|
||||
value: node.id.toString(),
|
||||
label: node.title ?? `#${node.id}`,
|
||||
children: subOptions,
|
||||
});
|
||||
}
|
||||
});
|
||||
return result;
|
||||
},
|
||||
{
|
||||
title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$jobsMapByNodeId',
|
||||
useOptions(current, types) {
|
||||
const upstreams = useAvailableUpstreams(current);
|
||||
const options: VariableOption[] = [];
|
||||
upstreams.forEach((node) => {
|
||||
const instruction = instructions.get(node.type);
|
||||
const subOptions = instruction.useVariables?.(node, types);
|
||||
if (subOptions) {
|
||||
options.push({
|
||||
key: node.id.toString(),
|
||||
value: node.id.toString(),
|
||||
label: node.title ?? `#${node.id}`,
|
||||
children: subOptions,
|
||||
});
|
||||
}
|
||||
});
|
||||
return options;
|
||||
},
|
||||
};
|
||||
|
||||
export const triggerOptions = {
|
||||
label: `{{t("Trigger variables", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$context',
|
||||
useOptions(current, options) {
|
||||
const { workflow } = useFlowContext();
|
||||
const trigger = triggers.get(workflow.type);
|
||||
return trigger?.useVariables?.(workflow.config, options) ?? null;
|
||||
},
|
||||
{
|
||||
title: `{{t("Trigger variables", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$context',
|
||||
useOptions(current, types) {
|
||||
const { workflow } = useFlowContext();
|
||||
const trigger = triggers.get(workflow.type);
|
||||
return trigger?.getOptions?.(workflow.config, types) ?? null;
|
||||
},
|
||||
};
|
||||
|
||||
export const scopeOptions = {
|
||||
label: `{{t("Scope variables", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$scopes',
|
||||
useOptions(current, options) {
|
||||
const scopes = useUpstreamScopes(current);
|
||||
const result: VariableOption[] = [];
|
||||
scopes.forEach((node) => {
|
||||
const instruction = instructions.get(node.type);
|
||||
const subOptions = instruction.useScopeVariables?.(node, options);
|
||||
if (subOptions) {
|
||||
result.push({
|
||||
key: node.id.toString(),
|
||||
value: node.id.toString(),
|
||||
label: node.title ?? `#${node.id}`,
|
||||
children: subOptions,
|
||||
});
|
||||
}
|
||||
});
|
||||
return result;
|
||||
},
|
||||
{
|
||||
title: `{{t("System variables", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$system',
|
||||
useOptions(current, types) {
|
||||
return [
|
||||
...(!types || types.includes('date')
|
||||
? [
|
||||
{
|
||||
key: 'now',
|
||||
value: 'now',
|
||||
label: `{{t("System time")}}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export const systemOptions = {
|
||||
label: `{{t("System variables", { ns: "${NAMESPACE}" })}}`,
|
||||
value: '$system',
|
||||
useOptions(current, { types }) {
|
||||
return [
|
||||
...(!types || types.includes('date')
|
||||
? [
|
||||
{
|
||||
key: 'now',
|
||||
value: 'now',
|
||||
label: `{{t("System time")}}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const BaseTypeSets = {
|
||||
boolean: new Set(['checkbox']),
|
||||
@ -107,7 +108,7 @@ export const BaseTypeSets = {
|
||||
// { type: 'reference', options: { collection: 'attachments', multiple: false } }
|
||||
// { type: 'reference', options: { collection: 'myExpressions', entity: false } }
|
||||
|
||||
function matchFieldType(field, type): boolean {
|
||||
function matchFieldType(field, type, depth): boolean {
|
||||
const inputType = typeof type;
|
||||
if (inputType === 'string') {
|
||||
return BaseTypeSets[type]?.has(field.interface);
|
||||
@ -129,7 +130,7 @@ function matchFieldType(field, type): boolean {
|
||||
}
|
||||
|
||||
if (inputType === 'function') {
|
||||
return type(field);
|
||||
return type(field, depth);
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -151,34 +152,31 @@ export function filterTypedFields(fields, types, depth = 1) {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return types.some((type) => matchFieldType(field, type));
|
||||
return types.some((type) => matchFieldType(field, type, depth));
|
||||
});
|
||||
}
|
||||
|
||||
export function useWorkflowVariableOptions(types?) {
|
||||
export function useWorkflowVariableOptions(options = {}) {
|
||||
const compile = useCompile();
|
||||
const current = useNodeContext();
|
||||
const options = VariableTypes.map((item: any) => {
|
||||
const opts = typeof item.useOptions === 'function' ? item.useOptions(current, types).filter(Boolean) : null;
|
||||
const result = [scopeOptions, nodesOptions, triggerOptions, systemOptions].map((item: any) => {
|
||||
const opts = typeof item.useOptions === 'function' ? item.useOptions(current, options).filter(Boolean) : null;
|
||||
return {
|
||||
label: compile(item.title),
|
||||
label: compile(item.label),
|
||||
value: item.value,
|
||||
key: item.value,
|
||||
children: compile(opts),
|
||||
disabled: opts && !opts.length,
|
||||
};
|
||||
});
|
||||
return options;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function useNormalizedFields(collectionName) {
|
||||
const compile = useCompile();
|
||||
const { getCollection } = useCollectionManager();
|
||||
const collection = getCollection(collectionName);
|
||||
if (!collection) {
|
||||
return [];
|
||||
}
|
||||
const { fields } = collection;
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const fields = getCollectionFields(collectionName);
|
||||
const foreignKeyFields: any[] = [];
|
||||
const otherFields: any[] = [];
|
||||
fields.forEach((field) => {
|
||||
@ -249,6 +247,7 @@ export function useCollectionFieldOptions(options): VariableOption[] {
|
||||
isAssociationField(field) && depth
|
||||
? useCollectionFieldOptions({ collection: field.target, types, depth: depth - 1 })
|
||||
: null,
|
||||
field,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,294 @@
|
||||
import { Application } from '@nocobase/server';
|
||||
import Database from '@nocobase/database';
|
||||
import { getApp, sleep } from '..';
|
||||
|
||||
describe('workflow > instructions > aggregate', () => {
|
||||
let app: Application;
|
||||
let db: Database;
|
||||
let PostRepo;
|
||||
let CommentRepo;
|
||||
let TagRepo;
|
||||
let WorkflowModel;
|
||||
let workflow;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await getApp();
|
||||
|
||||
db = app.db;
|
||||
WorkflowModel = db.getCollection('workflows').model;
|
||||
PostRepo = db.getCollection('posts').repository;
|
||||
CommentRepo = db.getCollection('comments').repository;
|
||||
TagRepo = db.getCollection('tags').repository;
|
||||
|
||||
workflow = await WorkflowModel.create({
|
||||
title: 'test workflow',
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {
|
||||
mode: 1,
|
||||
collection: 'posts',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => db.close());
|
||||
|
||||
describe('based on collection', () => {
|
||||
it('count', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'aggregate',
|
||||
config: {
|
||||
aggregator: 'count',
|
||||
collection: 'posts',
|
||||
params: {
|
||||
field: 'id',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result).toBe(1);
|
||||
});
|
||||
|
||||
it('sum', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'aggregate',
|
||||
config: {
|
||||
aggregator: 'sum',
|
||||
collection: 'posts',
|
||||
params: {
|
||||
field: 'read',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const p1 = await PostRepo.create({ values: { title: 't1', read: 1 } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [e1] = await workflow.getExecutions();
|
||||
const [j1] = await e1.getJobs();
|
||||
expect(j1.result).toBe(1);
|
||||
|
||||
const p2 = await PostRepo.create({ values: { title: 't2', read: 2 } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] });
|
||||
const [j2] = await e2.getJobs();
|
||||
expect(j2.result).toBe(3);
|
||||
});
|
||||
|
||||
it('avg', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'aggregate',
|
||||
config: {
|
||||
aggregator: 'avg',
|
||||
collection: 'posts',
|
||||
params: {
|
||||
field: 'read',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const p1 = await PostRepo.create({ values: { title: 't1', read: 1 } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [e1] = await workflow.getExecutions();
|
||||
const [j1] = await e1.getJobs();
|
||||
expect(j1.result).toBe(1);
|
||||
|
||||
const p2 = await PostRepo.create({ values: { title: 't2', read: 2 } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] });
|
||||
const [j2] = await e2.getJobs();
|
||||
expect(j2.result).toBe(1.5);
|
||||
});
|
||||
|
||||
it('min', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'aggregate',
|
||||
config: {
|
||||
aggregator: 'min',
|
||||
collection: 'posts',
|
||||
params: {
|
||||
field: 'read',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const p1 = await PostRepo.create({ values: { title: 't1', read: 1 } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [e1] = await workflow.getExecutions();
|
||||
const [j1] = await e1.getJobs();
|
||||
expect(j1.result).toBe(1);
|
||||
|
||||
const p2 = await PostRepo.create({ values: { title: 't2', read: 2 } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] });
|
||||
const [j2] = await e2.getJobs();
|
||||
expect(j2.result).toBe(1);
|
||||
});
|
||||
|
||||
it('max', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'aggregate',
|
||||
config: {
|
||||
aggregator: 'max',
|
||||
collection: 'posts',
|
||||
params: {
|
||||
field: 'read',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const p1 = await PostRepo.create({ values: { title: 't1', read: 1 } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [e1] = await workflow.getExecutions();
|
||||
const [j1] = await e1.getJobs();
|
||||
expect(j1.result).toBe(1);
|
||||
|
||||
const p2 = await PostRepo.create({ values: { title: 't2', read: 2 } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [e2] = await workflow.getExecutions({ order: [['id', 'desc']] });
|
||||
const [j2] = await e2.getJobs();
|
||||
expect(j2.result).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('based on data associated collection', () => {
|
||||
it('count', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'aggregate',
|
||||
config: {
|
||||
aggregator: 'count',
|
||||
collection: 'comments',
|
||||
associated: true,
|
||||
association: {
|
||||
name: 'comments',
|
||||
associatedKey: '{{$context.data.id}}',
|
||||
associatedCollection: 'posts',
|
||||
},
|
||||
params: {
|
||||
field: 'id',
|
||||
},
|
||||
},
|
||||
});
|
||||
const n2 = await workflow.createNode({
|
||||
upstreamId: n1.id,
|
||||
type: 'aggregate',
|
||||
config: {
|
||||
aggregator: 'count',
|
||||
collection: 'comments',
|
||||
associated: true,
|
||||
association: {
|
||||
name: 'comments',
|
||||
associatedKey: '{{$context.data.id}}',
|
||||
associatedCollection: 'posts',
|
||||
},
|
||||
params: {
|
||||
field: 'id',
|
||||
filter: {
|
||||
$and: [{ status: 1 }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await n1.setDownstream(n2);
|
||||
|
||||
await CommentRepo.create({ values: [{}, {}] });
|
||||
|
||||
const p1 = await PostRepo.create({ values: { title: 't1', comments: [{}, { status: 1 }] } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [e1] = await workflow.getExecutions();
|
||||
const [j1, j2] = await e1.getJobs({ order: [['id', 'ASC']] });
|
||||
expect(j1.result).toBe(2);
|
||||
expect(j2.result).toBe(1);
|
||||
});
|
||||
|
||||
it('sum', async () => {
|
||||
const PostModel = db.getCollection('posts').model;
|
||||
const p1 = await PostModel.create({ title: 't1', read: 1 });
|
||||
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'create',
|
||||
config: {
|
||||
collection: 'tags',
|
||||
params: {
|
||||
values: {
|
||||
posts: [p1.id, '{{$context.data.id}}'],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const n2 = await workflow.createNode({
|
||||
upstreamId: n1.id,
|
||||
type: 'aggregate',
|
||||
config: {
|
||||
aggregator: 'sum',
|
||||
collection: 'posts',
|
||||
associated: true,
|
||||
association: {
|
||||
name: 'posts',
|
||||
associatedKey: `{{$jobsMapByNodeId.${n1.id}.id}}`,
|
||||
associatedCollection: 'tags',
|
||||
},
|
||||
params: {
|
||||
field: 'read',
|
||||
},
|
||||
},
|
||||
});
|
||||
await n1.setDownstream(n2);
|
||||
const n3 = await workflow.createNode({
|
||||
upstreamId: n2.id,
|
||||
type: 'aggregate',
|
||||
config: {
|
||||
aggregator: 'sum',
|
||||
collection: 'posts',
|
||||
associated: true,
|
||||
association: {
|
||||
name: 'posts',
|
||||
associatedKey: `{{$jobsMapByNodeId.${n1.id}.id}}`,
|
||||
associatedCollection: 'tags',
|
||||
},
|
||||
params: {
|
||||
field: 'read',
|
||||
filter: {
|
||||
$and: [{ title: 't1' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await n2.setDownstream(n3);
|
||||
|
||||
await TagRepo.create({ values: [{}, {}] });
|
||||
|
||||
const p2 = await PostRepo.create({ values: { title: 't2', read: 2 } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [e1] = await workflow.getExecutions();
|
||||
const [j1, j2, j3] = await e1.getJobs({ order: [['id', 'ASC']] });
|
||||
expect(j2.result).toBe(3);
|
||||
expect(j3.result).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,43 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
import FlowNodeModel from '../models/FlowNode';
|
||||
import Processor from '../Processor';
|
||||
import { JOB_STATUS } from '../constants';
|
||||
import { BelongsToManyRepository, HasManyRepository } from '@nocobase/database';
|
||||
|
||||
const aggregators = {
|
||||
count: 'count',
|
||||
sum: 'sum',
|
||||
avg: 'avg',
|
||||
min: 'min',
|
||||
max: 'max',
|
||||
};
|
||||
|
||||
export default {
|
||||
async run(node: FlowNodeModel, input, processor: Processor) {
|
||||
const { aggregator, associated, collection, association = {}, params = {} } = node.config;
|
||||
const options = processor.getParsedValue(params);
|
||||
const { database } = <typeof FlowNodeModel>node.constructor;
|
||||
const repo = associated
|
||||
? database.getRepository<HasManyRepository | BelongsToManyRepository>(
|
||||
`${association?.associatedCollection}.${association.name}`,
|
||||
processor.getParsedValue(association?.associatedKey),
|
||||
)
|
||||
: database.getRepository(collection);
|
||||
|
||||
if (!options.dataType && aggregator === 'avg') {
|
||||
options.dataType = DataTypes.DOUBLE;
|
||||
}
|
||||
|
||||
const result = await repo.aggregate({
|
||||
...options,
|
||||
method: aggregators[aggregator],
|
||||
transaction: processor.transaction,
|
||||
});
|
||||
|
||||
return {
|
||||
result: options.dataType === DataTypes.DOUBLE ? Number(result) : result,
|
||||
status: JOB_STATUS.RESOLVED,
|
||||
};
|
||||
},
|
||||
};
|
@ -44,6 +44,7 @@ export default function <T extends Instruction>(plugin, more: { [key: string]: T
|
||||
'create',
|
||||
'update',
|
||||
'destroy',
|
||||
'aggregate',
|
||||
'request',
|
||||
].reduce(
|
||||
(result, key) =>
|
||||
|
Loading…
Reference in New Issue
Block a user