feat(plugin-workflow) config preload associations in triggers and nodes (#1548)
* feat(plugin-workflow): add preload associations for triggers and nodes * feat(plugin-workflow): add appends parameter to schedule trigger * fix(plugin-workflow): fix import * fix(plugin-workflow): fix component injection * test(plugin-workflow): add test case
This commit is contained in:
parent
bc5156d458
commit
ea4d4ac062
@ -78,7 +78,7 @@ export default observer(({ value, disabled, onChange }: any) => {
|
||||
}
|
||||
`}>
|
||||
<Variable.Input
|
||||
scope={['hasMany', 'belongsToMany'].includes(field.type) ? [] : scope}
|
||||
scope={scope}
|
||||
value={value[field.name]}
|
||||
onChange={(next) => {
|
||||
onChange({ ...value, [field.name]: next });
|
||||
|
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { observer, useForm } from '@formily/react';
|
||||
|
||||
import {
|
||||
useCollectionManager,
|
||||
useCompile
|
||||
} from '@nocobase/client';
|
||||
|
||||
|
||||
|
||||
export const FieldsSelect = observer((props: any) => {
|
||||
const { filter = () => true, ...others } = props;
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const { values } = useForm();
|
||||
const fields = getCollectionFields(values?.collection);
|
||||
|
||||
return (
|
||||
<Select
|
||||
className="full-width"
|
||||
{...others}
|
||||
options={fields
|
||||
.filter(filter)
|
||||
.map(field => ({
|
||||
label: compile(field.uiSchema?.title),
|
||||
value: field.name
|
||||
}))
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
@ -25,6 +25,8 @@ export default {
|
||||
'Changed fields': '发生变动的字段',
|
||||
'Triggered only if one of the selected fields changes. If unselected, it means that it will be triggered when any field changes. When record is added or deleted, any field is considered to have been changed.': '只有被选中的某个字段发生变动时才会触发。如果不选择,则表示任何字段变动时都会触发。新增或删除数据时,任意字段都被认为发生变动。',
|
||||
'Only triggers when match conditions': '满足以下条件才触发',
|
||||
'Preload associations': '预加载关联数据',
|
||||
'Only configured association field could be accessed in following nodes': '仅已配置的关系数据可以在后续节点中被访问',
|
||||
'Schedule event': '定时任务',
|
||||
'Trigger mode': '触发模式',
|
||||
'Based on certain date': '自定义时间',
|
||||
@ -134,6 +136,7 @@ export default {
|
||||
'Multiple records': '多条数据',
|
||||
'Please select collection first': '请先选择数据表',
|
||||
'Only update records matching conditions': '只更新满足条件的数据',
|
||||
'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.': '未被赋值的字段将被设置为默认值,没有默认值的设置为空值。',
|
||||
'Trigger in executed workflow cannot be modified': '已经执行过工作流的触发器不能被修改',
|
||||
'Node in executed workflow cannot be modified': '已经执行过工作流中的节点不能被修改',
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { SchemaInitializerItemOptions, useCollectionDataSource } from '@nocobase/client';
|
||||
|
||||
import { collection, values } from '../schemas/collection';
|
||||
import { appends, collection, values } from '../schemas/collection';
|
||||
import CollectionFieldset from '../components/CollectionFieldset';
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
||||
import { CollectionFieldInitializers } from '../components/CollectionFieldInitializers';
|
||||
import { useCollectionFieldOptions } from '../variable';
|
||||
import { FieldsSelect } from '../components/FieldsSelect';
|
||||
|
||||
|
||||
|
||||
@ -25,7 +26,13 @@ export default {
|
||||
// disabled: true
|
||||
// }
|
||||
// },
|
||||
'params.values': values
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
values,
|
||||
appends
|
||||
}
|
||||
}
|
||||
},
|
||||
view: {
|
||||
|
||||
@ -34,7 +41,8 @@ export default {
|
||||
useCollectionDataSource
|
||||
},
|
||||
components: {
|
||||
CollectionFieldset
|
||||
CollectionFieldset,
|
||||
FieldsSelect
|
||||
},
|
||||
getOptions(config, types) {
|
||||
return useCollectionFieldOptions({ collection: config.collection, types });
|
||||
|
@ -2,6 +2,8 @@ import { useCollectionDataSource } from '@nocobase/client';
|
||||
|
||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||
import { collection, filter } from '../schemas/collection';
|
||||
import { isValidFilter } from '../utils';
|
||||
import { NAMESPACE } from '../locale';
|
||||
|
||||
export default {
|
||||
title: '{{t("Delete record")}}',
|
||||
@ -11,10 +13,13 @@ export default {
|
||||
collection,
|
||||
params: {
|
||||
type: 'object',
|
||||
title: '',
|
||||
'x-decorator': 'FormItem',
|
||||
properties: {
|
||||
filter
|
||||
filter: {
|
||||
...filter,
|
||||
['x-validator'](value) {
|
||||
return isValidFilter(value) ? '' : `{{t("Please add at least one condition", { ns: "${NAMESPACE}" })}}`;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -426,8 +426,10 @@ export function NodeDefaultView(props) {
|
||||
.ant-picker,
|
||||
.ant-input-number,
|
||||
.ant-input-affix-wrapper{
|
||||
width: auto;
|
||||
min-width: 6em;
|
||||
&:not(.full-width){
|
||||
width: auto;
|
||||
min-width: 6em;
|
||||
}
|
||||
}
|
||||
`
|
||||
},
|
||||
|
@ -7,7 +7,6 @@ import { useWorkflowVariableOptions } from '../../variable';
|
||||
|
||||
export function AssigneesSelect({ multiple = false, value = [], onChange }) {
|
||||
const scope = useWorkflowVariableOptions();
|
||||
console.log(value);
|
||||
|
||||
return (
|
||||
<Variable.Input
|
||||
|
@ -543,7 +543,7 @@ export function SchemaConfig({ value, onChange }) {
|
||||
|
||||
const actionKeys = (Object.values(footer.properties.actions.properties ?? {}) as any[])
|
||||
.reduce((actions: number[], { ['x-action']: status }) => actions.concat(Number.parseInt(status, 10)), []);
|
||||
form.setValuesIn('config.actions', actionKeys);
|
||||
form.setValuesIn('actions', actionKeys);
|
||||
|
||||
onChange({
|
||||
collection,
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { SchemaInitializerItemOptions, useCollectionDataSource } from '@nocobase/client';
|
||||
|
||||
import { collection, filter } from '../schemas/collection';
|
||||
import { appends, collection, filter } from '../schemas/collection';
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
||||
import { CollectionFieldInitializers } from '../components/CollectionFieldInitializers';
|
||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||
import { useCollectionFieldOptions } from '../variable';
|
||||
import { FieldsSelect } from '../components/FieldsSelect';
|
||||
|
||||
|
||||
|
||||
@ -27,10 +28,9 @@ export default {
|
||||
// },
|
||||
params: {
|
||||
type: 'object',
|
||||
title: '',
|
||||
'x-decorator': 'FormItem',
|
||||
properties: {
|
||||
filter
|
||||
filter,
|
||||
appends
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -41,7 +41,8 @@ export default {
|
||||
useCollectionDataSource
|
||||
},
|
||||
components: {
|
||||
FilterDynamicComponent
|
||||
FilterDynamicComponent,
|
||||
FieldsSelect
|
||||
},
|
||||
getOptions(config, types) {
|
||||
return useCollectionFieldOptions({ collection: config.collection, types });
|
||||
|
@ -2,6 +2,8 @@ import { useCollectionDataSource } from '@nocobase/client';
|
||||
|
||||
import { FilterDynamicComponent } from '../components/FilterDynamicComponent';
|
||||
import CollectionFieldset from '../components/CollectionFieldset';
|
||||
|
||||
import { isValidFilter } from '../utils';
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { collection, filter, values } from '../schemas/collection';
|
||||
|
||||
@ -15,12 +17,13 @@ export default {
|
||||
collection,
|
||||
params: {
|
||||
type: 'object',
|
||||
title: '',
|
||||
'x-decorator': 'FormItem',
|
||||
properties: {
|
||||
filter: {
|
||||
...filter,
|
||||
title: `{{t("Only update records matching conditions", { ns: "${NAMESPACE}" })}}`,
|
||||
['x-validator'](value) {
|
||||
return isValidFilter(value) ? '' : `{{t("Please add at least one condition", { ns: "${NAMESPACE}" })}}`;
|
||||
},
|
||||
},
|
||||
values
|
||||
}
|
||||
|
@ -33,12 +33,6 @@ export const filter = {
|
||||
type: 'object',
|
||||
title: '{{t("Filter")}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {
|
||||
labelAlign: 'left',
|
||||
className: css`
|
||||
flex-direction: column;
|
||||
`
|
||||
},
|
||||
'x-component': 'Filter',
|
||||
'x-component-props': {
|
||||
useProps() {
|
||||
@ -55,3 +49,28 @@ export const filter = {
|
||||
dynamicComponent: 'FilterDynamicComponent'
|
||||
}
|
||||
};
|
||||
|
||||
export const appends = {
|
||||
type: 'array',
|
||||
title: `{{t("Preload associations", { ns: "${NAMESPACE}" })}}`,
|
||||
description: `{{t("Only configured association field could be accessed in following nodes", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'FieldsSelect',
|
||||
'x-component-props': {
|
||||
mode: 'multiple',
|
||||
placeholder: '{{t("Select Field")}}',
|
||||
filter(field) {
|
||||
return ['linkTo', 'belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type);
|
||||
}
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['collection'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$deps[0]}}',
|
||||
},
|
||||
}
|
||||
},
|
||||
]
|
||||
};
|
||||
|
@ -1,37 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { observer, useForm } from '@formily/react';
|
||||
|
||||
import {
|
||||
SchemaInitializerItemOptions,
|
||||
useCollectionDataSource,
|
||||
useCollectionManager,
|
||||
useCompile,
|
||||
} from '@nocobase/client';
|
||||
|
||||
import { collection, filter } from '../schemas/collection';
|
||||
import { appends, collection, filter } from '../schemas/collection';
|
||||
import { useCollectionFieldOptions } from '../variable';
|
||||
import { CollectionBlockInitializer } from '../components/CollectionBlockInitializer';
|
||||
import { CollectionFieldInitializers } from '../components/CollectionFieldInitializers';
|
||||
import { NAMESPACE, useWorkflowTranslation } from '../locale';
|
||||
|
||||
const FieldsSelect = observer((props: any) => {
|
||||
const { filter = () => true, ...others } = props;
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
const { values } = useForm();
|
||||
const fields = getCollectionFields(values?.collection);
|
||||
|
||||
return (
|
||||
<Select {...others}>
|
||||
{fields
|
||||
.filter(filter)
|
||||
.map(field => (
|
||||
<Select.Option key={field.name} value={field.name}>{compile(field.uiSchema?.title)}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
});
|
||||
import { FieldsSelect } from '../components/FieldsSelect';
|
||||
|
||||
const COLLECTION_TRIGGER_MODE = {
|
||||
CREATED: 1,
|
||||
@ -94,14 +72,6 @@ export default {
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
target: 'changed',
|
||||
fulfill: {
|
||||
state: {
|
||||
disabled: `{{!($self.value & ${COLLECTION_TRIGGER_MODE.UPDATED})}}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
changed: {
|
||||
@ -123,10 +93,10 @@ export default {
|
||||
},
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['collection'],
|
||||
dependencies: ['collection', 'mode'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$deps[0]}}',
|
||||
visible: `{{$deps[0] && $deps[1] & ${COLLECTION_TRIGGER_MODE.UPDATED}}}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -146,27 +116,20 @@ export default {
|
||||
},
|
||||
]
|
||||
},
|
||||
// appends: {
|
||||
// type: 'array',
|
||||
// title: `{{t("Prefetch fields", { ns: "${NAMESPACE}" })}}`,
|
||||
// description: `{{t("Triggered only if one of the selected fields changes. If unselected, it means that it will be triggered when any field changes. When record is added or deleted, any field is considered to have been changed.", { ns: "${NAMESPACE}" })}}`,
|
||||
// 'x-decorator': 'FormItem',
|
||||
// 'x-component': 'FieldsSelect',
|
||||
// 'x-component-props': {
|
||||
// mode: 'multiple',
|
||||
// placeholder: '{{t("Select Field")}}'
|
||||
// },
|
||||
// 'x-reactions': [
|
||||
// {
|
||||
// dependencies: ['collection'],
|
||||
// fulfill: {
|
||||
// state: {
|
||||
// visible: '{{!!$deps[0]}}',
|
||||
// },
|
||||
// }
|
||||
// },
|
||||
// ]
|
||||
// },
|
||||
appends: {
|
||||
...appends,
|
||||
'x-reactions': [
|
||||
...appends['x-reactions'],
|
||||
{
|
||||
dependencies: ['mode'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: `{{!($deps[0] & ${COLLECTION_TRIGGER_MODE.DELETED})}}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
scope: {
|
||||
useCollectionDataSource
|
||||
|
@ -164,7 +164,7 @@ export const TriggerConfig = () => {
|
||||
}
|
||||
const whiteSet = new Set(['workflow-node-meta', 'workflow-node-config-button', 'ant-input-disabled']);
|
||||
for (let el = ev.target; el && el !== ev.currentTarget; el = el.parentNode) {
|
||||
if (Array.from(el.classList).some((name: string) => whiteSet.has(name))) {
|
||||
if ((Array.from(el.classList) as string[]).some((name: string) => whiteSet.has(name))) {
|
||||
setEditingConfig(true);
|
||||
ev.stopPropagation();
|
||||
return;
|
||||
@ -234,7 +234,7 @@ export const TriggerConfig = () => {
|
||||
'x-component': 'fieldset',
|
||||
'x-component-props': {
|
||||
className: css`
|
||||
.ant-select{
|
||||
.ant-select:not(.full-width){
|
||||
width: auto;
|
||||
min-width: 6em;
|
||||
}
|
||||
@ -242,28 +242,30 @@ export const TriggerConfig = () => {
|
||||
},
|
||||
properties: fieldset
|
||||
},
|
||||
actions: executed
|
||||
? null
|
||||
: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{t("Cancel")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ cm.useCancelAction }}',
|
||||
actions: {
|
||||
...(executed
|
||||
? {}
|
||||
: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Drawer.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
title: '{{t("Cancel")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ cm.useCancelAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
submit: {
|
||||
title: '{{t("Submit")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: useUpdateConfigAction
|
||||
submit: {
|
||||
title: '{{t("Submit")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: useUpdateConfigAction
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ const ModeFieldsets = {
|
||||
[SCHEDULE_MODE.STATIC]: {
|
||||
startsOn: {
|
||||
type: 'datetime',
|
||||
name: 'startsOn',
|
||||
title: `{{t("Starts on", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker',
|
||||
@ -27,7 +26,6 @@ const ModeFieldsets = {
|
||||
},
|
||||
repeat: {
|
||||
type: 'string',
|
||||
name: 'repeat',
|
||||
title: `{{t("Repeat mode", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'RepeatField',
|
||||
@ -52,7 +50,6 @@ const ModeFieldsets = {
|
||||
},
|
||||
endsOn: {
|
||||
type: 'datetime',
|
||||
name: 'endsOn',
|
||||
title: `{{t("Ends on", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'DatePicker',
|
||||
@ -62,7 +59,6 @@ const ModeFieldsets = {
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
name: 'limit',
|
||||
title: `{{t("Repeat limit", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
@ -107,7 +103,6 @@ const ModeFieldsets = {
|
||||
},
|
||||
repeat: {
|
||||
type: 'string',
|
||||
name: 'repeat',
|
||||
title: `{{t("Repeat mode", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'RepeatField',
|
||||
@ -138,7 +133,6 @@ const ModeFieldsets = {
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
name: 'limit',
|
||||
title: `{{t("Repeat limit", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'InputNumber',
|
||||
|
@ -6,6 +6,8 @@ import { NAMESPACE, useWorkflowTranslation } from '../../locale';
|
||||
import { CollectionFieldInitializers } from '../../components/CollectionFieldInitializers';
|
||||
import { CollectionBlockInitializer } from '../../components/CollectionBlockInitializer';
|
||||
import { useCollectionFieldOptions } from '../../variable';
|
||||
import { appends } from '../../schemas/collection';
|
||||
import { FieldsSelect } from '../../components/FieldsSelect';
|
||||
|
||||
|
||||
|
||||
@ -18,13 +20,27 @@ export default {
|
||||
'x-component': 'ScheduleConfig',
|
||||
'x-component-props': {
|
||||
}
|
||||
}
|
||||
},
|
||||
appends: {
|
||||
...appends,
|
||||
'x-reactions': [
|
||||
{
|
||||
dependencies: ['mode', 'collection'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: `{{$deps[0] === ${SCHEDULE_MODE.COLLECTION_FIELD} && $deps[1]}}`,
|
||||
},
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
scope: {
|
||||
useCollectionDataSource
|
||||
},
|
||||
components: {
|
||||
ScheduleConfig
|
||||
ScheduleConfig,
|
||||
FieldsSelect
|
||||
},
|
||||
getOptions(config, types) {
|
||||
const { t } = useWorkflowTranslation();
|
||||
|
@ -11,3 +11,26 @@ export function linkNodes(nodes): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidFilter(condition) {
|
||||
const group = condition.$and || condition.$or;
|
||||
if (!group) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return group.some(item => {
|
||||
if (item.$and || item.$or) {
|
||||
return isValidFilter(item);
|
||||
}
|
||||
const [name] = Object.keys(item);
|
||||
if (!name || !item[name]) {
|
||||
return false;
|
||||
}
|
||||
const [op] = Object.keys(item[name]);
|
||||
if (!op || typeof item[name][op] === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
@ -5,7 +5,12 @@ import { useFlowContext } from "./FlowContext";
|
||||
import { triggers } from "./triggers";
|
||||
import { NAMESPACE } from "./locale";
|
||||
|
||||
export type VariableOption = { key: string, value: string; label: string; children?: VariableOption[] };
|
||||
export type VariableOption = {
|
||||
key: string;
|
||||
value: string;
|
||||
label: string;
|
||||
children?: VariableOption[]
|
||||
};
|
||||
|
||||
const VariableTypes = [
|
||||
{
|
||||
@ -97,17 +102,26 @@ export function useCollectionFieldOptions(props) {
|
||||
const { fields, collection, types } = props;
|
||||
const compile = useCompile();
|
||||
const { getCollectionFields } = useCollectionManager();
|
||||
return filterTypedFields((fields ?? getCollectionFields(collection)), types)
|
||||
.filter(field => field.interface && (!field.target || field.type === 'belongsTo'))
|
||||
.map(field => field.type === 'belongsTo'
|
||||
? {
|
||||
label: `${compile(field.uiSchema?.title || field.name)} ID`,
|
||||
key: field.foreignKey,
|
||||
value: field.foreignKey,
|
||||
}
|
||||
: {
|
||||
const result = filterTypedFields((fields ?? getCollectionFields(collection)), types)
|
||||
.map(field => ({
|
||||
label: compile(field.uiSchema?.title || field.name),
|
||||
key: field.name,
|
||||
value: field.name,
|
||||
});
|
||||
children: ['linkTo', 'belongsTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.type)
|
||||
? getCollectionFields(field.target)?.filter(subField => subField.interface && (!subField.target || subField.type === 'belongsTo'))
|
||||
.map(subField => subField.type === 'belongsTo'
|
||||
? {
|
||||
label: `${compile(subField.uiSchema?.title || subField.name)} ID`,
|
||||
key: subField.foreignKey,
|
||||
value: subField.foreignKey,
|
||||
}
|
||||
: {
|
||||
label: compile(subField.uiSchema?.title || subField.name),
|
||||
key: subField.name,
|
||||
value: subField.name,
|
||||
})
|
||||
: null
|
||||
}));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
import { CollectionOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
name: 'categories',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
}
|
||||
],
|
||||
} as CollectionOptions;
|
@ -11,6 +11,10 @@ export default {
|
||||
type: 'integer',
|
||||
name: 'status',
|
||||
defaultValue: 0
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'replies',
|
||||
}
|
||||
],
|
||||
} as CollectionOptions;
|
||||
|
@ -17,6 +17,10 @@ export default {
|
||||
name: 'read',
|
||||
defaultValue: 0
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'category'
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'comments'
|
||||
|
@ -0,0 +1,9 @@
|
||||
export default {
|
||||
name: 'replies',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'content',
|
||||
}
|
||||
]
|
||||
}
|
@ -8,6 +8,7 @@ describe('workflow > instructions > create', () => {
|
||||
let app: Application;
|
||||
let db: Database;
|
||||
let PostRepo;
|
||||
let ReplyRepo;
|
||||
let WorkflowModel;
|
||||
let workflow;
|
||||
|
||||
@ -17,6 +18,7 @@ describe('workflow > instructions > create', () => {
|
||||
db = app.db;
|
||||
WorkflowModel = db.getCollection('workflows').model;
|
||||
PostRepo = db.getCollection('posts').repository;
|
||||
ReplyRepo = db.getCollection('replies').repository;
|
||||
|
||||
workflow = await WorkflowModel.create({
|
||||
title: 'test workflow',
|
||||
@ -53,5 +55,77 @@ describe('workflow > instructions > create', () => {
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result.postId).toBe(post.id);
|
||||
});
|
||||
|
||||
it('params.values with hasMany', async () => {
|
||||
const replies = await ReplyRepo.create({ values: [{}, {}] });
|
||||
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'create',
|
||||
config: {
|
||||
collection: 'comments',
|
||||
params: {
|
||||
values: {
|
||||
replies: replies.map(item => item.id)
|
||||
},
|
||||
appends: ['replies']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result.replies.length).toBe(2);
|
||||
});
|
||||
|
||||
it('params.appends: belongsTo', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'create',
|
||||
config: {
|
||||
collection: 'comments',
|
||||
params: {
|
||||
values: {
|
||||
postId: '{{$context.data.id}}'
|
||||
},
|
||||
appends: ['post'],
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result.post.id).toBe(post.id);
|
||||
});
|
||||
|
||||
it('params.appends: belongsToMany', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'create',
|
||||
config: {
|
||||
collection: 'tags',
|
||||
params: {
|
||||
values: {
|
||||
posts: ['{{$context.data.id}}']
|
||||
},
|
||||
appends: ['posts'],
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result.posts.length).toBe(1);
|
||||
expect(job.result.posts[0].id).toBe(post.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ describe('workflow > instructions > query', () => {
|
||||
let PostCollection;
|
||||
let PostRepo;
|
||||
let TagModel;
|
||||
let CommentRepo;
|
||||
let WorkflowModel;
|
||||
let workflow;
|
||||
|
||||
@ -20,6 +21,7 @@ describe('workflow > instructions > query', () => {
|
||||
WorkflowModel = db.getCollection('workflows').model;
|
||||
PostCollection = db.getCollection('posts');
|
||||
PostRepo = PostCollection.repository;
|
||||
CommentRepo = db.getCollection('comments').repository;
|
||||
TagModel = db.getCollection('tags').model;
|
||||
|
||||
workflow = await WorkflowModel.create({
|
||||
@ -160,7 +162,7 @@ describe('workflow > instructions > query', () => {
|
||||
});
|
||||
|
||||
const tag = await TagModel.create({ name: 'tag1' });
|
||||
const post = await PostCollection.repository.create({
|
||||
const post = await PostRepo.create({
|
||||
values: { title: 't1', tags: [tag.id] }
|
||||
});
|
||||
|
||||
@ -171,7 +173,31 @@ describe('workflow > instructions > query', () => {
|
||||
expect(job.result.id).toBe(tag.id);
|
||||
});
|
||||
|
||||
it('params.appends: with associations', async () => {
|
||||
it('params.appends: hasMany', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'query',
|
||||
config: {
|
||||
collection: 'posts',
|
||||
params: {
|
||||
appends: ['comments']
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const comment = await CommentRepo.create({});
|
||||
const post = await PostRepo.create({
|
||||
values: { title: 't1', comments: [comment.id] }
|
||||
});
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result.comments.length).toBe(1);
|
||||
expect(job.result.comments[0].id).toBe(comment.id);
|
||||
});
|
||||
|
||||
it('params.appends: belongsToMany', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'query',
|
||||
config: {
|
||||
@ -183,7 +209,7 @@ describe('workflow > instructions > query', () => {
|
||||
});
|
||||
|
||||
const tag = await TagModel.create({ name: 'tag1' });
|
||||
const post = await PostCollection.repository.create({
|
||||
const post = await PostRepo.create({
|
||||
values: { title: 't1', tags: [tag.id] }
|
||||
});
|
||||
|
||||
|
@ -8,7 +8,10 @@ import { EXECUTION_STATUS } from '../../constants';
|
||||
describe('workflow > triggers > collection', () => {
|
||||
let app: Application;
|
||||
let db: Database;
|
||||
let CategoryRepo;
|
||||
let PostRepo;
|
||||
let CommentRepo;
|
||||
let TagRepo;
|
||||
let WorkflowModel;
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -16,7 +19,10 @@ describe('workflow > triggers > collection', () => {
|
||||
|
||||
db = app.db;
|
||||
WorkflowModel = db.getCollection('workflows').model;
|
||||
CategoryRepo = db.getCollection('categories').repository;
|
||||
PostRepo = db.getCollection('posts').repository;
|
||||
CommentRepo = db.getCollection('comments').repository;
|
||||
TagRepo = db.getCollection('tags').repository;
|
||||
});
|
||||
|
||||
afterEach(() => db.close());
|
||||
@ -111,4 +117,133 @@ describe('workflow > triggers > collection', () => {
|
||||
expect(executions.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('config.appends', () => {
|
||||
it('non-appended association could not be accessed', async () => {
|
||||
const workflow = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {
|
||||
mode: 1,
|
||||
collection: 'posts',
|
||||
}
|
||||
});
|
||||
|
||||
await workflow.createNode({
|
||||
type: 'echo'
|
||||
});
|
||||
|
||||
const category = await CategoryRepo.create({ values: { title: 'c1' } });
|
||||
|
||||
const post = await PostRepo.create({
|
||||
values: {
|
||||
title: 't1',
|
||||
categoryId: category.id
|
||||
}
|
||||
});
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result.data.category).toBeUndefined();
|
||||
});
|
||||
|
||||
it('appends association could be accessed', async () => {
|
||||
const workflow = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {
|
||||
mode: 1,
|
||||
collection: 'posts',
|
||||
appends: ['category']
|
||||
}
|
||||
});
|
||||
|
||||
await workflow.createNode({
|
||||
type: 'echo'
|
||||
});
|
||||
|
||||
const category = await CategoryRepo.create({ values: { title: 'c1' } });
|
||||
|
||||
const post = await PostRepo.create({
|
||||
values: {
|
||||
title: 't1',
|
||||
categoryId: category.id
|
||||
}
|
||||
});
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result.data.category.title).toBe('c1');
|
||||
});
|
||||
|
||||
it('appends hasMany', async () => {
|
||||
const workflow = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {
|
||||
mode: 1,
|
||||
collection: 'posts',
|
||||
appends: ['comments']
|
||||
}
|
||||
});
|
||||
|
||||
await workflow.createNode({
|
||||
type: 'echo'
|
||||
});
|
||||
|
||||
const comments = await CommentRepo.create({ values: [{}] });
|
||||
|
||||
const post = await PostRepo.create({
|
||||
values: {
|
||||
title: 't1',
|
||||
comments: comments.map(item => item.id)
|
||||
}
|
||||
});
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result.data.comments.length).toBe(1);
|
||||
});
|
||||
|
||||
it('appends belongsToMany', async () => {
|
||||
const workflow = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {
|
||||
mode: 1,
|
||||
collection: 'posts',
|
||||
appends: ['tags']
|
||||
}
|
||||
});
|
||||
|
||||
await workflow.createNode({
|
||||
type: 'echo'
|
||||
});
|
||||
|
||||
const tags = await TagRepo.create({ values: [{}] });
|
||||
|
||||
const post = await PostRepo.create({
|
||||
values: {
|
||||
title: 't1',
|
||||
tags: tags.map(item => item.id)
|
||||
}
|
||||
});
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result.data.tags.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -8,6 +8,7 @@ describe.skip('workflow > triggers > schedule', () => {
|
||||
let app: Application;
|
||||
let db: Database;
|
||||
let PostRepo;
|
||||
let CategoryRepo;
|
||||
let WorkflowModel;
|
||||
let WorkflowRepo;
|
||||
|
||||
@ -19,6 +20,7 @@ describe.skip('workflow > triggers > schedule', () => {
|
||||
WorkflowModel = workflow.model;
|
||||
WorkflowRepo = workflow.repository;
|
||||
PostRepo = db.getCollection('posts').repository;
|
||||
CategoryRepo = db.getCollection('categories').repository;
|
||||
});
|
||||
|
||||
afterEach(() => app.destroy());
|
||||
@ -231,7 +233,6 @@ describe.skip('workflow > triggers > schedule', () => {
|
||||
await sleep((2.5 - now.getSeconds() % 2) * 1000 - now.getMilliseconds());
|
||||
const startTime = new Date();
|
||||
startTime.setMilliseconds(500);
|
||||
console.log(startTime);
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' }});
|
||||
|
||||
@ -340,5 +341,31 @@ describe.skip('workflow > triggers > schedule', () => {
|
||||
const d1 = Date.parse(executions[0].context.date);
|
||||
expect(d1 - 1500).toBe(startTime.getTime());
|
||||
});
|
||||
|
||||
it('appends', async () => {
|
||||
const category = await CategoryRepo.create({ values: { name: 'c1' } });
|
||||
|
||||
const workflow = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
type: 'schedule',
|
||||
config: {
|
||||
mode: 1,
|
||||
collection: 'posts',
|
||||
startsOn: {
|
||||
field: 'createdAt',
|
||||
offset: 2
|
||||
},
|
||||
appends: ['category']
|
||||
}
|
||||
});
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1', categoryId: category.id } });
|
||||
|
||||
await sleep(5000);
|
||||
|
||||
const executions = await workflow.getExecutions();
|
||||
expect(executions.length).toBe(1);
|
||||
expect(executions[0].context.data.category.id).toBe(category.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -5,12 +5,12 @@ export default {
|
||||
async run(node: FlowNodeModel, input, processor) {
|
||||
const {
|
||||
collection,
|
||||
params = {}
|
||||
params: { appends = [], ...params } = {}
|
||||
} = node.config;
|
||||
|
||||
const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
|
||||
const { repository, model } = (<typeof FlowNodeModel>node.constructor).database.getCollection(collection);
|
||||
const options = processor.getParsedValue(params);
|
||||
const result = await repo.create({
|
||||
const result = await repository.create({
|
||||
...options,
|
||||
context: {
|
||||
executionId: processor.execution.id
|
||||
@ -18,6 +18,18 @@ export default {
|
||||
transaction: processor.transaction
|
||||
});
|
||||
|
||||
if (appends.length) {
|
||||
const includeFields = appends.filter(field => !result.get(field) || !result[field]);
|
||||
const included = await model.findByPk(result[model.primaryKeyAttribute], {
|
||||
attributes: [model.primaryKeyAttribute],
|
||||
include: includeFields,
|
||||
transaction: processor.transaction
|
||||
});
|
||||
includeFields.forEach(field => {
|
||||
result.set(field, included!.get(field), { raw: true });
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
// NOTE: get() for non-proxied instance (#380)
|
||||
result: result.get(),
|
||||
|
@ -36,9 +36,10 @@ function getFieldRawName(collection: Collection, name: string) {
|
||||
|
||||
// async function, should return promise
|
||||
async function handler(this: CollectionTrigger, workflow: WorkflowModel, data: Model, options) {
|
||||
const { collection: collectionName, condition, changed } = workflow.config;
|
||||
const { collection: collectionName, condition, changed, mode, appends } = workflow.config;
|
||||
const collection = (<typeof Model>data.constructor).database.getCollection(collectionName);
|
||||
const { transaction, context } = options;
|
||||
const { repository, model } = collection;
|
||||
|
||||
// NOTE: if no configured fields changed, do not trigger
|
||||
if (changed
|
||||
@ -53,7 +54,6 @@ async function handler(this: CollectionTrigger, workflow: WorkflowModel, data: M
|
||||
if (condition && condition.$and?.length) {
|
||||
// TODO: change to map filter format to calculation format
|
||||
// const calculation = toCalculation(condition);
|
||||
const { repository, model } = collection;
|
||||
const count = await repository.count({
|
||||
filter: {
|
||||
$and: [
|
||||
@ -70,6 +70,18 @@ async function handler(this: CollectionTrigger, workflow: WorkflowModel, data: M
|
||||
}
|
||||
}
|
||||
|
||||
if (appends?.length && !(mode & MODE_BITMAP.DESTROY)) {
|
||||
const includeFields = appends.filter(field => !data.get(field) || !data[field]);
|
||||
const included = await model.findByPk(data[model.primaryKeyAttribute], {
|
||||
attributes: [model.primaryKeyAttribute],
|
||||
include: includeFields,
|
||||
transaction
|
||||
});
|
||||
includeFields.forEach(field => {
|
||||
data.set(field, included!.get(field), { raw: true });
|
||||
});
|
||||
}
|
||||
|
||||
this.plugin.trigger(workflow, { data: data.get() }, {
|
||||
context
|
||||
});
|
||||
|
@ -273,7 +273,7 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
|
||||
},
|
||||
|
||||
async trigger(workflow, now: Date) {
|
||||
const { startsOn, repeat, endsOn, collection } = workflow.config;
|
||||
const { startsOn, repeat, endsOn, collection, appends } = workflow.config;
|
||||
const timestamp = now.getTime();
|
||||
|
||||
const startTimestamp = getOnTimestampWithOffset(startsOn, now);
|
||||
@ -333,11 +333,12 @@ ScheduleModes.set(SCHEDULE_MODE.COLLECTION_FIELD, {
|
||||
});
|
||||
}
|
||||
|
||||
const { model } = this.plugin.app.db.getCollection(collection);
|
||||
const instances = await model.findAll({
|
||||
where: {
|
||||
[Op.and]: conditions
|
||||
}
|
||||
const repo = this.plugin.app.db.getRepository(collection);
|
||||
const instances = await repo.find({
|
||||
filter: {
|
||||
$and: conditions
|
||||
},
|
||||
appends
|
||||
});
|
||||
|
||||
instances.forEach(item => {
|
||||
|
Loading…
Reference in New Issue
Block a user