mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-02 03:02:19 +08:00
refactor(plugin-workflow): change api of manually execute (#5850)
* feat: add subflow node * feat: add execution event * feat: add force option * feat: avoid recurring call * feat: add stack to execution * feat: add cyclic call validator * feat: collection trigger add execution stack * fix: stack * fix: manual execute * refactor(plugin-workflow): adjust api and implementent * chore: revert locale back * feat: trigger field set add scope variables * feat: add changeOnSelect * feat: variables support ID reference * feat: change locale * refactor(plugin-workflow): support execute by passing filterByTk for a record * refactor(plugin-workflow): adjust props name * fix(plugin-workflow): fix execute variable context * fix(plugin-workflow): fix variables and locales * chore(plugin-workflow): remove demo code * fix(plugin-workflow): fix import * fix(plugin-workflow): fix build error --------- Co-authored-by: mytharcher <mytharcher@gmail.com>
This commit is contained in:
parent
84cfa82304
commit
be44388e90
@ -434,7 +434,7 @@ export function Input(props: VariableInputProps) {
|
|||||||
style={{ overflow: 'hidden' }}
|
style={{ overflow: 'hidden' }}
|
||||||
className={cx('ant-input', { 'ant-input-disabled': disabled }, hashId)}
|
className={cx('ant-input', { 'ant-input-disabled': disabled }, hashId)}
|
||||||
>
|
>
|
||||||
<Tag contentEditable={false} color="blue">
|
<Tag color="blue">
|
||||||
{variableText.map((item, index) => {
|
{variableText.map((item, index) => {
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={item}>
|
<React.Fragment key={item}>
|
||||||
|
@ -9,7 +9,13 @@
|
|||||||
|
|
||||||
import { useForm } from '@formily/react';
|
import { useForm } from '@formily/react';
|
||||||
|
|
||||||
import { SchemaInitializerItemType, parseCollectionName, useCollectionDataSource, useCompile } from '@nocobase/client';
|
import {
|
||||||
|
SchemaInitializerItemType,
|
||||||
|
parseCollectionName,
|
||||||
|
useCollectionDataSource,
|
||||||
|
useCompile,
|
||||||
|
RemoteSelect,
|
||||||
|
} from '@nocobase/client';
|
||||||
import {
|
import {
|
||||||
Trigger,
|
Trigger,
|
||||||
CollectionBlockInitializer,
|
CollectionBlockInitializer,
|
||||||
@ -19,8 +25,10 @@ import {
|
|||||||
RadioWithTooltip,
|
RadioWithTooltip,
|
||||||
useGetCollectionFields,
|
useGetCollectionFields,
|
||||||
TriggerCollectionRecordSelect,
|
TriggerCollectionRecordSelect,
|
||||||
|
WorkflowVariableWrapper,
|
||||||
} from '@nocobase/plugin-workflow/client';
|
} from '@nocobase/plugin-workflow/client';
|
||||||
import { NAMESPACE, useLang } from '../locale';
|
import { NAMESPACE, useLang } from '../locale';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
const COLLECTION_TRIGGER_ACTION = {
|
const COLLECTION_TRIGGER_ACTION = {
|
||||||
CREATE: 'create',
|
CREATE: 'create',
|
||||||
@ -36,8 +44,8 @@ function useVariables(config, options) {
|
|||||||
const getMainCollectionFields = useGetCollectionFields();
|
const getMainCollectionFields = useGetCollectionFields();
|
||||||
|
|
||||||
const langTriggerData = useLang('Trigger data');
|
const langTriggerData = useLang('Trigger data');
|
||||||
const langUserSubmittedForm = useLang('User submitted action');
|
const langUserSubmittedForm = useLang('User acted');
|
||||||
const langRoleSubmittedForm = useLang('Role of user submitted action');
|
const langRoleSubmittedForm = useLang('Role of user acted');
|
||||||
const result = [
|
const result = [
|
||||||
...getCollectionFieldOptions({
|
...getCollectionFieldOptions({
|
||||||
// depth,
|
// depth,
|
||||||
@ -198,8 +206,8 @@ export default class extends Trigger {
|
|||||||
triggerFieldset = {
|
triggerFieldset = {
|
||||||
data: {
|
data: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Trigger data", { ns: "workflow" })}}`,
|
||||||
description: `{{t("Choose a record of the collection to trigger.", { ns: "workflow" })}}`,
|
description: `{{t("Choose a record or primary key of a record in the collection to trigger.", { ns: "workflow" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'TriggerCollectionRecordSelect',
|
'x-component': 'TriggerCollectionRecordSelect',
|
||||||
default: null,
|
default: null,
|
||||||
@ -207,37 +215,90 @@ export default class extends Trigger {
|
|||||||
},
|
},
|
||||||
userId: {
|
userId: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
title: `{{t("User submitted action", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("User acted", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'RemoteSelect',
|
'x-component': 'WorkflowVariableWrapper',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
fieldNames: {
|
nullable: false,
|
||||||
label: 'nickname',
|
changeOnSelect: true,
|
||||||
value: 'id',
|
variableOptions: {
|
||||||
|
types: [
|
||||||
|
(field) => {
|
||||||
|
if (field.isForeignKey || field.type === 'context') {
|
||||||
|
return field.target === 'users';
|
||||||
|
}
|
||||||
|
return field.collectionName === 'users' && field.name === 'id';
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
service: {
|
render(props) {
|
||||||
resource: 'users',
|
return (
|
||||||
|
<RemoteSelect
|
||||||
|
fieldNames={{ label: 'nickname', value: 'id' }}
|
||||||
|
service={{ resource: 'users' }}
|
||||||
|
manual={false}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
manual: false,
|
|
||||||
},
|
},
|
||||||
|
// properties: {
|
||||||
|
// remoteSelect: {
|
||||||
|
// 'x-component': 'RemoteSelect',
|
||||||
|
// 'x-component-props': {
|
||||||
|
// fieldNames: {
|
||||||
|
// label: 'nickname',
|
||||||
|
// value: 'id',
|
||||||
|
// },
|
||||||
|
// service: {
|
||||||
|
// resource: 'users',
|
||||||
|
// },
|
||||||
|
// manual: false,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
default: null,
|
default: null,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
roleName: {
|
roleName: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
title: `{{t("Role of user submitted action", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Role of user acted", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'RemoteSelect',
|
'x-component': 'WorkflowVariableWrapper',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
fieldNames: {
|
nullable: false,
|
||||||
label: 'title',
|
changeOnSelect: true,
|
||||||
value: 'name',
|
variableOptions: {
|
||||||
|
types: [
|
||||||
|
(field) => {
|
||||||
|
if (field.isForeignKey) {
|
||||||
|
return field.target === 'roles';
|
||||||
|
}
|
||||||
|
return field.collectionName === 'roles' && field.name === 'name';
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
service: {
|
render(props) {
|
||||||
resource: 'roles',
|
return (
|
||||||
|
<RemoteSelect
|
||||||
|
fieldNames={{ label: 'title', value: 'name' }}
|
||||||
|
service={{ resource: 'roles' }}
|
||||||
|
manual={false}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
manual: false,
|
|
||||||
},
|
},
|
||||||
|
// 'x-component-props': {
|
||||||
|
// fieldNames: {
|
||||||
|
// label: 'title',
|
||||||
|
// value: 'name',
|
||||||
|
// },
|
||||||
|
// service: {
|
||||||
|
// resource: 'roles',
|
||||||
|
// },
|
||||||
|
// manual: false,
|
||||||
|
// },
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -252,6 +313,7 @@ export default class extends Trigger {
|
|||||||
RadioWithTooltip,
|
RadioWithTooltip,
|
||||||
CheckboxGroupWithTooltip,
|
CheckboxGroupWithTooltip,
|
||||||
TriggerCollectionRecordSelect,
|
TriggerCollectionRecordSelect,
|
||||||
|
WorkflowVariableWrapper,
|
||||||
};
|
};
|
||||||
isActionTriggerable = (config, context) => {
|
isActionTriggerable = (config, context) => {
|
||||||
return !config.global && ['submit', 'customize:save', 'customize:update'].includes(context.buttonAction);
|
return !config.global && ['submit', 'customize:save', 'customize:update'].includes(context.buttonAction);
|
||||||
|
@ -12,6 +12,6 @@
|
|||||||
"Update record action": "更新记录操作",
|
"Update record action": "更新记录操作",
|
||||||
"Associations to use": "待使用的关系数据",
|
"Associations to use": "待使用的关系数据",
|
||||||
"Trigger data": "触发器数据",
|
"Trigger data": "触发器数据",
|
||||||
"User submitted action": "提交操作的用户",
|
"User acted": "操作者",
|
||||||
"Role of user submitted action": "提交操作用户的角色"
|
"Role of user acted": "操作者角色"
|
||||||
}
|
}
|
||||||
|
@ -177,7 +177,7 @@ export default class extends Trigger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const event of syncGroup) {
|
for (const event of syncGroup) {
|
||||||
await this.workflow.trigger(event[0], event[1], { httpContext: context });
|
await this.workflow.trigger(event[0], event[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of asyncGroup) {
|
for (const event of asyncGroup) {
|
||||||
@ -185,18 +185,27 @@ export default class extends Trigger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(workflow: WorkflowModel, context: Context, options: EventOptions) {
|
async execute(workflow: WorkflowModel, values, options: EventOptions) {
|
||||||
const { values } = context.action.params;
|
// const { values } = context.action.params;
|
||||||
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
|
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
|
||||||
const { collectionManager } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName);
|
const { collectionManager } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName);
|
||||||
const { filterTargetKey, repository } = collectionManager.getCollection(collectionName);
|
const { filterTargetKey, repository } = collectionManager.getCollection(collectionName);
|
||||||
const filterByTk = Array.isArray(filterTargetKey)
|
|
||||||
? pick(
|
let { data } = values;
|
||||||
values.data,
|
let filterByTk;
|
||||||
filterTargetKey.sort((a, b) => a.localeCompare(b)),
|
let loadNeeded = false;
|
||||||
)
|
if (data && typeof data === 'object') {
|
||||||
: values.data[filterTargetKey];
|
filterByTk = Array.isArray(filterTargetKey)
|
||||||
const UserRepo = context.app.db.getRepository('users');
|
? pick(
|
||||||
|
data,
|
||||||
|
filterTargetKey.sort((a, b) => a.localeCompare(b)),
|
||||||
|
)
|
||||||
|
: data[filterTargetKey];
|
||||||
|
} else {
|
||||||
|
filterByTk = data;
|
||||||
|
loadNeeded = true;
|
||||||
|
}
|
||||||
|
const UserRepo = this.workflow.app.db.getRepository('users');
|
||||||
const actor = await UserRepo.findOne({
|
const actor = await UserRepo.findOne({
|
||||||
filterByTk: values.userId,
|
filterByTk: values.userId,
|
||||||
appends: ['roles'],
|
appends: ['roles'],
|
||||||
@ -207,8 +216,7 @@ export default class extends Trigger {
|
|||||||
const { roles, ...user } = actor.desensitize().get();
|
const { roles, ...user } = actor.desensitize().get();
|
||||||
const roleName = values.roleName || roles?.[0]?.name;
|
const roleName = values.roleName || roles?.[0]?.name;
|
||||||
|
|
||||||
let { data } = values;
|
if (loadNeeded || workflow.config.appends?.length) {
|
||||||
if (workflow.config.appends?.length) {
|
|
||||||
data = await repository.findOne({
|
data = await repository.findOne({
|
||||||
filterByTk,
|
filterByTk,
|
||||||
appends: workflow.config.appends,
|
appends: workflow.config.appends,
|
||||||
@ -221,10 +229,7 @@ export default class extends Trigger {
|
|||||||
user,
|
user,
|
||||||
roleName,
|
roleName,
|
||||||
},
|
},
|
||||||
{
|
options,
|
||||||
...options,
|
|
||||||
httpContext: context,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import { RemoteSelect, Variable } from '@nocobase/client';
|
|||||||
import { useWorkflowVariableOptions } from '@nocobase/plugin-workflow/client';
|
import { useWorkflowVariableOptions } from '@nocobase/plugin-workflow/client';
|
||||||
|
|
||||||
function isUserKeyField(field) {
|
function isUserKeyField(field) {
|
||||||
if (field.isForeignKey) {
|
if (field.isForeignKey || field.type === 'context') {
|
||||||
return field.target === 'users';
|
return field.target === 'users';
|
||||||
}
|
}
|
||||||
return field.collectionName === 'users' && field.name === 'id';
|
return field.collectionName === 'users' && field.name === 'id';
|
||||||
|
@ -14,3 +14,9 @@ export const FlowContext = React.createContext<any>({});
|
|||||||
export function useFlowContext() {
|
export function useFlowContext() {
|
||||||
return useContext(FlowContext);
|
return useContext(FlowContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CurrentWorkflowContext = React.createContext<any>({});
|
||||||
|
|
||||||
|
export function useCurrentWorkflowContext() {
|
||||||
|
return useContext(CurrentWorkflowContext);
|
||||||
|
}
|
||||||
|
@ -12,6 +12,8 @@ import { Trans, useTranslation } from 'react-i18next';
|
|||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { Alert, App, Breadcrumb, Button, Dropdown, Result, Spin, Switch, Tag, Tooltip } from 'antd';
|
import { Alert, App, Breadcrumb, Button, Dropdown, Result, Spin, Switch, Tag, Tooltip } from 'antd';
|
||||||
import { DownOutlined, EllipsisOutlined, RightOutlined } from '@ant-design/icons';
|
import { DownOutlined, EllipsisOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
|
import { NoticeType } from 'antd/es/message/interface';
|
||||||
|
import { useField, useForm } from '@formily/react';
|
||||||
import {
|
import {
|
||||||
ActionContextProvider,
|
ActionContextProvider,
|
||||||
ResourceActionProvider,
|
ResourceActionProvider,
|
||||||
@ -30,10 +32,11 @@ import {
|
|||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import { dayjs } from '@nocobase/utils/client';
|
import { dayjs } from '@nocobase/utils/client';
|
||||||
|
|
||||||
|
import PluginWorkflowClient from '.';
|
||||||
import { CanvasContent } from './CanvasContent';
|
import { CanvasContent } from './CanvasContent';
|
||||||
import { ExecutionStatusColumn } from './components/ExecutionStatus';
|
import { ExecutionStatusColumn } from './components/ExecutionStatus';
|
||||||
import { ExecutionLink } from './ExecutionLink';
|
import { ExecutionLink } from './ExecutionLink';
|
||||||
import { FlowContext, useFlowContext } from './FlowContext';
|
import { CurrentWorkflowContext, FlowContext, useFlowContext } from './FlowContext';
|
||||||
import { lang, NAMESPACE } from './locale';
|
import { lang, NAMESPACE } from './locale';
|
||||||
import { executionSchema } from './schemas/executions';
|
import { executionSchema } from './schemas/executions';
|
||||||
import useStyles from './style';
|
import useStyles from './style';
|
||||||
@ -41,10 +44,8 @@ import { linkNodes, getWorkflowDetailPath } from './utils';
|
|||||||
import { Fieldset } from './components/Fieldset';
|
import { Fieldset } from './components/Fieldset';
|
||||||
import { useRefreshActionProps } from './hooks/useRefreshActionProps';
|
import { useRefreshActionProps } from './hooks/useRefreshActionProps';
|
||||||
import { useTrigger } from './triggers';
|
import { useTrigger } from './triggers';
|
||||||
import { useField, useForm } from '@formily/react';
|
|
||||||
import { ExecutionStatusOptionsMap } from './constants';
|
import { ExecutionStatusOptionsMap } from './constants';
|
||||||
import PluginWorkflowClient from '.';
|
import { HideVariableContext } from './variable';
|
||||||
import { NoticeType } from 'antd/es/message/interface';
|
|
||||||
|
|
||||||
function ExecutionResourceProvider({ request, filter = {}, ...others }) {
|
function ExecutionResourceProvider({ request, filter = {}, ...others }) {
|
||||||
const { workflow } = useFlowContext();
|
const { workflow } = useFlowContext();
|
||||||
@ -144,99 +145,103 @@ function ExecuteActionButton() {
|
|||||||
const trigger = useTrigger();
|
const trigger = useTrigger();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaComponent
|
<CurrentWorkflowContext.Provider value={workflow}>
|
||||||
components={{
|
<HideVariableContext.Provider value={true}>
|
||||||
Alert,
|
<SchemaComponent
|
||||||
Fieldset,
|
components={{
|
||||||
ActionDisabledProvider,
|
Alert,
|
||||||
...trigger.components,
|
Fieldset,
|
||||||
}}
|
ActionDisabledProvider,
|
||||||
scope={{
|
...trigger.components,
|
||||||
useCancelAction,
|
}}
|
||||||
useExecuteConfirmAction,
|
scope={{
|
||||||
}}
|
useCancelAction,
|
||||||
schema={{
|
useExecuteConfirmAction,
|
||||||
name: `trigger-modal-${workflow.type}-${workflow.id}`,
|
}}
|
||||||
type: 'void',
|
schema={{
|
||||||
'x-decorator': 'ActionDisabledProvider',
|
name: `trigger-modal-${workflow.type}-${workflow.id}`,
|
||||||
'x-component': 'Action',
|
|
||||||
'x-component-props': {
|
|
||||||
openSize: 'small',
|
|
||||||
},
|
|
||||||
title: `{{t('Execute manually', { ns: "${NAMESPACE}" })}}`,
|
|
||||||
properties: {
|
|
||||||
drawer: {
|
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-decorator': 'FormV2',
|
'x-decorator': 'ActionDisabledProvider',
|
||||||
'x-component': 'Action.Modal',
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
openSize: 'small',
|
||||||
|
},
|
||||||
title: `{{t('Execute manually', { ns: "${NAMESPACE}" })}}`,
|
title: `{{t('Execute manually', { ns: "${NAMESPACE}" })}}`,
|
||||||
properties: {
|
properties: {
|
||||||
...(Object.keys(trigger.triggerFieldset ?? {}).length
|
drawer: {
|
||||||
? {
|
|
||||||
alert: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Alert',
|
|
||||||
'x-component-props': {
|
|
||||||
message: `{{t('Trigger variables need to be filled for executing.', { ns: "${NAMESPACE}" })}}`,
|
|
||||||
className: css`
|
|
||||||
margin-bottom: 1em;
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
description: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'p',
|
|
||||||
'x-content': `{{t('This will perform all the actions configured in the workflow. Are you sure you want to continue?', { ns: "${NAMESPACE}" })}}`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
fieldset: {
|
|
||||||
type: 'void',
|
type: 'void',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormV2',
|
||||||
'x-component': 'Fieldset',
|
'x-component': 'Action.Modal',
|
||||||
title: `{{t('Trigger variables', { ns: "${NAMESPACE}" })}}`,
|
title: `{{t('Execute manually', { ns: "${NAMESPACE}" })}}`,
|
||||||
properties: trigger.triggerFieldset,
|
|
||||||
},
|
|
||||||
...(workflow.executed
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
autoRevision: {
|
|
||||||
type: 'boolean',
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Checkbox',
|
|
||||||
'x-content': `{{t('Automatically create a new version after execution', { ns: "${NAMESPACE}" })}}`,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
footer: {
|
|
||||||
type: 'void',
|
|
||||||
'x-component': 'Action.Modal.Footer',
|
|
||||||
properties: {
|
properties: {
|
||||||
cancel: {
|
...(Object.keys(trigger.triggerFieldset ?? {}).length
|
||||||
|
? {
|
||||||
|
alert: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'Alert',
|
||||||
|
'x-component-props': {
|
||||||
|
message: `{{t('Trigger variables need to be filled for executing.', { ns: "${NAMESPACE}" })}}`,
|
||||||
|
className: css`
|
||||||
|
margin-bottom: 1em;
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
description: {
|
||||||
|
type: 'void',
|
||||||
|
'x-component': 'p',
|
||||||
|
'x-content': `{{t('This will perform all the actions configured in the workflow. Are you sure you want to continue?', { ns: "${NAMESPACE}" })}}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
fieldset: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: `{{t('Cancel')}}`,
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'Action',
|
'x-component': 'Fieldset',
|
||||||
'x-component-props': {
|
title: `{{t('Trigger variables', { ns: "${NAMESPACE}" })}}`,
|
||||||
useAction: '{{useCancelAction}}',
|
properties: trigger.triggerFieldset,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
submit: {
|
...(workflow.executed
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
autoRevision: {
|
||||||
|
type: 'boolean',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Checkbox',
|
||||||
|
'x-content': `{{t('Automatically create a new version after execution', { ns: "${NAMESPACE}" })}}`,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
footer: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
title: `{{t('Confirm')}}`,
|
'x-component': 'Action.Modal.Footer',
|
||||||
'x-component': 'Action',
|
properties: {
|
||||||
'x-component-props': {
|
cancel: {
|
||||||
type: 'primary',
|
type: 'void',
|
||||||
useAction: '{{useExecuteConfirmAction}}',
|
title: `{{t('Cancel')}}`,
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
useAction: '{{useCancelAction}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
submit: {
|
||||||
|
type: 'void',
|
||||||
|
title: `{{t('Confirm')}}`,
|
||||||
|
'x-component': 'Action',
|
||||||
|
'x-component-props': {
|
||||||
|
type: 'primary',
|
||||||
|
useAction: '{{useExecuteConfirmAction}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}}
|
||||||
},
|
/>
|
||||||
}}
|
</HideVariableContext.Provider>
|
||||||
/>
|
</CurrentWorkflowContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,17 +11,17 @@ import React from 'react';
|
|||||||
|
|
||||||
import { parseCollectionName, RemoteSelect, useApp } from '@nocobase/client';
|
import { parseCollectionName, RemoteSelect, useApp } from '@nocobase/client';
|
||||||
|
|
||||||
import { useFlowContext } from '../FlowContext';
|
import { useCurrentWorkflowContext } from '../FlowContext';
|
||||||
|
import { WorkflowVariableWrapper } from '../variable';
|
||||||
|
|
||||||
export function TriggerCollectionRecordSelect(props) {
|
export function TriggerCollectionRecordSelect(props) {
|
||||||
const { workflow } = useFlowContext();
|
const workflow = useCurrentWorkflowContext();
|
||||||
const app = useApp();
|
const app = useApp();
|
||||||
|
|
||||||
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
|
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
|
||||||
const { collectionManager } = app.dataSourceManager.getDataSource(dataSourceName);
|
const { collectionManager } = app.dataSourceManager.getDataSource(dataSourceName);
|
||||||
const collection = collectionManager.getCollection(collectionName);
|
const collection = collectionManager.getCollection(collectionName);
|
||||||
|
const render = (props) => (
|
||||||
return (
|
|
||||||
<RemoteSelect
|
<RemoteSelect
|
||||||
objectValue
|
objectValue
|
||||||
dataSource={dataSourceName}
|
dataSource={dataSourceName}
|
||||||
@ -36,4 +36,13 @@ export function TriggerCollectionRecordSelect(props) {
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
return (
|
||||||
|
<WorkflowVariableWrapper
|
||||||
|
value={props.value}
|
||||||
|
onChange={props.onChange}
|
||||||
|
nullable={false}
|
||||||
|
changeOnSelect
|
||||||
|
render={render}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
/**
|
|
||||||
* This file is part of the NocoBase (R) project.
|
|
||||||
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
||||||
* Authors: NocoBase Team.
|
|
||||||
*
|
|
||||||
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Instruction } from '.';
|
|
||||||
import { NAMESPACE } from '../locale';
|
|
||||||
import { JOB_STATUS } from '../constants';
|
|
||||||
import { TriggerCollectionRecordSelect } from '../components/TriggerCollectionRecordSelect';
|
|
||||||
import { Fieldset } from '../components/Fieldset';
|
|
||||||
|
|
||||||
export default class extends Instruction {
|
|
||||||
title = `{{t("Call another workflow", { ns: "${NAMESPACE}" })}}`;
|
|
||||||
type = 'subflow';
|
|
||||||
group = 'control';
|
|
||||||
description = `{{t("Run another workflow and use its output as variables.", { ns: "${NAMESPACE}" })}}`;
|
|
||||||
fieldset = {
|
|
||||||
workflow: {
|
|
||||||
type: 'number',
|
|
||||||
title: `{{t("Workflow", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Select',
|
|
||||||
enum: [
|
|
||||||
{ label: `{{t("Post added", { ns: "${NAMESPACE}" })}}`, value: 1 },
|
|
||||||
{ label: `{{t("Some other action", { ns: "${NAMESPACE}" })}}`, value: 2 },
|
|
||||||
],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
context: {
|
|
||||||
type: 'object',
|
|
||||||
title: `{{t("Context", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'Fieldset',
|
|
||||||
properties: {
|
|
||||||
data: {
|
|
||||||
type: 'object',
|
|
||||||
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
description: `{{t("Choose a record of the collection to trigger.", { ns: "${NAMESPACE}" })}}`,
|
|
||||||
'x-decorator': 'FormItem',
|
|
||||||
'x-component': 'TriggerCollectionRecordSelect',
|
|
||||||
default: null,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'x-reactions': [
|
|
||||||
{
|
|
||||||
dependencies: ['workflow'],
|
|
||||||
fulfill: {
|
|
||||||
state: {
|
|
||||||
visible: '{{workflow === 1}}',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
components = {
|
|
||||||
TriggerCollectionRecordSelect,
|
|
||||||
Fieldset,
|
|
||||||
};
|
|
||||||
}
|
|
@ -197,7 +197,7 @@ export default class extends Trigger {
|
|||||||
data: {
|
data: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
||||||
description: `{{t("Choose a record of the collection to trigger.", { ns: "${NAMESPACE}" })}}`,
|
description: `{{t("Choose a record or primary key of a record in the collection to trigger.", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'TriggerCollectionRecordSelect',
|
'x-component': 'TriggerCollectionRecordSelect',
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -7,7 +7,9 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NAMESPACE } from '../../locale';
|
import React from 'react';
|
||||||
|
import { DatePicker } from 'antd';
|
||||||
|
import { NAMESPACE, lang } from '../../locale';
|
||||||
import { appends, collection } from '../../schemas/collection';
|
import { appends, collection } from '../../schemas/collection';
|
||||||
import { SCHEDULE_MODE } from './constants';
|
import { SCHEDULE_MODE } from './constants';
|
||||||
|
|
||||||
@ -73,13 +75,21 @@ export const ScheduleModes = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
title: `{{t('Execute on', { ns: "${NAMESPACE}" })}}`,
|
title: `{{t('Execute on', { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'DatePicker',
|
'x-component': 'WorkflowVariableWrapper',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
showTime: true,
|
// showTime: true,
|
||||||
placeholder: `{{t('Current time', { ns: "${NAMESPACE}" })}}`,
|
// placeholder: `{{t('Current time', { ns: "${NAMESPACE}" })}}`,
|
||||||
|
nullable: false,
|
||||||
|
changeOnSelect: true,
|
||||||
|
render(props) {
|
||||||
|
return <DatePicker showTime placeholder={lang('Current time')} {...props} />;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
validate(config) {
|
||||||
|
return Boolean(config.startsOn);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
[SCHEDULE_MODE.DATE_FIELD]: {
|
[SCHEDULE_MODE.DATE_FIELD]: {
|
||||||
fieldset: {
|
fieldset: {
|
||||||
@ -180,7 +190,7 @@ export const ScheduleModes = {
|
|||||||
data: {
|
data: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
||||||
description: `{{t("Choose a record of the collection to trigger.", { ns: "${NAMESPACE}" })}}`,
|
description: `{{t("Choose a record or an ID reference of the collection to trigger.", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'TriggerCollectionRecordSelect',
|
'x-component': 'TriggerCollectionRecordSelect',
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -9,14 +9,15 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { SchemaComponent } from '@nocobase/client';
|
import { SchemaComponent, usePlugin } from '@nocobase/client';
|
||||||
import { useFlowContext } from '../../FlowContext';
|
import WorkflowPlugin from '../..';
|
||||||
import { useTrigger } from '..';
|
import { useCurrentWorkflowContext } from '../../FlowContext';
|
||||||
import { ScheduleModes } from './ScheduleModes';
|
import { ScheduleModes } from './ScheduleModes';
|
||||||
|
|
||||||
export function TriggerScheduleConfig() {
|
export function TriggerScheduleConfig() {
|
||||||
const { workflow } = useFlowContext();
|
const workflow = useCurrentWorkflowContext();
|
||||||
const trigger = useTrigger();
|
const workflowPlugin = usePlugin(WorkflowPlugin);
|
||||||
|
const trigger = workflowPlugin.triggers.get(workflow.type);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
|
@ -17,6 +17,7 @@ import { ScheduleConfig } from './ScheduleConfig';
|
|||||||
import { SCHEDULE_MODE } from './constants';
|
import { SCHEDULE_MODE } from './constants';
|
||||||
import { TriggerScheduleConfig } from './TriggerScheduleConfig';
|
import { TriggerScheduleConfig } from './TriggerScheduleConfig';
|
||||||
import { ScheduleModes } from './ScheduleModes';
|
import { ScheduleModes } from './ScheduleModes';
|
||||||
|
import { WorkflowVariableWrapper } from '../../variable';
|
||||||
import { TriggerCollectionRecordSelect } from '../../components/TriggerCollectionRecordSelect';
|
import { TriggerCollectionRecordSelect } from '../../components/TriggerCollectionRecordSelect';
|
||||||
|
|
||||||
function useVariables(config, opts) {
|
function useVariables(config, opts) {
|
||||||
@ -89,6 +90,7 @@ export default class extends Trigger {
|
|||||||
ScheduleConfig,
|
ScheduleConfig,
|
||||||
TriggerScheduleConfig,
|
TriggerScheduleConfig,
|
||||||
TriggerCollectionRecordSelect,
|
TriggerCollectionRecordSelect,
|
||||||
|
WorkflowVariableWrapper,
|
||||||
};
|
};
|
||||||
useVariables = useVariables;
|
useVariables = useVariables;
|
||||||
useInitializers(config): SchemaInitializerItemType | null {
|
useInitializers(config): SchemaInitializerItemType | null {
|
||||||
|
@ -7,11 +7,11 @@
|
|||||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback } from 'react';
|
import React, { createContext, useCallback, useContext } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { uniqBy } from 'lodash';
|
import { uniqBy } from 'lodash';
|
||||||
|
|
||||||
import { Variable, parseCollectionName, useApp, useCompile, usePlugin } from '@nocobase/client';
|
import { Variable, parseCollectionName, useApp, useCompile, usePlugin, useVariableScope } from '@nocobase/client';
|
||||||
|
|
||||||
import { useFlowContext } from './FlowContext';
|
import { useFlowContext } from './FlowContext';
|
||||||
import { NAMESPACE, lang } from './locale';
|
import { NAMESPACE, lang } from './locale';
|
||||||
@ -120,6 +120,9 @@ export const systemOptions = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
export const BaseTypeSets = {
|
export const BaseTypeSets = {
|
||||||
boolean: new Set(['checkbox']),
|
boolean: new Set(['checkbox']),
|
||||||
number: new Set(['integer', 'number', 'percent']),
|
number: new Set(['integer', 'number', 'percent']),
|
||||||
@ -402,3 +405,34 @@ export function WorkflowVariableJSON({ variableOptions, ...props }): JSX.Element
|
|||||||
const scope = useWorkflowVariableOptions(variableOptions);
|
const scope = useWorkflowVariableOptions(variableOptions);
|
||||||
return <Variable.JSON scope={scope} {...props} />;
|
return <Variable.JSON scope={scope} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export function WorkflowVariableWrapper(props): JSX.Element {
|
||||||
|
const { render, variableOptions, changeOnSelect, nullable, ...others } = props;
|
||||||
|
const hideVariable = useHideVariable();
|
||||||
|
const scope = useWorkflowVariableOptions(variableOptions);
|
||||||
|
|
||||||
|
if (!hideVariable && scope?.length > 0) {
|
||||||
|
return (
|
||||||
|
<Variable.Input scope={scope} changeOnSelect={changeOnSelect} nullable={nullable} {...others}>
|
||||||
|
{render?.()}
|
||||||
|
</Variable.Input>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return render?.(others);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export const HideVariableContext = createContext(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
export function useHideVariable() {
|
||||||
|
return useContext(HideVariableContext);
|
||||||
|
}
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
"Preload associations": "预加载关联数据",
|
"Preload associations": "预加载关联数据",
|
||||||
"Please select the associated fields that need to be accessed in subsequent nodes. With more than two levels of to-many associations may cause performance issue, please use with caution.":
|
"Please select the associated fields that need to be accessed in subsequent nodes. With more than two levels of to-many associations may cause performance issue, please use with caution.":
|
||||||
"请选中需要在后续节点中被访问的关系字段。超过两层的对多关联可能会导致性能问题,请谨慎使用。",
|
"请选中需要在后续节点中被访问的关系字段。超过两层的对多关联可能会导致性能问题,请谨慎使用。",
|
||||||
"Choose a record of the collection to trigger.": "选择数据表中的一行记录来触发。",
|
"Choose a record or primary key of a record in the collection to trigger.": "选择数据表中的一行记录或者记录的主键来触发。",
|
||||||
|
|
||||||
"Schedule event": "定时任务",
|
"Schedule event": "定时任务",
|
||||||
"Triggered according to preset time conditions. Suitable for one-time or periodic tasks, such as sending notifications and cleaning data on a schedule.": "按预设的时间条件定时触发。适用于一次性或周期性的任务,如定时发送通知、清理数据等。",
|
"Triggered according to preset time conditions. Suitable for one-time or periodic tasks, such as sending notifications and cleaning data on a schedule.": "按预设的时间条件定时触发。适用于一次性或周期性的任务,如定时发送通知、清理数据等。",
|
||||||
|
@ -47,6 +47,9 @@ export type EventOptions = {
|
|||||||
context?: any;
|
context?: any;
|
||||||
deferred?: boolean;
|
deferred?: boolean;
|
||||||
manually?: boolean;
|
manually?: boolean;
|
||||||
|
force?: boolean;
|
||||||
|
stack?: Array<ID>;
|
||||||
|
onTriggerFail?: Function;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
} & Transactionable;
|
} & Transactionable;
|
||||||
|
|
||||||
@ -371,7 +374,7 @@ export default class PluginWorkflowServer extends Plugin {
|
|||||||
logger.debug(`ignored event data:`, context);
|
logger.debug(`ignored event data:`, context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!options.manually && !workflow.enabled) {
|
if (!options.force && !options.manually && !workflow.enabled) {
|
||||||
logger.warn(`workflow ${workflow.id} is not enabled, event will be ignored`);
|
logger.warn(`workflow ${workflow.id} is not enabled, event will be ignored`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -448,6 +451,33 @@ export default class PluginWorkflowServer extends Plugin {
|
|||||||
return new Processor(execution, { ...options, plugin: this });
|
return new Processor(execution, { ...options, plugin: this });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async validateEvent(workflow: WorkflowModel, context: any, options: EventOptions) {
|
||||||
|
const trigger = this.triggers.get(workflow.type);
|
||||||
|
const triggerValid = await trigger.validateEvent(workflow, context, options);
|
||||||
|
if (!triggerValid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stack } = options;
|
||||||
|
let valid = true;
|
||||||
|
if (stack?.length > 0) {
|
||||||
|
const existed = await workflow.countExecutions({
|
||||||
|
where: {
|
||||||
|
id: stack,
|
||||||
|
},
|
||||||
|
transaction: options.transaction,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existed) {
|
||||||
|
this.getLogger(workflow.id).warn(
|
||||||
|
`workflow ${workflow.id} has already been triggered in stacks executions (${stack}), and newly triggering will be skipped.`,
|
||||||
|
);
|
||||||
|
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
private async createExecution(
|
private async createExecution(
|
||||||
workflow: WorkflowModel,
|
workflow: WorkflowModel,
|
||||||
context,
|
context,
|
||||||
@ -456,13 +486,13 @@ export default class PluginWorkflowServer extends Plugin {
|
|||||||
const { deferred } = options;
|
const { deferred } = options;
|
||||||
const transaction = await this.useDataSourceTransaction('main', options.transaction, true);
|
const transaction = await this.useDataSourceTransaction('main', options.transaction, true);
|
||||||
const sameTransaction = options.transaction === transaction;
|
const sameTransaction = options.transaction === transaction;
|
||||||
const trigger = this.triggers.get(workflow.type);
|
const valid = await this.validateEvent(workflow, context, { ...options, transaction });
|
||||||
const valid = await trigger.validateEvent(workflow, context, { ...options, transaction });
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
if (!sameTransaction) {
|
if (!sameTransaction) {
|
||||||
await transaction.commit();
|
await transaction.commit();
|
||||||
}
|
}
|
||||||
return null;
|
options.onTriggerFail?.(workflow, context, options);
|
||||||
|
return Promise.reject(new Error('event is not valid'));
|
||||||
}
|
}
|
||||||
|
|
||||||
let execution;
|
let execution;
|
||||||
@ -472,6 +502,7 @@ export default class PluginWorkflowServer extends Plugin {
|
|||||||
context,
|
context,
|
||||||
key: workflow.key,
|
key: workflow.key,
|
||||||
eventKey: options.eventKey ?? randomUUID(),
|
eventKey: options.eventKey ?? randomUUID(),
|
||||||
|
stack: options.stack,
|
||||||
status: deferred ? EXECUTION_STATUS.STARTED : EXECUTION_STATUS.QUEUEING,
|
status: deferred ? EXECUTION_STATUS.STARTED : EXECUTION_STATUS.QUEUEING,
|
||||||
},
|
},
|
||||||
{ transaction },
|
{ transaction },
|
||||||
@ -533,8 +564,8 @@ export default class PluginWorkflowServer extends Plugin {
|
|||||||
if (execution?.status === EXECUTION_STATUS.QUEUEING && !this.executing && !this.pending.length) {
|
if (execution?.status === EXECUTION_STATUS.QUEUEING && !this.executing && !this.pending.length) {
|
||||||
this.pending.push([execution]);
|
this.pending.push([execution]);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
logger.error(`failed to create execution: ${err.message}`, err);
|
logger.error(`failed to create execution:`, { error });
|
||||||
// this.events.push(event); // NOTE: retry will cause infinite loop
|
// this.events.push(event); // NOTE: retry will cause infinite loop
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -587,8 +618,11 @@ export default class PluginWorkflowServer extends Plugin {
|
|||||||
if (next) {
|
if (next) {
|
||||||
await this.process(...next);
|
await this.process(...next);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
} finally {
|
} finally {
|
||||||
this.executing = null;
|
this.executing = null;
|
||||||
|
this.getLogger('dispatcher').info(`execution dispatched finished`);
|
||||||
|
|
||||||
if (next) {
|
if (next) {
|
||||||
this.dispatch();
|
this.dispatch();
|
||||||
@ -624,7 +658,7 @@ export default class PluginWorkflowServer extends Plugin {
|
|||||||
return processor;
|
return processor;
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(workflow: WorkflowModel, context: Context, options: EventOptions = {}) {
|
async execute(workflow: WorkflowModel, values, options: EventOptions = {}) {
|
||||||
const trigger = this.triggers.get(workflow.type);
|
const trigger = this.triggers.get(workflow.type);
|
||||||
if (!trigger) {
|
if (!trigger) {
|
||||||
throw new Error(`trigger type "${workflow.type}" of workflow ${workflow.id} is not registered`);
|
throw new Error(`trigger type "${workflow.type}" of workflow ${workflow.id} is not registered`);
|
||||||
@ -632,7 +666,7 @@ export default class PluginWorkflowServer extends Plugin {
|
|||||||
if (!trigger.execute) {
|
if (!trigger.execute) {
|
||||||
throw new Error(`"execute" method of trigger ${workflow.type} is not implemented`);
|
throw new Error(`"execute" method of trigger ${workflow.type} is not implemented`);
|
||||||
}
|
}
|
||||||
return trigger.execute(workflow, context, options);
|
return trigger.execute(workflow, values, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -108,7 +108,10 @@ export async function trigger(context: Context, next) {
|
|||||||
|
|
||||||
export async function execute(context: Context, next) {
|
export async function execute(context: Context, next) {
|
||||||
const plugin = context.app.pm.get(Plugin) as Plugin;
|
const plugin = context.app.pm.get(Plugin) as Plugin;
|
||||||
const { filterByTk, autoRevision } = context.action.params;
|
const { filterByTk, values, autoRevision } = context.action.params;
|
||||||
|
if (!values) {
|
||||||
|
return context.throw(400, 'values is required');
|
||||||
|
}
|
||||||
if (!filterByTk) {
|
if (!filterByTk) {
|
||||||
return context.throw(400, 'filterByTk is required');
|
return context.throw(400, 'filterByTk is required');
|
||||||
}
|
}
|
||||||
@ -124,7 +127,7 @@ export async function execute(context: Context, next) {
|
|||||||
const { executed } = workflow;
|
const { executed } = workflow;
|
||||||
let processor;
|
let processor;
|
||||||
try {
|
try {
|
||||||
processor = (await plugin.execute(workflow, context, { manually: true })) as Processor;
|
processor = (await plugin.execute(workflow, values, { manually: true })) as Processor;
|
||||||
if (!processor) {
|
if (!processor) {
|
||||||
return context.throw(400, 'workflow not triggered');
|
return context.throw(400, 'workflow not triggered');
|
||||||
}
|
}
|
||||||
|
@ -42,5 +42,13 @@ export default {
|
|||||||
type: 'integer',
|
type: 'integer',
|
||||||
name: 'status',
|
name: 'status',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'json',
|
||||||
|
name: 'stack',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'json',
|
||||||
|
name: 'output',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
} as CollectionOptions;
|
} as CollectionOptions;
|
||||||
|
@ -29,7 +29,7 @@ export class CreateInstruction extends Instruction {
|
|||||||
const created = await repository.create({
|
const created = await repository.create({
|
||||||
...options,
|
...options,
|
||||||
context: {
|
context: {
|
||||||
stack: Array.from(new Set((processor.execution.context.stack ?? []).concat(processor.execution.id))),
|
stack: Array.from(new Set((processor.execution.stack ?? []).concat(processor.execution.id))),
|
||||||
},
|
},
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
@ -27,7 +27,7 @@ export class DestroyInstruction extends Instruction {
|
|||||||
const result = await repository.destroy({
|
const result = await repository.destroy({
|
||||||
...options,
|
...options,
|
||||||
context: {
|
context: {
|
||||||
stack: Array.from(new Set((processor.execution.context.stack ?? []).concat(processor.execution.id))),
|
stack: Array.from(new Set((processor.execution.stack ?? []).concat(processor.execution.id))),
|
||||||
},
|
},
|
||||||
transaction: this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction),
|
transaction: this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction),
|
||||||
});
|
});
|
||||||
|
@ -27,7 +27,7 @@ export class UpdateInstruction extends Instruction {
|
|||||||
const result = await repository.update({
|
const result = await repository.update({
|
||||||
...options,
|
...options,
|
||||||
context: {
|
context: {
|
||||||
stack: Array.from(new Set((processor.execution.context.stack ?? []).concat(processor.execution.id))),
|
stack: Array.from(new Set((processor.execution.stack ?? []).concat(processor.execution.id))),
|
||||||
},
|
},
|
||||||
transaction: this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction),
|
transaction: this.workflow.useDataSourceTransaction(dataSourceName, processor.transaction),
|
||||||
});
|
});
|
||||||
|
@ -59,23 +59,24 @@ export default class CollectionTrigger extends Trigger {
|
|||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { stack } = options.context ?? {};
|
||||||
if (workflow.sync) {
|
if (workflow.sync) {
|
||||||
await this.workflow.trigger(workflow, ctx, {
|
await this.workflow.trigger(workflow, ctx, {
|
||||||
transaction,
|
transaction,
|
||||||
|
stack,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
transaction.afterCommit(() => {
|
transaction.afterCommit(() => {
|
||||||
this.workflow.trigger(workflow, ctx);
|
this.workflow.trigger(workflow, ctx, { stack });
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.workflow.trigger(workflow, ctx);
|
this.workflow.trigger(workflow, ctx, { stack });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async prepare(workflow: WorkflowModel, data: Model | Record<string, any>, options) {
|
async prepare(workflow: WorkflowModel, data: Model | Record<string, any> | string | number, options) {
|
||||||
const { condition, changed, mode, appends } = workflow.config;
|
const { condition, changed, mode, appends } = workflow.config;
|
||||||
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
|
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
|
||||||
const { collectionManager } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName);
|
const { collectionManager } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName);
|
||||||
@ -83,9 +84,23 @@ export default class CollectionTrigger extends Trigger {
|
|||||||
const { transaction, context } = options;
|
const { transaction, context } = options;
|
||||||
const { repository, filterTargetKey } = collection;
|
const { repository, filterTargetKey } = collection;
|
||||||
|
|
||||||
|
let target = data;
|
||||||
|
let filterByTk;
|
||||||
|
let loadNeeded = false;
|
||||||
|
if (target && typeof target === 'object') {
|
||||||
|
filterByTk = Array.isArray(filterTargetKey)
|
||||||
|
? pick(
|
||||||
|
target,
|
||||||
|
filterTargetKey.sort((a, b) => a.localeCompare(b)),
|
||||||
|
)
|
||||||
|
: target[filterTargetKey];
|
||||||
|
} else {
|
||||||
|
filterByTk = target;
|
||||||
|
loadNeeded = true;
|
||||||
|
}
|
||||||
// NOTE: if no configured fields changed, do not trigger
|
// NOTE: if no configured fields changed, do not trigger
|
||||||
if (
|
if (
|
||||||
data instanceof Model &&
|
target instanceof Model &&
|
||||||
changed &&
|
changed &&
|
||||||
changed.length &&
|
changed.length &&
|
||||||
changed
|
changed
|
||||||
@ -93,14 +108,11 @@ export default class CollectionTrigger extends Trigger {
|
|||||||
const field = collection.getField(name);
|
const field = collection.getField(name);
|
||||||
return field && !['linkTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.options.type);
|
return field && !['linkTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(field.options.type);
|
||||||
})
|
})
|
||||||
.every((name) => !data.changedWithAssociations(getFieldRawName(collection, name)))
|
.every((name) => !(<Model>target).changedWithAssociations(getFieldRawName(collection, name)))
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filterByTk = Array.isArray(filterTargetKey)
|
|
||||||
? pick(data, filterTargetKey)
|
|
||||||
: { [filterTargetKey]: data[filterTargetKey] };
|
|
||||||
// NOTE: if no configured condition, or not match, do not trigger
|
// NOTE: if no configured condition, or not match, do not trigger
|
||||||
if (isValidFilter(condition) && !(mode & MODE_BITMAP.DESTROY)) {
|
if (isValidFilter(condition) && !(mode & MODE_BITMAP.DESTROY)) {
|
||||||
// TODO: change to map filter format to calculation format
|
// TODO: change to map filter format to calculation format
|
||||||
@ -117,17 +129,14 @@ export default class CollectionTrigger extends Trigger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = data;
|
if (loadNeeded || (appends?.length && !(mode & MODE_BITMAP.DESTROY))) {
|
||||||
|
|
||||||
if (appends?.length && !(mode & MODE_BITMAP.DESTROY)) {
|
|
||||||
const includeFields = appends.reduce((set, field) => {
|
const includeFields = appends.reduce((set, field) => {
|
||||||
set.add(field.split('.')[0]);
|
set.add(field.split('.')[0]);
|
||||||
set.add(field);
|
set.add(field);
|
||||||
return set;
|
return set;
|
||||||
}, new Set());
|
}, new Set());
|
||||||
|
|
||||||
// @ts-ignore
|
target = await repository.findOne({
|
||||||
result = await repository.findOne({
|
|
||||||
filterByTk,
|
filterByTk,
|
||||||
appends: Array.from(includeFields),
|
appends: Array.from(includeFields),
|
||||||
transaction,
|
transaction,
|
||||||
@ -135,8 +144,7 @@ export default class CollectionTrigger extends Trigger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: toJSON(result),
|
data: toJSON(target),
|
||||||
stack: context?.stack,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,31 +203,31 @@ export default class CollectionTrigger extends Trigger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateEvent(workflow: WorkflowModel, context: any, options: Transactionable): Promise<boolean> {
|
// async validateEvent(workflow: WorkflowModel, context: any, options: Transactionable): Promise<boolean> {
|
||||||
if (context.stack) {
|
// if (context.stack) {
|
||||||
const existed = await workflow.countExecutions({
|
// const existed = await workflow.countExecutions({
|
||||||
where: {
|
// where: {
|
||||||
id: context.stack,
|
// id: context.stack,
|
||||||
},
|
// },
|
||||||
transaction: options.transaction,
|
// transaction: options.transaction,
|
||||||
});
|
// });
|
||||||
|
|
||||||
if (existed) {
|
// if (existed) {
|
||||||
this.workflow
|
// this.workflow
|
||||||
.getLogger(workflow.id)
|
// .getLogger(workflow.id)
|
||||||
.warn(
|
// .warn(
|
||||||
`workflow ${workflow.id} has already been triggered in stack executions (${context.stack}), and newly triggering will be skipped.`,
|
// `workflow ${workflow.id} has already been triggered in stack executions (${context.stack}), and newly triggering will be skipped.`,
|
||||||
);
|
// );
|
||||||
|
|
||||||
return false;
|
// return false;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
|
|
||||||
async execute(workflow: WorkflowModel, context: Context, options: EventOptions) {
|
async execute(workflow: WorkflowModel, values, options: EventOptions) {
|
||||||
const ctx = await this.prepare(workflow, context.action.params.values?.data, options);
|
const ctx = await this.prepare(workflow, values?.data, options);
|
||||||
const [dataSourceName] = parseCollectionName(workflow.config.collection);
|
const [dataSourceName] = parseCollectionName(workflow.config.collection);
|
||||||
const { transaction } = options;
|
const { transaction } = options;
|
||||||
return this.workflow.trigger(workflow, ctx, {
|
return this.workflow.trigger(workflow, ctx, {
|
||||||
|
@ -13,6 +13,7 @@ import type Plugin from '../../Plugin';
|
|||||||
import type { WorkflowModel } from '../../types';
|
import type { WorkflowModel } from '../../types';
|
||||||
import { parseDateWithoutMs, SCHEDULE_MODE } from './utils';
|
import { parseDateWithoutMs, SCHEDULE_MODE } from './utils';
|
||||||
import { parseCollectionName, SequelizeCollectionManager, SequelizeDataSource } from '@nocobase/data-source-manager';
|
import { parseCollectionName, SequelizeCollectionManager, SequelizeDataSource } from '@nocobase/data-source-manager';
|
||||||
|
import { pick } from 'lodash';
|
||||||
|
|
||||||
export type ScheduleOnField = {
|
export type ScheduleOnField = {
|
||||||
field: string;
|
field: string;
|
||||||
@ -403,4 +404,33 @@ export default class DateFieldScheduleTrigger {
|
|||||||
this.events.delete(name);
|
this.events.delete(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async execute(workflow, values, options) {
|
||||||
|
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
|
||||||
|
const { collectionManager } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName);
|
||||||
|
const { filterTargetKey, repository } = collectionManager.getCollection(collectionName);
|
||||||
|
|
||||||
|
let { data } = values;
|
||||||
|
let filterByTk;
|
||||||
|
let loadNeeded = false;
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
filterByTk = Array.isArray(filterTargetKey)
|
||||||
|
? pick(
|
||||||
|
data,
|
||||||
|
filterTargetKey.sort((a, b) => a.localeCompare(b)),
|
||||||
|
)
|
||||||
|
: data[filterTargetKey];
|
||||||
|
} else {
|
||||||
|
filterByTk = data;
|
||||||
|
loadNeeded = true;
|
||||||
|
}
|
||||||
|
if (loadNeeded || workflow.config.appends?.length) {
|
||||||
|
data = await repository.findOne({
|
||||||
|
filterByTk,
|
||||||
|
appends: workflow.config.appends,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.workflow.trigger(workflow, { ...values, data, date: values?.date ?? new Date() }, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,4 +136,8 @@ export default class StaticScheduleTrigger {
|
|||||||
off(workflow) {
|
off(workflow) {
|
||||||
this.schedule(workflow, null, false);
|
this.schedule(workflow, null, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
execute(workflow, values, options) {
|
||||||
|
return this.workflow.trigger(workflow, { ...values, date: values?.date ?? new Date() }, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Context } from '@nocobase/actions';
|
import { Context } from '@nocobase/actions';
|
||||||
|
import { parseCollectionName } from '@nocobase/data-source-manager';
|
||||||
import Trigger from '..';
|
import Trigger from '..';
|
||||||
import type Plugin from '../../Plugin';
|
import type Plugin from '../../Plugin';
|
||||||
import DateFieldScheduleTrigger from './DateFieldScheduleTrigger';
|
import DateFieldScheduleTrigger from './DateFieldScheduleTrigger';
|
||||||
@ -46,9 +47,12 @@ export default class ScheduleTrigger extends Trigger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(workflow, context: Context, options) {
|
async execute(workflow, values: any, options) {
|
||||||
const { values } = context.action.params;
|
const mode = workflow.config.mode;
|
||||||
return this.workflow.trigger(workflow, { ...values, date: values?.date ?? new Date() }, options);
|
const trigger = this.getTrigger(mode);
|
||||||
|
if (trigger) {
|
||||||
|
return trigger.execute(workflow, values, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// async validateEvent(workflow: WorkflowModel, context: any, options: Transactionable): Promise<boolean> {
|
// async validateEvent(workflow: WorkflowModel, context: any, options: Transactionable): Promise<boolean> {
|
||||||
|
@ -23,7 +23,7 @@ export abstract class Trigger {
|
|||||||
sync?: boolean;
|
sync?: boolean;
|
||||||
execute?(
|
execute?(
|
||||||
workflow: WorkflowModel,
|
workflow: WorkflowModel,
|
||||||
context: any,
|
values: any,
|
||||||
options: Transactionable,
|
options: Transactionable,
|
||||||
): void | Processor | Promise<void | Processor>;
|
): void | Processor | Promise<void | Processor>;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user