From 5acee2f57f23c00d60ae0a75059676cd07628a17 Mon Sep 17 00:00:00 2001 From: sealday Date: Fri, 14 Jun 2024 00:56:39 +0800 Subject: [PATCH] feat: now workflow can response (#1186) Co-authored-by: sealday Reviewed-on: https://git.daoyoucloud.com/daoyoucloud/tachybase/pulls/1186 --- .../antd/action/Action.Designer.tsx | 25 +- .../src/data-source-manager.ts | 57 +++- .../core/data-source-manager/src/types.ts | 1 + .../plugin-workflow/src/client/Plugin.tsx | 17 +- .../client/features/action-trigger/index.ts | 5 +- .../WorkflowTriggerInterceptor.tsx | 6 +- .../client/features/omni-trigger/index.tsx | 297 ++++++++++++++++++ .../src/client/features/response/index.ts | 37 +++ .../src/client/locale/index.ts | 2 +- .../plugin-workflow/src/server/Plugin.ts | 8 + .../omni-trigger/CustomActionTrigger.ts | 132 ++++++++ .../server/features/omni-trigger/Plugin.ts | 11 + .../src/server/features/omni-trigger/index.ts | 1 + .../src/server/features/response/Plugin.ts | 11 + .../response/ResponseMessageInstruction.ts | 22 ++ .../src/server/features/response/index.ts | 1 + 16 files changed, 596 insertions(+), 37 deletions(-) create mode 100644 packages/plugins/@tachybase/plugin-workflow/src/client/features/omni-trigger/index.tsx create mode 100644 packages/plugins/@tachybase/plugin-workflow/src/client/features/response/index.ts create mode 100644 packages/plugins/@tachybase/plugin-workflow/src/server/features/omni-trigger/CustomActionTrigger.ts create mode 100644 packages/plugins/@tachybase/plugin-workflow/src/server/features/omni-trigger/Plugin.ts create mode 100644 packages/plugins/@tachybase/plugin-workflow/src/server/features/omni-trigger/index.ts create mode 100644 packages/plugins/@tachybase/plugin-workflow/src/server/features/response/Plugin.ts create mode 100644 packages/plugins/@tachybase/plugin-workflow/src/server/features/response/ResponseMessageInstruction.ts create mode 100644 packages/plugins/@tachybase/plugin-workflow/src/server/features/response/index.ts diff --git a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx index b705a05a4..daed13ad6 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Designer.tsx @@ -336,7 +336,7 @@ export function RemoveButton( ); } -function WorkflowSelect({ actionType, direct = false, ...props }) { +function WorkflowSelect({ formAction, buttonAction, actionType, direct = false, ...props }) { const { t } = useTranslation(); const index = ArrayTable.useIndex(); const { setValuesIn } = useForm(); @@ -377,17 +377,28 @@ function WorkflowSelect({ actionType, direct = false, ...props }) { }); const optionFilter = useCallback( - ({ type, config }) => { + ({ key, type, config }) => { + if (key === props.value) { + return true; + } const trigger = workflowPlugin.triggers.get(type); if (trigger.isActionTriggerable === true) { return true; } if (typeof trigger.isActionTriggerable === 'function') { - return trigger.isActionTriggerable(config, { action: actionType, direct }); + return trigger.isActionTriggerable(config, { + action: actionType, + formAction, + buttonAction, + /** + * @deprecated + */ + direct: buttonAction === 'customize:triggerWorkflows', + }); } return false; }, - [workflowPlugin.triggers, actionType, direct], + [props.value, workflowPlugin.triggers, actionType, formAction, buttonAction], ); return ( @@ -436,6 +447,8 @@ export function WorkflowConfig() { // TODO(refactor): should refactor for getting certain action type, better from 'x-action'. const formBlock = useFormBlockContext(); const actionType = formBlock?.type || fieldSchema['x-action']; + const formAction = formBlock?.type; + const buttonAction = fieldSchema['x-action']; const description = { submit: t('Workflow will be triggered before or after submitting succeeded based on workflow type.', { @@ -510,7 +523,7 @@ export function WorkflowConfig() { value: '', }, allowClear: false, - loadData: actionType === 'destroy' ? null : undefined, + loadData: buttonAction === 'destroy' ? null : undefined, }, default: '', }, @@ -530,6 +543,8 @@ export function WorkflowConfig() { 'x-component-props': { placeholder: t('Select workflow', { ns: 'workflow' }), actionType, + formAction, + buttonAction, direct: fieldSchema['x-action'] === 'customize:triggerWorkflows', }, required: true, diff --git a/packages/core/data-source-manager/src/data-source-manager.ts b/packages/core/data-source-manager/src/data-source-manager.ts index de9845ac2..a6eddcf5e 100644 --- a/packages/core/data-source-manager/src/data-source-manager.ts +++ b/packages/core/data-source-manager/src/data-source-manager.ts @@ -3,20 +3,33 @@ import { ToposortOptions } from '@tachybase/utils'; import { DataSource } from './data-source'; import { DataSourceFactory } from './data-source-factory'; +type DataSourceHook = (dataSource: DataSource) => void; + export class DataSourceManager { dataSources: Map; + /** + * @internal + */ factory: DataSourceFactory = new DataSourceFactory(); - protected middlewares = []; + private onceHooks: Array = []; constructor(public options = {}) { this.dataSources = new Map(); this.middlewares = []; } + get(dataSourceKey: string) { + return this.dataSources.get(dataSourceKey); + } + async add(dataSource: DataSource, options: any = {}) { await dataSource.load(options); this.dataSources.set(dataSource.name, dataSource); + + for (const hook of this.onceHooks) { + hook(dataSource); + } } use(fn: any, options?: ToposortOptions) { @@ -25,17 +38,39 @@ export class DataSourceManager { middleware() { return async (ctx, next) => { - const name = ctx.get('x-data-source'); - if (name) { - if (this.dataSources.has(name)) { - const ds = this.dataSources.get(name); - ctx.dataSource = ds; - return ds.middleware(this.middlewares)(ctx, next); - } else { - ctx.throw(`data source ${name} does not exist`); - } + const name = ctx.get('x-data-source') || 'main'; + + if (!this.dataSources.has(name)) { + ctx.throw(`data source ${name} does not exist`); } - await next(); + + const ds = this.dataSources.get(name); + ctx.dataSource = ds; + + return ds.middleware(this.middlewares)(ctx, next); }; } + + registerDataSourceType(type: string, DataSourceClass: typeof DataSource) { + this.factory.register(type, DataSourceClass); + } + + getDataSourceType(type: string): typeof DataSource | undefined { + return this.factory.getClass(type); + } + + buildDataSourceByType(type: string, options: any = {}): DataSource { + return this.factory.create(type, options); + } + + afterAddDataSource(hook: DataSourceHook) { + this.addHookAndRun(hook); + } + + private addHookAndRun(hook: DataSourceHook) { + this.onceHooks.push(hook); + for (const dataSource of this.dataSources.values()) { + hook(dataSource); + } + } } diff --git a/packages/core/data-source-manager/src/types.ts b/packages/core/data-source-manager/src/types.ts index 887c3ae84..e2c16f279 100644 --- a/packages/core/data-source-manager/src/types.ts +++ b/packages/core/data-source-manager/src/types.ts @@ -54,6 +54,7 @@ export type MergeOptions = { }; export interface ICollectionManager { + db: any; registerFieldTypes(types: Record): void; registerFieldInterfaces(interfaces: Record): void; registerCollectionTemplates(templates: Record): void; diff --git a/packages/plugins/@tachybase/plugin-workflow/src/client/Plugin.tsx b/packages/plugins/@tachybase/plugin-workflow/src/client/Plugin.tsx index 99f87217a..0ece251a8 100644 --- a/packages/plugins/@tachybase/plugin-workflow/src/client/Plugin.tsx +++ b/packages/plugins/@tachybase/plugin-workflow/src/client/Plugin.tsx @@ -13,8 +13,10 @@ import PluginWorkflowJSParseClient from './features/js-parse'; import PluginWorkflowJsonParseClient from './features/json-parse'; import { PluginLoop } from './features/loop'; import { PluginManual } from './features/manual'; +import { PluginOmniTrigger } from './features/omni-trigger'; import { PluginParallel } from './features/parallel'; import { PluginRequest } from './features/request'; +import { PluginResponse } from './features/response'; import { PluginSql } from './features/sql'; import { PluginVariables } from './features/variables'; import { NAMESPACE } from './locale'; @@ -85,6 +87,8 @@ export class PluginWorkflow extends Plugin { await this.pm.add(PluginAPIRegularClient); await this.pm.add(PluginWorkflowInterceptor); await this.pm.add(PluginVariables); + await this.pm.add(PluginResponse); + await this.pm.add(PluginOmniTrigger); } async load() { @@ -131,16 +135,3 @@ export class PluginWorkflow extends Plugin { }); } } - -export * from './Branch'; -export * from './FlowContext'; -export * from './constants'; -export * from './nodes'; -export { Trigger, useTrigger } from './triggers'; -export * from './variable'; -export * from './components'; -export * from './utils'; -export * from './hooks'; -export { default as useStyles } from './style'; -export * from './variable'; -export * from './ExecutionContextProvider'; diff --git a/packages/plugins/@tachybase/plugin-workflow/src/client/features/action-trigger/index.ts b/packages/plugins/@tachybase/plugin-workflow/src/client/features/action-trigger/index.ts index 10e3934ba..e7f1840f5 100644 --- a/packages/plugins/@tachybase/plugin-workflow/src/client/features/action-trigger/index.ts +++ b/packages/plugins/@tachybase/plugin-workflow/src/client/features/action-trigger/index.ts @@ -33,12 +33,9 @@ const recordTriggerWorkflowActionInitializer: SchemaInitializerItemType = { schema: { title: '{{t("Submit to workflow", { ns: "workflow" })}}', 'x-component': 'Action', - 'x-component-props': { - useProps: '{{ useRecordTriggerWorkflowsActionProps }}', - }, + 'x-use-component-props': 'useRecordTriggerWorkflowsActionProps', 'x-designer': 'Action.Designer', 'x-action-settings': { - // assignedValues: {}, onSuccess: { manualClose: true, redirecting: false, diff --git a/packages/plugins/@tachybase/plugin-workflow/src/client/features/interceptor/WorkflowTriggerInterceptor.tsx b/packages/plugins/@tachybase/plugin-workflow/src/client/features/interceptor/WorkflowTriggerInterceptor.tsx index e44b11821..76d8873d3 100644 --- a/packages/plugins/@tachybase/plugin-workflow/src/client/features/interceptor/WorkflowTriggerInterceptor.tsx +++ b/packages/plugins/@tachybase/plugin-workflow/src/client/features/interceptor/WorkflowTriggerInterceptor.tsx @@ -95,9 +95,9 @@ export class WorkflowTriggerInterceptor extends Trigger { RadioWithTooltip, CheckboxGroupWithTooltip, }; - isActionTriggerable = (a, u) => { - const { global: m } = a; - return !m && !u.direct; + isActionTriggerable = (config, context) => { + const { global } = config; + return !global && !context.direct; }; useVariables(config, options) { diff --git a/packages/plugins/@tachybase/plugin-workflow/src/client/features/omni-trigger/index.tsx b/packages/plugins/@tachybase/plugin-workflow/src/client/features/omni-trigger/index.tsx new file mode 100644 index 000000000..9177c9f39 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-workflow/src/client/features/omni-trigger/index.tsx @@ -0,0 +1,297 @@ +import { + Plugin, + useActionContext, + useBlockRequestContext, + useCollectionDataSource, + useCollectionManager_deprecated, + useCollectValuesToSubmit, + useCompile, + useFilterByTk, +} from '@tachybase/client'; +import { useField, useFieldSchema, useForm } from '@tachybase/schema'; +import { isURL } from '@tachybase/utils/client'; + +import { App, message } from 'antd'; +import { useNavigate } from 'react-router-dom'; + +import { CollectionBlockInitializer } from '../../components'; +import { lang, tval } from '../../locale'; +import { PluginWorkflow } from '../../Plugin'; +import { Trigger } from '../../triggers'; +import { getCollectionFieldOptions, UseVariableOptions } from '../../variable'; + +class OmniAction extends Trigger { + title = tval('Custom action event'); + description = tval( + `When the "Trigger Workflow" button is clicked, the event is triggered based on the single piece of data where the button is located. For complex data processing that cannot be handled simply by NocoBase\\'s built-in operations (CRUD), you can define a series of operations through a workflow and trigger it with the "Trigger Workflow" button.`, + ); + fieldset = { + collection: { + type: 'string', + title: tval('Collection'), + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'DataSourceCollectionCascader', + 'x-reactions': [{ target: 'changed', effects: ['onFieldValueChange'], fulfill: { state: { value: [] } } }], + }, + appends: { + type: 'array', + title: tval('Associations to use'), + description: tval( + '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.', + ), + 'x-decorator': 'FormItem', + 'x-component': 'AppendsTreeSelect', + 'x-component-props': { + multiple: true, + useCollection() { + const form = useForm(); + return form.values?.collection; + }, + }, + 'x-reactions': [{ dependencies: ['collection'], fulfill: { state: { visible: '{{!!$deps[0]}}' } } }], + }, + }; + scope = { useCollectionDataSource }; + components = {}; + isActionTriggerable = (config, context) => { + return context.buttonAction === 'customize:triggerWorkflows'; + }; + useVariables(config: Record, options?: UseVariableOptions) { + // eslint-disable-next-line react-hooks/rules-of-hooks + const compile = useCompile(); + // eslint-disable-next-line react-hooks/rules-of-hooks + const { getCollectionFields: getCollectionFields } = useCollectionManager_deprecated(); + const m = getCollectionFieldOptions({ + appends: ['user'], + ...options, + fields: [ + { + collectionName: 'users', + name: 'user', + type: 'hasOne', + target: 'users', + uiSchema: { title: lang('User acted') }, + }, + ], + compile, + getCollectionFields, + }); + return [ + ...getCollectionFieldOptions({ + appends: ['data', ...(config.appends?.map((append) => `data.${append}`) || [])], + fields: [ + { + collectionName: config.collection, + name: 'data', + type: 'hasOne', + target: config.collection, + uiSchema: { title: lang('Trigger data') }, + }, + ], + compile, + getCollectionFields, + }), + ...m, + { label: lang('Role of user acted'), value: 'roleName' }, + ]; + } + useInitializers(config) { + return config.collection + ? { + name: 'triggerData', + type: 'item', + key: 'triggerData', + title: tval('Trigger data'), + Component: CollectionBlockInitializer, + collection: config.collection, + dataPath: '$context.data', + } + : null; + } +} +function useFormWorkflowCustomActionProps() { + const form = useForm(); + const { field, __parent, resource } = useBlockRequestContext(); + const { setVisible } = useActionContext(); + const filterByTk = useFilterByTk(); + const navigate = useNavigate(); + const fieldSchema = useFieldSchema(); + const _field = useField(); + const compile = useCompile(); + const { modal } = App.useApp(); + const collectionValuesToSubmit = useCollectValuesToSubmit(); + return ( + _field.componentProps.filterKeys, + { + async onClick() { + const { onSuccess, skipValidator, triggerWorkflows } = fieldSchema?.['x-action-settings'] || {}; + if (!skipValidator) { + await form.submit(); + } + const values = await collectionValuesToSubmit(); + (_field.data = field.data || {}), (_field.data.loading = true); + try { + const B = await resource.trigger({ + values, + filterByTk, + triggerWorkflows: triggerWorkflows?.length + ? triggerWorkflows + .map((workflow) => [workflow.workflowKey, workflow.context].filter(Boolean).join('!')) + .join(',') + : void 0, + }); + if ( + ((_field.data.data = B), + __parent?.service?.refresh?.(), + setVisible == null || setVisible(false), + !(onSuccess != null && onSuccess.successMessage)) + ) + return; + onSuccess != null && onSuccess.manualClose + ? modal.success({ + title: compile(onSuccess?.successMessage), + onOk: async () => { + await form.reset(); + return ( + onSuccess?.redirecting && + onSuccess?.redirectTo && + (isURL(onSuccess.redirectTo) + ? (window.location.href = onSuccess.redirectTo) + : navigate(onSuccess.redirectTo)) + ); + }, + }) + : message.success(compile(onSuccess == null ? void 0 : onSuccess.successMessage)); + } catch (B) { + console.error(B); + } finally { + _field.data.loading = false; + } + }, + } + ); +} +function useRecordWorkflowCustomTriggerActionProps() { + const compile = useCompile(); + const filterByTk = useFilterByTk(); + const _field = useField(); + const fieldSchema = useFieldSchema(); + const { field, resource } = useBlockRequestContext(); + const { setVisible, setSubmitted } = useActionContext() as any; + const { modal } = App.useApp(); + const navigate = useNavigate(); + const { onSuccess, triggerWorkflows } = fieldSchema?.['x-action-settings'] || {}; + return { + async onClick(N, w) { + (_field.data = field.data || {}), (_field.data.loading = true); + try { + if ( + (await resource.trigger({ + filterByTk: filterByTk, + triggerWorkflows: triggerWorkflows?.length + ? triggerWorkflows + .map((workflow) => [workflow.workflowKey, workflow.context].filter(Boolean).join('!')) + .join(',') + : void 0, + }), + w && w(), + setVisible == null || setVisible(false), + setSubmitted == null || setSubmitted(true), + !onSuccess?.successMessage) + ) + return; + if (onSuccess?.manualClose) { + modal.success({ + title: compile(onSuccess?.successMessage), + onOk() { + onSuccess?.redirecting && + onSuccess?.redirectTo && + (isURL(onSuccess.redirectTo) + ? (window.location.href = onSuccess.redirectTo) + : navigate(onSuccess.redirectTo)); + }, + }); + } else message.success(compile(onSuccess?.successMessage)); + } catch (error) { + console.error(error); + } finally { + _field.data.loading = false; + } + }, + }; +} +const triggerWorkflowItem = { + name: 'triggerWorkflow', + title: tval('Trigger workflow'), + Component: 'CustomizeActionInitializer', + schema: { + title: tval('Trigger workflow'), + 'x-component': 'Action', + 'x-use-component-props': 'useFormWorkflowCustomActionProps', + 'x-designer': 'Action.Designer', + 'x-action-settings': { + skipValidator: false, + onSuccess: { manualClose: true, redirecting: false, successMessage: tval('Submitted successfully') }, + triggerWorkflows: [], + }, + 'x-action': 'customize:triggerWorkflows', + }, +}; +const triggerWorkflowAction = { + name: 'triggerWorkflow', + title: tval('Trigger workflow'), + Component: 'CustomizeActionInitializer', + schema: { + title: tval('Trigger workflow'), + 'x-component': 'Action', + 'x-use-component-props': 'useRecordWorkflowCustomTriggerActionProps', + 'x-designer': 'Action.Designer', + 'x-action-settings': { + onSuccess: { + manualClose: true, + redirecting: false, + successMessage: tval('Submitted successfully'), + }, + triggerWorkflows: [], + }, + 'x-action': 'customize:triggerWorkflows', + }, +}; +const triggerWorkflowLinkItem = { + ...triggerWorkflowAction, + schema: { ...triggerWorkflowAction.schema, 'x-component': 'Action.Link' }, +}; +export class PluginOmniTrigger extends Plugin { + async load() { + this.app.pm.get('workflow').registerTrigger('custom-action', OmniAction); + this.app.addScopes({ + useFormWorkflowCustomActionProps, + useRecordWorkflowCustomTriggerActionProps, + }); + this.app.schemaInitializerManager + .get('FormActionInitializers') + .add('customize.triggerWorkflow', triggerWorkflowItem); + this.app.schemaInitializerManager + .get('createForm:configureActions') + .add('customize.triggerWorkflow', triggerWorkflowItem); + this.app.schemaInitializerManager + .get('editForm:configureActions') + .add('customize.triggerWorkflow', triggerWorkflowItem); + this.app.schemaInitializerManager + .get('detailsWithPaging:configureActions') + .add('customize.triggerWorkflow', triggerWorkflowAction); + this.app.schemaInitializerManager + .get('details:configureActions') + .add('customize.triggerWorkflow', triggerWorkflowAction); + this.app.schemaInitializerManager + .get('table:configureItemActions') + .add('customize.triggerWorkflow', triggerWorkflowLinkItem); + this.app.schemaInitializerManager + .get('gridCard:configureItemActions') + .add('customize.triggerWorkflow', triggerWorkflowLinkItem); + this.app.schemaInitializerManager + .get('list:configureItemActions') + .add('customize.triggerWorkflow', triggerWorkflowLinkItem); + } +} diff --git a/packages/plugins/@tachybase/plugin-workflow/src/client/features/response/index.ts b/packages/plugins/@tachybase/plugin-workflow/src/client/features/response/index.ts new file mode 100644 index 000000000..c491c4446 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-workflow/src/client/features/response/index.ts @@ -0,0 +1,37 @@ +import { Plugin } from '@tachybase/client'; + +import { RadioWithTooltip } from '../../components'; +import { tval } from '../../locale'; +import { Instruction } from '../../nodes'; +import { PluginWorkflow } from '../../Plugin'; +import { WorkflowVariableInput, WorkflowVariableTextArea } from '../../variable'; + +class ResponseInstruction extends Instruction { + title = tval('Response message'); + type = 'response-message'; + group = 'extended'; + description = tval('Add response message, will be send to client when process of request ends.'); + fieldset = { + message: { + type: 'string', + title: tval('Message content'), + description: tval('Supports variables in template.', { name: '{{name}}' }), + 'x-decorator': 'FormItem', + 'x-component': 'WorkflowVariableTextArea', + }, + }; + scope = {}; + components = { + RadioWithTooltip, + WorkflowVariableTextArea, + WorkflowVariableInput, + }; + isAvailable({ workflow }) { + return workflow.type === 'request-interception' || ('action' === workflow.type && workflow.sync); + } +} +export class PluginResponse extends Plugin { + async load() { + this.app.pm.get('workflow').registerInstruction('response-message', ResponseInstruction); + } +} diff --git a/packages/plugins/@tachybase/plugin-workflow/src/client/locale/index.ts b/packages/plugins/@tachybase/plugin-workflow/src/client/locale/index.ts index fecc25b3a..19e80d96f 100644 --- a/packages/plugins/@tachybase/plugin-workflow/src/client/locale/index.ts +++ b/packages/plugins/@tachybase/plugin-workflow/src/client/locale/index.ts @@ -11,5 +11,5 @@ export function useWorkflowTranslation() { return useTranslation(NAMESPACE); } -export const tval = (key: string) => nTval(key, { ns: NAMESPACE }); +export const tval = (key: string, opts={}) => nTval(key, { ns: NAMESPACE, ...opts }); diff --git a/packages/plugins/@tachybase/plugin-workflow/src/server/Plugin.ts b/packages/plugins/@tachybase/plugin-workflow/src/server/Plugin.ts index 105625ce3..6effa6ebe 100644 --- a/packages/plugins/@tachybase/plugin-workflow/src/server/Plugin.ts +++ b/packages/plugins/@tachybase/plugin-workflow/src/server/Plugin.ts @@ -18,8 +18,10 @@ import PluginWorkflowJSParseServer from './features/js-parse/plugin'; import PluginWorkflowJSONParseServer from './features/json-parse/plugin'; import { PluginLoop } from './features/loop/Plugin'; import { PluginManual } from './features/manual/Plugin'; +import { PluginOmniTrigger } from './features/omni-trigger'; import { PluginParallel } from './features/parallel/Plugin'; import { PluginRequest } from './features/request/Plugin'; +import { PluginResponse } from './features/response'; import { PluginSql } from './features/sql/Plugin'; import { PluginVariables } from './features/variables'; import initFunctions, { CustomFunction } from './functions'; @@ -72,6 +74,8 @@ export default class PluginWorkflowServer extends Plugin { pluginAPIRegular: PluginWorkflowAPIRegularServer; pluginInterception: PluginInterception; pluginVariables: PluginVariables; + pluginResponse: PluginResponse; + pluginOmni: PluginOmniTrigger; constructor(app: Application, options?: PluginOptions) { super(app, options); @@ -89,6 +93,8 @@ export default class PluginWorkflowServer extends Plugin { this.pluginAPIRegular = new PluginWorkflowAPIRegularServer(app, options); this.pluginInterception = new PluginInterception(app, options); this.pluginVariables = new PluginVariables(app, options); + this.pluginResponse = new PluginResponse(app, options); + this.pluginOmni = new PluginOmniTrigger(app, options); } getLogger(workflowId: ID): Logger { @@ -317,6 +323,8 @@ export default class PluginWorkflowServer extends Plugin { await this.pluginAPIRegular.load(); await this.pluginInterception.load(); await this.pluginVariables.load(); + await this.pluginResponse.load(); + await this.pluginOmni.load(); } toggle(workflow: WorkflowModel, enable?: boolean) { diff --git a/packages/plugins/@tachybase/plugin-workflow/src/server/features/omni-trigger/CustomActionTrigger.ts b/packages/plugins/@tachybase/plugin-workflow/src/server/features/omni-trigger/CustomActionTrigger.ts new file mode 100644 index 000000000..ff131206d --- /dev/null +++ b/packages/plugins/@tachybase/plugin-workflow/src/server/features/omni-trigger/CustomActionTrigger.ts @@ -0,0 +1,132 @@ +import { joinCollectionName, parseCollectionName } from '@tachybase/data-source-manager'; +import PluginErrorHandler from '@tachybase/plugin-error-handler'; + +import _ from 'lodash'; + +import { EXECUTION_STATUS } from '../../constants'; +import Trigger from '../../triggers'; +import { toJSON } from '../../utils'; + +class CustomActionInterceptionError extends Error { + status = 400; + messages = []; + constructor(message) { + super(message); + this.name = 'CustomActionInterceptionError'; + } +} +export class OmniTrigger extends Trigger { + static TYPE = 'custom-action'; + triggerAction = async (context, next) => { + const { + resourceName, + actionName, + params: { filterByTk, values, triggerWorkflows = '' }, + } = context.action; + if (actionName !== 'trigger' || resourceName === 'workflows') { + return next(); + } + const { currentUser, currentRole } = context.state; + const { model: UserModel } = this.workflow.db.getCollection('users'); + const userInfo = { + user: UserModel.build(currentUser).desensitize(), + roleName: currentRole, + }; + const dataSourceHeader = context.get('x-data-source'); + const jointCollectionName = joinCollectionName(dataSourceHeader, resourceName); + const triggerWorkflowsMap = new Map(); + const triggerWorkflowsArray = []; + for (const trigger of triggerWorkflows.split(',')) { + const [key, path] = trigger.split('!'); + triggerWorkflowsMap.set(key, path); + triggerWorkflowsArray.push(key); + } + const workflows = Array.from(this.workflow.enabledCache.values()) + .filter((item) => item.type === OmniTrigger.TYPE && item.config.collection === jointCollectionName) + .sort((a, b) => { + const aIndex = triggerWorkflowsArray.indexOf(a.key); + const bIndex = triggerWorkflowsArray.indexOf(b.key); + if (aIndex === -1 && bIndex === -1) { + return a.id - b.id; + } + if (aIndex === -1) { + return 1; + } + if (bIndex === -1) { + return -1; + } + return aIndex - bIndex; + }); + const syncGroup = []; + const asyncGroup = []; + for (const workflow of workflows) { + const { appends = [] } = workflow.config; + const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection); + const dataPath = triggerWorkflowsMap.get(workflow.key); + const event = [workflow]; + const { repository } = context.app.dataSourceManager.dataSources + .get(dataSourceName) + .collectionManager.getCollection(collectionName); + const formData = dataPath ? _.get(values, dataPath) : values; + let data = formData; + if (filterByTk != null) { + data = await repository.findOne({ filterByTk, appends }); + if (!data) { + continue; + } + Object.assign(data, formData); + } + // @ts-ignore + event.push({ data: toJSON(data), ...userInfo }); + (workflow.sync ? syncGroup : asyncGroup).push(event); + } + for (const event of syncGroup) { + const processor = await this.workflow.trigger(event[0], event[1], { httpContext: context }); + if (!processor) { + return context.throw(500); + } + const { lastSavedJob, nodesMap } = processor; + const lastNode = nodesMap.get(lastSavedJob?.nodeId); + if (processor.execution.status === EXECUTION_STATUS.RESOLVED) { + if (lastNode?.type === 'end') { + return; + } + continue; + } + if (processor.execution.status < EXECUTION_STATUS.STARTED) { + if (lastNode?.type !== 'end') { + return context.throw(500, 'Workflow on your action failed, please contact the administrator'); + } + const err = new CustomActionInterceptionError('Request is intercepted by workflow'); + err.status = 400; + err.messages = context.state.messages; + return context.throw(err.status, err); + } + return context.throw(500, 'Workflow on your action hangs, please contact the administrator'); + } + for (const event of asyncGroup) { + this.workflow.trigger(event[0], event[1]); + } + await next(); + }; + constructor(workflow) { + super(workflow); + this.workflow.app.dataSourceManager.afterAddDataSource((dataSource) => { + if (!dataSource.collectionManager?.db) { + return; + } + dataSource.resourceManager.registerActionHandler('trigger', this.triggerAction); + }); + workflow.app.pm.get(PluginErrorHandler).errorHandler.register( + (err) => err instanceof CustomActionInterceptionError, + async (err, ctx) => { + ctx.body = { + errors: err.messages, + }; + ctx.status = err.status; + }, + ); + } + on() {} + off() {} +} diff --git a/packages/plugins/@tachybase/plugin-workflow/src/server/features/omni-trigger/Plugin.ts b/packages/plugins/@tachybase/plugin-workflow/src/server/features/omni-trigger/Plugin.ts new file mode 100644 index 000000000..96de47713 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-workflow/src/server/features/omni-trigger/Plugin.ts @@ -0,0 +1,11 @@ +import { Plugin } from '@tachybase/server'; + +import PluginWorkflowServer from '../../Plugin'; +import { OmniTrigger } from './CustomActionTrigger'; + +export class PluginOmniTrigger extends Plugin { + async load() { + const workflowPlugin = this.app.pm.get(PluginWorkflowServer) as PluginWorkflowServer; + workflowPlugin.registerTrigger('custom-action', OmniTrigger); + } +} diff --git a/packages/plugins/@tachybase/plugin-workflow/src/server/features/omni-trigger/index.ts b/packages/plugins/@tachybase/plugin-workflow/src/server/features/omni-trigger/index.ts new file mode 100644 index 000000000..7f95bfcb7 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-workflow/src/server/features/omni-trigger/index.ts @@ -0,0 +1 @@ +export * from './Plugin'; diff --git a/packages/plugins/@tachybase/plugin-workflow/src/server/features/response/Plugin.ts b/packages/plugins/@tachybase/plugin-workflow/src/server/features/response/Plugin.ts new file mode 100644 index 000000000..7d921886f --- /dev/null +++ b/packages/plugins/@tachybase/plugin-workflow/src/server/features/response/Plugin.ts @@ -0,0 +1,11 @@ +import { Plugin } from '@tachybase/server'; + +import PluginWorkflowServer from '../../Plugin'; +import { ResponseInstruction } from './ResponseMessageInstruction'; + +export class PluginResponse extends Plugin { + async load() { + const workflowPlugin = this.app.pm.get(PluginWorkflowServer) as PluginWorkflowServer; + workflowPlugin.registerInstruction('response-message', ResponseInstruction); + } +} diff --git a/packages/plugins/@tachybase/plugin-workflow/src/server/features/response/ResponseMessageInstruction.ts b/packages/plugins/@tachybase/plugin-workflow/src/server/features/response/ResponseMessageInstruction.ts new file mode 100644 index 000000000..08c17179e --- /dev/null +++ b/packages/plugins/@tachybase/plugin-workflow/src/server/features/response/ResponseMessageInstruction.ts @@ -0,0 +1,22 @@ +import { JOB_STATUS } from '../../constants'; +import Instruction from '../../instructions'; + +export class ResponseInstruction extends Instruction { + async run(node, prevJob, processor) { + const { httpContext } = processor.options; + if (!httpContext.state) { + httpContext.state = {}; + } + if (!httpContext.state.messages) { + httpContext.state.messages = []; + } + const message = processor.getParsedValue(node.config.message, node.id); + if (message) { + httpContext.state.messages.push({ message }); + } + return { + status: JOB_STATUS.RESOLVED, + result: message, + }; + } +} diff --git a/packages/plugins/@tachybase/plugin-workflow/src/server/features/response/index.ts b/packages/plugins/@tachybase/plugin-workflow/src/server/features/response/index.ts new file mode 100644 index 000000000..7f95bfcb7 --- /dev/null +++ b/packages/plugins/@tachybase/plugin-workflow/src/server/features/response/index.ts @@ -0,0 +1 @@ +export * from './Plugin';