From 46129499be0e38051f315def4e7053414421b7dc Mon Sep 17 00:00:00 2001 From: sealday Date: Tue, 30 Apr 2024 16:27:50 +0800 Subject: [PATCH] feat: support workflow bulk (#858) Co-authored-by: sealday Reviewed-on: https://git.daoyoucloud.com/daoyoucloud/tachybase/pulls/858 --- .../client/src/schema-component/antd/index.ts | 1 + .../plugin-approval/src/server/actions.ts | 2 +- .../plugins/@hera/plugin-core/package.json | 10 +- .../filter-form/FormFilterScope.tsx | 2 +- .../OutboundLinkActionInitializer.tsx | 33 +--- .../outbound}/OutboundPage.tsx | 2 +- .../src/client/features/outbound/index.ts | 33 ++++ .../outbound}/useOutboundActionProps.ts | 2 +- .../features/workflow-bulk/ApiTrigger.tsx | 142 +++++++++++++++++ .../BulkUpdateAction.Settings.tsx | 104 +++++++++++++ .../BulkUpdateActionInitializer.tsx | 30 ++++ .../CustomizeActionInitializer.tsx | 7 + .../client/features/workflow-bulk/index.tsx | 37 +++++ .../client/features/workflow-bulk/utils.tsx | 102 ++++++++++++ .../WorkflowTriggerInterceptor.tsx | 145 ++++++++++++++++++ .../features/workflow-interceptor/index.ts | 10 ++ .../@hera/plugin-core/src/client/index.tsx | 23 ++- .../src/client/pages/AdminLayout.tsx | 2 +- .../src/client/pages/DetailsPage.tsx | 11 +- .../plugin-core/src/client/pages/index.ts | 2 +- .../plugin-core/src/client/pages/style.ts | 2 +- .../src/client/schema-initializer/index.ts | 2 +- .../src/client/schema-settings/index.tsx | 3 +- .../@hera/plugin-core/src/locale/en-US.json | 15 ++ .../@hera/plugin-core/src/locale/zh-CN.json | 15 ++ .../src/server/features/ApiTrigger.ts | 7 + .../features/RequestInterceptionTrigger.ts | 114 ++++++++++++++ .../src/server/features/interception.ts | 12 ++ .../@hera/plugin-core/src/server/plugin.ts | 6 +- .../src/server/actions/workflows.ts | 13 +- pnpm-lock.yaml | 84 ++++++++-- 31 files changed, 889 insertions(+), 84 deletions(-) rename packages/plugins/@hera/plugin-core/src/client/{schema-initializer/actions => features/outbound}/OutboundLinkActionInitializer.tsx (56%) rename packages/plugins/@hera/plugin-core/src/client/{pages => features/outbound}/OutboundPage.tsx (89%) create mode 100644 packages/plugins/@hera/plugin-core/src/client/features/outbound/index.ts rename packages/plugins/@hera/plugin-core/src/client/{schema-initializer/actions/hooks => features/outbound}/useOutboundActionProps.ts (92%) create mode 100644 packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/ApiTrigger.tsx create mode 100644 packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/BulkUpdateAction.Settings.tsx create mode 100644 packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/BulkUpdateActionInitializer.tsx create mode 100644 packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/CustomizeActionInitializer.tsx create mode 100644 packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/index.tsx create mode 100644 packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/utils.tsx create mode 100644 packages/plugins/@hera/plugin-core/src/client/features/workflow-interceptor/WorkflowTriggerInterceptor.tsx create mode 100644 packages/plugins/@hera/plugin-core/src/client/features/workflow-interceptor/index.ts create mode 100644 packages/plugins/@hera/plugin-core/src/server/features/ApiTrigger.ts create mode 100644 packages/plugins/@hera/plugin-core/src/server/features/RequestInterceptionTrigger.ts create mode 100644 packages/plugins/@hera/plugin-core/src/server/features/interception.ts diff --git a/packages/core/client/src/schema-component/antd/index.ts b/packages/core/client/src/schema-component/antd/index.ts index ff8194fe9..0499c8fc5 100644 --- a/packages/core/client/src/schema-component/antd/index.ts +++ b/packages/core/client/src/schema-component/antd/index.ts @@ -53,5 +53,6 @@ export * from './variable'; export * from './custom-cascader'; export * from './unixTimestamp'; export * from './nanoIDInput'; +export { PageHeader } from '@ant-design/pro-layout'; import './index.less'; diff --git a/packages/plugins/@hera/plugin-approval/src/server/actions.ts b/packages/plugins/@hera/plugin-approval/src/server/actions.ts index 3c23be238..52e29362e 100644 --- a/packages/plugins/@hera/plugin-approval/src/server/actions.ts +++ b/packages/plugins/@hera/plugin-approval/src/server/actions.ts @@ -30,7 +30,7 @@ const approvals = { const workflow = await context.db.getRepository('workflows').findOne({ filterByTk: workflowId, }); - if (!(workflow == null ? void 0 : workflow.enabled)) { + if (!workflow?.enabled) { return context.throw(400, 'Current workflow not found or disabled, please refresh and try again'); } if (status !== APPROVAL_STATUS.DRAFT) { diff --git a/packages/plugins/@hera/plugin-core/package.json b/packages/plugins/@hera/plugin-core/package.json index c7daf1e4f..1ed4c1231 100644 --- a/packages/plugins/@hera/plugin-core/package.json +++ b/packages/plugins/@hera/plugin-core/package.json @@ -2,7 +2,7 @@ "name": "@hera/plugin-core", "displayName": "Hera platform", "version": "1.7.28", - "description": "Hera platform as nocobase plugin.", + "description": "Hera platform", "keywords": [ "System management" ], @@ -11,13 +11,13 @@ "@ant-design/cssinjs": "^1.11.1", "@ant-design/icons": "~5.3.6", "@ant-design/pro-layout": "^7.16.11", - "@emotion/css": "^11.11.2", + "@formily/antd-v5": "1.x", "@react-pdf/renderer": "^3.3.2", "@tachybase/components": "workspace:*", "@tachybase/schema": "workspace:*", + "@types/lodash": "~4.17.0", "ahooks": "^3.7.2", "antd": "5.16.1", - "antd-style": "3.4.5", "classnames": "^2.3.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.8", @@ -50,7 +50,9 @@ "@nocobase/plugin-acl": "workspace:*", "@nocobase/server": "workspace:*", "@nocobase/test": "workspace:*", - "@nocobase/utils": "workspace:*" + "@nocobase/utils": "workspace:*", + "@tachybase/plugin-workflow": "workspace:*", + "@tachybase/schema": "workspace:*" }, "description.zh-CN": "提供标准赫拉平台能力", "displayName.zh-CN": "赫拉平台" diff --git a/packages/plugins/@hera/plugin-core/src/client/components/filter-form/FormFilterScope.tsx b/packages/plugins/@hera/plugin-core/src/client/components/filter-form/FormFilterScope.tsx index 74d2f3cbc..3ce2b7e12 100644 --- a/packages/plugins/@hera/plugin-core/src/client/components/filter-form/FormFilterScope.tsx +++ b/packages/plugins/@hera/plugin-core/src/client/components/filter-form/FormFilterScope.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/css'; +import { css } from '@nocobase/client'; import { ArrayCollapse } from '@tachybase/components'; import { Form } from '@tachybase/schema'; import { observer, useField, useFieldSchema } from '@tachybase/schema'; diff --git a/packages/plugins/@hera/plugin-core/src/client/schema-initializer/actions/OutboundLinkActionInitializer.tsx b/packages/plugins/@hera/plugin-core/src/client/features/outbound/OutboundLinkActionInitializer.tsx similarity index 56% rename from packages/plugins/@hera/plugin-core/src/client/schema-initializer/actions/OutboundLinkActionInitializer.tsx rename to packages/plugins/@hera/plugin-core/src/client/features/outbound/OutboundLinkActionInitializer.tsx index fca81b467..73b7a04ba 100644 --- a/packages/plugins/@hera/plugin-core/src/client/schema-initializer/actions/OutboundLinkActionInitializer.tsx +++ b/packages/plugins/@hera/plugin-core/src/client/features/outbound/OutboundLinkActionInitializer.tsx @@ -1,39 +1,12 @@ import React from 'react'; -import { ActionInitializer, Application } from '@nocobase/client'; +import { ActionInitializer } from '@nocobase/client'; import { Popover, Space, Button, Input } from 'antd'; import { useFieldSchema } from '@tachybase/schema'; import { useProps } from '@nocobase/client'; import { ShareAltOutlined } from '@ant-design/icons'; -import { lang, tval, useTranslation } from '../../locale'; -import { useOutboundActionProps } from './hooks/useOutboundActionProps'; +import { tval, useTranslation } from '../../locale'; -export class OutboundActionHelper { - constructor(private app: Application) {} - - async load() { - this.app.addScopes({ - useOutboundActionProps, - }); - this.app.addComponents({ - OutboundButton, - OutboundLinkActionInitializer, - }); - const outboundItem = { - type: 'item', - name: 'enableActions.outbound', - title: lang('outbound'), - Component: 'OutboundLinkActionInitializer', - schema: { - 'x-align': 'right', - }, - }; - this.app.schemaInitializerManager.addItem('table:configureActions', outboundItem.name, outboundItem); - this.app.schemaInitializerManager.addItem('details:configureActions', outboundItem.name, outboundItem); - this.app.schemaInitializerManager.addItem('kanban:configureActions', outboundItem.name, outboundItem); - } -} - -export const OutboundLinkActionInitializer: React.FC = (props) => { +export const OutboundLinkActionInitializer = (props) => { const schema = { title: tval('outbound'), 'x-action': 'outbound', diff --git a/packages/plugins/@hera/plugin-core/src/client/pages/OutboundPage.tsx b/packages/plugins/@hera/plugin-core/src/client/features/outbound/OutboundPage.tsx similarity index 89% rename from packages/plugins/@hera/plugin-core/src/client/pages/OutboundPage.tsx rename to packages/plugins/@hera/plugin-core/src/client/features/outbound/OutboundPage.tsx index c0d9d7fee..d6f164ec6 100644 --- a/packages/plugins/@hera/plugin-core/src/client/pages/OutboundPage.tsx +++ b/packages/plugins/@hera/plugin-core/src/client/features/outbound/OutboundPage.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { AdminProvider, RemoteCollectionManagerProvider, RemoteSchemaComponent } from '@nocobase/client'; import { useParams } from 'react-router-dom'; -export const OutboundPage: React.FC = () => { +export const OutboundPage = () => { const params = useParams(); return ( diff --git a/packages/plugins/@hera/plugin-core/src/client/features/outbound/index.ts b/packages/plugins/@hera/plugin-core/src/client/features/outbound/index.ts new file mode 100644 index 000000000..0c3e83b5d --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/client/features/outbound/index.ts @@ -0,0 +1,33 @@ +import { Plugin } from '@nocobase/client'; +import { OutboundPage } from './OutboundPage'; +import { OutboundButton, OutboundLinkActionInitializer } from './OutboundLinkActionInitializer'; +import { lang } from '../../locale'; +import { useOutboundActionProps } from './useOutboundActionProps'; + +export class PluginOutbound extends Plugin { + async load() { + this.app.router.add('outbound', { + path: '/r/:id', + Component: OutboundPage, + }); + this.app.addScopes({ + useOutboundActionProps, + }); + this.app.addComponents({ + OutboundButton, + OutboundLinkActionInitializer, + }); + const outboundItem = { + type: 'item', + name: 'enableActions.outbound', + title: lang('outbound'), + Component: 'OutboundLinkActionInitializer', + schema: { + 'x-align': 'right', + }, + }; + this.app.schemaInitializerManager.addItem('table:configureActions', outboundItem.name, outboundItem); + this.app.schemaInitializerManager.addItem('details:configureActions', outboundItem.name, outboundItem); + this.app.schemaInitializerManager.addItem('kanban:configureActions', outboundItem.name, outboundItem); + } +} diff --git a/packages/plugins/@hera/plugin-core/src/client/schema-initializer/actions/hooks/useOutboundActionProps.ts b/packages/plugins/@hera/plugin-core/src/client/features/outbound/useOutboundActionProps.ts similarity index 92% rename from packages/plugins/@hera/plugin-core/src/client/schema-initializer/actions/hooks/useOutboundActionProps.ts rename to packages/plugins/@hera/plugin-core/src/client/features/outbound/useOutboundActionProps.ts index 6b1c02cf2..cd889d2ab 100644 --- a/packages/plugins/@hera/plugin-core/src/client/schema-initializer/actions/hooks/useOutboundActionProps.ts +++ b/packages/plugins/@hera/plugin-core/src/client/features/outbound/useOutboundActionProps.ts @@ -1,7 +1,7 @@ import { message } from 'antd'; import { useFieldSchema } from '@tachybase/schema'; import copy from 'copy-to-clipboard'; -import { useTranslation } from '../../../locale'; +import { useTranslation } from '../../locale'; export const useOutboundActionProps = () => { let schema = useFieldSchema(); diff --git a/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/ApiTrigger.tsx b/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/ApiTrigger.tsx new file mode 100644 index 000000000..42062ece2 --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/ApiTrigger.tsx @@ -0,0 +1,142 @@ +import { useCompile, useCollectionManager_deprecated, useCollectionDataSource } from '@nocobase/client'; +import { lang, tval } from '../../locale'; + +import { + Trigger, + CollectionBlockInitializer, + getCollectionFieldOptions, + FieldsSelect, + RadioWithTooltip, + CheckboxGroupWithTooltip, +} from '@tachybase/plugin-workflow/client'; + +const enum ACTION_TYPES { + CREATE = 'create', + UPDATE = 'update', + UPSERT = 'updateOrCreate', + DESTROY = 'destroy', +} + +function useItems(item, options) { + const compile = useCompile(); + const { getCollectionFields } = useCollectionManager_deprecated(); + return [ + { label: lang('ID'), value: 'filterByTk' }, + ...(item.action !== ACTION_TYPES.DESTROY + ? [ + { + label: lang('Values submitted'), + value: 'values', + children: getCollectionFieldOptions({ + ...options, + appends: null, + depth: 3, + collection: item.collection, + compile, + getCollectionFields, + }), + }, + ] + : []), + ]; +} +export class ApiTrigger extends Trigger { + title = lang('API event'); + description = lang('api trigger'); + fieldset = { + collection: { + type: 'string', + title: tval('Collection'), + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'DataSourceCollectionCascader', + 'x-reactions': [{ target: 'changed', effects: ['onFieldValueChange'], fulfill: { state: { value: [] } } }], + }, + global: { + type: 'boolean', + title: tval('Trigger mode'), + 'x-decorator': 'FormItem', + 'x-component': 'RadioWithTooltip', + 'x-component-props': { + direction: 'vertical', + options: [ + { + label: tval('Local mode, triggered before executing the actions bound to this workflow'), + value: false, + }, + { + label: tval('Global mode, triggered before executing the following actions'), + value: true, + }, + ], + }, + default: false, + }, + actions: { + type: 'number', + title: tval('Select actions'), + 'x-decorator': 'FormItem', + 'x-component': 'CheckboxGroupWithTooltip', + 'x-component-props': { + direction: 'vertical', + options: [ + { label: tval('Create record action'), value: ACTION_TYPES.CREATE }, + { label: tval('Update record action'), value: ACTION_TYPES.UPDATE }, + { label: tval('Delete record action'), value: ACTION_TYPES.DESTROY }, + ], + }, + required: true, + 'x-reactions': [ + { dependencies: ['collection', 'global'], fulfill: { state: { visible: '{{!!$deps[0] && !!$deps[1]}}' } } }, + ], + }, + }; + scope = { useCollectionDataSource }; + components = { + FieldsSelect: FieldsSelect, + RadioWithTooltip: RadioWithTooltip, + CheckboxGroupWithTooltip: CheckboxGroupWithTooltip, + }; + isActionTriggerable = (a, u) => { + const { global: m } = a; + return !m && !u.direct; + }; + + useVariables(config, options) { + // eslint-disable-next-line react-hooks/rules-of-hooks + const compile = useCompile(); + // eslint-disable-next-line react-hooks/rules-of-hooks + const { getCollectionFields } = useCollectionManager_deprecated(); + const result = getCollectionFieldOptions({ + appends: ['user'], + ...options, + fields: [ + { + collectionName: 'users', + name: 'user', + type: 'hasOne', + target: 'users', + uiSchema: { title: tval('User acted') }, + }, + ], + compile, + getCollectionFields, + }); + // eslint-disable-next-line react-hooks/rules-of-hooks + const parametersSchema = [{ label: lang('Parameters'), value: 'params', children: useItems(config, options) }]; + return [...result, { label: lang('Role of user acted'), value: 'roleName' }, ...parametersSchema]; + } + useInitializers(item) { + return item.collection + ? { + name: 'triggerData', + type: 'item', + key: 'triggerData', + title: tval('Trigger data'), + Component: CollectionBlockInitializer, + collection: item.collection, + dataPath: '$context.params.values', + } + : null; + } +} diff --git a/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/BulkUpdateAction.Settings.tsx b/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/BulkUpdateAction.Settings.tsx new file mode 100644 index 000000000..11355aabd --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/BulkUpdateAction.Settings.tsx @@ -0,0 +1,104 @@ +import { + ActionDesigner, + SchemaSettings, + SchemaSettingsItemType, + useDesignable, + useRequest, + useSchemaToolbar, +} from '@nocobase/client'; +import { useFieldSchema } from '@tachybase/schema'; +import { useTranslation } from 'react-i18next'; + +const schemaSettingsItems: SchemaSettingsItemType[] = [ + { + name: 'editButton', + Component: ActionDesigner.ButtonEditor, + useComponentProps() { + const { buttonEditorProps } = useSchemaToolbar(); + return buttonEditorProps; + }, + }, + { + name: 'Date', + type: 'select', + useComponentProps() { + const { dn } = useDesignable(); + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + return { + title: t('Data will be updated'), + options: [ + { label: t('Selected'), value: 'selected' }, + { label: t('All'), value: 'all' }, + ], + value: fieldSchema['x-action-settings']?.['updateMode'], + onChange: (value) => { + fieldSchema['x-action-settings']['updateMode'] = value; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-action-settings': fieldSchema['x-action-settings'], + }, + }); + }, + }; + }, + }, + { + name: 'Bind workflow', + type: 'select', + useComponentProps() { + const { dn } = useDesignable(); + const { t } = useTranslation(); + const fieldSchema = useFieldSchema(); + const { data } = useRequest({ + resource: 'workflows', + action: 'list', + params: { + filter: { + type: 'api', + }, + }, + }); + // FIXME 不会实时生效 + const options = + data?.data?.map((workflow) => ({ + label: workflow.title, + value: workflow.id, + })) ?? []; + return { + title: t('Bind workflow'), + value: fieldSchema['x-action-settings'].bindWorkflow, + options: options.concat({ + label: t('disabled'), + value: false, + }), + onChange(value) { + fieldSchema['x-action-settings'].bindWorkflow = value; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + 'x-action-settings': fieldSchema['x-action-settings'], + }, + }); + }, + }; + }, + }, + { + name: 'remove', + sort: 100, + Component: ActionDesigner.RemoveButton as any, + useComponentProps() { + const { removeButtonProps } = useSchemaToolbar(); + return removeButtonProps; + }, + }, +]; + +const bulkWorkflowActionSettings = new SchemaSettings({ + name: 'actionSettings:bulkWorkflow', + items: schemaSettingsItems, +}); + +export { bulkWorkflowActionSettings }; diff --git a/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/BulkUpdateActionInitializer.tsx b/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/BulkUpdateActionInitializer.tsx new file mode 100644 index 000000000..04b600b44 --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/BulkUpdateActionInitializer.tsx @@ -0,0 +1,30 @@ +import { BlockInitializer, useSchemaInitializerItem } from '@nocobase/client'; +import React from 'react'; +import { tval } from '../../locale'; + +export const BulkWorkflowActionInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + const schema = { + type: 'void', + title: tval('Bulk workflow'), + 'x-component': 'Action', + 'x-use-component-props': 'useCustomizeBulkWorkflowActionProps', + 'x-align': 'right', + 'x-acl-action': 'update', + 'x-decorator': 'ACLActionProvider', + 'x-acl-action-props': { + skipScopeCheck: true, + }, + 'x-action': 'customize:bulkWorkflow', + 'x-toolbar': 'ActionSchemaToolbar', + 'x-settings': 'actionSettings:bulkWorkflow', + 'x-action-settings': { + bindWorkflow: false, + updateMode: 'selected', + }, + 'x-component-props': { + icon: 'CarryOutOutlined', + }, + }; + return ; +}; diff --git a/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/CustomizeActionInitializer.tsx b/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/CustomizeActionInitializer.tsx new file mode 100644 index 000000000..482095fbc --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/CustomizeActionInitializer.tsx @@ -0,0 +1,7 @@ +import { BlockInitializer, useSchemaInitializerItem } from '@nocobase/client'; +import React from 'react'; + +export const CustomizeActionInitializer = () => { + const itemConfig = useSchemaInitializerItem(); + return ; +}; diff --git a/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/index.tsx b/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/index.tsx new file mode 100644 index 000000000..097a244eb --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/index.tsx @@ -0,0 +1,37 @@ +import { Plugin, useCollection } from '@nocobase/client'; +import { tval } from '../../locale'; +import { BulkWorkflowActionInitializer } from './BulkUpdateActionInitializer'; +import { bulkWorkflowActionSettings } from './BulkUpdateAction.Settings'; +import { CustomizeActionInitializer } from './CustomizeActionInitializer'; +import { useCustomizeBulkWorkflowActionProps } from './utils'; +import PluginWorkflow from '@tachybase/plugin-workflow/client'; +import { ApiTrigger } from './ApiTrigger'; + +export class PluginWorkflowBulk extends Plugin { + async load() { + this.app.addComponents({ CustomizeActionInitializer }); + this.app.addScopes({ useCustomizeBulkWorkflowActionProps }); + this.app.schemaSettingsManager.add(bulkWorkflowActionSettings); + + const initializerData = { + title: tval('Bulk workflow'), + Component: BulkWorkflowActionInitializer, + name: 'bulkWorkflow', + useVisible() { + const collection = useCollection(); + return ( + (collection.template !== 'view' || collection?.writableView) && + collection.template !== 'file' && + collection.template !== 'sql' + ); + }, + }; + + ['table', 'details'].forEach((block) => { + this.app.schemaInitializerManager.addItem(block + ':configureActions', 'customize.bulkWorkflow', initializerData); + }); + + const plugin = this.app.pm.get(PluginWorkflow) as PluginWorkflow; + plugin.registerTrigger('api', ApiTrigger); + } +} diff --git a/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/utils.tsx b/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/utils.tsx new file mode 100644 index 000000000..f16496c27 --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/client/features/workflow-bulk/utils.tsx @@ -0,0 +1,102 @@ +import { SchemaExpressionScopeContext, useField, useFieldSchema } from '@tachybase/schema'; +import { + isVariable, + transformVariableValue, + useBlockRequestContext, + useCollection_deprecated, + useCompile, + useLocalVariables, + useRecord, + useRequest, + useTableBlockContext, + useVariables, +} from '@nocobase/client'; +import { App } from 'antd'; +import { useContext } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useTranslation } from '../../locale'; + +export const useCustomizeBulkWorkflowActionProps = () => { + const { field, resource, __parent, service } = useBlockRequestContext(); + const expressionScope = useContext(SchemaExpressionScopeContext); + const actionSchema = useFieldSchema(); + const tableBlockContext = useTableBlockContext(); + const { rowKey } = tableBlockContext; + const selectedRecordKeys = + tableBlockContext.field?.data?.selectedRowKeys ?? expressionScope?.selectedRecordKeys ?? {}; + const navigate = useNavigate(); + const compile = useCompile(); + const { t } = useTranslation(); + const actionField: any = useField(); + const { modal } = App.useApp(); + const variables = useVariables(); + const record = useRecord(); + const { name, getField } = useCollection_deprecated(); + const localVariables = useLocalVariables(); + const { run } = useRequest( + { + resource: 'workflows', + action: 'trigger', + }, + { + manual: true, + }, + ); + + return { + async onClick() { + const { + bindWorkflow = false, + assignedValues: originalAssignedValues = {}, + updateMode, + } = actionSchema?.['x-action-settings'] ?? {}; + actionField.data = field.data || {}; + actionField.data.loading = true; + + if (!bindWorkflow) { + return modal.info({ + title: t('Not bind workflow!'), + }); + } + + const assignedValues = {}; + const waitList = Object.keys(originalAssignedValues).map(async (key) => { + const value = originalAssignedValues[key]; + const collectionField = getField(key); + + if (process.env.NODE_ENV !== 'production') { + if (!collectionField) { + throw new Error(`useCustomizeBulkUpdateActionProps: field "${key}" not found in collection "${name}"`); + } + } + + if (isVariable(value)) { + const result = await variables?.parseVariable(value, localVariables); + if (result) { + assignedValues[key] = transformVariableValue(result, { targetCollectionField: collectionField }); + } + } else if (value != null && value !== '') { + assignedValues[key] = value; + } + }); + await Promise.all(waitList); + + modal.confirm({ + title: t('Bulk update', { ns: 'client' }), + content: + updateMode === 'selected' + ? t('Update selected data?', { ns: 'client' }) + : t('Update all data?', { ns: 'client' }), + async onOk() { + run({ + filterByTk: bindWorkflow, + }); + actionField.data.loading = false; + }, + async onCancel() { + actionField.data.loading = false; + }, + }); + }, + }; +}; diff --git a/packages/plugins/@hera/plugin-core/src/client/features/workflow-interceptor/WorkflowTriggerInterceptor.tsx b/packages/plugins/@hera/plugin-core/src/client/features/workflow-interceptor/WorkflowTriggerInterceptor.tsx new file mode 100644 index 000000000..ff3debdf4 --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/client/features/workflow-interceptor/WorkflowTriggerInterceptor.tsx @@ -0,0 +1,145 @@ +import { useCompile, useCollectionManager_deprecated, useCollectionDataSource } from '@nocobase/client'; +import { lang, tval } from '../../locale'; + +import { + Trigger, + CollectionBlockInitializer, + getCollectionFieldOptions, + FieldsSelect, + RadioWithTooltip, + CheckboxGroupWithTooltip, +} from '@tachybase/plugin-workflow/client'; + +const enum ACTION_TYPES { + CREATE = 'create', + UPDATE = 'update', + UPSERT = 'updateOrCreate', + DESTROY = 'destroy', +} + +function useItems(item, options) { + const compile = useCompile(); + const { getCollectionFields } = useCollectionManager_deprecated(); + return [ + { label: lang('ID'), value: 'filterByTk' }, + ...(item.action !== ACTION_TYPES.DESTROY + ? [ + { + label: lang('Values submitted'), + value: 'values', + children: getCollectionFieldOptions({ + ...options, + appends: null, + depth: 3, + collection: item.collection, + compile, + getCollectionFields, + }), + }, + ] + : []), + ]; +} +export class WorkflowTriggerInterceptor extends Trigger { + sync = true; + title = lang('Pre-action event'); + description = lang( + 'Triggered before the execution of a request initiated through an action button or API, such as before adding, updating, or deleting data. Suitable for data validation and logic judgment before action, and the request could be rejected by using the "End process" node.', + ); + fieldset = { + collection: { + type: 'string', + title: tval('Collection'), + required: true, + 'x-decorator': 'FormItem', + 'x-component': 'DataSourceCollectionCascader', + 'x-reactions': [{ target: 'changed', effects: ['onFieldValueChange'], fulfill: { state: { value: [] } } }], + }, + global: { + type: 'boolean', + title: tval('Trigger mode'), + 'x-decorator': 'FormItem', + 'x-component': 'RadioWithTooltip', + 'x-component-props': { + direction: 'vertical', + options: [ + { + label: tval('Local mode, triggered before executing the actions bound to this workflow'), + value: false, + }, + { + label: tval('Global mode, triggered before executing the following actions'), + value: true, + }, + ], + }, + default: false, + }, + actions: { + type: 'number', + title: tval('Select actions'), + 'x-decorator': 'FormItem', + 'x-component': 'CheckboxGroupWithTooltip', + 'x-component-props': { + direction: 'vertical', + options: [ + { label: tval('Create record action'), value: ACTION_TYPES.CREATE }, + { label: tval('Update record action'), value: ACTION_TYPES.UPDATE }, + { label: tval('Delete record action'), value: ACTION_TYPES.DESTROY }, + ], + }, + required: true, + 'x-reactions': [ + { dependencies: ['collection', 'global'], fulfill: { state: { visible: '{{!!$deps[0] && !!$deps[1]}}' } } }, + ], + }, + }; + scope = { useCollectionDataSource }; + components = { + FieldsSelect: FieldsSelect, + RadioWithTooltip: RadioWithTooltip, + CheckboxGroupWithTooltip: CheckboxGroupWithTooltip, + }; + isActionTriggerable = (a, u) => { + const { global: m } = a; + return !m && !u.direct; + }; + + useVariables(config, options) { + // eslint-disable-next-line react-hooks/rules-of-hooks + const compile = useCompile(); + // eslint-disable-next-line react-hooks/rules-of-hooks + const { getCollectionFields } = useCollectionManager_deprecated(); + const result = getCollectionFieldOptions({ + appends: ['user'], + ...options, + fields: [ + { + collectionName: 'users', + name: 'user', + type: 'hasOne', + target: 'users', + uiSchema: { title: tval('User acted') }, + }, + ], + compile, + getCollectionFields, + }); + // eslint-disable-next-line react-hooks/rules-of-hooks + const parametersSchema = [{ label: lang('Parameters'), value: 'params', children: useItems(config, options) }]; + return [...result, { label: lang('Role of user acted'), value: 'roleName' }, ...parametersSchema]; + } + useInitializers(item) { + return item.collection + ? { + name: 'triggerData', + type: 'item', + key: 'triggerData', + title: tval('Trigger data'), + Component: CollectionBlockInitializer, + collection: item.collection, + dataPath: '$context.params.values', + } + : null; + } +} diff --git a/packages/plugins/@hera/plugin-core/src/client/features/workflow-interceptor/index.ts b/packages/plugins/@hera/plugin-core/src/client/features/workflow-interceptor/index.ts new file mode 100644 index 000000000..92615fafd --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/client/features/workflow-interceptor/index.ts @@ -0,0 +1,10 @@ +import { Plugin } from '@nocobase/client'; +import PluginWorkflow from '@tachybase/plugin-workflow/client'; +import { WorkflowTriggerInterceptor } from './WorkflowTriggerInterceptor'; + +export class PluginWorkflowInterceptor extends Plugin { + async load() { + const plugin = this.app.pm.get(PluginWorkflow) as PluginWorkflow; + plugin.registerTrigger('request-interception', WorkflowTriggerInterceptor); + } +} diff --git a/packages/plugins/@hera/plugin-core/src/client/index.tsx b/packages/plugins/@hera/plugin-core/src/client/index.tsx index fe0d14f60..b6d5592ff 100644 --- a/packages/plugins/@hera/plugin-core/src/client/index.tsx +++ b/packages/plugins/@hera/plugin-core/src/client/index.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { Plugin, RemoteSchemaTemplateManagerProvider, EditTitleField, useCollection } from '@nocobase/client'; -import { remove } from 'lodash'; +import { Plugin, EditTitleField } from '@nocobase/client'; import { useFieldSchema } from '@tachybase/schema'; import { isValid } from '@tachybase/schema'; import { autorun } from '@tachybase/schema'; @@ -16,7 +15,7 @@ import { import { useCreateActionProps } from './schema-initializer/actions/hooks/useCreateActionProps'; import { useGetCustomAssociatedComponents } from './hooks/useGetCustomAssociatedComponents'; import { useGetCustomComponents } from './hooks/useGetCustomComponents'; -import { AdminLayout, DetailsPage, HomePage, OutboundPage, PageLayout } from './pages'; +import { AdminLayout, DetailsPage, HomePage, PageLayout } from './pages'; import { PluginSettingsHelper } from './settings-manager-components'; import { AssociatedFieldInterface, @@ -39,12 +38,7 @@ import { ExcelFile, } from './components'; import { AutoComplete } from './schema-components'; -import { - CreateSubmitActionInitializer, - GroupBlockPlugin, - OutboundActionHelper, - SettingBlockInitializer, -} from './schema-initializer'; +import { CreateSubmitActionInitializer, GroupBlockPlugin, SettingBlockInitializer } from './schema-initializer'; import { SheetBlock, SheetBlockInitializer, @@ -67,8 +61,11 @@ import { PluginHeraVersion } from './features/hera-version'; import { PluginAssistant } from './features/assistant'; import { TstzrangeFieldInterface } from './interfaces/TstzrangeFieldInterface'; import { PluginContextMenu } from './features/context-menu'; +import { PluginWorkflowBulk } from './features/workflow-bulk'; +import { PluginWorkflowInterceptor } from './features/workflow-interceptor'; import { PluginPDF } from './features/pdf'; import { PluginExtendedFilterForm } from './features/extended-filter-form'; +import { PluginOutbound } from './features/outbound'; export { usePDFViewerRef } from './schema-initializer'; export * from './components/custom-components/custom-components'; @@ -83,8 +80,11 @@ export class PluginCoreClient extends Plugin { await this.app.pm.add(PluginHeraVersion); await this.app.pm.add(PluginContextMenu); await this.app.pm.add(PluginAssistant); + await this.app.pm.add(PluginWorkflowBulk); + await this.app.pm.add(PluginWorkflowInterceptor); await this.app.pm.add(PluginPDF); await this.app.pm.add(PluginExtendedFilterForm); + await this.app.pm.add(PluginOutbound); } async registerSettings() { @@ -162,10 +162,6 @@ export class PluginCoreClient extends Plugin { } async registerRouters() { - this.app.router.add('outbound', { - path: '/r/:id', - element: , - }); this.app.router.remove('root'); this.app.router.add('home', { path: '/', @@ -242,7 +238,6 @@ export class PluginCoreClient extends Plugin { async load() { this.locale = new Locale(this.app); - await new OutboundActionHelper(this.app).load(); await new PluginSettingsHelper(this.app).load(); await this.registerScopesAndComponents(); await this.registerSettings(); diff --git a/packages/plugins/@hera/plugin-core/src/client/pages/AdminLayout.tsx b/packages/plugins/@hera/plugin-core/src/client/pages/AdminLayout.tsx index 18b73f651..f62e13bc7 100644 --- a/packages/plugins/@hera/plugin-core/src/client/pages/AdminLayout.tsx +++ b/packages/plugins/@hera/plugin-core/src/client/pages/AdminLayout.tsx @@ -1,4 +1,4 @@ -import { css } from '@emotion/css'; +import { css } from '@nocobase/client'; import { useSessionStorageState } from 'ahooks'; import { App, Layout } from 'antd'; import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'; diff --git a/packages/plugins/@hera/plugin-core/src/client/pages/DetailsPage.tsx b/packages/plugins/@hera/plugin-core/src/client/pages/DetailsPage.tsx index ea9123379..c6369c8f7 100644 --- a/packages/plugins/@hera/plugin-core/src/client/pages/DetailsPage.tsx +++ b/packages/plugins/@hera/plugin-core/src/client/pages/DetailsPage.tsx @@ -1,15 +1,8 @@ import React from 'react'; -import { - CollectionRecordProvider, - DataBlockProvider, - RecordProvider, - RemoteSchemaComponent, - css, -} from '@nocobase/client'; +import { DataBlockProvider, RecordProvider, RemoteSchemaComponent, css, PageHeader } from '@nocobase/client'; import { useNavigate, useParams } from 'react-router-dom'; import { pathToRegexp } from 'path-to-regexp'; import { useHeadStyles } from './style'; -import { PageHeader as AntdPageHeader } from '@ant-design/pro-layout'; export const DetailsPage: React.FC = () => { const params = useParams(); @@ -32,7 +25,7 @@ export const DetailsPage: React.FC = () => { `} >
- { diff --git a/packages/plugins/@hera/plugin-core/src/client/pages/index.ts b/packages/plugins/@hera/plugin-core/src/client/pages/index.ts index 82bd00c35..88c6a2d55 100644 --- a/packages/plugins/@hera/plugin-core/src/client/pages/index.ts +++ b/packages/plugins/@hera/plugin-core/src/client/pages/index.ts @@ -1,5 +1,5 @@ export * from './AdminLayout'; export * from './DetailsPage'; export * from './Home'; -export * from './OutboundPage'; +export * from '../features/outbound/OutboundPage'; export * from './PageLayout'; diff --git a/packages/plugins/@hera/plugin-core/src/client/pages/style.ts b/packages/plugins/@hera/plugin-core/src/client/pages/style.ts index bca63fba7..b96f87065 100644 --- a/packages/plugins/@hera/plugin-core/src/client/pages/style.ts +++ b/packages/plugins/@hera/plugin-core/src/client/pages/style.ts @@ -1,4 +1,4 @@ -import { createStyles } from 'antd-style'; +import { createStyles } from '@nocobase/client'; export const useStyles = createStyles(({ css }) => ({ home: css` diff --git a/packages/plugins/@hera/plugin-core/src/client/schema-initializer/index.ts b/packages/plugins/@hera/plugin-core/src/client/schema-initializer/index.ts index ba2bfdfd0..ea2e8bd10 100644 --- a/packages/plugins/@hera/plugin-core/src/client/schema-initializer/index.ts +++ b/packages/plugins/@hera/plugin-core/src/client/schema-initializer/index.ts @@ -1,6 +1,6 @@ export * from './items/CustomFilterFormItemInitializer'; export * from './blocks/PDFVIewerBlockInitializer'; -export * from './actions/OutboundLinkActionInitializer'; +export * from '../features/outbound/OutboundLinkActionInitializer'; export * from './actions/CreateSubmitActionInitializer'; export * from './blocks/SettingBlockInitializer'; export * from './blocks/GroupBlockInitializer'; diff --git a/packages/plugins/@hera/plugin-core/src/client/schema-settings/index.tsx b/packages/plugins/@hera/plugin-core/src/client/schema-settings/index.tsx index 1d44c575f..ee7266c6b 100644 --- a/packages/plugins/@hera/plugin-core/src/client/schema-settings/index.tsx +++ b/packages/plugins/@hera/plugin-core/src/client/schema-settings/index.tsx @@ -15,7 +15,6 @@ import { useDesignable, useFormBlockContext, useFormBlockType, - useLinkageCollectionFilterOptions, useLocalVariables, useRecord, useSchemaTemplateManager, @@ -24,7 +23,7 @@ import { VariableScopeProvider, } from '@nocobase/client'; import _ from 'lodash'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from '../locale'; import { FormFilterScope } from '../components/filter-form/FormFilterScope'; import { useFieldComponents } from '../schema-initializer'; diff --git a/packages/plugins/@hera/plugin-core/src/locale/en-US.json b/packages/plugins/@hera/plugin-core/src/locale/en-US.json index 3e72d98ed..b770edded 100644 --- a/packages/plugins/@hera/plugin-core/src/locale/en-US.json +++ b/packages/plugins/@hera/plugin-core/src/locale/en-US.json @@ -12,10 +12,13 @@ "Configure": "Configure", "Copy Failed": "Copy Failed", "Copy embedded link": "Copy embedded link", + "Copy link": "Copy link", "Copy successful": "Copy successful", + "Create a record": "Create a record", "Custom filter": "Custom filter", "Custom option label": "Custom option label", "Delete department": "Delete department", + "Delete": "Delete", "Department name": "Department name", "Department": "Department", "Departments management": "Departments management", @@ -37,6 +40,7 @@ "Owners": "Owners", "Page style": "Page Style", "Pagination": "Pagination", + "Parameters": "Parameters", "Pick a data entry for viewing and editing": "Pick a data entry for viewing and editing", "Please select departments": "Please select departments", "Please select members": "Please select members", @@ -45,6 +49,9 @@ "Remove departments": "Remove departments", "Remove member": "Remove member", "Remove members": "Remove members", + "Request inteception": "Request inteception", + "Request to intercept": "Request to intercept", + "Role of user acted": "Role of user acted", "Roles management": "Roles management", "Search for departments, users": "Search for departments, users", "Search results": "Search results", @@ -56,15 +63,23 @@ "System setting": "System setting", "The department has members, please remove them first": "The department has members, please remove them first", "The department has sub-departments, please delete them first": "The department has sub-departments, please delete them first", + "The following selected requests all trigger interception on submission": "The following selected requests all trigger interception on submission", "Third-party integration configuration": "Third-party integration configuration", "This field is currently not supported for use in form blocks.": "This field is currently not supported for use in form blocks.", "This month": "This month", "This year": "This year", + "Trigger mode": "Trigger mode", + "Triggered before submitting a create, update, or delete request to a collection, so that data validation, logical judgment, etc., can be performed before executing the request, and then either approving the request or rejecting the request by using the \"End process\" node.": "Triggered before submitting a create, update, or delete request to a collection, so that data validation, logical judgment, etc., can be performed before executing the request, and then either approving the request or rejecting the request by using the \"End process\" node.", + "Triggered only when a form bound to this workflow is submitted": "Triggered only when a form bound to this workflow is submitted", + "Update a record": "Update a record", + "User acted": "User acted", + "Values submitted": "Values submitted", "classical": "classical", "component": "component", "default": "default", "error": "error", "loading...": "loading...", + "outbound": "outbound", "preview block": "preview block", "tabs": "tabs", "year": "year" diff --git a/packages/plugins/@hera/plugin-core/src/locale/zh-CN.json b/packages/plugins/@hera/plugin-core/src/locale/zh-CN.json index f4553bf34..a4b53d190 100644 --- a/packages/plugins/@hera/plugin-core/src/locale/zh-CN.json +++ b/packages/plugins/@hera/plugin-core/src/locale/zh-CN.json @@ -12,21 +12,26 @@ "Configure": "配置", "Copy Failed": "复制失败", "Copy embedded link": "复制嵌入链接", + "Copy link": "复制链接", "Copy successful": "复制成功", + "Create record action": "创建记录操作", "Custom filter": "自定义筛选", "Custom option label": "自定义选项标签", "Delete department": "删除部门", + "Delete record action": "删除记录操作", "Department name": "部门名称", "Department": "部门", "Departments management": "部门管理", "Departments": "部门", "Edit department": "编辑部门", + "Global mode, triggered before executing the following actions": "全局模式,以下操作执行前都触发", "Group block": "汇总区块", "Group": "汇总", "Hera integration": "赫拉集成", "HomePage Config": "主页配置", "Last month": "上月", "Link manager": "链接管理器", + "Local mode, triggered before executing the actions bound to this workflow": "局部模式,绑定该工作流的操作执行前触发", "Main department": "主属部门", "Main": "主属部门", "Mobile UI": "移动端页面", @@ -37,17 +42,21 @@ "Owners": "负责人", "Page style": "页面版本", "Pagination": "分页", + "Parameters": "参数", "Pick a data entry for viewing and editing": "选择一条数据用于查看与编辑", "Please select departments": "请选择部门", "Please select members": "请选择成员", "Please select": "请选择", + "Pre-action event": "操作前事件", "Remove department": "移除部门", "Remove departments": "移除部门", "Remove member": "移除成员", "Remove members": "移除成员", + "Role of user acted": "操作者角色", "Roles management": "角色管理", "Search for departments, users": "搜索部门、用户", "Search results": "搜索结果", + "Select actions": "选择操作", "Select submit data type": "增量提交", "Set as main department": "设置为主属部门", "Signature input": "手写签名", @@ -60,11 +69,17 @@ "This field is currently not supported for use in form blocks.": "该字段目前不支持在表单区块中使用。", "This month": "本月", "This year": "今年", + "Trigger mode": "触发模式", + "Triggered before the execution of a request initiated through an action button or API, such as before adding, updating, or deleting data. Suitable for data validation and logic judgment before action, and the request could be rejected by using the \"End process\" node.": "通过操作按钮或 API 发起请求并在执行前触发,比如新增、更新、删除数据之前。适用于在操作前进行数据验证、逻辑判断,并可通过“结束节点”来拦截请求。", + "Update record action": "更新记录操作", + "User acted": "操作者", + "Values submitted": "提交的值对象", "classical": "经典版", "component": "组件", "default": "默认", "error": "出错了", "loading...": "加载中...", + "outbound": "外链", "preview block": "预览区块", "tabs": "多标签", "year": "年" diff --git a/packages/plugins/@hera/plugin-core/src/server/features/ApiTrigger.ts b/packages/plugins/@hera/plugin-core/src/server/features/ApiTrigger.ts new file mode 100644 index 000000000..58fdf1f59 --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/server/features/ApiTrigger.ts @@ -0,0 +1,7 @@ +import { Trigger } from '@tachybase/plugin-workflow'; + +export class ApiTrigger extends Trigger { + static TYPE = 'api'; + on() {} + off() {} +} diff --git a/packages/plugins/@hera/plugin-core/src/server/features/RequestInterceptionTrigger.ts b/packages/plugins/@hera/plugin-core/src/server/features/RequestInterceptionTrigger.ts new file mode 100644 index 000000000..3608a0713 --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/server/features/RequestInterceptionTrigger.ts @@ -0,0 +1,114 @@ +import PluginErrorHandler from '@nocobase/plugin-error-handler'; +import { Trigger, EXECUTION_STATUS } from '@tachybase/plugin-workflow'; +import { joinCollectionName } from '@nocobase/data-source-manager'; +import type { Context, Next } from '@nocobase/actions'; +import PluginWorkflow from '@tachybase/plugin-workflow'; + +class RequestInterceptionError extends Error { + status = 400; + messages = []; + constructor(message) { + super(message); + this.name = 'RequestInterceptionError'; + } +} + +export class RequestInterceptionTrigger extends Trigger { + static TYPE = 'request-interception'; + sync = true; + middleware = async (context: Context, next: Next) => { + const { + resourceName, + actionName, + params: { filterByTk, filter, values, triggerWorkflows = '' }, + } = context.action; + 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 === RequestInterceptionTrigger.TYPE && item.config.collection === jointCollectionName, + ); + const globalWorkflows = workflows + .filter((item) => { + return item.config.global && item.config.actions?.includes(actionName); + }) + .sort((a, b) => a.id - b.id); + const localWorkflows = workflows + .filter((item) => !item.config.global) + .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; + }); + for (const workflow of localWorkflows.concat(globalWorkflows)) { + if (!workflow.config.global && !triggerWorkflowsMap.has(workflow.key)) { + continue; + } + const processor = await this.workflow.trigger( + workflow, + { + user: context.state.currentUser, + roleName: context.state.currentRole, + params: { + filterByTk, + filter, + values, + }, + }, + { 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 RequestInterceptionError('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'); + } + await next(); + }; + constructor(workflow: PluginWorkflow) { + super(workflow); + workflow.app.use(this.middleware, { after: 'dataSource' }); + workflow.app.pm.get(PluginErrorHandler).errorHandler.register( + (err) => err instanceof RequestInterceptionError, + async (err, ctx, next) => { + ctx.body = { + errors: err.messages, + }; + ctx.status = err.status; + }, + ); + } + on() {} + off() {} +} diff --git a/packages/plugins/@hera/plugin-core/src/server/features/interception.ts b/packages/plugins/@hera/plugin-core/src/server/features/interception.ts new file mode 100644 index 000000000..5df2fedfa --- /dev/null +++ b/packages/plugins/@hera/plugin-core/src/server/features/interception.ts @@ -0,0 +1,12 @@ +import { Plugin } from '@nocobase/server'; +import PluginWorkflow from '@tachybase/plugin-workflow'; +import { RequestInterceptionTrigger } from './RequestInterceptionTrigger'; +import { ApiTrigger } from './ApiTrigger'; + +export class PluginInterception extends Plugin { + async load() { + const workflowPlugin = this.app.pm.get(PluginWorkflow) as PluginWorkflow; + workflowPlugin.registerTrigger('request-interception', RequestInterceptionTrigger); + workflowPlugin.registerTrigger('api', ApiTrigger); + } +} diff --git a/packages/plugins/@hera/plugin-core/src/server/plugin.ts b/packages/plugins/@hera/plugin-core/src/server/plugin.ts index 7223ba213..4925893dd 100644 --- a/packages/plugins/@hera/plugin-core/src/server/plugin.ts +++ b/packages/plugins/@hera/plugin-core/src/server/plugin.ts @@ -10,12 +10,15 @@ import './actions'; import { Container } from '@nocobase/utils'; import { DepartmentsPlugin } from './features/departments'; import TstzrangeField from './fields/tstzrange'; +import { PluginInterception } from './features/interception'; export class PluginCoreServer extends Plugin { pluginDepartments: DepartmentsPlugin; + pluginInterception: PluginInterception; constructor(app: Application, options?: PluginOptions) { super(app, options); this.pluginDepartments = new DepartmentsPlugin(app, options); + this.pluginInterception = new PluginInterception(app, options); } async afterAdd() { this.db.registerFieldTypes({ @@ -28,6 +31,7 @@ export class PluginCoreServer extends Plugin { } async load() { await this.pluginDepartments.load(); + await this.pluginInterception.load(); Container.reset(); try { Container.set({ id: 'db', value: this.db }); @@ -47,7 +51,7 @@ export class PluginCoreServer extends Plugin { } } async install(options?: InstallOptions) { - this.pluginDepartments.install(options); + await this.pluginDepartments.install(options); } async afterEnable() {} async afterDisable() {} diff --git a/packages/plugins/@tachybase/plugin-workflow/src/server/actions/workflows.ts b/packages/plugins/@tachybase/plugin-workflow/src/server/actions/workflows.ts index f6bf94d75..dc7404d45 100644 --- a/packages/plugins/@tachybase/plugin-workflow/src/server/actions/workflows.ts +++ b/packages/plugins/@tachybase/plugin-workflow/src/server/actions/workflows.ts @@ -2,6 +2,7 @@ import actions, { Context, utils } from '@nocobase/actions'; import { Op, Repository } from '@nocobase/database'; import Plugin from '../Plugin'; +import { WorkflowModel } from '../types'; export async function update(context: Context, next) { const repository = utils.getRepositoryFromParams(context) as Repository; @@ -160,6 +161,14 @@ export async function sync(context: Context, next) { await next(); } -export async function trigger(context: Context, next) { - return next(); +export async function trigger(ctx: Context) { + const plugin = ctx.app.getPlugin(Plugin) as Plugin; + const workflow = (await ctx.db.getRepository('workflows').findById(ctx.action.params.filterByTk)) as WorkflowModel; + plugin.trigger( + workflow, + { + data: {}, + }, + { httpContext: ctx }, + ); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c71ff490..dd677e1ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1507,6 +1507,9 @@ importers: '@nocobase/utils': specifier: workspace:* version: link:../../../core/utils + '@tachybase/plugin-workflow': + specifier: workspace:* + version: link:../../@tachybase/plugin-workflow devDependencies: '@ant-design/cssinjs': specifier: ^1.11.1 @@ -1517,9 +1520,9 @@ importers: '@ant-design/pro-layout': specifier: ^7.16.11 version: 7.17.16(antd@5.16.1)(react-dom@18.2.0)(react@18.2.0) - '@emotion/css': - specifier: ^11.11.2 - version: 11.11.2 + '@formily/antd-v5': + specifier: 1.x + version: 1.2.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(antd@5.16.1)(react-dom@18.2.0)(react-is@18.3.1)(react@18.2.0)(typescript@4.9.5) '@react-pdf/renderer': specifier: ^3.3.2 version: 3.3.8(react@18.2.0) @@ -1529,15 +1532,15 @@ importers: '@tachybase/schema': specifier: workspace:* version: link:../../../core/schema + '@types/lodash': + specifier: ~4.17.0 + version: 4.17.0 ahooks: specifier: ^3.7.2 version: 3.7.8(react@18.2.0) antd: specifier: 5.16.1 version: 5.16.1(react-dom@18.2.0)(react@18.2.0) - antd-style: - specifier: 3.4.5 - version: 3.4.5(@types/react@18.2.79)(antd@5.16.1)(react-dom@18.2.0)(react@18.2.0) classnames: specifier: ^2.3.1 version: 2.5.1 @@ -7511,6 +7514,19 @@ packages: react-dom: 18.2.0(react@18.2.0) tslib: 2.6.2 + /@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@dnd-kit/accessibility': 3.1.0(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + tslib: 2.6.2 + dev: true + /@dnd-kit/modifiers@6.0.1(@dnd-kit/core@5.0.3)(react@18.2.0): resolution: {integrity: sha512-rbxcsg3HhzlcMHVHWDuh9LCjpOVAgqbV78wLGI8tziXY3+qcMQ61qVXIvNKQFuhj75dSfD+o+PYZQ/NUk2A23A==} peerDependencies: @@ -7535,6 +7551,18 @@ packages: tslib: 2.6.2 dev: false + /@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.1.0)(react@18.2.0): + resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==} + peerDependencies: + '@dnd-kit/core': ^6.0.7 + react: '>=16.8.0' + dependencies: + '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/utilities': 3.2.2(react@18.2.0) + react: 18.2.0 + tslib: 2.6.2 + dev: true + /@dnd-kit/utilities@3.2.2(react@18.2.0): resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} peerDependencies: @@ -8754,6 +8782,40 @@ packages: resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} dev: true + /@formily/antd-v5@1.2.0(@types/react-dom@18.2.25)(@types/react@18.2.79)(antd@5.16.1)(react-dom@18.2.0)(react-is@18.3.1)(react@18.2.0)(typescript@4.9.5): + resolution: {integrity: sha512-6K1fV7pwvy7j5KoO/5dEe5llosann7211I/bH/WJkLQtDSDK+XLFUQ4/lU7CdNpwHMrspzXS3lF82EM3hGyM8w==} + engines: {npm: '>=3.0.0'} + peerDependencies: + '@types/react': '>=16.8.0 || >=17.0.0' + '@types/react-dom': '>=16.8.0 || >=17.0.0' + antd: ^5.13.0 + react: '>=16.8.0 || >=17.0.0' + react-dom: '>=16.8.0' + react-is: '>=16.8.0 || >=17.0.0' + dependencies: + '@ant-design/cssinjs': 1.20.0(react-dom@18.2.0)(react@18.2.0) + '@ant-design/icons': 5.3.6(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) + '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.1.0)(react@18.2.0) + '@formily/core': 2.2.27 + '@formily/grid': 2.2.27(typescript@4.9.5) + '@formily/json-schema': 2.2.27(typescript@4.9.5) + '@formily/react': 2.2.27(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react-is@18.3.1)(react@18.2.0)(typescript@4.9.5) + '@formily/reactive': 2.2.27 + '@formily/reactive-react': 2.2.27(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react-is@18.3.1)(react@18.2.0) + '@formily/shared': 2.2.27 + '@types/react': 18.2.79 + '@types/react-dom': 18.2.25 + antd: 5.16.1(react-dom@18.2.0)(react@18.2.0) + classnames: 2.5.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.3.1 + react-sticky-box: 1.0.2(react@18.2.0) + transitivePeerDependencies: + - typescript + dev: true + /@formily/core@2.2.27: resolution: {integrity: sha512-f8e4f/WlimOA7zN1ARmiRKMlIpsIH00XxT7NWAE7EV3DO2B6sq6gSq9GDc5JLwHqhiyJBAIPzUdfmIpRLwPxyQ==} engines: {npm: '>=3.0.0'} @@ -8771,7 +8833,6 @@ packages: '@formily/reactive': 2.2.27 '@juggle/resize-observer': 3.4.0 typescript: 4.9.5 - dev: false /@formily/json-schema@2.2.27(typescript@4.9.5): resolution: {integrity: sha512-YqtpPp9bmXO28iw10ZLT5KCAqipr2SwPrzfskUJ/Oa8O3xZi64+9TMVPylX73cBe49lVaOgFiQPNAljLudeveg==} @@ -8783,7 +8844,6 @@ packages: '@formily/reactive': 2.2.27 '@formily/shared': 2.2.27 typescript: 4.9.5 - dev: false /@formily/json-schema@2.2.27(typescript@5.4.5): resolution: {integrity: sha512-YqtpPp9bmXO28iw10ZLT5KCAqipr2SwPrzfskUJ/Oa8O3xZi64+9TMVPylX73cBe49lVaOgFiQPNAljLudeveg==} @@ -8830,7 +8890,6 @@ packages: react-is: 18.3.1 transitivePeerDependencies: - typescript - dev: false /@formily/reactive-react@2.2.27(@types/react-dom@18.2.25)(@types/react@18.2.79)(react-dom@18.2.0)(react-is@18.3.1)(react@18.2.0): resolution: {integrity: sha512-LJefK/9g8MorhGexMiVF+FVWyMR/Vc4C63Djrj08XeXtTF0NUNUUKVBT1eZqmtdRHJSyDE7SSgUmpxcXP7QmQQ==} @@ -8854,7 +8913,6 @@ packages: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) react-is: 18.3.1 - dev: false /@formily/reactive@2.2.27: resolution: {integrity: sha512-ksdA4uIw3WMo0FmHcGsyqZgPwhujVgtXqxiWPSV+Tc2eEoJrInZhr15Q0IeTQ6glMh8izNsb19YK+PphBHwXGg==} @@ -9105,7 +9163,6 @@ packages: /@juggle/resize-observer@3.4.0: resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} - dev: false /@koa/cors@3.4.3: resolution: {integrity: sha512-WPXQUaAeAMVaLTEFpoq3T2O1C+FstkjJnDQqy95Ck1UdILajsRhu6mhJ8H2f4NFPRBoCNN+qywTJfq/gGki5mw==} @@ -13083,7 +13140,7 @@ packages: '@babel/plugin-transform-react-jsx-self': 7.24.1(@babel/core@7.24.4) '@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.24.4) react-refresh: 0.14.0 - vite: 4.5.2(@types/node@20.12.2)(less@4.1.3) + vite: 4.5.2(@types/node@20.12.7)(less@4.1.3) transitivePeerDependencies: - supports-color @@ -26145,7 +26202,6 @@ packages: dependencies: react: 18.2.0 resize-observer-polyfill: 1.5.1 - dev: false /react-to-print@2.14.15(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-SKnwOzU2cJ8eaAkoJO7+gNhvfEDmm+Y34IdcHsjtHioUevUPhprqbVtvNJlZ2JkGJ8ExK2QNWM9pXECTDR5D8w==} @@ -29961,6 +30017,7 @@ packages: rollup: 3.29.4 optionalDependencies: fsevents: 2.3.3 + dev: false /vite@4.5.2(@types/node@20.12.7)(less@4.1.3): resolution: {integrity: sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==} @@ -29997,7 +30054,6 @@ packages: rollup: 3.29.4 optionalDependencies: fsevents: 2.3.3 - dev: true /vite@5.1.5(@types/node@20.12.7)(sass@1.75.0): resolution: {integrity: sha512-BdN1xh0Of/oQafhU+FvopafUp6WaYenLU/NFoL5WyJL++GxkNfieKzBhM24H3HVsPQrlAqB7iJYTHabzaRed5Q==}