From 4d4a70b2f4723ed840953886b191dbfae6380df9 Mon Sep 17 00:00:00 2001 From: Junyi Date: Fri, 2 Feb 2024 10:32:27 +0800 Subject: [PATCH] refactor(plugin-workflow): support any context in processor as options (#3473) * refactor(plugin-workflow): support any context in processor as options * fix(plugin-workflow): fix collection trigger * fix(plugin-workflow): fix collection trigger and instructions * fix(plugin-workflow): fix bind workflow configuration on button * fix(plugin-workflow): fix e2e test cases --- .../antd/action/Action.Designer.tsx | 60 +++++++++++++++---- .../appends-tree-select/AppendsTreeSelect.tsx | 25 ++++---- .../antd/remote-select/RemoteSelect.tsx | 7 ++- .../items/BulkDestroyActionInitializer.tsx | 3 + .../items/DestroyActionInitializer.tsx | 3 + .../src/client/FormTrigger.tsx | 2 +- .../src/client/__e2e__/configuration.test.ts | 4 +- .../src/server/FormTrigger.ts | 9 +-- .../components/CheckboxGroupWithTooltip.tsx | 40 +++++++++++++ .../src/client/components/index.ts | 1 + .../src/client/triggers/index.tsx | 2 +- .../plugin-workflow/src/server/Plugin.ts | 12 ++-- .../plugin-workflow/src/server/Processor.ts | 1 + .../server/instructions/CreateInstruction.ts | 1 + .../src/server/triggers/CollectionTrigger.ts | 25 ++------ 15 files changed, 133 insertions(+), 62 deletions(-) create mode 100644 packages/plugins/@nocobase/plugin-workflow/src/client/components/CheckboxGroupWithTooltip.tsx 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 c26425041..f06489bc4 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 @@ -3,7 +3,7 @@ import { Field, onFieldValueChange } from '@formily/core'; import { ISchema, connect, mapProps, useField, useFieldSchema, useForm, useFormEffects } from '@formily/react'; import { isValid, uid } from '@formily/shared'; import { Alert, Tree as AntdTree, ModalProps } from 'antd'; -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { RemoteSelect, useCompile, useDesignable } from '../..'; import { useApp } from '../../../application'; @@ -28,6 +28,7 @@ import { import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; import { useLinkageAction } from './hooks'; import { requestSettingsSchema } from './utils'; +import { useFormBlockContext } from '../../../block-provider'; const Tree = connect( AntdTree, @@ -447,13 +448,26 @@ function RemoveButton( ); } -function WorkflowSelect({ types, ...props }) { +function WorkflowSelect({ actionType, ...props }) { const { t } = useTranslation(); const index = ArrayTable.useIndex(); const { setValuesIn } = useForm(); const baseCollection = useCollection(); const { getCollection } = useCollectionManager(); const [workflowCollection, setWorkflowCollection] = useState(baseCollection.name); + + const workflowPlugin = usePlugin('workflow') as any; + const workflowTypes = useMemo( + () => + workflowPlugin + .getTriggersOptions() + .filter((item) => { + return typeof item.options.isActionTriggerable === 'function' || item.options.isActionTriggerable === true; + }) + .map((item) => item.value), + [workflowPlugin], + ); + useFormEffects(() => { onFieldValueChange(`group[${index}].context`, (field) => { let collection: CollectionOptions = baseCollection; @@ -472,6 +486,20 @@ function WorkflowSelect({ types, ...props }) { }); }); + const optionFilter = useCallback( + ({ type, config }) => { + const trigger = workflowPlugin.triggers.get(type); + if (trigger.isActionTriggerable === true) { + return true; + } + if (typeof trigger.isActionTriggerable === 'function') { + return trigger.isActionTriggerable(config, { action: actionType }); + } + return false; + }, + [workflowPlugin, actionType], + ); + return ( ); @@ -501,23 +530,26 @@ function WorkflowConfig() { const { t } = useTranslation(); const fieldSchema = useFieldSchema(); const { name: collection } = useCollection(); - const workflowPlugin = usePlugin('workflow') as any; - const workflowTypes = workflowPlugin.getTriggersOptions().filter((item) => { - return typeof item.options.useActionTriggerable === 'function' - ? item.options.useActionTriggerable() - : Boolean(item.options.useActionTriggerable); - }); + // TODO(refactor): should refactor for getting certain action type, better from 'x-action'. + const formBlock = useFormBlockContext(); + const actionType = formBlock?.type || fieldSchema['x-action']; + const description = { - submit: t('Workflow will be triggered after submitting succeeded.', { ns: 'workflow' }), - 'customize:save': t('Workflow will be triggered after saving succeeded.', { ns: 'workflow' }), + submit: t('Workflow will be triggered before or after submitting succeeded based on workflow type.', { + ns: 'workflow', + }), + 'customize:save': t('Workflow will be triggered before or after saving succeeded based on workflow type.', { + ns: 'workflow', + }), 'customize:triggerWorkflows': t( 'Workflow will be triggered directly once the button clicked, without data saving.', { ns: 'workflow' }, ), + destroy: t('Workflow will be triggered before deleting succeeded.', { ns: 'workflow' }), }[fieldSchema?.['x-action']]; return ( - item.value), + placeholder: t('Select workflow', { ns: 'workflow' }), + actionType, }, required: true, }, diff --git a/packages/core/client/src/schema-component/antd/appends-tree-select/AppendsTreeSelect.tsx b/packages/core/client/src/schema-component/antd/appends-tree-select/AppendsTreeSelect.tsx index d1104c3c6..5357f7272 100644 --- a/packages/core/client/src/schema-component/antd/appends-tree-select/AppendsTreeSelect.tsx +++ b/packages/core/client/src/schema-component/antd/appends-tree-select/AppendsTreeSelect.tsx @@ -1,6 +1,6 @@ import { CloseCircleFilled } from '@ant-design/icons'; import { Tag, TreeSelect } from 'antd'; -import type { DefaultOptionType } from 'rc-tree-select/es/TreeSelect'; +import type { DefaultOptionType, TreeSelectProps } from 'rc-tree-select/es/TreeSelect'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { CollectionFieldOptions, useCollectionManager, useCompile } from '../../..'; @@ -72,7 +72,7 @@ function getCollectionFieldOptions(this: CallScope, collection, parentNode?): Tr }); } -export const AppendsTreeSelect: React.FC = (props) => { +export const AppendsTreeSelect: React.FC = (props) => { const { title, value: propsValue, @@ -81,6 +81,7 @@ export const AppendsTreeSelect: React.FC = (props) => { useCollection = usePropsCollection, filter = trueFilter, rootOption, + loadData: propsLoadData, ...restProps } = props; const { getCollectionFields } = useCollectionManager(); @@ -98,6 +99,9 @@ export const AppendsTreeSelect: React.FC = (props) => { const loadData = useCallback( async (option) => { + if (propsLoadData !== null) { + return propsLoadData(option); + } if (!option.isLeaf && option.loadChildren) { const children = option.loadChildren(option); setOptionsMap((prev) => { @@ -105,7 +109,7 @@ export const AppendsTreeSelect: React.FC = (props) => { }); } }, - [setOptionsMap], + [propsLoadData], ); useEffect(() => { @@ -119,17 +123,16 @@ export const AppendsTreeSelect: React.FC = (props) => { isLeaf: false, } : null; - const treeData = getCollectionFieldOptions.call( - { compile, getCollectionFields, filter }, - baseCollection, - parentNode, - ); + const treeData = + propsLoadData === null + ? [] + : getCollectionFieldOptions.call({ compile, getCollectionFields, filter }, baseCollection, parentNode); const map = treeData.reduce((result, item) => Object.assign(result, { [item.value]: item }), {}); if (parentNode) { map[parentNode.value] = parentNode; } setOptionsMap(map); - }, [collection, baseCollection, rootOption, filter]); + }, [collection, baseCollection, rootOption, filter, propsLoadData]); useEffect(() => { const arr = (props.multiple ? propsValue : propsValue ? [propsValue] : []) as string[]; @@ -217,8 +220,8 @@ export const AppendsTreeSelect: React.FC = (props) => { const valueKeys: string[] = props.multiple ? (propsValue as string[]) : propsValue != null - ? [propsValue as string] - : []; + ? [propsValue as string] + : []; return ( = SelectProps & { targetField?: any; service: ResourceActionOptions

; CustomDropdownRender?: (v: any) => any; + optionFilter?: (option: any) => boolean; }; const InternalRemoteSelect = connect( @@ -37,6 +38,7 @@ const InternalRemoteSelect = connect( mapOptions, targetField: _targetField, CustomDropdownRender, + optionFilter, ...others } = props; const [open, setOpen] = useState(false); @@ -192,8 +194,9 @@ const InternalRemoteSelect = connect( } const valueOptions = (v != null && (Array.isArray(v) ? v : [{ ...v, [fieldNames.value]: v[fieldNames.value] || v }])) || []; - return uniqBy(data?.data?.concat(valueOptions ?? []), fieldNames.value); - }, [value, defaultValue, data?.data, fieldNames.value]); + const filtered = typeof optionFilter === 'function' ? data.data.filter(optionFilter) : data.data; + return uniqBy(filtered.concat(valueOptions ?? []), fieldNames.value); + }, [value, defaultValue, data?.data, fieldNames.value, optionFilter]); const onDropdownVisibleChange = (visible) => { setOpen(visible); searchData.current = null; diff --git a/packages/core/client/src/schema-initializer/items/BulkDestroyActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/BulkDestroyActionInitializer.tsx index f3faab157..c8a3245e2 100644 --- a/packages/core/client/src/schema-initializer/items/BulkDestroyActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/BulkDestroyActionInitializer.tsx @@ -22,6 +22,9 @@ export const BulkDestroyActionInitializer = (props) => { }, useProps: '{{ useBulkDestroyActionProps }}', }, + 'x-action-settings': { + triggerWorkflows: [], + }, }; if (collection) { schema['x-acl-action'] = `${collection.name}:destroy`; diff --git a/packages/core/client/src/schema-initializer/items/DestroyActionInitializer.tsx b/packages/core/client/src/schema-initializer/items/DestroyActionInitializer.tsx index e43d1fa3e..6afcd0566 100644 --- a/packages/core/client/src/schema-initializer/items/DestroyActionInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/DestroyActionInitializer.tsx @@ -16,6 +16,9 @@ export const DestroyActionInitializer = (props) => { }, useProps: '{{ useDestroyActionProps }}', }, + 'x-action-settings': { + triggerWorkflows: [], + }, }; return ; }; diff --git a/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/FormTrigger.tsx b/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/FormTrigger.tsx index fd1ba08bd..551a3a691 100644 --- a/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/FormTrigger.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/FormTrigger.tsx @@ -59,7 +59,7 @@ export default class extends Trigger { scope = { useCollectionDataSource, }; - useActionTriggerable = true; + isActionTriggerable = true; useVariables(config, options) { // eslint-disable-next-line react-hooks/rules-of-hooks const compile = useCompile(); diff --git a/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/__e2e__/configuration.test.ts b/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/__e2e__/configuration.test.ts index 8ea0ae543..33721f737 100644 --- a/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/__e2e__/configuration.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/client/__e2e__/configuration.test.ts @@ -93,7 +93,7 @@ test.describe('Configuration page to configure the Trigger node', () => { await page.getByRole('button', { name: 'plus Add workflow' }).click(); await page.getByRole('button', { name: 'Select workflow' }).click(); await page.getByRole('option', { name: workFlowName }).click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Submit', exact: true }).click(); // 2、测试步骤:进入“数据区块”-“添加”按钮,填写表单,点击“确定”按钮 const fieldData = triggerNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString(); @@ -187,7 +187,7 @@ test.describe('Configuration page to configure the Trigger node', () => { await page.getByRole('button', { name: 'plus Add workflow' }).click(); await page.getByRole('button', { name: 'Select workflow' }).click(); await page.getByRole('option', { name: workFlowName }).click(); - await page.getByRole('button', { name: 'OK', exact: true }).click(); + await page.getByRole('button', { name: 'Submit', exact: true }).click(); // 2、测试步骤:进入“数据区块”-“添加”按钮,填写表单,点击“确定”按钮 const fieldData = triggerNodeFieldDisplayName + dayjs().format('YYYYMMDDHHmmss.SSS').toString(); diff --git a/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/server/FormTrigger.ts b/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/server/FormTrigger.ts index a55f5ac31..07acc6041 100644 --- a/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/server/FormTrigger.ts +++ b/packages/plugins/@nocobase/plugin-workflow-form-trigger/src/server/FormTrigger.ts @@ -116,14 +116,7 @@ export default class extends Trigger { } for (const event of syncGroup) { - await this.workflow.trigger(event[0], event[1]); - // if (processor.execution.status < EXECUTION_STATUS.STARTED) { - // // error handling - // return context.throw( - // 500, - // 'Your data saved, but some workflow on your action failed, please contact the administrator.', - // ); - // } + await this.workflow.trigger(event[0], event[1], { httpContext: context }); } for (const event of asyncGroup) { diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/components/CheckboxGroupWithTooltip.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/components/CheckboxGroupWithTooltip.tsx new file mode 100644 index 000000000..32af5ee7f --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/CheckboxGroupWithTooltip.tsx @@ -0,0 +1,40 @@ +import { QuestionCircleOutlined } from '@ant-design/icons'; +import { css, useCompile } from '@nocobase/client'; +import { Checkbox, Space, Tooltip } from 'antd'; +import React from 'react'; + +export interface CheckboxGroupWithTooltipOption { + value: any; + label: string; + tooltip?: string; +} + +export function CheckboxGroupWithTooltip(props) { + const { options = [], direction, ...other } = props; + const compile = useCompile(); + + return ( + + + {options.map((option) => ( + + + {compile(option.label)} + + {option.tooltip && ( + + + + )} + + ))} + + + ); +} diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/components/index.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/components/index.ts index 3190699da..bdcf67aa7 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/components/index.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/index.ts @@ -2,4 +2,5 @@ export * from './CollectionBlockInitializer'; export * from './FieldsSelect'; export * from './FilterDynamicComponent'; export * from './RadioWithTooltip'; +export * from './CheckboxGroupWithTooltip'; export * from './ValueBlock'; diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/index.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/index.tsx index ec9e7d171..5149f6d0f 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/triggers/index.tsx @@ -65,7 +65,7 @@ export abstract class Trigger { components?: { [key: string]: any }; useInitializers?(config): SchemaInitializerItemType | null; initializers?: any; - useActionTriggerable?: boolean | (() => boolean); + isActionTriggerable?: boolean | ((config: object, context?: object) => boolean); } function TriggerExecution() { diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts index dfdf3726d..fd62c35a9 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts @@ -190,6 +190,7 @@ export default class PluginWorkflowServer extends Plugin { 'workflows.nodes:*', 'executions:list', 'executions:get', + 'executions:cancel', 'flow_nodes:update', 'flow_nodes:destroy', ], @@ -239,8 +240,8 @@ export default class PluginWorkflowServer extends Plugin { }); this.app.on('beforeStop', async () => { - const collection = db.getCollection('workflows'); - const workflows = await collection.repository.find({ + const repository = db.getRepository('workflows'); + const workflows = await repository.find({ filter: { enabled: true }, }); @@ -280,7 +281,7 @@ export default class PluginWorkflowServer extends Plugin { public trigger( workflow: WorkflowModel, context: object, - options: { context?: any } & Transactionable = {}, + options: { [key: string]: any } & Transactionable = {}, ): void | Promise { const logger = this.getLogger(workflow.id); if (!this.ready) { @@ -298,7 +299,8 @@ export default class PluginWorkflowServer extends Plugin { return this.triggerSync(workflow, context, options); } - this.events.push([workflow, context, { context: options.context }]); + const { transaction, ...rest } = options; + this.events.push([workflow, context, rest]); this.eventsCount = this.events.length; logger.info(`new event triggered, now events: ${this.events.length}`); @@ -317,7 +319,7 @@ export default class PluginWorkflowServer extends Plugin { private async triggerSync( workflow: WorkflowModel, context: object, - options: { context?: any } & Transactionable = {}, + options: { [key: string]: any } & Transactionable = {}, ): Promise { let execution; try { diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts index aa60555f7..798dce40c 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts @@ -9,6 +9,7 @@ import type { ExecutionModel, FlowNodeModel, JobModel } from './types'; export interface ProcessorOptions extends Transactionable { plugin: Plugin; + [key: string]: any; } export default class Processor { diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/instructions/CreateInstruction.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/instructions/CreateInstruction.ts index d8f1790aa..9095032d3 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/instructions/CreateInstruction.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/instructions/CreateInstruction.ts @@ -10,6 +10,7 @@ export class CreateInstruction extends Instruction { const { repository, model } = (node.constructor).database.getCollection(collection); const options = processor.getParsedValue(params, node.id); + const created = await repository.create({ ...options, context: { diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/CollectionTrigger.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/CollectionTrigger.ts index abe6de525..eb5f5b7ce 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/CollectionTrigger.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/CollectionTrigger.ts @@ -88,20 +88,13 @@ async function handler(this: CollectionTrigger, workflow: WorkflowModel, data: M if (workflow.sync) { await this.workflow.trigger( workflow, - { data: json }, + { data: json, stack: context?.stack }, { - context, transaction, }, ); } else { - this.workflow.trigger( - workflow, - { data: json }, - { - context, - }, - ); + this.workflow.trigger(workflow, { data: json, stack: context?.stack }); } } @@ -155,15 +148,11 @@ export default class CollectionTrigger extends Trigger { } } - async validateEvent( - workflow: WorkflowModel, - context: any, - options: { context?: { stack?: number[] } } & Transactionable, - ): Promise { - if (options.context?.stack) { + async validateEvent(workflow: WorkflowModel, context: any, options: Transactionable): Promise { + if (context.stack) { const existed = await workflow.countExecutions({ where: { - id: options.context.stack, + id: context.stack, }, transaction: options.transaction, }); @@ -172,13 +161,11 @@ export default class CollectionTrigger extends Trigger { this.workflow .getLogger(workflow.id) .warn( - `workflow ${workflow.id} has already been triggered in stack executions (${options.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; } - - context.stack = options.context.stack; } return true;