chore(plugin-workflow): cherry pick (#6057)

* 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>

* fix(plugin-workflow): fix schedule picker value (#6039)

* fix(plugin-workflow): fix variable compoent onchange (#5964)

---------

Co-authored-by: shz <huanghui9850@gmail.com>
This commit is contained in:
Junyi 2025-01-15 17:42:49 +08:00 committed by GitHub
parent 540a22ba31
commit 04b3540739
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 420 additions and 197 deletions

View File

@ -434,7 +434,7 @@ export function Input(props: VariableInputProps) {
style={{ overflow: 'hidden' }}
className={cx('ant-input', { 'ant-input-disabled': disabled }, hashId)}
>
<Tag contentEditable={false} color="blue">
<Tag color="blue">
{variableText.map((item, index) => {
return (
<React.Fragment key={item}>

View File

@ -9,7 +9,13 @@
import { useForm } from '@formily/react';
import { SchemaInitializerItemType, parseCollectionName, useCollectionDataSource, useCompile } from '@nocobase/client';
import {
SchemaInitializerItemType,
parseCollectionName,
useCollectionDataSource,
useCompile,
RemoteSelect,
} from '@nocobase/client';
import {
Trigger,
CollectionBlockInitializer,
@ -19,8 +25,10 @@ import {
RadioWithTooltip,
useGetCollectionFields,
TriggerCollectionRecordSelect,
WorkflowVariableWrapper,
} from '@nocobase/plugin-workflow/client';
import { NAMESPACE, useLang } from '../locale';
import React from 'react';
const COLLECTION_TRIGGER_ACTION = {
CREATE: 'create',
@ -36,8 +44,8 @@ function useVariables(config, options) {
const getMainCollectionFields = useGetCollectionFields();
const langTriggerData = useLang('Trigger data');
const langUserSubmittedForm = useLang('User submitted action');
const langRoleSubmittedForm = useLang('Role of user submitted action');
const langUserSubmittedForm = useLang('User acted');
const langRoleSubmittedForm = useLang('Role of user acted');
const result = [
...getCollectionFieldOptions({
// depth,
@ -198,8 +206,8 @@ export default class extends Trigger {
triggerFieldset = {
data: {
type: 'object',
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
description: `{{t("Choose a record of the collection to trigger.", { ns: "workflow" })}}`,
title: `{{t("Trigger data", { ns: "workflow" })}}`,
description: `{{t("Choose a record or primary key of a record in the collection to trigger.", { ns: "workflow" })}}`,
'x-decorator': 'FormItem',
'x-component': 'TriggerCollectionRecordSelect',
default: null,
@ -207,37 +215,90 @@ export default class extends Trigger {
},
userId: {
type: 'number',
title: `{{t("User submitted action", { ns: "${NAMESPACE}" })}}`,
title: `{{t("User acted", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'RemoteSelect',
'x-component': 'WorkflowVariableWrapper',
'x-component-props': {
fieldNames: {
label: 'nickname',
value: 'id',
nullable: false,
changeOnSelect: true,
variableOptions: {
types: [
(field) => {
if (field.isForeignKey || field.type === 'context') {
return field.target === 'users';
}
return field.collectionName === 'users' && field.name === 'id';
},
],
},
service: {
resource: 'users',
render(props) {
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,
required: true,
},
roleName: {
type: 'string',
title: `{{t("Role of user submitted action", { ns: "${NAMESPACE}" })}}`,
title: `{{t("Role of user acted", { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'RemoteSelect',
'x-component': 'WorkflowVariableWrapper',
'x-component-props': {
fieldNames: {
label: 'title',
value: 'name',
nullable: false,
changeOnSelect: true,
variableOptions: {
types: [
(field) => {
if (field.isForeignKey) {
return field.target === 'roles';
}
return field.collectionName === 'roles' && field.name === 'name';
},
],
},
service: {
resource: 'roles',
render(props) {
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,
},
};
@ -252,6 +313,7 @@ export default class extends Trigger {
RadioWithTooltip,
CheckboxGroupWithTooltip,
TriggerCollectionRecordSelect,
WorkflowVariableWrapper,
};
isActionTriggerable = (config, context) => {
return !config.global && ['submit', 'customize:save', 'customize:update'].includes(context.buttonAction);

View File

@ -12,6 +12,6 @@
"Update record action": "更新记录操作",
"Associations to use": "待使用的关系数据",
"Trigger data": "触发器数据",
"User submitted action": "提交操作的用户",
"Role of user submitted action": "提交操作用户的角色"
"User acted": "操作者",
"Role of user acted": "操作者角色"
}

View File

@ -177,7 +177,7 @@ export default class extends Trigger {
}
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) {
@ -185,18 +185,27 @@ export default class extends Trigger {
}
}
async execute(workflow: WorkflowModel, context: Context, options: EventOptions) {
const { values } = context.action.params;
async execute(workflow: WorkflowModel, values, options: EventOptions) {
// const { values } = context.action.params;
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
const { collectionManager } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName);
const { filterTargetKey, repository } = collectionManager.getCollection(collectionName);
const filterByTk = Array.isArray(filterTargetKey)
? pick(
values.data,
filterTargetKey.sort((a, b) => a.localeCompare(b)),
)
: values.data[filterTargetKey];
const UserRepo = context.app.db.getRepository('users');
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;
}
const UserRepo = this.workflow.app.db.getRepository('users');
const actor = await UserRepo.findOne({
filterByTk: values.userId,
appends: ['roles'],
@ -207,8 +216,7 @@ export default class extends Trigger {
const { roles, ...user } = actor.desensitize().get();
const roleName = values.roleName || roles?.[0]?.name;
let { data } = values;
if (workflow.config.appends?.length) {
if (loadNeeded || workflow.config.appends?.length) {
data = await repository.findOne({
filterByTk,
appends: workflow.config.appends,
@ -221,10 +229,7 @@ export default class extends Trigger {
user,
roleName,
},
{
...options,
httpContext: context,
},
options,
);
}
}

View File

@ -12,7 +12,7 @@ import { RemoteSelect, Variable } from '@nocobase/client';
import { useWorkflowVariableOptions } from '@nocobase/plugin-workflow/client';
function isUserKeyField(field) {
if (field.isForeignKey) {
if (field.isForeignKey || field.type === 'context') {
return field.target === 'users';
}
return field.collectionName === 'users' && field.name === 'id';

View File

@ -14,3 +14,9 @@ export const FlowContext = React.createContext<any>({});
export function useFlowContext() {
return useContext(FlowContext);
}
export const CurrentWorkflowContext = React.createContext<any>({});
export function useCurrentWorkflowContext() {
return useContext(CurrentWorkflowContext);
}

View File

@ -12,6 +12,8 @@ import { Trans, useTranslation } from 'react-i18next';
import { Link, useNavigate } from 'react-router-dom';
import { Alert, App, Breadcrumb, Button, Dropdown, Result, Spin, Switch, Tag, Tooltip } from 'antd';
import { DownOutlined, EllipsisOutlined, RightOutlined } from '@ant-design/icons';
import { NoticeType } from 'antd/es/message/interface';
import { useField, useForm } from '@formily/react';
import {
ActionContextProvider,
ResourceActionProvider,
@ -30,10 +32,11 @@ import {
} from '@nocobase/client';
import { dayjs } from '@nocobase/utils/client';
import PluginWorkflowClient from '.';
import { CanvasContent } from './CanvasContent';
import { ExecutionStatusColumn } from './components/ExecutionStatus';
import { ExecutionLink } from './ExecutionLink';
import { FlowContext, useFlowContext } from './FlowContext';
import { CurrentWorkflowContext, FlowContext, useFlowContext } from './FlowContext';
import { lang, NAMESPACE } from './locale';
import { executionSchema } from './schemas/executions';
import useStyles from './style';
@ -41,10 +44,8 @@ import { linkNodes, getWorkflowDetailPath } from './utils';
import { Fieldset } from './components/Fieldset';
import { useRefreshActionProps } from './hooks/useRefreshActionProps';
import { useTrigger } from './triggers';
import { useField, useForm } from '@formily/react';
import { ExecutionStatusOptionsMap } from './constants';
import PluginWorkflowClient from '.';
import { NoticeType } from 'antd/es/message/interface';
import { HideVariableContext } from './variable';
function ExecutionResourceProvider({ request, filter = {}, ...others }) {
const { workflow } = useFlowContext();
@ -144,99 +145,103 @@ function ExecuteActionButton() {
const trigger = useTrigger();
return (
<SchemaComponent
components={{
Alert,
Fieldset,
ActionDisabledProvider,
...trigger.components,
}}
scope={{
useCancelAction,
useExecuteConfirmAction,
}}
schema={{
name: `trigger-modal-${workflow.type}-${workflow.id}`,
type: 'void',
'x-decorator': 'ActionDisabledProvider',
'x-component': 'Action',
'x-component-props': {
openSize: 'small',
},
title: `{{t('Execute manually', { ns: "${NAMESPACE}" })}}`,
properties: {
drawer: {
<CurrentWorkflowContext.Provider value={workflow}>
<HideVariableContext.Provider value={true}>
<SchemaComponent
components={{
Alert,
Fieldset,
ActionDisabledProvider,
...trigger.components,
}}
scope={{
useCancelAction,
useExecuteConfirmAction,
}}
schema={{
name: `trigger-modal-${workflow.type}-${workflow.id}`,
type: 'void',
'x-decorator': 'FormV2',
'x-component': 'Action.Modal',
'x-decorator': 'ActionDisabledProvider',
'x-component': 'Action',
'x-component-props': {
openSize: 'small',
},
title: `{{t('Execute manually', { ns: "${NAMESPACE}" })}}`,
properties: {
...(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: {
drawer: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'Fieldset',
title: `{{t('Trigger variables', { 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',
'x-decorator': 'FormV2',
'x-component': 'Action.Modal',
title: `{{t('Execute manually', { ns: "${NAMESPACE}" })}}`,
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',
title: `{{t('Cancel')}}`,
'x-component': 'Action',
'x-component-props': {
useAction: '{{useCancelAction}}',
},
'x-decorator': 'FormItem',
'x-component': 'Fieldset',
title: `{{t('Trigger variables', { ns: "${NAMESPACE}" })}}`,
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',
title: `{{t('Confirm')}}`,
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction: '{{useExecuteConfirmAction}}',
'x-component': 'Action.Modal.Footer',
properties: {
cancel: {
type: 'void',
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>
);
}

View File

@ -11,17 +11,17 @@ import React from 'react';
import { parseCollectionName, RemoteSelect, useApp } from '@nocobase/client';
import { useFlowContext } from '../FlowContext';
import { useCurrentWorkflowContext } from '../FlowContext';
import { WorkflowVariableWrapper } from '../variable';
export function TriggerCollectionRecordSelect(props) {
const { workflow } = useFlowContext();
const workflow = useCurrentWorkflowContext();
const app = useApp();
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
const { collectionManager } = app.dataSourceManager.getDataSource(dataSourceName);
const collection = collectionManager.getCollection(collectionName);
return (
const render = (props) => (
<RemoteSelect
objectValue
dataSource={dataSourceName}
@ -36,4 +36,13 @@ export function TriggerCollectionRecordSelect(props) {
{...props}
/>
);
return (
<WorkflowVariableWrapper
value={props.value}
onChange={props.onChange}
nullable={false}
changeOnSelect
render={render}
/>
);
}

View File

@ -197,7 +197,7 @@ export default class extends Trigger {
data: {
type: 'object',
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-component': 'TriggerCollectionRecordSelect',
default: null,

View File

@ -7,9 +7,12 @@
* 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 { SCHEDULE_MODE } from './constants';
import { dayjs } from '@nocobase/utils/client';
export const ScheduleModes = {
[SCHEDULE_MODE.STATIC]: {
@ -73,13 +76,21 @@ export const ScheduleModes = {
type: 'string',
title: `{{t('Execute on', { ns: "${NAMESPACE}" })}}`,
'x-decorator': 'FormItem',
'x-component': 'DatePicker',
'x-component': 'WorkflowVariableWrapper',
'x-component-props': {
showTime: true,
placeholder: `{{t('Current time', { ns: "${NAMESPACE}" })}}`,
// showTime: true,
// placeholder: `{{t('Current time', { ns: "${NAMESPACE}" })}}`,
nullable: false,
changeOnSelect: true,
render(props) {
return <DatePicker showTime placeholder={lang('Current time')} {...props} value={dayjs(props.value)} />;
},
},
},
},
validate(config) {
return Boolean(config.startsOn);
},
},
[SCHEDULE_MODE.DATE_FIELD]: {
fieldset: {
@ -180,7 +191,7 @@ export const ScheduleModes = {
data: {
type: 'object',
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-component': 'TriggerCollectionRecordSelect',
default: null,

View File

@ -9,14 +9,15 @@
import React from 'react';
import { SchemaComponent } from '@nocobase/client';
import { useFlowContext } from '../../FlowContext';
import { useTrigger } from '..';
import { SchemaComponent, usePlugin } from '@nocobase/client';
import WorkflowPlugin from '../..';
import { useCurrentWorkflowContext } from '../../FlowContext';
import { ScheduleModes } from './ScheduleModes';
export function TriggerScheduleConfig() {
const { workflow } = useFlowContext();
const trigger = useTrigger();
const workflow = useCurrentWorkflowContext();
const workflowPlugin = usePlugin(WorkflowPlugin);
const trigger = workflowPlugin.triggers.get(workflow.type);
return (
<SchemaComponent

View File

@ -17,6 +17,7 @@ import { ScheduleConfig } from './ScheduleConfig';
import { SCHEDULE_MODE } from './constants';
import { TriggerScheduleConfig } from './TriggerScheduleConfig';
import { ScheduleModes } from './ScheduleModes';
import { WorkflowVariableWrapper } from '../../variable';
import { TriggerCollectionRecordSelect } from '../../components/TriggerCollectionRecordSelect';
function useVariables(config, opts) {
@ -89,6 +90,7 @@ export default class extends Trigger {
ScheduleConfig,
TriggerScheduleConfig,
TriggerCollectionRecordSelect,
WorkflowVariableWrapper,
};
useVariables = useVariables;
useInitializers(config): SchemaInitializerItemType | null {

View File

@ -7,11 +7,11 @@
* 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 { 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 { NAMESPACE, lang } from './locale';
@ -120,6 +120,9 @@ export const systemOptions = {
},
};
/**
* @deprecated
*/
export const BaseTypeSets = {
boolean: new Set(['checkbox']),
number: new Set(['integer', 'number', 'percent']),
@ -402,3 +405,34 @@ export function WorkflowVariableJSON({ variableOptions, ...props }): JSX.Element
const scope = useWorkflowVariableOptions(variableOptions);
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?.(others)}
</Variable.Input>
);
}
return render?.(others);
}
/**
* @experimental
*/
export const HideVariableContext = createContext(false);
/**
* @experimental
*/
export function useHideVariable() {
return useContext(HideVariableContext);
}

View File

@ -73,7 +73,7 @@
"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.":
"请选中需要在后续节点中被访问的关系字段。超过两层的对多关联可能会导致性能问题,请谨慎使用。",
"Choose a record of the collection to trigger.": "选择数据表中的一行记录来触发。",
"Choose a record or primary key of a record in the collection to trigger.": "选择数据表中的一行记录或者记录的主键来触发。",
"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.": "按预设的时间条件定时触发。适用于一次性或周期性的任务,如定时发送通知、清理数据等。",

View File

@ -48,6 +48,9 @@ export type EventOptions = {
context?: any;
deferred?: boolean;
manually?: boolean;
force?: boolean;
stack?: Array<ID>;
onTriggerFail?: Function;
[key: string]: any;
} & Transactionable;
@ -378,7 +381,7 @@ export default class PluginWorkflowServer extends Plugin {
logger.debug(`ignored event data:`, context);
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`);
return;
}
@ -467,6 +470,33 @@ export default class PluginWorkflowServer extends Plugin {
this.dispatch();
}
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(
workflow: WorkflowModel,
context,
@ -475,13 +505,13 @@ export default class PluginWorkflowServer extends Plugin {
const { deferred } = options;
const transaction = await this.useDataSourceTransaction('main', options.transaction, true);
const sameTransaction = options.transaction === transaction;
const trigger = this.triggers.get(workflow.type);
const valid = await trigger.validateEvent(workflow, context, { ...options, transaction });
const valid = await this.validateEvent(workflow, context, { ...options, transaction });
if (!valid) {
if (!sameTransaction) {
await transaction.commit();
}
return null;
options.onTriggerFail?.(workflow, context, options);
return Promise.reject(new Error('event is not valid'));
}
let execution;
@ -491,6 +521,7 @@ export default class PluginWorkflowServer extends Plugin {
context,
key: workflow.key,
eventKey: options.eventKey ?? randomUUID(),
stack: options.stack,
status: deferred ? EXECUTION_STATUS.STARTED : EXECUTION_STATUS.QUEUEING,
},
{ transaction },
@ -552,8 +583,8 @@ export default class PluginWorkflowServer extends Plugin {
if (execution?.status === EXECUTION_STATUS.QUEUEING && !this.executing && !this.pending.length) {
this.pending.push([execution]);
}
} catch (err) {
logger.error(`failed to create execution: ${err.message}`, err);
} catch (error) {
logger.error(`failed to create execution:`, { error });
// this.events.push(event); // NOTE: retry will cause infinite loop
}
@ -663,7 +694,7 @@ export default class PluginWorkflowServer extends Plugin {
return processor;
}
async execute(workflow: WorkflowModel, context: Context, options: EventOptions = {}) {
async execute(workflow: WorkflowModel, values, options: EventOptions = {}) {
const trigger = this.triggers.get(workflow.type);
if (!trigger) {
throw new Error(`trigger type "${workflow.type}" of workflow ${workflow.id} is not registered`);
@ -671,7 +702,7 @@ export default class PluginWorkflowServer extends Plugin {
if (!trigger.execute) {
throw new Error(`"execute" method of trigger ${workflow.type} is not implemented`);
}
return trigger.execute(workflow, context, options);
return trigger.execute(workflow, values, options);
}
/**

View File

@ -108,7 +108,10 @@ export async function trigger(context: Context, next) {
export async function execute(context: Context, next) {
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) {
return context.throw(400, 'filterByTk is required');
}
@ -124,7 +127,7 @@ export async function execute(context: Context, next) {
const { executed } = workflow;
let processor;
try {
processor = (await plugin.execute(workflow, context, { manually: true })) as Processor;
processor = (await plugin.execute(workflow, values, { manually: true })) as Processor;
if (!processor) {
return context.throw(400, 'workflow not triggered');
}

View File

@ -42,5 +42,13 @@ export default {
type: 'integer',
name: 'status',
},
{
type: 'json',
name: 'stack',
},
{
type: 'json',
name: 'output',
},
],
} as CollectionOptions;

View File

@ -29,7 +29,7 @@ export class CreateInstruction extends Instruction {
const created = await repository.create({
...options,
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,
});

View File

@ -27,7 +27,7 @@ export class DestroyInstruction extends Instruction {
const result = await repository.destroy({
...options,
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),
});

View File

@ -27,7 +27,7 @@ export class UpdateInstruction extends Instruction {
const result = await repository.update({
...options,
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),
});

View File

@ -59,23 +59,24 @@ export default class CollectionTrigger extends Trigger {
if (!ctx) {
return;
}
const { stack } = options.context ?? {};
if (workflow.sync) {
await this.workflow.trigger(workflow, ctx, {
transaction,
stack,
});
} else {
if (transaction) {
transaction.afterCommit(() => {
this.workflow.trigger(workflow, ctx);
this.workflow.trigger(workflow, ctx, { stack });
});
} 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 [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
const { collectionManager } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName);
@ -83,9 +84,23 @@ export default class CollectionTrigger extends Trigger {
const { transaction, context } = options;
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
if (
data instanceof Model &&
target instanceof Model &&
changed &&
changed.length &&
changed
@ -93,14 +108,11 @@ export default class CollectionTrigger extends Trigger {
const field = collection.getField(name);
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;
}
const filterByTk = Array.isArray(filterTargetKey)
? pick(data, filterTargetKey)
: { [filterTargetKey]: data[filterTargetKey] };
// NOTE: if no configured condition, or not match, do not trigger
if (isValidFilter(condition) && !(mode & MODE_BITMAP.DESTROY)) {
// TODO: change to map filter format to calculation format
@ -117,17 +129,14 @@ export default class CollectionTrigger extends Trigger {
}
}
let result = data;
if (appends?.length && !(mode & MODE_BITMAP.DESTROY)) {
if (loadNeeded || (appends?.length && !(mode & MODE_BITMAP.DESTROY))) {
const includeFields = appends.reduce((set, field) => {
set.add(field.split('.')[0]);
set.add(field);
return set;
}, new Set());
// @ts-ignore
result = await repository.findOne({
target = await repository.findOne({
filterByTk,
appends: Array.from(includeFields),
transaction,
@ -135,8 +144,7 @@ export default class CollectionTrigger extends Trigger {
}
return {
data: toJSON(result),
stack: context?.stack,
data: toJSON(target),
};
}
@ -195,31 +203,31 @@ export default class CollectionTrigger extends Trigger {
}
}
async validateEvent(workflow: WorkflowModel, context: any, options: Transactionable): Promise<boolean> {
if (context.stack) {
const existed = await workflow.countExecutions({
where: {
id: context.stack,
},
transaction: options.transaction,
});
// async validateEvent(workflow: WorkflowModel, context: any, options: Transactionable): Promise<boolean> {
// if (context.stack) {
// const existed = await workflow.countExecutions({
// where: {
// id: context.stack,
// },
// transaction: options.transaction,
// });
if (existed) {
this.workflow
.getLogger(workflow.id)
.warn(
`workflow ${workflow.id} has already been triggered in stack executions (${context.stack}), and newly triggering will be skipped.`,
);
// if (existed) {
// this.workflow
// .getLogger(workflow.id)
// .warn(
// `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) {
const ctx = await this.prepare(workflow, context.action.params.values?.data, options);
async execute(workflow: WorkflowModel, values, options: EventOptions) {
const ctx = await this.prepare(workflow, values?.data, options);
const [dataSourceName] = parseCollectionName(workflow.config.collection);
const { transaction } = options;
return this.workflow.trigger(workflow, ctx, {

View File

@ -13,6 +13,7 @@ import type Plugin from '../../Plugin';
import type { WorkflowModel } from '../../types';
import { parseDateWithoutMs, SCHEDULE_MODE } from './utils';
import { parseCollectionName, SequelizeCollectionManager, SequelizeDataSource } from '@nocobase/data-source-manager';
import { pick } from 'lodash';
export type ScheduleOnField = {
field: string;
@ -407,4 +408,33 @@ export default class DateFieldScheduleTrigger {
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);
}
}

View File

@ -136,4 +136,8 @@ export default class StaticScheduleTrigger {
off(workflow) {
this.schedule(workflow, null, false);
}
execute(workflow, values, options) {
return this.workflow.trigger(workflow, { ...values, date: values?.date ?? new Date() }, options);
}
}

View File

@ -8,6 +8,7 @@
*/
import { Context } from '@nocobase/actions';
import { parseCollectionName } from '@nocobase/data-source-manager';
import Trigger from '..';
import type Plugin from '../../Plugin';
import DateFieldScheduleTrigger from './DateFieldScheduleTrigger';
@ -46,9 +47,12 @@ export default class ScheduleTrigger extends Trigger {
}
}
async execute(workflow, context: Context, options) {
const { values } = context.action.params;
return this.workflow.trigger(workflow, { ...values, date: values?.date ?? new Date() }, options);
async execute(workflow, values: any, options) {
const mode = workflow.config.mode;
const trigger = this.getTrigger(mode);
if (trigger) {
return trigger.execute(workflow, values, options);
}
}
// async validateEvent(workflow: WorkflowModel, context: any, options: Transactionable): Promise<boolean> {

View File

@ -23,7 +23,7 @@ export abstract class Trigger {
sync?: boolean;
execute?(
workflow: WorkflowModel,
context: any,
values: any,
options: Transactionable,
): void | Processor | Promise<void | Processor>;
}