diff --git a/packages/core/cli/src/commands/test.js b/packages/core/cli/src/commands/test.js index e592ee5b7..071e45d39 100644 --- a/packages/core/cli/src/commands/test.js +++ b/packages/core/cli/src/commands/test.js @@ -19,6 +19,7 @@ function addTestCommand(name, cli) { .arguments('[paths...]') .allowUnknownOption() .action(async (paths, opts) => { + process.argv.push('--disable-console-intercept'); if (name === 'test:server') { process.env.TEST_ENV = 'server-side'; } else if (name === 'test:client') { 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 0f26e5bef..f73a0592b 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 @@ -11,7 +11,11 @@ import { usePlugin } from '../../../application/hooks'; import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings'; import { useSchemaToolbar } from '../../../application/schema-toolbar'; import { useFormBlockContext } from '../../../block-provider'; -import { useCollectionManager_deprecated, useCollection_deprecated } from '../../../collection-manager'; +import { + joinCollectionName, + useCollectionManager_deprecated, + useCollection_deprecated, +} from '../../../collection-manager'; import { FlagProvider } from '../../../flag-provider'; import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings'; import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items'; @@ -28,6 +32,7 @@ import { import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue'; import { useLinkageAction } from './hooks'; import { requestSettingsSchema } from './utils'; +import { DataSourceProvider, useDataSourceKey } from '../../../data-source'; const MenuGroup = (props) => { return props.children; @@ -327,7 +332,8 @@ function WorkflowSelect({ actionType, direct = false, ...props }) { const { setValuesIn } = useForm(); const baseCollection = useCollection_deprecated(); const { getCollection } = useCollectionManager_deprecated(); - const [workflowCollection, setWorkflowCollection] = useState(baseCollection.name); + const dataSourceKey = useDataSourceKey(); + const [workflowCollection, setWorkflowCollection] = useState(joinCollectionName(dataSourceKey, baseCollection.name)); const compile = useCompile(); const workflowPlugin = usePlugin('workflow') as any; @@ -351,11 +357,11 @@ function WorkflowSelect({ actionType, direct = false, ...props }) { const path = paths[i]; const associationField = collection.fields.find((f) => f.name === path); if (associationField) { - collection = getCollection(associationField.target); + collection = getCollection(associationField.target, dataSourceKey); } } } - setWorkflowCollection(collection.name); + setWorkflowCollection(joinCollectionName(dataSourceKey, collection.name)); setValuesIn(`group[${index}].workflowKey`, null); }); }); @@ -375,38 +381,40 @@ function WorkflowSelect({ actionType, direct = false, ...props }) { ); return ( - + { - const typeOption = workflowPlugin.getTriggersOptions().find((item) => item.value === data.type); - return typeOption ? ( - - {label} - {compile(typeOption.label)} - - ) : ( - label - ); - }} - {...props} - /> + }} + optionFilter={optionFilter} + optionRender={({ label, data }) => { + const typeOption = workflowPlugin.getTriggersOptions().find((item) => item.value === data.type); + return typeOption ? ( + + {label} + {compile(typeOption.label)} + + ) : ( + label + ); + }} + {...props} + /> + ); } @@ -414,7 +422,7 @@ export function WorkflowConfig() { const { dn } = useDesignable(); const { t } = useTranslation(); const fieldSchema = useFieldSchema(); - const { name: collection } = useCollection_deprecated(); + const collection = useCollection_deprecated(); // TODO(refactor): should refactor for getting certain action type, better from 'x-action'. const formBlock = useFormBlockContext(); const actionType = formBlock?.type || fieldSchema['x-action']; @@ -483,7 +491,9 @@ export function WorkflowConfig() { 'x-component-props': { placeholder: t('Select context', { ns: 'workflow' }), popupMatchSelectWidth: false, - collection, + collection: `${ + collection.dataSource && collection.dataSource !== 'main' ? `${collection.dataSource}:` : '' + }${collection.name}`, filter: '{{ fieldFilter }}', rootOption: { label: t('Full form data', { ns: 'workflow' }), 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 812cf48de..4cef09c37 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 @@ -3,7 +3,12 @@ import { Tag, TreeSelect } from 'antd'; 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_deprecated, useCollectionManager_deprecated, useCompile } from '../../..'; +import { + CollectionFieldOptions_deprecated, + parseCollectionName, + useCollectionManager_deprecated, + useCompile, +} from '../../..'; export type AppendsTreeSelectProps = { value: string[] | string; @@ -27,7 +32,7 @@ function usePropsCollection({ collection }) { type CallScope = { compile?(value: string): string; - getCollectionFields?(name: any): CollectionFieldOptions_deprecated[]; + getCollectionFields?(name: any, dataSource?: string): CollectionFieldOptions_deprecated[]; filter(field): boolean; }; @@ -52,7 +57,8 @@ function trueFilter(field) { } function getCollectionFieldOptions(this: CallScope, collection, parentNode?): TreeOptionType[] { - const fields = this.getCollectionFields(collection).filter(isAssociation); + const [dataSourceName, collectionName] = parseCollectionName(collection); + const fields = this.getCollectionFields(collectionName, dataSourceName).filter(isAssociation); const boundLoadChildren = loadChildren.bind(this); return fields.filter(this.filter).map((field) => { const key = parentNode ? `${parentNode.value ? `${parentNode.value}.` : ''}${field.name}` : field.name; @@ -84,11 +90,12 @@ export const AppendsTreeSelect: React.FC { if (props.multiple) { @@ -111,7 +118,7 @@ export const AppendsTreeSelect: React.FC { const parentNode = rootOption ? { @@ -123,17 +130,19 @@ export const AppendsTreeSelect: React.FC Object.assign(result, { [item.value]: item }), {}); + : getCollectionFieldOptions.call({ compile, getCollectionFields, filter }, collectionString, parentNode); + + const map = tData.reduce((result, item) => Object.assign(result, { [item.value]: item }), {}); if (parentNode) { map[parentNode.value] = parentNode; } setOptionsMap(map); - }, [collection, baseCollection, rootOption, filter, propsLoadData]); + }, [collectionString, rootOption, filter, propsLoadData]); + // NOTE: preload options in value useEffect(() => { const arr = (props.multiple ? propsValue : propsValue ? [propsValue] : []) as string[]; if (!arr?.length || arr.every((v) => Boolean(optionsMap[v]))) { diff --git a/packages/core/data-source-manager/src/utils.ts b/packages/core/data-source-manager/src/utils.ts index 79eec06f6..392a01bd2 100644 --- a/packages/core/data-source-manager/src/utils.ts +++ b/packages/core/data-source-manager/src/utils.ts @@ -7,3 +7,10 @@ export function parseCollectionName(collection: string) { const dataSourceName = dataSourceCollection[0] ?? 'main'; return [dataSourceName, collectionName]; } + +export function joinCollectionName(dataSourceName: string, collectionName: string) { + if (!dataSourceName || dataSourceName === 'main') { + return collectionName; + } + return `${dataSourceName}:${collectionName}`; +} diff --git a/packages/core/test/package.json b/packages/core/test/package.json index fc3172483..f6ee33665 100644 --- a/packages/core/test/package.json +++ b/packages/core/test/package.json @@ -56,7 +56,7 @@ "sqlite3": "^5.0.8", "supertest": "^6.1.6", "vite": "^5.0.0", - "vitest": "^1.0.0", + "vitest": "^1.4.0", "vitest-dom": "^0.1.1", "ws": "^8.13.0" }, diff --git a/packages/core/utils/src/json-templates.ts b/packages/core/utils/src/json-templates.ts index 9a38a29f3..baad0ca02 100644 --- a/packages/core/utils/src/json-templates.ts +++ b/packages/core/utils/src/json-templates.ts @@ -101,18 +101,13 @@ const parseString = (() => { value = value(); } - if (typeof value === 'object' && value !== null) { + // Accommodate numbers as values. + if (str.startsWith('{{') && str.endsWith('}}')) { return value; } - // Accommodate numbers as values. - if (matches.length === 1 && str.startsWith('{{') && str.endsWith('}}')) { - return value; - } - - // Accommodate numbers as values. - if (matches.length === 1 && str.startsWith('{{') && str.endsWith('}}')) { - return value; + if (value instanceof Date) { + value = value.toISOString(); } return result.replace(match, value == null ? '' : value); diff --git a/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/client/ActionTrigger.tsx b/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/client/ActionTrigger.tsx index cbf88b9e7..a49e6ada5 100644 --- a/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/client/ActionTrigger.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/client/ActionTrigger.tsx @@ -6,7 +6,12 @@ import { useCollectionManager_deprecated, useCompile, } from '@nocobase/client'; -import { Trigger, CollectionBlockInitializer, getCollectionFieldOptions } from '@nocobase/plugin-workflow/client'; +import { + Trigger, + CollectionBlockInitializer, + getCollectionFieldOptions, + useWorkflowAnyExecuted, +} from '@nocobase/plugin-workflow/client'; import { NAMESPACE, useLang } from '../locale'; export default class extends Trigger { @@ -17,10 +22,8 @@ export default class extends Trigger { type: 'string', required: true, 'x-decorator': 'FormItem', - 'x-component': 'CollectionSelect', - 'x-component-props': { - className: 'auto-width', - }, + 'x-component': 'DataSourceCollectionCascader', + 'x-disabled': '{{ useWorkflowAnyExecuted() }}', title: `{{t("Collection", { ns: "${NAMESPACE}" })}}`, description: `{{t("Which collection record belongs to.", { ns: "${NAMESPACE}" })}}`, 'x-reactions': [ @@ -63,6 +66,7 @@ export default class extends Trigger { }; scope = { useCollectionDataSource, + useWorkflowAnyExecuted, }; isActionTriggerable = (config, context) => { return ['create', 'update', 'customize:update', 'customize:triggerWorkflows'].includes(context.action); @@ -126,7 +130,7 @@ export default class extends Trigger { title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`, Component: CollectionBlockInitializer, collection: config.collection, - dataSource: '{{$context.data}}', + dataPath: '$context.data', }; } } diff --git a/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/client/__e2e__/configuration.test.ts b/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/client/__e2e__/configuration.test.ts index c3aab3ade..8c2024354 100644 --- a/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/client/__e2e__/configuration.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/client/__e2e__/configuration.test.ts @@ -9,6 +9,7 @@ import { apiUpdateWorkflowTrigger, appendJsonCollectionName, generalWithNoRelationalFields, + apiGetDataSourceCount, } from '@nocobase/plugin-workflow-test/e2e'; import { expect, test } from '@nocobase/test/e2e'; import { dayjs } from '@nocobase/utils'; @@ -51,7 +52,8 @@ test.describe('Configuration page to configure the Trigger node', () => { const formEventTriggerNode = new FormEventTriggerNode(page, workFlowName, triggerNodeCollectionName); await formEventTriggerNode.nodeConfigure.click(); await formEventTriggerNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await formEventTriggerNode.submitButton.click(); //配置录入数据区块 @@ -60,6 +62,10 @@ test.describe('Configuration page to configure the Trigger node', () => { await page.waitForLoadState('networkidle'); await page.getByLabel('schema-initializer-Grid-page:addBlock').hover(); await page.getByRole('menuitem', { name: 'table Table' }).hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: `${triggerNodeCollectionDisplayName}` }).click(); // 移开鼠标,关闭菜单 @@ -143,7 +149,8 @@ test.describe('Configuration page to configure the Trigger node', () => { const formEventTriggerNode = new FormEventTriggerNode(page, workFlowName, triggerNodeCollectionName); await formEventTriggerNode.nodeConfigure.click(); await formEventTriggerNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await formEventTriggerNode.submitButton.click(); //配置录入数据区块 @@ -152,6 +159,10 @@ test.describe('Configuration page to configure the Trigger node', () => { await page.waitForLoadState('networkidle'); await page.getByLabel('schema-initializer-Grid-page:addBlock').hover(); await page.getByRole('menuitem', { name: 'table Table' }).hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: `${triggerNodeCollectionDisplayName}` }).click(); // 移开鼠标,关闭菜单 @@ -649,8 +660,11 @@ test.describe('Configuration page copy to new version', () => { // 3、预期结果:新版本工作流配置内容同旧版本一样 const formEventTriggerNode = new FormEventTriggerNode(page, workFlowName, triggerNodeCollectionName); await formEventTriggerNode.nodeConfigure.click(); - await expect(page.getByRole('button', { name: triggerNodeCollectionDisplayName })).toBeVisible(); - + await expect( + page + .getByLabel('block-item-DataSourceCollectionCascader-workflows-Collection') + .getByText(`Main / ${triggerNodeCollectionDisplayName}`), + ).toBeVisible(); // 4、后置处理:删除工作流 await apiDeleteWorkflow(workflowId); }); diff --git a/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/ActionTrigger.ts b/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/ActionTrigger.ts index b113924b0..0fc614183 100644 --- a/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/ActionTrigger.ts +++ b/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/ActionTrigger.ts @@ -1,11 +1,11 @@ import { get } from 'lodash'; import { BelongsTo, HasOne } from 'sequelize'; import { Model, modelAssociationByKey } from '@nocobase/database'; -import { DefaultContext } from '@nocobase/server'; -import { ActionContext } from '@nocobase/resourcer'; -import { Next } from '@nocobase/actions'; +import Application, { DefaultContext } from '@nocobase/server'; +import { Context as ActionContext, Next } from '@nocobase/actions'; import WorkflowPlugin, { Trigger, WorkflowModel, toJSON } from '@nocobase/plugin-workflow'; +import { parseCollectionName } from '@nocobase/data-source-manager'; interface Context extends ActionContext, DefaultContext {} @@ -13,7 +13,7 @@ export default class extends Trigger { constructor(workflow: WorkflowPlugin) { super(workflow); - workflow.app.resourcer.use(this.middleware); + workflow.app.use(this.middleware, { after: 'dataSource' }); } async triggerAction(context: Context, next: Next) { @@ -55,6 +55,7 @@ export default class extends Trigger { private async trigger(context: Context) { const { triggerWorkflows = '', values } = context.action.params; + const dataSourceHeader = context.get('x-data-source') || 'main'; const { currentUser, currentRole } = context.state; const { model: UserModel } = this.workflow.db.getCollection('users'); @@ -79,12 +80,16 @@ export default class extends Trigger { const asyncGroup = []; for (const workflow of workflows) { const { collection, appends = [] } = workflow.config; + const [dataSourceName, collectionName] = parseCollectionName(collection); const trigger = triggers.find((trigger) => trigger[0] == workflow.key); const event = [workflow]; if (context.action.resourceName !== 'workflows') { if (!context.body) { continue; } + if (dataSourceName !== dataSourceHeader) { + continue; + } const { body: data } = context; for (const row of Array.isArray(data) ? data : [data]) { let payload = row; @@ -101,7 +106,7 @@ export default class extends Trigger { } const model = payload.constructor; if (payload instanceof Model) { - if (collection !== model.collection.name) { + if (collectionName !== model.collection.name) { continue; } if (appends.length) { @@ -115,7 +120,9 @@ export default class extends Trigger { event.push({ data: toJSON(payload), ...userInfo }); } } else { - const { model, repository } = context.db.getCollection(collection); + const { model, repository } = (context.app).dataSourceManager.dataSources + .get(dataSourceName) + .collectionManager.getCollection(collectionName); let data = trigger[1] ? get(values, trigger[1]) : values; const pk = get(data, model.primaryKeyAttribute); if (appends.length && pk != null) { diff --git a/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/__tests__/trigger.test.ts b/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/__tests__/trigger.test.ts index d30cc936a..ae16cfda0 100644 --- a/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/__tests__/trigger.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-action-trigger/src/server/__tests__/trigger.test.ts @@ -12,7 +12,7 @@ describe('workflow > action-trigger', () => { let PostRepo; let CommentRepo; let WorkflowModel; - let UserModel; + let UserRepo; let users; let userAgents; @@ -26,12 +26,14 @@ describe('workflow > action-trigger', () => { WorkflowModel = db.getCollection('workflows').model; PostRepo = db.getCollection('posts').repository; CommentRepo = db.getCollection('comments').repository; - UserModel = db.getCollection('users').model; + UserRepo = db.getCollection('users').repository; - users = await UserModel.bulkCreate([ - { id: 2, nickname: 'a' }, - { id: 3, nickname: 'b' }, - ]); + users = await UserRepo.create({ + values: [ + { id: 2, nickname: 'a', roles: [{ name: 'root' }] }, + { id: 3, nickname: 'b' }, + ], + }); userAgents = users.map((user) => app.agent().login(user)); }); @@ -569,4 +571,49 @@ describe('workflow > action-trigger', () => { expect(e3s[0].status).toBe(EXECUTION_STATUS.RESOLVED); }); }); + + describe('multiple data source', () => { + it('trigger on different data source', async () => { + const workflow = await WorkflowModel.create({ + enabled: true, + type: 'action', + config: { + collection: 'another:posts', + }, + }); + + const res1 = await userAgents[0].resource('posts').create({ + values: { title: 't1' }, + triggerWorkflows: `${workflow.key}`, + }); + expect(res1.status).toBe(200); + + await sleep(500); + + const e1s = await workflow.getExecutions(); + expect(e1s.length).toBe(0); + + // const res2 = await userAgents[0] + // .set('x-data-source', 'another') + // .resource('posts') + // .create({ + // values: { title: 't2' }, + // triggerWorkflows: `${workflow.key}`, + // }); + const res2 = await agent + .login(users[0]) + .set('x-data-source', 'another') + .post('/api/posts:create') + .query({ triggerWorkflows: `${workflow.key}` }) + .send({ title: 't2' }); + + expect(res2.status).toBe(200); + + await sleep(500); + + const e2s = await workflow.getExecutions(); + expect(e2s.length).toBe(1); + expect(e2s[0].status).toBe(EXECUTION_STATUS.RESOLVED); + }); + }); }); diff --git a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/AggregateInstruction.tsx b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/AggregateInstruction.tsx index 9ee696428..a2dc67c77 100644 --- a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/AggregateInstruction.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/AggregateInstruction.tsx @@ -6,6 +6,8 @@ import { SchemaComponentContext, SchemaInitializerItemType, css, + joinCollectionName, + parseCollectionName, useCollectionDataSource, useCollectionFilterOptions, useCollectionManager_deprecated, @@ -116,25 +118,34 @@ function AssociatedConfig({ value, onChange, ...props }): JSX.Element { // need to get: // * source collection (from node.config) // * target collection (from field name) - const { collectionName, target, name } = field; + const { collectionName, target, name, dataSourceKey } = field; - const collection = getCollection(collectionName); + const collection = getCollection(collectionName, dataSourceKey); const primaryKeyField = collection.fields.find((f) => f.primaryKey); - setValuesIn('collection', target); + setValuesIn('collection', `${dataSourceKey}:${target}`); onChange({ name, // primary key data path associatedKey: `{{${path.slice(0, -1).join('.')}.${primaryKeyField.name}}}`, // data associated collection name - associatedCollection: collectionName, + associatedCollection: joinCollectionName(dataSourceKey, collectionName), }); }, [onChange], ); - return ; + return ( + + ); } // based on collection: @@ -217,7 +228,7 @@ export default class extends Instruction { type: 'string', required: true, 'x-decorator': 'FormItem', - 'x-component': 'CollectionSelect', + 'x-component': 'DataSourceCollectionCascader', title: `{{t("Data of collection", { ns: "${NAMESPACE}" })}}`, 'x-reactions': [ { @@ -333,7 +344,8 @@ export default class extends Instruction { 'x-component-props': { useProps() { const { values } = useForm(); - const options = useCollectionFilterOptions(values?.collection); + const [dataSourceName, collectionName] = parseCollectionName(values?.collection); + const options = useCollectionFilterOptions(collectionName, dataSourceName); return { options, className: css` diff --git a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/__e2e__/DataOfCollection.test.ts b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/__e2e__/DataOfCollection.test.ts index f788fa7c1..aca22522a 100644 --- a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/__e2e__/DataOfCollection.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/client/__e2e__/DataOfCollection.test.ts @@ -80,7 +80,8 @@ test.describe('no filter', () => { const aggregateRecordNodeId = await aggregateRecordNode.node.locator('.workflow-node-id').innerText(); await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.submitButton.click(); @@ -179,7 +180,8 @@ test.describe('no filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.sumRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.submitButton.click(); @@ -281,7 +283,8 @@ test.describe('no filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.avgRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.submitButton.click(); @@ -385,7 +388,8 @@ test.describe('no filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.minRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.submitButton.click(); @@ -488,7 +492,8 @@ test.describe('no filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.maxRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.submitButton.click(); @@ -590,7 +595,8 @@ test.describe('no filter', () => { const aggregateRecordNodeId = await aggregateRecordNode.node.locator('.workflow-node-id').innerText(); await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.distinctCheckBox.click(); @@ -690,7 +696,8 @@ test.describe('no filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.sumRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.distinctCheckBox.click(); @@ -795,7 +802,8 @@ test.describe('no filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.avgRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.distinctCheckBox.click(); @@ -902,7 +910,8 @@ test.describe('no filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.minRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.distinctCheckBox.click(); @@ -1006,7 +1015,8 @@ test.describe('no filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.maxRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.distinctCheckBox.click(); @@ -1111,7 +1121,8 @@ test.describe('filter', () => { const aggregateRecordNodeId = await aggregateRecordNode.node.locator('.workflow-node-id').innerText(); await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); // 过滤条件 @@ -1217,7 +1228,8 @@ test.describe('filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.sumRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); // 过滤条件 @@ -1327,7 +1339,8 @@ test.describe('filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.avgRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); // 过滤条件 @@ -1442,7 +1455,8 @@ test.describe('filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.minRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); // 过滤条件 @@ -1553,7 +1567,8 @@ test.describe('filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.maxRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); // 过滤条件 @@ -1663,7 +1678,8 @@ test.describe('filter', () => { const aggregateRecordNodeId = await aggregateRecordNode.node.locator('.workflow-node-id').innerText(); await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.distinctCheckBox.click(); @@ -1770,7 +1786,8 @@ test.describe('filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.sumRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.distinctCheckBox.click(); @@ -1885,7 +1902,8 @@ test.describe('filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.avgRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.distinctCheckBox.click(); @@ -2009,7 +2027,8 @@ test.describe('filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.minRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.distinctCheckBox.click(); @@ -2121,7 +2140,8 @@ test.describe('filter', () => { await aggregateRecordNode.nodeConfigure.click(); await aggregateRecordNode.maxRadio.click(); await aggregateRecordNode.collectionDropDown.click(); - await page.getByText(aggregateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: aggregateNodeCollectionDisplayName }).click(); await aggregateRecordNode.aggregatedFieldDropDown.click(); await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click(); await aggregateRecordNode.distinctCheckBox.click(); diff --git a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/AggregateInstruction.ts b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/AggregateInstruction.ts index 50bf8e144..f5d5fc79d 100644 --- a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/AggregateInstruction.ts +++ b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/AggregateInstruction.ts @@ -1,4 +1,5 @@ -import { BelongsToManyRepository, DataTypes, HasManyRepository } from '@nocobase/database'; +import { parseCollectionName } from '@nocobase/data-source-manager'; +import { DataTypes } from '@nocobase/database'; import { Processor, Instruction, JOB_STATUS, FlowNodeModel } from '@nocobase/plugin-workflow'; const aggregators = { @@ -13,13 +14,14 @@ export default class extends Instruction { async run(node: FlowNodeModel, input, processor: Processor) { const { aggregator, associated, collection, association = {}, params = {} } = node.config; const options = processor.getParsedValue(params, node.id); - const { database } = node.constructor; + const [dataSourceName, collectionName] = parseCollectionName(collection); + const { collectionManager } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName); const repo = associated - ? database.getRepository( + ? collectionManager.getRepository( `${association?.associatedCollection}.${association.name}`, processor.getParsedValue(association?.associatedKey, node.id), ) - : database.getRepository(collection); + : collectionManager.getRepository(collectionName); if (!options.dataType && aggregator === 'avg') { options.dataType = DataTypes.DOUBLE; diff --git a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/__tests__/instruction.test.ts b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/__tests__/instruction.test.ts index 3bb1efe94..712822afa 100644 --- a/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/__tests__/instruction.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-aggregate/src/server/__tests__/instruction.test.ts @@ -3,6 +3,7 @@ import { Application } from '@nocobase/server'; import { getApp, sleep } from '@nocobase/plugin-workflow-test'; import Plugin from '..'; +import { EXECUTION_STATUS } from '@nocobase/plugin-workflow'; describe('workflow > instructions > aggregate', () => { let app: Application; @@ -295,4 +296,33 @@ describe('workflow > instructions > aggregate', () => { expect(j3.result).toBe(1); }); }); + + describe('multiple data source', () => { + it('query on another data source', async () => { + const AnotherPostRepo = app.dataSourceManager.dataSources.get('another').collectionManager.getRepository('posts'); + const post = await AnotherPostRepo.create({ values: { title: 't1' } }); + const p1s = await AnotherPostRepo.find(); + expect(p1s.length).toBe(1); + + const n1 = await workflow.createNode({ + type: 'aggregate', + config: { + collection: 'another:posts', + aggregator: 'count', + params: { + field: 'id', + }, + }, + }); + + await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); + const [job] = await execution.getJobs(); + expect(job.result).toBe(1); + }); + }); }); diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx index a429c146b..3ef15cdef 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/WorkflowTodo.tsx @@ -24,10 +24,10 @@ import WorkflowPlugin, { linkNodes, useAvailableUpstreams, useFlowContext, + DetailsBlockProvider, } from '@nocobase/plugin-workflow/client'; import { NAMESPACE, useLang } from '../locale'; -import { DetailsBlockProvider } from './instruction/DetailsBlockProvider'; import { FormBlockProvider } from './instruction/FormBlockProvider'; import { ManualFormType, manualFormTypes } from './instruction/SchemaConfig'; diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/createRecordForm.test.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/createRecordForm.test.ts index b1988c123..7b4e2a96f 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/createRecordForm.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/createRecordForm.test.ts @@ -10,6 +10,7 @@ import { apiUpdateWorkflowTrigger, appendJsonCollectionName, generalWithNoRelationalFields, + apiGetDataSourceCount, } from '@nocobase/plugin-workflow-test/e2e'; import { expect, test } from '@nocobase/test/e2e'; import { dayjs } from '@nocobase/utils'; @@ -81,6 +82,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -216,6 +221,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -351,6 +360,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -486,6 +499,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -621,6 +638,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -756,6 +777,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -891,6 +916,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1027,6 +1056,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1162,6 +1195,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1297,6 +1334,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1440,6 +1481,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1574,6 +1619,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1715,6 +1764,10 @@ test.describe('field data entry', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1865,6 +1918,10 @@ test.describe('action button', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -2000,6 +2057,10 @@ test.describe('action button', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -2141,6 +2202,10 @@ test.describe('action button', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/updateRecordForm.test.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/updateRecordForm.test.ts index ddff9eb21..d6c32fb6c 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/updateRecordForm.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/updateRecordForm.test.ts @@ -10,6 +10,7 @@ import { apiUpdateWorkflowTrigger, appendJsonCollectionName, generalWithNoRelationalFields, + apiGetDataSourceCount, } from '@nocobase/plugin-workflow-test/e2e'; import { expect, test } from '@nocobase/test/e2e'; import { dayjs } from '@nocobase/utils'; @@ -97,6 +98,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -249,6 +254,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -401,6 +410,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -553,6 +566,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -705,6 +722,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -857,6 +878,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1025,6 +1050,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1193,6 +1222,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1361,6 +1394,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1530,6 +1567,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1707,6 +1748,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -1875,6 +1920,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -2049,6 +2098,10 @@ test.describe('field data update', () => { await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.updateRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/workflowTodo.test.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/workflowTodo.test.ts index 672dd6e89..a93881f5c 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/workflowTodo.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/__e2e__/workflowTodo.test.ts @@ -8,6 +8,7 @@ import { apiUpdateWorkflowTrigger, appendJsonCollectionName, generalWithNoRelationalFields, + apiGetDataSourceCount, } from '@nocobase/plugin-workflow-test/e2e'; import { expect, test } from '@nocobase/test/e2e'; import { dayjs } from '@nocobase/utils'; @@ -69,6 +70,10 @@ test('filter task node', async ({ page, mockPage, mockCollections, mockRecords } await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page @@ -179,6 +184,10 @@ test('filter workflow name', async ({ page, mockPage, mockCollections, mockRecor await manualNode.configureUserInterfaceButton.click(); await manualNode.addBlockButton.hover(); await manualNode.createRecordFormMenu.hover(); + const dataSourcesCount = await apiGetDataSourceCount(); + if (dataSourcesCount > 1) { + await page.getByRole('menuitem', { name: 'Main right' }).hover(); + } await page.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click(); await page.mouse.move(300, 0, { steps: 100 }); await page diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/SchemaConfig.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/SchemaConfig.tsx index 271830b44..6af0e1b92 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/SchemaConfig.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/SchemaConfig.tsx @@ -35,6 +35,8 @@ import { } from '@nocobase/client'; import WorkflowPlugin, { JOB_STATUS, + DetailsBlockProvider, + SimpleDesigner, useAvailableUpstreams, useFlowContext, useNodeContext, @@ -44,7 +46,6 @@ import WorkflowPlugin, { import { Registry, lodash } from '@nocobase/utils/client'; import { NAMESPACE, useLang } from '../../locale'; -import { DetailsBlockProvider } from './DetailsBlockProvider'; import { FormBlockProvider } from './FormBlockProvider'; import createRecordForm from './forms/create'; import customRecordForm from './forms/custom'; @@ -104,24 +105,6 @@ const blockTypeNames = { record: `{{t("Data record", { ns: "${NAMESPACE}" })}}`, }; -function SimpleDesigner() { - const schema = useFieldSchema(); - const title = blockTypeNames[schema['x-designer-props']?.type] ?? '{{t("Block")}}'; - const compile = useCompile(); - return ( - - - - - - ); -} - /** * @deprecated */ @@ -153,7 +136,7 @@ export const addBlockButton_deprecated = new CompatibleSchemaInitializer({ { name: 'nodes', type: 'subMenu', - title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`, + title: `{{t("Node result", { ns: "workflow" })}}`, children: nodeBlockInitializers, }, ] diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/custom.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/custom.tsx index f81a1057c..13a48ae2c 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/custom.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/forms/custom.tsx @@ -3,7 +3,7 @@ import React, { useContext, useMemo, useState } from 'react'; import { ArrayTable } from '@formily/antd-v5'; import { Field, createForm } from '@formily/core'; import { useField, useFieldSchema, useForm } from '@formily/react'; -import lodash from 'lodash'; +import { cloneDeep, pick, set } from 'lodash'; import { ActionContextProvider, @@ -161,7 +161,7 @@ function getOptions(interfaces) { const schema = interfaces[type]; const { group = 'others' } = schema; fields[group] = fields[group] || {}; - lodash.set(fields, [group, type], schema); + set(fields, [group, type], schema); }); return Object.keys(GroupLabels) @@ -208,33 +208,51 @@ const CustomItemsComponent = (props) => { const items = useCommonInterfaceInitializers(); const collection = useCollection_deprecated(); const { setCollectionFields } = useContext(FormBlockContext); + const form = useMemo(() => createForm(), [interfaceOptions]); return ( = pick(item, [ + 'name', + 'group', + 'title', + 'default', + 'validateSchema', + ]); const { - properties: { unique, type, ...properties }, - ...options - } = lodash.cloneDeep(item); - delete properties.name['x-disabled']; - setInterface({ - ...options, - properties, - }); + properties: { unique, type, layout, autoIncrement, ...properties }, + } = item; + fieldInterface.properties = properties; + const result = cloneDeep(fieldInterface); + delete result.properties.name['x-disabled']; + setInterface(result); }, setCallback, }} > - + {interfaceOptions ? ( { 'x-component-props': { type: 'primary', useAction() { - const { values, query } = useForm(); + const { values, query, reset } = useForm(); const messages = [useLang('Field name existed in form')]; return { async run() { @@ -301,6 +319,7 @@ const CustomItemsComponent = (props) => { 'x-toolbar': 'FormItemSchemaToolbar', 'x-settings': 'fieldSettings:FormItem', }); + reset(); setCallback(null); setInterface(null); }, diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/index.tsx b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/index.tsx index 886f1d3f4..7167e8e17 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/index.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/index.tsx @@ -138,7 +138,7 @@ export default class extends Instruction { title: form.title ?? formKey, Component: CollectionBlockInitializer, collection: form.collection, - dataSource: `{{$jobsMapByNodeKey.${node.key}.${formKey}}}`, + dataPath: `$jobsMapByNodeKey.${node.key}.${formKey}`, } as SchemaInitializerItemType) : null; }) diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/data-source.test.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/data-source.test.ts new file mode 100644 index 000000000..e92dc9cdb --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/data-source.test.ts @@ -0,0 +1,223 @@ +import Database from '@nocobase/database'; +import { EXECUTION_STATUS, JOB_STATUS } from '@nocobase/plugin-workflow'; +import { getApp, sleep } from '@nocobase/plugin-workflow-test'; +import { MockServer } from '@nocobase/test'; + +// NOTE: skipped because time is not stable on github ci, but should work in local +describe('workflow > instructions > manual', () => { + let app: MockServer; + let agent; + let userAgents; + let db: Database; + let PostRepo; + let AnotherPostRepo; + let WorkflowModel; + let workflow; + let UserModel; + let users; + let UserJobModel; + + beforeEach(async () => { + app = await getApp({ + plugins: ['users', 'auth', 'workflow-manual'], + }); + // await app.getPlugin('auth').install(); + agent = app.agent(); + db = app.db; + WorkflowModel = db.getCollection('workflows').model; + PostRepo = db.getCollection('posts').repository; + AnotherPostRepo = app.dataSourceManager.dataSources.get('another').collectionManager.getRepository('posts'); + UserModel = db.getCollection('users').model; + UserJobModel = db.getModel('users_jobs'); + + users = await UserModel.bulkCreate([ + { id: 2, nickname: 'a' }, + { id: 3, nickname: 'b' }, + ]); + + userAgents = users.map((user) => app.agent().login(user)); + + workflow = await WorkflowModel.create({ + enabled: true, + type: 'collection', + config: { + mode: 1, + collection: 'posts', + }, + }); + }); + + afterEach(() => app.destroy()); + + describe('multiple data source', () => { + describe('create', () => { + it('create as configured', async () => { + const n1 = await workflow.createNode({ + type: 'manual', + config: { + assignees: [users[0].id], + forms: { + f1: { + type: 'create', + actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }], + collection: 'posts', + dataSource: 'another', + }, + }, + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const UserJobModel = db.getModel('users_jobs'); + const pendingJobs = await UserJobModel.findAll({ + order: [['userId', 'ASC']], + }); + expect(pendingJobs.length).toBe(1); + + const res1 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: pendingJobs[0].get('id'), + values: { + result: { f1: { title: 't1' }, _: 'resolve' }, + }, + }); + expect(res1.status).toBe(202); + + await sleep(500); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); + const [j1] = await e1.getJobs(); + expect(j1.status).toBe(JOB_STATUS.RESOLVED); + expect(j1.result).toMatchObject({ f1: { title: 't1' } }); + + const posts = await AnotherPostRepo.find(); + expect(posts.length).toBe(1); + expect(posts[0]).toMatchObject({ title: 't1' }); + }); + + it('save first and then commit', async () => { + const n1 = await workflow.createNode({ + type: 'manual', + config: { + assignees: [users[0].id], + forms: { + f1: { + type: 'create', + actions: [ + { status: JOB_STATUS.RESOLVED, key: 'resolve' }, + { status: JOB_STATUS.PENDING, key: 'pending' }, + ], + collection: 'posts', + dataSource: 'another', + }, + }, + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const UserJobModel = db.getModel('users_jobs'); + const pendingJobs = await UserJobModel.findAll({ + order: [['userId', 'ASC']], + }); + expect(pendingJobs.length).toBe(1); + + const res1 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: pendingJobs[0].get('id'), + values: { + result: { f1: { title: 't1' }, _: 'pending' }, + }, + }); + expect(res1.status).toBe(202); + + await sleep(500); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.STARTED); + const [j1] = await e1.getJobs(); + expect(j1.status).toBe(JOB_STATUS.PENDING); + expect(j1.result).toMatchObject({ f1: { title: 't1' } }); + + const c1 = await AnotherPostRepo.find(); + expect(c1.length).toBe(0); + + const res2 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: pendingJobs[0].get('id'), + values: { + result: { f1: { title: 't2' }, _: 'resolve' }, + }, + }); + + await sleep(500); + + const [e2] = await workflow.getExecutions(); + expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED); + const [j2] = await e2.getJobs(); + expect(j2.status).toBe(JOB_STATUS.RESOLVED); + expect(j2.result).toMatchObject({ f1: { title: 't2' } }); + + const c2 = await AnotherPostRepo.find(); + expect(c2.length).toBe(1); + }); + }); + + describe('update', () => { + it('update as configured', async () => { + const post = await AnotherPostRepo.create({ values: { title: 't1' } }); + + const n1 = await workflow.createNode({ + type: 'manual', + config: { + assignees: [users[0].id], + forms: { + f1: { + type: 'update', + actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }], + collection: 'posts', + dataSource: 'another', + filter: { + id: post.id, + }, + }, + }, + }, + }); + + await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const UserJobModel = db.getModel('users_jobs'); + const pendingJobs = await UserJobModel.findAll({ + order: [['userId', 'ASC']], + }); + expect(pendingJobs.length).toBe(1); + + const res1 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: pendingJobs[0].get('id'), + values: { + result: { f1: { title: 't2' }, _: 'resolve' }, + }, + }); + expect(res1.status).toBe(202); + + await sleep(500); + + const [e2] = await workflow.getExecutions(); + expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED); + const [j1] = await e2.getJobs(); + expect(j1.status).toBe(JOB_STATUS.RESOLVED); + expect(j1.result).toMatchObject({ f1: { title: 't2' } }); + + const postsAfter = await AnotherPostRepo.find(); + expect(postsAfter.length).toBe(1); + expect(postsAfter[0]).toMatchObject({ title: 't2' }); + }); + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/instruction.test.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/form.test.ts similarity index 56% rename from packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/instruction.test.ts rename to packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/form.test.ts index 4f3acca75..1aa2fee67 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/instruction.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/form.test.ts @@ -333,516 +333,6 @@ describe('workflow > instructions > manual', () => { }); }); - describe('mode: 0 (single record)', () => { - it('the only user assigned could submit', async () => { - const n1 = await workflow.createNode({ - type: 'manual', - config: { - assignees: [users[0].id], - forms: { - f1: { - actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }], - }, - }, - }, - }); - - const post = await PostRepo.create({ values: { title: 't1' } }); - - await sleep(500); - - const [pending] = await workflow.getExecutions(); - expect(pending.status).toBe(EXECUTION_STATUS.STARTED); - const [j1] = await pending.getJobs(); - expect(j1.status).toBe(JOB_STATUS.PENDING); - - const usersJobs = await UserJobModel.findAll(); - expect(usersJobs.length).toBe(1); - expect(usersJobs[0].status).toBe(JOB_STATUS.PENDING); - expect(usersJobs[0].userId).toBe(users[0].id); - expect(usersJobs[0].jobId).toBe(j1.id); - - const res1 = await agent.resource('users_jobs').submit({ - filterByTk: usersJobs[0].id, - values: { result: { f1: {}, _: 'resolve' } }, - }); - expect(res1.status).toBe(401); - - const res2 = await userAgents[1].resource('users_jobs').submit({ - filterByTk: usersJobs[0].id, - values: { - result: { f1: {}, _: 'resolve' }, - }, - }); - expect(res2.status).toBe(403); - - const res3 = await userAgents[0].resource('users_jobs').submit({ - filterByTk: usersJobs[0].id, - values: { - result: { f1: { a: 1 }, _: 'resolve' }, - }, - }); - expect(res3.status).toBe(202); - - await sleep(1000); - - const [j2] = await pending.getJobs(); - expect(j2.status).toBe(JOB_STATUS.RESOLVED); - expect(j2.result).toEqual({ f1: { a: 1 }, _: 'resolve' }); - - const usersJobsAfter = await UserJobModel.findAll(); - expect(usersJobsAfter.length).toBe(1); - expect(usersJobsAfter[0].status).toBe(JOB_STATUS.RESOLVED); - expect(usersJobsAfter[0].result).toEqual({ f1: { a: 1 }, _: 'resolve' }); - - const res4 = await userAgents[0].resource('users_jobs').submit({ - filterByTk: usersJobs[0].id, - values: { - result: { f1: { a: 2 }, _: 'resolve' }, - }, - }); - expect(res4.status).toBe(400); - }); - - it('any user assigned could submit', async () => { - const n1 = await workflow.createNode({ - type: 'manual', - config: { - assignees: [users[0].id, users[1].id], - forms: { - f1: { - actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }], - }, - }, - }, - }); - - const post = await PostRepo.create({ values: { title: 't1' } }); - - await sleep(500); - - const [pending] = await workflow.getExecutions(); - expect(pending.status).toBe(EXECUTION_STATUS.STARTED); - const [j1] = await pending.getJobs(); - expect(j1.status).toBe(JOB_STATUS.PENDING); - - const usersJobs = await j1.getUsersJobs(); - - const res1 = await userAgents[1].resource('users_jobs').submit({ - filterByTk: usersJobs.find((item) => item.userId === users[1].id).id, - values: { - result: { f1: { a: 1 }, _: 'resolve' }, - }, - }); - expect(res1.status).toBe(202); - - await sleep(1000); - - const [j2] = await pending.getJobs(); - expect(j2.status).toBe(JOB_STATUS.RESOLVED); - expect(j2.result).toEqual({ f1: { a: 1 }, _: 'resolve' }); - - const res2 = await userAgents[0].resource('users_jobs').submit({ - filterByTk: usersJobs.find((item) => item.userId === users[0].id).id, - values: { - result: { f1: { a: 1 }, _: 'resolve' }, - }, - }); - expect(res2.status).toBe(400); - }); - - it('also could submit to users_jobs api', async () => { - const n1 = await workflow.createNode({ - type: 'manual', - config: { - assignees: [users[0].id], - forms: { - f1: { - actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }], - }, - }, - }, - }); - - const post = await PostRepo.create({ values: { title: 't1' } }); - - await sleep(500); - - const UserJobModel = db.getModel('users_jobs'); - const usersJobs = await UserJobModel.findAll(); - expect(usersJobs.length).toBe(1); - expect(usersJobs[0].get('status')).toBe(JOB_STATUS.PENDING); - expect(usersJobs[0].get('userId')).toBe(users[0].id); - - const res = await userAgents[0].resource('users_jobs').submit({ - filterByTk: usersJobs[0].get('id'), - values: { - result: { f1: { a: 1 }, _: 'resolve' }, - }, - }); - expect(res.status).toBe(202); - - await sleep(1000); - - const [execution] = await workflow.getExecutions(); - expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); - const [job] = await execution.getJobs(); - expect(job.status).toBe(JOB_STATUS.RESOLVED); - expect(job.result).toEqual({ f1: { a: 1 }, _: 'resolve' }); - }); - }); - - describe('mode: 1 (multiple record, all)', () => { - it('all resolved', async () => { - const n1 = await workflow.createNode({ - type: 'manual', - config: { - assignees: [users[0].id, users[1].id], - mode: 1, - forms: { - f1: { - actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }], - }, - }, - }, - }); - - const post = await PostRepo.create({ values: { title: 't1' } }); - - await sleep(500); - - const UserJobModel = db.getModel('users_jobs'); - const pendingJobs = await UserJobModel.findAll({ - order: [['userId', 'ASC']], - }); - expect(pendingJobs.length).toBe(2); - - const res1 = await userAgents[0].resource('users_jobs').submit({ - filterByTk: pendingJobs[0].get('id'), - values: { - result: { f1: { a: 1 }, _: 'resolve' }, - }, - }); - expect(res1.status).toBe(202); - - await sleep(1000); - - const [e1] = await workflow.getExecutions(); - expect(e1.status).toBe(EXECUTION_STATUS.STARTED); - const [j1] = await e1.getJobs(); - expect(j1.status).toBe(JOB_STATUS.PENDING); - expect(j1.result).toBe(0.5); - const usersJobs1 = await UserJobModel.findAll({ - order: [['userId', 'ASC']], - }); - expect(usersJobs1.length).toBe(2); - - const res2 = await userAgents[1].resource('users_jobs').submit({ - filterByTk: pendingJobs[1].get('id'), - values: { - result: { f1: { a: 2 }, _: 'resolve' }, - }, - }); - expect(res2.status).toBe(202); - - await sleep(1000); - - const [e2] = await workflow.getExecutions(); - expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED); - const [j2] = await e2.getJobs(); - expect(j2.status).toBe(JOB_STATUS.RESOLVED); - expect(j2.result).toBe(1); - }); - - it('first rejected', async () => { - const n1 = await workflow.createNode({ - type: 'manual', - config: { - assignees: [users[0].id, users[1].id], - mode: 1, - forms: { - f1: { - actions: [{ status: JOB_STATUS.REJECTED, key: 'reject' }], - }, - }, - }, - }); - - const post = await PostRepo.create({ values: { title: 't1' } }); - - await sleep(500); - - const UserJobModel = db.getModel('users_jobs'); - const pendingJobs = await UserJobModel.findAll({ - order: [['userId', 'ASC']], - }); - expect(pendingJobs.length).toBe(2); - - const res1 = await userAgents[0].resource('users_jobs').submit({ - filterByTk: pendingJobs[0].get('id'), - values: { - result: { f1: { a: 0 }, _: 'reject' }, - }, - }); - expect(res1.status).toBe(202); - - await sleep(1000); - - const [e1] = await workflow.getExecutions(); - expect(e1.status).toBe(EXECUTION_STATUS.REJECTED); - const [j1] = await e1.getJobs(); - expect(j1.status).toBe(JOB_STATUS.REJECTED); - expect(j1.result).toBe(0.5); - const usersJobs1 = await UserJobModel.findAll({ - order: [['userId', 'ASC']], - }); - expect(usersJobs1.length).toBe(2); - - const res2 = await userAgents[1].resource('users_jobs').submit({ - filterByTk: pendingJobs[1].get('id'), - values: { - result: { f1: { a: 0 }, _: 'reject' }, - }, - }); - expect(res2.status).toBe(400); - }); - - it('last rejected', async () => { - const n1 = await workflow.createNode({ - type: 'manual', - config: { - assignees: [users[0].id, users[1].id], - mode: 1, - forms: { - f1: { - actions: [ - { status: JOB_STATUS.RESOLVED, key: 'resolve' }, - { status: JOB_STATUS.REJECTED, key: 'reject' }, - ], - }, - }, - }, - }); - - const post = await PostRepo.create({ values: { title: 't1' } }); - - await sleep(500); - - const UserJobModel = db.getModel('users_jobs'); - const pendingJobs = await UserJobModel.findAll({ - order: [['userId', 'ASC']], - }); - expect(pendingJobs.length).toBe(2); - - const res1 = await userAgents[0].resource('users_jobs').submit({ - filterByTk: pendingJobs[0].get('id'), - values: { - result: { f1: { a: 1 }, _: 'resolve' }, - }, - }); - expect(res1.status).toBe(202); - - await sleep(1000); - - const [e1] = await workflow.getExecutions(); - expect(e1.status).toBe(EXECUTION_STATUS.STARTED); - const [j1] = await e1.getJobs(); - expect(j1.status).toBe(JOB_STATUS.PENDING); - expect(j1.result).toBe(0.5); - const usersJobs1 = await UserJobModel.findAll({ - order: [['userId', 'ASC']], - }); - expect(usersJobs1.length).toBe(2); - - const res2 = await userAgents[1].resource('users_jobs').submit({ - filterByTk: pendingJobs[1].get('id'), - values: { - result: { f1: { a: 0 }, _: 'reject' }, - }, - }); - expect(res2.status).toBe(202); - - await sleep(1000); - - const [e2] = await workflow.getExecutions(); - expect(e2.status).toBe(EXECUTION_STATUS.REJECTED); - const [j2] = await e2.getJobs(); - expect(j2.status).toBe(JOB_STATUS.REJECTED); - expect(j2.result).toBe(1); - }); - }); - - describe('mode: -1 (multiple record, any)', () => { - it('first resolved', async () => { - const n1 = await workflow.createNode({ - type: 'manual', - config: { - assignees: [users[0].id, users[1].id], - mode: -1, - forms: { - f1: { - actions: [ - { status: JOB_STATUS.RESOLVED, key: 'resolve' }, - { status: JOB_STATUS.REJECTED, key: 'reject' }, - ], - }, - }, - }, - }); - - const post = await PostRepo.create({ values: { title: 't1' } }); - - await sleep(500); - - const UserJobModel = db.getModel('users_jobs'); - const pendingJobs = await UserJobModel.findAll({ - order: [['userId', 'ASC']], - }); - expect(pendingJobs.length).toBe(2); - - const res1 = await userAgents[0].resource('users_jobs').submit({ - filterByTk: pendingJobs[0].get('id'), - values: { - result: { f1: { a: 1 }, _: 'resolve' }, - }, - }); - expect(res1.status).toBe(202); - - await sleep(1000); - - const [e1] = await workflow.getExecutions(); - expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); - const [j1] = await e1.getJobs(); - expect(j1.status).toBe(JOB_STATUS.RESOLVED); - expect(j1.result).toBe(0.5); - - const res2 = await userAgents[1].resource('users_jobs').submit({ - filterByTk: pendingJobs[1].get('id'), - values: { - result: { f1: { a: 0 }, _: 'reject' }, - }, - }); - expect(res2.status).toBe(400); - }); - - it('any resolved', async () => { - const n1 = await workflow.createNode({ - type: 'manual', - config: { - assignees: [users[0].id, users[1].id], - mode: -1, - forms: { - f1: { - actions: [ - { status: JOB_STATUS.RESOLVED, key: 'resolve' }, - { status: JOB_STATUS.REJECTED, key: 'reject' }, - ], - }, - }, - }, - }); - - const post = await PostRepo.create({ values: { title: 't1' } }); - - await sleep(500); - - const UserJobModel = db.getModel('users_jobs'); - const pendingJobs = await UserJobModel.findAll({ - order: [['userId', 'ASC']], - }); - expect(pendingJobs.length).toBe(2); - - const res1 = await userAgents[0].resource('users_jobs').submit({ - filterByTk: pendingJobs[0].get('id'), - values: { - result: { f1: { a: 0 }, _: 'reject' }, - }, - }); - expect(res1.status).toBe(202); - - await sleep(1000); - - const [e1] = await workflow.getExecutions(); - expect(e1.status).toBe(EXECUTION_STATUS.STARTED); - const [j1] = await e1.getJobs(); - expect(j1.status).toBe(JOB_STATUS.PENDING); - expect(j1.result).toBe(0.5); - - const res2 = await userAgents[1].resource('users_jobs').submit({ - filterByTk: pendingJobs[1].get('id'), - values: { - result: { f1: { a: 1 }, _: 'resolve' }, - }, - }); - expect(res2.status).toBe(202); - - await sleep(1000); - - const [e2] = await workflow.getExecutions(); - expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED); - const [j2] = await e2.getJobs(); - expect(j2.status).toBe(JOB_STATUS.RESOLVED); - expect(j2.result).toBe(1); - }); - - it('all rejected', async () => { - const n1 = await workflow.createNode({ - type: 'manual', - config: { - assignees: [users[0].id, users[1].id], - mode: -1, - forms: { - f1: { - actions: [{ status: JOB_STATUS.REJECTED, key: 'reject' }], - }, - }, - }, - }); - - const post = await PostRepo.create({ values: { title: 't1' } }); - - await sleep(500); - - const UserJobModel = db.getModel('users_jobs'); - const pendingJobs = await UserJobModel.findAll({ - order: [['userId', 'ASC']], - }); - expect(pendingJobs.length).toBe(2); - - const res1 = await userAgents[0].resource('users_jobs').submit({ - filterByTk: pendingJobs[0].get('id'), - values: { - result: { f1: { a: 0 }, _: 'reject' }, - }, - }); - expect(res1.status).toBe(202); - - await sleep(1000); - - const [e1] = await workflow.getExecutions(); - expect(e1.status).toBe(EXECUTION_STATUS.STARTED); - const [j1] = await e1.getJobs(); - expect(j1.status).toBe(JOB_STATUS.PENDING); - expect(j1.result).toBe(0.5); - - const res2 = await userAgents[1].resource('users_jobs').submit({ - filterByTk: pendingJobs[1].get('id'), - values: { - result: { f1: { a: 0 }, _: 'reject' }, - }, - }); - expect(res2.status).toBe(202); - - await sleep(1000); - - const [e2] = await workflow.getExecutions(); - expect(e2.status).toBe(EXECUTION_STATUS.REJECTED); - const [j2] = await e2.getJobs(); - expect(j2.status).toBe(JOB_STATUS.REJECTED); - expect(j2.result).toBe(1); - }); - }); - describe('use result of submitted form in manual node', () => { it('result should be available and correct', async () => { const n1 = await workflow.createNode({ diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/mode.test.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/mode.test.ts new file mode 100644 index 000000000..8ea37def4 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/__tests__/mode.test.ts @@ -0,0 +1,561 @@ +import Database from '@nocobase/database'; +import { EXECUTION_STATUS, JOB_STATUS } from '@nocobase/plugin-workflow'; +import { getApp, sleep } from '@nocobase/plugin-workflow-test'; +import { MockServer } from '@nocobase/test'; + +// NOTE: skipped because time is not stable on github ci, but should work in local +describe('workflow > instructions > manual', () => { + let app: MockServer; + let agent; + let userAgents; + let db: Database; + let PostRepo; + let CommentRepo; + let WorkflowModel; + let workflow; + let UserModel; + let users; + let UserJobModel; + + beforeEach(async () => { + app = await getApp({ + plugins: ['users', 'auth', 'workflow-manual'], + }); + // await app.getPlugin('auth').install(); + agent = app.agent(); + db = app.db; + WorkflowModel = db.getCollection('workflows').model; + PostRepo = db.getCollection('posts').repository; + CommentRepo = db.getCollection('comments').repository; + UserModel = db.getCollection('users').model; + UserJobModel = db.getModel('users_jobs'); + + users = await UserModel.bulkCreate([ + { id: 2, nickname: 'a' }, + { id: 3, nickname: 'b' }, + ]); + + userAgents = users.map((user) => app.agent().login(user)); + + workflow = await WorkflowModel.create({ + enabled: true, + type: 'collection', + config: { + mode: 1, + collection: 'posts', + }, + }); + }); + + afterEach(() => app.destroy()); + + describe('mode: 0 (single record)', () => { + it('the only user assigned could submit', async () => { + const n1 = await workflow.createNode({ + type: 'manual', + config: { + assignees: [users[0].id], + forms: { + f1: { + actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }], + }, + }, + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [pending] = await workflow.getExecutions(); + expect(pending.status).toBe(EXECUTION_STATUS.STARTED); + const [j1] = await pending.getJobs(); + expect(j1.status).toBe(JOB_STATUS.PENDING); + + const usersJobs = await UserJobModel.findAll(); + expect(usersJobs.length).toBe(1); + expect(usersJobs[0].status).toBe(JOB_STATUS.PENDING); + expect(usersJobs[0].userId).toBe(users[0].id); + expect(usersJobs[0].jobId).toBe(j1.id); + + const res1 = await agent.resource('users_jobs').submit({ + filterByTk: usersJobs[0].id, + values: { result: { f1: {}, _: 'resolve' } }, + }); + expect(res1.status).toBe(401); + + const res2 = await userAgents[1].resource('users_jobs').submit({ + filterByTk: usersJobs[0].id, + values: { + result: { f1: {}, _: 'resolve' }, + }, + }); + expect(res2.status).toBe(403); + + const res3 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: usersJobs[0].id, + values: { + result: { f1: { a: 1 }, _: 'resolve' }, + }, + }); + expect(res3.status).toBe(202); + + await sleep(1000); + + const [j2] = await pending.getJobs(); + expect(j2.status).toBe(JOB_STATUS.RESOLVED); + expect(j2.result).toEqual({ f1: { a: 1 }, _: 'resolve' }); + + const usersJobsAfter = await UserJobModel.findAll(); + expect(usersJobsAfter.length).toBe(1); + expect(usersJobsAfter[0].status).toBe(JOB_STATUS.RESOLVED); + expect(usersJobsAfter[0].result).toEqual({ f1: { a: 1 }, _: 'resolve' }); + + const res4 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: usersJobs[0].id, + values: { + result: { f1: { a: 2 }, _: 'resolve' }, + }, + }); + expect(res4.status).toBe(400); + }); + + it('any user assigned could submit', async () => { + const n1 = await workflow.createNode({ + type: 'manual', + config: { + assignees: [users[0].id, users[1].id], + forms: { + f1: { + actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }], + }, + }, + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [pending] = await workflow.getExecutions(); + expect(pending.status).toBe(EXECUTION_STATUS.STARTED); + const [j1] = await pending.getJobs(); + expect(j1.status).toBe(JOB_STATUS.PENDING); + + const usersJobs = await j1.getUsersJobs(); + + const res1 = await userAgents[1].resource('users_jobs').submit({ + filterByTk: usersJobs.find((item) => item.userId === users[1].id).id, + values: { + result: { f1: { a: 1 }, _: 'resolve' }, + }, + }); + expect(res1.status).toBe(202); + + await sleep(1000); + + const [j2] = await pending.getJobs(); + expect(j2.status).toBe(JOB_STATUS.RESOLVED); + expect(j2.result).toEqual({ f1: { a: 1 }, _: 'resolve' }); + + const res2 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: usersJobs.find((item) => item.userId === users[0].id).id, + values: { + result: { f1: { a: 1 }, _: 'resolve' }, + }, + }); + expect(res2.status).toBe(400); + }); + + it('also could submit to users_jobs api', async () => { + const n1 = await workflow.createNode({ + type: 'manual', + config: { + assignees: [users[0].id], + forms: { + f1: { + actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }], + }, + }, + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const UserJobModel = db.getModel('users_jobs'); + const usersJobs = await UserJobModel.findAll(); + expect(usersJobs.length).toBe(1); + expect(usersJobs[0].get('status')).toBe(JOB_STATUS.PENDING); + expect(usersJobs[0].get('userId')).toBe(users[0].id); + + const res = await userAgents[0].resource('users_jobs').submit({ + filterByTk: usersJobs[0].get('id'), + values: { + result: { f1: { a: 1 }, _: 'resolve' }, + }, + }); + expect(res.status).toBe(202); + + await sleep(1000); + + const [execution] = await workflow.getExecutions(); + expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); + const [job] = await execution.getJobs(); + expect(job.status).toBe(JOB_STATUS.RESOLVED); + expect(job.result).toEqual({ f1: { a: 1 }, _: 'resolve' }); + }); + }); + + describe('mode: 1 (multiple record, all)', () => { + it('all resolved', async () => { + const n1 = await workflow.createNode({ + type: 'manual', + config: { + assignees: [users[0].id, users[1].id], + mode: 1, + forms: { + f1: { + actions: [{ status: JOB_STATUS.RESOLVED, key: 'resolve' }], + }, + }, + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const UserJobModel = db.getModel('users_jobs'); + const pendingJobs = await UserJobModel.findAll({ + order: [['userId', 'ASC']], + }); + expect(pendingJobs.length).toBe(2); + + const res1 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: pendingJobs[0].get('id'), + values: { + result: { f1: { a: 1 }, _: 'resolve' }, + }, + }); + expect(res1.status).toBe(202); + + await sleep(1000); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.STARTED); + const [j1] = await e1.getJobs(); + expect(j1.status).toBe(JOB_STATUS.PENDING); + expect(j1.result).toBe(0.5); + const usersJobs1 = await UserJobModel.findAll({ + order: [['userId', 'ASC']], + }); + expect(usersJobs1.length).toBe(2); + + const res2 = await userAgents[1].resource('users_jobs').submit({ + filterByTk: pendingJobs[1].get('id'), + values: { + result: { f1: { a: 2 }, _: 'resolve' }, + }, + }); + expect(res2.status).toBe(202); + + await sleep(1000); + + const [e2] = await workflow.getExecutions(); + expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED); + const [j2] = await e2.getJobs(); + expect(j2.status).toBe(JOB_STATUS.RESOLVED); + expect(j2.result).toBe(1); + }); + + it('first rejected', async () => { + const n1 = await workflow.createNode({ + type: 'manual', + config: { + assignees: [users[0].id, users[1].id], + mode: 1, + forms: { + f1: { + actions: [{ status: JOB_STATUS.REJECTED, key: 'reject' }], + }, + }, + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const UserJobModel = db.getModel('users_jobs'); + const pendingJobs = await UserJobModel.findAll({ + order: [['userId', 'ASC']], + }); + expect(pendingJobs.length).toBe(2); + + const res1 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: pendingJobs[0].get('id'), + values: { + result: { f1: { a: 0 }, _: 'reject' }, + }, + }); + expect(res1.status).toBe(202); + + await sleep(1000); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.REJECTED); + const [j1] = await e1.getJobs(); + expect(j1.status).toBe(JOB_STATUS.REJECTED); + expect(j1.result).toBe(0.5); + const usersJobs1 = await UserJobModel.findAll({ + order: [['userId', 'ASC']], + }); + expect(usersJobs1.length).toBe(2); + + const res2 = await userAgents[1].resource('users_jobs').submit({ + filterByTk: pendingJobs[1].get('id'), + values: { + result: { f1: { a: 0 }, _: 'reject' }, + }, + }); + expect(res2.status).toBe(400); + }); + + it('last rejected', async () => { + const n1 = await workflow.createNode({ + type: 'manual', + config: { + assignees: [users[0].id, users[1].id], + mode: 1, + forms: { + f1: { + actions: [ + { status: JOB_STATUS.RESOLVED, key: 'resolve' }, + { status: JOB_STATUS.REJECTED, key: 'reject' }, + ], + }, + }, + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const UserJobModel = db.getModel('users_jobs'); + const pendingJobs = await UserJobModel.findAll({ + order: [['userId', 'ASC']], + }); + expect(pendingJobs.length).toBe(2); + + const res1 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: pendingJobs[0].get('id'), + values: { + result: { f1: { a: 1 }, _: 'resolve' }, + }, + }); + expect(res1.status).toBe(202); + + await sleep(1000); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.STARTED); + const [j1] = await e1.getJobs(); + expect(j1.status).toBe(JOB_STATUS.PENDING); + expect(j1.result).toBe(0.5); + const usersJobs1 = await UserJobModel.findAll({ + order: [['userId', 'ASC']], + }); + expect(usersJobs1.length).toBe(2); + + const res2 = await userAgents[1].resource('users_jobs').submit({ + filterByTk: pendingJobs[1].get('id'), + values: { + result: { f1: { a: 0 }, _: 'reject' }, + }, + }); + expect(res2.status).toBe(202); + + await sleep(1000); + + const [e2] = await workflow.getExecutions(); + expect(e2.status).toBe(EXECUTION_STATUS.REJECTED); + const [j2] = await e2.getJobs(); + expect(j2.status).toBe(JOB_STATUS.REJECTED); + expect(j2.result).toBe(1); + }); + }); + + describe('mode: -1 (multiple record, any)', () => { + it('first resolved', async () => { + const n1 = await workflow.createNode({ + type: 'manual', + config: { + assignees: [users[0].id, users[1].id], + mode: -1, + forms: { + f1: { + actions: [ + { status: JOB_STATUS.RESOLVED, key: 'resolve' }, + { status: JOB_STATUS.REJECTED, key: 'reject' }, + ], + }, + }, + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const UserJobModel = db.getModel('users_jobs'); + const pendingJobs = await UserJobModel.findAll({ + order: [['userId', 'ASC']], + }); + expect(pendingJobs.length).toBe(2); + + const res1 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: pendingJobs[0].get('id'), + values: { + result: { f1: { a: 1 }, _: 'resolve' }, + }, + }); + expect(res1.status).toBe(202); + + await sleep(1000); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.RESOLVED); + const [j1] = await e1.getJobs(); + expect(j1.status).toBe(JOB_STATUS.RESOLVED); + expect(j1.result).toBe(0.5); + + const res2 = await userAgents[1].resource('users_jobs').submit({ + filterByTk: pendingJobs[1].get('id'), + values: { + result: { f1: { a: 0 }, _: 'reject' }, + }, + }); + expect(res2.status).toBe(400); + }); + + it('any resolved', async () => { + const n1 = await workflow.createNode({ + type: 'manual', + config: { + assignees: [users[0].id, users[1].id], + mode: -1, + forms: { + f1: { + actions: [ + { status: JOB_STATUS.RESOLVED, key: 'resolve' }, + { status: JOB_STATUS.REJECTED, key: 'reject' }, + ], + }, + }, + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const UserJobModel = db.getModel('users_jobs'); + const pendingJobs = await UserJobModel.findAll({ + order: [['userId', 'ASC']], + }); + expect(pendingJobs.length).toBe(2); + + const res1 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: pendingJobs[0].get('id'), + values: { + result: { f1: { a: 0 }, _: 'reject' }, + }, + }); + expect(res1.status).toBe(202); + + await sleep(1000); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.STARTED); + const [j1] = await e1.getJobs(); + expect(j1.status).toBe(JOB_STATUS.PENDING); + expect(j1.result).toBe(0.5); + + const res2 = await userAgents[1].resource('users_jobs').submit({ + filterByTk: pendingJobs[1].get('id'), + values: { + result: { f1: { a: 1 }, _: 'resolve' }, + }, + }); + expect(res2.status).toBe(202); + + await sleep(1000); + + const [e2] = await workflow.getExecutions(); + expect(e2.status).toBe(EXECUTION_STATUS.RESOLVED); + const [j2] = await e2.getJobs(); + expect(j2.status).toBe(JOB_STATUS.RESOLVED); + expect(j2.result).toBe(1); + }); + + it('all rejected', async () => { + const n1 = await workflow.createNode({ + type: 'manual', + config: { + assignees: [users[0].id, users[1].id], + mode: -1, + forms: { + f1: { + actions: [{ status: JOB_STATUS.REJECTED, key: 'reject' }], + }, + }, + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const UserJobModel = db.getModel('users_jobs'); + const pendingJobs = await UserJobModel.findAll({ + order: [['userId', 'ASC']], + }); + expect(pendingJobs.length).toBe(2); + + const res1 = await userAgents[0].resource('users_jobs').submit({ + filterByTk: pendingJobs[0].get('id'), + values: { + result: { f1: { a: 0 }, _: 'reject' }, + }, + }); + expect(res1.status).toBe(202); + + await sleep(1000); + + const [e1] = await workflow.getExecutions(); + expect(e1.status).toBe(EXECUTION_STATUS.STARTED); + const [j1] = await e1.getJobs(); + expect(j1.status).toBe(JOB_STATUS.PENDING); + expect(j1.result).toBe(0.5); + + const res2 = await userAgents[1].resource('users_jobs').submit({ + filterByTk: pendingJobs[1].get('id'), + values: { + result: { f1: { a: 0 }, _: 'reject' }, + }, + }); + expect(res2.status).toBe(202); + + await sleep(1000); + + const [e2] = await workflow.getExecutions(); + expect(e2.status).toBe(EXECUTION_STATUS.REJECTED); + const [j2] = await e2.getJobs(); + expect(j2.status).toBe(JOB_STATUS.REJECTED); + expect(j2.result).toBe(1); + }); + }); +}); diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/forms/create.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/forms/create.ts index a8df62961..9afc9b760 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/forms/create.ts +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/forms/create.ts @@ -1,8 +1,15 @@ import { Processor } from '@nocobase/plugin-workflow'; import ManualInstruction from '../ManualInstruction'; -export default async function (this: ManualInstruction, instance, { collection }, processor: Processor) { - const repo = this.workflow.db.getRepository(collection); +export default async function ( + this: ManualInstruction, + instance, + { dataSource = 'main', collection }, + processor: Processor, +) { + const repo = this.workflow.app.dataSourceManager.dataSources + .get(dataSource) + .collectionManager.getRepository(collection); if (!repo) { throw new Error(`collection ${collection} for create data on manual node not found`); } @@ -18,6 +25,6 @@ export default async function (this: ManualInstruction, instance, { collection } context: { executionId: processor.execution.id, }, - // transaction: processor.transaction, + transaction: processor.transaction, }); } diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/forms/update.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/forms/update.ts index 62702b83a..83adfe3b8 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/forms/update.ts +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/forms/update.ts @@ -1,8 +1,15 @@ import { Processor } from '@nocobase/plugin-workflow'; import ManualInstruction from '../ManualInstruction'; -export default async function (this: ManualInstruction, instance, { collection, filter = {} }, processor: Processor) { - const repo = this.workflow.db.getRepository(collection); +export default async function ( + this: ManualInstruction, + instance, + { dataSource = 'main', collection, filter = {} }, + processor: Processor, +) { + const repo = this.workflow.app.dataSourceManager.dataSources + .get(dataSource) + .collectionManager.getRepository(collection); if (!repo) { throw new Error(`collection ${collection} for update data on manual node not found`); } @@ -18,6 +25,6 @@ export default async function (this: ManualInstruction, instance, { collection, context: { executionId: processor.execution.id, }, - // transaction: processor.transaction, + transaction: processor.transaction, }); } diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/server/migrations/20240314163421-change-schema-data-source.ts b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/migrations/20240314163421-change-schema-data-source.ts new file mode 100644 index 000000000..4d452c2e5 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow-manual/src/server/migrations/20240314163421-change-schema-data-source.ts @@ -0,0 +1,76 @@ +import { Migration } from '@nocobase/server'; + +function findSchema(root, filter, onlyLeaf = false) { + const result = []; + + if (!root) { + return result; + } + + if (filter(root) && (!onlyLeaf || !root.properties)) { + result.push(root); + return result; + } + + if (root.properties) { + Object.keys(root.properties).forEach((key) => { + result.push(...findSchema(root.properties[key], filter)); + }); + } + return result; +} + +function migrateSchema(schema) { + const root = { properties: schema }; + + const [node] = findSchema(root, (item) => { + return ( + item['x-decorator'] === 'DetailsBlockProvider' && + item['x-component'] === 'CardItem' && + item['x-designer'] === 'SimpleDesigner' + ); + }); + + if (node && node['x-decorator-props']?.dataSource) { + node['x-decorator-props'].dataPath = node['x-decorator-props'].dataSource.replace(/^{{|}}$/g, ''); + delete node['x-decorator-props'].dataSource; + } + + return schema; +} + +export default class extends Migration { + async up() { + const { db } = this.context; + const NodeRepo = db.getRepository('flow_nodes'); + await db.sequelize.transaction(async (transaction) => { + const nodes = await NodeRepo.find({ + filter: { + type: 'manual', + }, + transaction, + }); + console.log('%d nodes need to be migrated.', nodes.length); + + await nodes.reduce( + (promise, node) => + promise.then(() => { + const { schema, ...config } = node.config; + return node.update( + { + config: { + ...config, + ...migrateSchema(schema), + }, + }, + { + silent: true, + transaction, + }, + ); + }), + Promise.resolve(), + ); + }); + } +} diff --git a/packages/plugins/@nocobase/plugin-workflow-sql/src/client/SQLInstruction.tsx b/packages/plugins/@nocobase/plugin-workflow-sql/src/client/SQLInstruction.tsx index a640ec4d6..bdb41f516 100644 --- a/packages/plugins/@nocobase/plugin-workflow-sql/src/client/SQLInstruction.tsx +++ b/packages/plugins/@nocobase/plugin-workflow-sql/src/client/SQLInstruction.tsx @@ -12,6 +12,21 @@ export default class extends Instruction { group = 'collection'; description = `{{t("Execute a SQL statement in database.", { ns: "${NAMESPACE}" })}}`; fieldset = { + dataSource: { + type: 'string', + required: true, + title: `{{t("Data source")}}`, + description: `{{t("Select a data source to execute SQL.", { ns: "${NAMESPACE}" })}}`, + 'x-decorator': 'FormItem', + 'x-component': 'DataSourceSelect', + 'x-component-props': { + className: 'auto-width', + filter(item) { + return item.options.isDBInstance; + }, + }, + default: 'main', + }, sql: { type: 'string', required: true, diff --git a/packages/plugins/@nocobase/plugin-workflow-sql/src/locale/zh-CN.json b/packages/plugins/@nocobase/plugin-workflow-sql/src/locale/zh-CN.json index 4d841b8e4..bb9549300 100644 --- a/packages/plugins/@nocobase/plugin-workflow-sql/src/locale/zh-CN.json +++ b/packages/plugins/@nocobase/plugin-workflow-sql/src/locale/zh-CN.json @@ -1,5 +1,6 @@ { "SQL action": "SQL 操作", "Execute a SQL statement in database.": "在数据库中执行一个 SQL 语句", + "Select a data source to execute SQL.": "选择一个数据源来执行 SQL", "SQL query result could be used through <1>JSON query node (Commercial plugin).": "SQL 执行的结果可在 <1>JSON 解析节点 中使用(商业插件)。" } diff --git a/packages/plugins/@nocobase/plugin-workflow-sql/src/server/SQLInstruction.ts b/packages/plugins/@nocobase/plugin-workflow-sql/src/server/SQLInstruction.ts index f75a1beaa..d47a0bfbb 100644 --- a/packages/plugins/@nocobase/plugin-workflow-sql/src/server/SQLInstruction.ts +++ b/packages/plugins/@nocobase/plugin-workflow-sql/src/server/SQLInstruction.ts @@ -2,15 +2,22 @@ import { Processor, Instruction, JOB_STATUS, FlowNodeModel } from '@nocobase/plu export default class extends Instruction { async run(node: FlowNodeModel, input, processor: Processor) { - const { sequelize } = (node.constructor).database; - const sql = processor.getParsedValue(node.config.sql ?? '', node.id).trim(); + // @ts-ignore + const { db } = this.workflow.app.dataSourceManager.dataSources.get( + node.config.dataSource || 'main', + ).collectionManager; + if (!db) { + throw new Error(`type of data source "${node.config.dataSource}" is not database`); + } + const sql = processor.getParsedValue(node.config.sql || '', node.id).trim(); if (!sql) { return { status: JOB_STATUS.RESOLVED, }; } - const result = await sequelize.query(sql, { + // @ts-ignore + const result = await db.sequelize.query(sql, { transaction: processor.transaction, // plain: true, // model: db.getCollection(node.config.collection).model diff --git a/packages/plugins/@nocobase/plugin-workflow-sql/src/server/__tests__/instruction.test.ts b/packages/plugins/@nocobase/plugin-workflow-sql/src/server/__tests__/instruction.test.ts index 4c626a66a..39bb2acb4 100644 --- a/packages/plugins/@nocobase/plugin-workflow-sql/src/server/__tests__/instruction.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow-sql/src/server/__tests__/instruction.test.ts @@ -92,6 +92,26 @@ describe('workflow > instructions > sql', () => { }); describe('sql with variables', () => { + it('$system.now', async () => { + const queryInterface = db.sequelize.getQueryInterface(); + const n1 = await workflow.createNode({ + type: 'sql', + config: { + sql: `select '{{$system.now}}' as a`, + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + const [sqlJob] = await execution.getJobs({ order: [['id', 'ASC']] }); + expect(sqlJob.status).toBe(JOB_STATUS.RESOLVED); + // expect(queryJob.status).toBe(JOB_STATUS.RESOLVED); + // expect(queryJob.result.read).toBe(post.id); + }); + it('update', async () => { const queryInterface = db.sequelize.getQueryInterface(); const n1 = await workflow.createNode({ @@ -195,4 +215,35 @@ describe('workflow > instructions > sql', () => { expect(job.status).toBe(JOB_STATUS.RESOLVED); }); }); + + describe('multiple data source', () => { + it('query on another data source', async () => { + const anotherSource = app.dataSourceManager.dataSources.get('another'); + const PostCollection = anotherSource.collectionManager.getCollection('posts'); + const { repository: AnotherPostRepo } = PostCollection; + const post = await AnotherPostRepo.create({ values: { title: 't1' } }); + const p1s = await AnotherPostRepo.find(); + expect(p1s.length).toBe(1); + + const n1 = await workflow.createNode({ + type: 'sql', + config: { + dataSource: 'another', + sql: `select * from ${PostCollection.quotedTableName()}`, + }, + }); + + await PostRepo.create({ values: { title: 't1' } }); + + await sleep(500); + + const [execution] = await workflow.getExecutions(); + expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED); + const [job] = await execution.getJobs(); + expect(job.result.length).toBe(2); + expect(job.result[0].length).toBe(1); + // @ts-ignore + expect(job.result[0][0].id).toBe(post.id); + }); + }); }); diff --git a/packages/plugins/@nocobase/plugin-workflow-test/src/e2e/e2ePageObjectModel.ts b/packages/plugins/@nocobase/plugin-workflow-test/src/e2e/e2ePageObjectModel.ts index a5bed4fc2..8f744651a 100644 --- a/packages/plugins/@nocobase/plugin-workflow-test/src/e2e/e2ePageObjectModel.ts +++ b/packages/plugins/@nocobase/plugin-workflow-test/src/e2e/e2ePageObjectModel.ts @@ -79,62 +79,102 @@ export class ApprovalTriggerNode { nodeTitle: Locator; nodeConfigure: Locator; collectionDropDown: Locator; - checkWthdrawable: Locator; - configureUserInterfaceButton: Locator; + dataBlocksInitiationRadio: Locator; + dataBlocksAndGlobalApprovalBlocksInitiationRadio: Locator; + allowedToBeWithdrawnCheckbox: Locator; + goToconfigureButton: Locator; addBlockButton: Locator; addApplyFormMenu: Locator; configureFieldsButton: Locator; configureActionsButton: Locator; saveDraftSwitch: Locator; + submitButton: Locator; + cancelButton: Locator; addNodeButton: Locator; constructor(page: Page, triggerName: string, collectionName: string) { this.page = page; this.node = page.getByText('TriggeraConfigure'); this.nodeTitle = page.locator('textarea').filter({ hasText: triggerName }); this.nodeConfigure = page.getByRole('button', { name: 'Configure' }); - this.collectionDropDown = page.getByRole('button', { name: 'Select collection' }); - this.checkWthdrawable = page.getByLabel('Withdrawable'); - this.configureUserInterfaceButton = page.getByRole('button', { name: 'Configure user interface' }); - this.addBlockButton = page.getByRole('button', { name: 'Add block' }); + this.collectionDropDown = page + .getByLabel('block-item-DataSourceCollectionCascader-workflows-Collection') + .locator('.ant-select-selection-search-input'); + this.dataBlocksInitiationRadio = page.getByLabel('Initiate and approve in data blocks only'); + this.dataBlocksAndGlobalApprovalBlocksInitiationRadio = page.getByLabel( + 'Initiate and approve in both data blocks and global approval blocks', + ); + this.allowedToBeWithdrawnCheckbox = page.getByLabel('Allowed to be withdrawn'); + this.goToconfigureButton = page.getByRole('button', { name: 'Go to configure' }); + this.addBlockButton = page.getByLabel(`schema-initializer-Grid-ApprovalApplyAddBlockButton-${collectionName}`); this.addApplyFormMenu = page.getByRole('menuitem', { name: 'Apply form' }); - this.configureFieldsButton = page.getByTestId('configure-fields-button-of-form-item-' + collectionName); - this.configureActionsButton = page.getByTestId( - 'approval-trigger-configure-form-actions-add-action-button-' + collectionName, + this.configureFieldsButton = page.getByLabel(`schema-initializer-Grid-form:configureFields-${collectionName}`); + this.configureActionsButton = page.getByLabel( + `schema-initializer-ActionBar-ApprovalApplyAddActionButton-${collectionName}`, ); this.saveDraftSwitch = page.getByRole('menuitem', { name: 'Save draft' }).getByRole('switch'); - this.addNodeButton = page.getByLabel('add-button', { exact: true }); + this.submitButton = page.getByLabel('action-Action-Submit-workflows'); + this.cancelButton = page.getByLabel('action-Action-Cancel-workflows'); + this.addNodeButton = this.addNodeButton = page.getByLabel('add-button', { exact: true }); } } -export class ApprovalNode { +export class ApprovalPassthroughModeNode { readonly page: Page; node: Locator; nodeTitle: Locator; nodeConfigure: Locator; + addAssigneesButton: Locator; assigneesDropDown: Locator; - checkReturnable: Locator; - configureUserInterfaceButton: Locator; + OrRadio: Locator; + AndRadio: Locator; + votingRadio: Locator; + parallellyRadio: Locator; + sequentiallyRadio: Locator; + goToconfigureButton: Locator; addBlockButton: Locator; - addApplyFormMenu: Locator; - configureFieldsButton: Locator; - configureActionsButton: Locator; - saveDraftSwitch: Locator; + addDetailsMenu: Locator; + detailsConfigureFieldsButton: Locator; + addActionsMenu: Locator; + actionsConfigureFieldsButton: Locator; + actionsConfigureActionsButton: Locator; + addApproveButton: Locator; + addRejectButton: Locator; + addReturnButton: Locator; + addNodeResult: Locator; + submitButton: Locator; + cancelButton: Locator; addNodeButton: Locator; constructor(page: Page, nodeName: string, collectionName: string) { this.page = page; - this.node = page.getByText('TriggeraConfigure'); - this.nodeTitle = page.locator('textarea').filter({ hasText: nodeName }); - this.nodeConfigure = page.getByRole('button', { name: 'Configure' }); - this.assigneesDropDown = page.getByLabel('Search'); - this.checkReturnable = page.getByLabel('Returnable'); - this.configureUserInterfaceButton = page.getByRole('button', { name: 'Configure user interface' }); - this.addBlockButton = page.getByTestId('add-block-button-in-workflow-workflows'); - this.addApplyFormMenu = page.getByRole('menuitem', { name: 'Apply form' }); - this.configureFieldsButton = page.getByTestId('configure-fields-button-of-form-item-' + collectionName); - this.configureActionsButton = page.getByTestId( - 'approval-trigger-configure-form-actions-add-action-button-' + collectionName, + this.node = page.getByLabel(`Approval-${nodeName}`, { exact: true }); + this.nodeTitle = page.getByLabel(`Approval-${nodeName}`, { exact: true }).getByRole('textbox'); + this.nodeConfigure = page + .getByLabel(`Approval-${nodeName}`, { exact: true }) + .getByRole('button', { name: 'Configure' }); + this.addAssigneesButton = page.getByRole('button', { name: 'plus Add assignee' }); + this.assigneesDropDown = page.getByTestId('select-single'); + this.OrRadio = page.getByLabel('Or', { exact: true }); + this.AndRadio = page.getByLabel('And', { exact: true }); + this.votingRadio = page.getByLabel('Voting', { exact: true }); + this.parallellyRadio = page.getByLabel('Parallelly', { exact: true }); + this.sequentiallyRadio = page.getByLabel('Sequentially', { exact: true }); + this.goToconfigureButton = page.getByRole('button', { name: 'Go to configure' }); + this.addBlockButton = page.getByLabel('schema-initializer-Grid-ApprovalProcessAddBlockButton-workflows'); + this.addDetailsMenu = page.getByRole('menuitem', { name: 'Details' }); + this.detailsConfigureFieldsButton = page.getByLabel( + `schema-initializer-Grid-ReadPrettyFormItemInitializers-${collectionName}`, ); - this.saveDraftSwitch = page.getByRole('menuitem', { name: 'Save draft' }).getByRole('switch'); + this.addActionsMenu = page.getByRole('menuitem', { name: 'Actions' }).getByRole('switch'); + this.actionsConfigureFieldsButton = page.getByLabel('schema-initializer-Grid-FormItemInitializers-approvalRecords'); + this.actionsConfigureActionsButton = page.getByLabel( + 'schema-initializer-ActionBar-ApprovalProcessAddActionButton-approvalRecords', + ); + this.addApproveButton = page.getByRole('menuitem', { name: 'Approve' }).getByRole('switch'); + this.addRejectButton = page.getByRole('menuitem', { name: 'Reject' }).getByRole('switch'); + this.addReturnButton = page.getByRole('menuitem', { name: 'Return' }).getByRole('switch'); + this.addNodeResult = page.getByRole('menuitem', { name: 'Node result right' }); + this.submitButton = page.getByLabel('action-Action-Submit-workflows'); + this.cancelButton = page.getByLabel('action-Action-Cancel-workflows'); this.addNodeButton = page.getByLabel(`add-button-calculation-${nodeName}`, { exact: true }); } } @@ -164,7 +204,9 @@ export class ScheduleTriggerNode { this.RrpeatModeDropdown = page.getByLabel('block-item-RepeatField-workflows-Repeat mode'); this.dataTableTimeFieldOptions = page.getByLabel('Based on date field of collection'); - this.collectionDropDown = page.getByRole('button', { name: 'Select collection' }); + this.collectionDropDown = page + .getByLabel('block-item-DataSourceCollectionCascader-workflows-Collection') + .locator('.ant-select-selection-search-input'); this.startTimeDropdown = page.getByLabel('block-item-OnField-workflows-Starts on'); this.submitButton = page.getByLabel('action-Action-Submit-workflows'); this.cancelButton = page.getByLabel('action-Action-Cancel-workflows'); @@ -187,7 +229,10 @@ export class CollectionTriggerNode { this.node = page.getByLabel(`Trigger-${triggerName}`); this.nodeTitle = page.getByLabel(`Trigger-${triggerName}`).getByRole('textbox'); this.nodeConfigure = page.getByLabel(`Trigger-${triggerName}`).getByRole('button', { name: 'Configure' }); - this.collectionDropDown = page.getByRole('button', { name: 'Select collection' }); + // this.collectionDropDown = page.getByRole('button', { name: 'Select collection' }); + this.collectionDropDown = page + .getByLabel('block-item-DataSourceCollectionCascader-workflows-Collection') + .locator('.ant-select-selection-search-input'); this.triggerOnDropdown = page .getByLabel('block-item-Select-workflows-Trigger on') .getByRole('button', { name: 'Trigger on' }); @@ -212,7 +257,9 @@ export class FormEventTriggerNode { this.node = page.getByLabel(`Trigger-${triggerName}`); this.nodeTitle = page.getByLabel(`Trigger-${triggerName}`).getByRole('textbox'); this.nodeConfigure = page.getByLabel(`Trigger-${triggerName}`).getByRole('button', { name: 'Configure' }); - this.collectionDropDown = page.getByRole('button', { name: 'Select collection' }); + this.collectionDropDown = page + .getByLabel('block-item-DataSourceCollectionCascader-workflows-Collection') + .locator('.ant-select-selection-search-input'); this.relationalDataDropdown = page.getByTestId('select-field-Preload associations'); this.submitButton = page.getByLabel('action-Action-Submit-workflows'); this.cancelButton = page.getByLabel('action-Action-Cancel-workflows'); @@ -269,7 +316,9 @@ export class QueryRecordNode { this.nodeConfigure = page .getByLabel(`Query record-${nodeName}`, { exact: true }) .getByRole('button', { name: 'Configure' }); - this.collectionDropDown = page.getByRole('button', { name: 'Select collection' }); + this.collectionDropDown = page + .getByLabel('block-item-DataSourceCollectionCascader-workflows-Collection') + .locator('.ant-select-selection-search-input'); this.allowMultipleDataBoxesForResults = page.getByLabel('Allow multiple records as'); this.addSortFieldsButton = page.getByRole('button', { name: 'plus Add sort field' }); this.pageNumberEditBox = page.getByLabel('variable-constant'); @@ -299,7 +348,9 @@ export class CreateRecordNode { this.nodeConfigure = page .getByLabel(`Create record-${nodeName}`, { exact: true }) .getByRole('button', { name: 'Configure' }); - this.collectionDropDown = page.getByRole('button', { name: 'Select collection' }); + this.collectionDropDown = page + .getByLabel('block-item-DataSourceCollectionCascader-workflows-Collection') + .locator('.ant-select-selection-search-input'); this.addFieldsButton = page.getByRole('button', { name: 'plus Add field' }); this.submitButton = page.getByLabel('action-Action-Submit-workflows'); this.cancelButton = page.getByLabel('action-Action-Cancel-workflows'); @@ -326,7 +377,9 @@ export class UpdateRecordNode { this.nodeConfigure = page .getByLabel(`Update record-${nodeName}`, { exact: true }) .getByRole('button', { name: 'Configure' }); - this.collectionDropDown = page.getByRole('button', { name: 'Select collection' }); + this.collectionDropDown = page + .getByLabel('block-item-DataSourceCollectionCascader-workflows-Collection') + .locator('.ant-select-selection-search-input'); this.batchUpdateModeRadio = page .getByLabel('block-item-IndividualHooksRadioWithTooltip-workflows-Update mode') .getByLabel('Update in a batch'); @@ -356,7 +409,9 @@ export class DeleteRecordNode { this.nodeConfigure = page .getByLabel(`Delete record-${nodeName}`, { exact: true }) .getByRole('button', { name: 'Configure' }); - this.collectionDropDown = page.getByRole('button', { name: 'Select collection' }); + this.collectionDropDown = page + .getByLabel('block-item-DataSourceCollectionCascader-workflows-Collection') + .locator('.ant-select-selection-search-input'); this.submitButton = page.getByLabel('action-Action-Submit-workflows'); this.cancelButton = page.getByLabel('action-Action-Cancel-workflows'); this.addNodeButton = page.getByLabel(`add-button-delete-${nodeName}`, { exact: true }); @@ -395,11 +450,12 @@ export class AggregateNode { this.minRadio = page.getByLabel('MIN', { exact: true }); this.dataTableDataRadio = page.getByLabel('Data of collection'); this.linkedDataTableDataRadio = page.getByLabel('Data of associated collection'); - this.collectionDropDown = page.getByRole('button', { name: 'Select collection' }); - // this.aggregatedFieldDropDown = page.getByLabel('block-item-FieldsSelect-workflows-Field to aggregate').getByRole('textbox').getByRole('combobox'); - this.aggregatedFieldDropDown = page.locator( - 'input.ant-select-selection-search-input[role="combobox"][aria-haspopup="listbox"]', - ); + this.collectionDropDown = page + .getByLabel('block-item-DataSourceCollectionCascader-workflows-Data of collection') + .locator('.ant-select-selection-search-input'); + this.aggregatedFieldDropDown = page + .getByLabel('block-item-FieldsSelect-workflows-Field to aggregate') + .locator('.ant-select-selection-search-input'); this.distinctCheckBox = page .getByLabel('block-item-Checkbox-workflows-Distinct') .locator('input.ant-checkbox-input[type="checkbox"]'); @@ -572,7 +628,7 @@ export default module.exports = { WorkflowManagement, WorkflowListRecords, ApprovalTriggerNode, - ApprovalNode, + ApprovalPassthroughModeNode, ScheduleTriggerNode, CollectionTriggerNode, FormEventTriggerNode, diff --git a/packages/plugins/@nocobase/plugin-workflow-test/src/e2e/e2eUtils.ts b/packages/plugins/@nocobase/plugin-workflow-test/src/e2e/e2eUtils.ts index debedba5e..4d39877eb 100644 --- a/packages/plugins/@nocobase/plugin-workflow-test/src/e2e/e2eUtils.ts +++ b/packages/plugins/@nocobase/plugin-workflow-test/src/e2e/e2eUtils.ts @@ -1,4 +1,4 @@ -import { request } from '@nocobase/test/e2e'; +import { request, Page } from '@nocobase/test/e2e'; const PORT = process.env.APP_PORT || 20000; const APP_BASE_URL = process.env.APP_BASE_URL || `http://localhost:${PORT}`; @@ -722,6 +722,156 @@ export const apiSubmitRecordTriggerFormEvent = async (triggerWorkflows: string, return await result.json(); }; +// 获取数据源个数 +export const apiGetDataSourceCount = async () => { + const api = await request.newContext({ + storageState: process.env.PLAYWRIGHT_AUTH_FILE, + }); + const state = await api.storageState(); + const headers = getHeaders(state); + const result = await api.get(`/api/dataSources:list?pageSize=50`, { + headers, + }); + + if (!result.ok()) { + throw new Error(await result.text()); + } + /* + { + "data": 1 + } + */ + return (await result.json()).meta.count; +}; + +// 添加业务表单条数据触发工作流表单事件,triggerWorkflows=key1!field,key2,key3!field.subfield +export const apiCreateRecordTriggerActionEvent = async ( + collectionName: string, + triggerWorkflows: string, + data: any, +) => { + const api = await request.newContext({ + storageState: process.env.PLAYWRIGHT_AUTH_FILE, + }); + const state = await api.storageState(); + const headers = getHeaders(state); + /* + { + "title": "a11", + "enabled": true, + "description": null + } + */ + const result = await api.post(`/api/${collectionName}:create?triggerWorkflows=${triggerWorkflows}`, { + headers, + data, + }); + + if (!result.ok()) { + throw new Error(await result.text()); + } + /* + { + "data": { + "id": 1, + "createdAt": "2023-12-12T02:43:53.793Z", + "updatedAt": "2023-12-12T05:41:33.300Z", + "key": "fzk3j2oj4el", + "title": "a11", + "enabled": true, + "description": null + }, + "meta": { + "allowedActions": { + "view": [ + 1 + ], + "update": [ + 1 + ], + "destroy": [ + 1 + ] + } + } + } + */ + return (await result.json()).data; +}; + +// 审批中心发起审批 +export const apiApplyApprovalEvent = async (data: any) => { + const api = await request.newContext({ + storageState: process.env.PLAYWRIGHT_AUTH_FILE, + }); + const state = await api.storageState(); + const headers = getHeaders(state); + /* + { + "title": "a11", + "enabled": true, + "description": null + } + */ + const result = await api.post('/api/approvals:create', { + headers, + data, + }); + + if (!result.ok()) { + throw new Error(await result.text()); + } + /* + { + "data": { + "id": 35, + "collectionName": "tt_amt_orgREmwr", + "data": { + "id": 6, + "url": null, + "sort": 3, + "email": null, + "phone": null, + "address": null, + "orgcode": "区域编码000000006", + "orgname": "阿三大苏打实打实的", + "isenable": null, + "staffnum": null, + "createdAt": "2024-03-09T11:37:47.620Z", + "sharesnum": null, + "updatedAt": "2024-03-09T11:37:47.620Z", + "insurednum": null, + "range_json": null, + "regcapital": null, + "testdataid": null, + "createdById": 1, + "paidcapital": null, + "range_check": [], + "updatedById": 1, + "status_radio": null, + "establishdate": null, + "insuranceratio": null, + "range_markdown": null, + "range_richtext": null, + "status_singleselect": null, + "range_multipleselect": [], + "insuranceratio_formula": null + }, + "status": 2, + "workflowId": 39, + "dataKey": "6", + "updatedAt": "2024-03-09T11:37:47.640Z", + "createdAt": "2024-03-09T11:37:47.640Z", + "createdById": 1, + "updatedById": 1, + "workflowKey": null, + "latestExecutionId": null + } + } + */ + return (await result.json()).data; +}; + const getStorageItem = (key: string, storageState: any) => { return storageState.origins .find((item) => item.origin === APP_BASE_URL) @@ -767,6 +917,16 @@ function getHeaders(storageState: any) { return headers; } +// 用户登录新会话 +export const userLogin = async (page: Page, approvalUserEmail: string, approvalUser: string) => { + await page.goto(`${process.env.APP_BASE_URL}/signin`); + await page.getByPlaceholder('Email').fill(approvalUserEmail); + await page.getByPlaceholder('Password').fill(approvalUser); + await page.getByRole('button', { name: 'Sign in' }).click(); + await page.waitForLoadState('networkidle'); + return page; +}; + export default module.exports = { apiCreateWorkflow, apiUpdateWorkflow, @@ -783,4 +943,8 @@ export default module.exports = { apiCreateRecordTriggerFormEvent, apiSubmitRecordTriggerFormEvent, apiFilterList, + apiGetDataSourceCount, + apiCreateRecordTriggerActionEvent, + apiApplyApprovalEvent, + userLogin, }; diff --git a/packages/plugins/@nocobase/plugin-workflow-test/src/server/index.ts b/packages/plugins/@nocobase/plugin-workflow-test/src/server/index.ts index 1f384d5bc..85116ef5b 100644 --- a/packages/plugins/@nocobase/plugin-workflow-test/src/server/index.ts +++ b/packages/plugins/@nocobase/plugin-workflow-test/src/server/index.ts @@ -1,11 +1,14 @@ import path from 'path'; import { ApplicationOptions, Plugin } from '@nocobase/server'; -import { MockServer, createMockServer } from '@nocobase/test'; +import { MockServer, createMockServer, mockDatabase } from '@nocobase/test'; import functions from './functions'; import triggers from './triggers'; import instructions from './instructions'; +import { Resourcer } from '@nocobase/resourcer'; +import { SequelizeDataSource } from '@nocobase/data-source-manager'; +import { uid } from '@nocobase/utils'; export interface MockServerOptions extends ApplicationOptions { collectionsPath?: string; @@ -33,7 +36,7 @@ export async function getApp(options: MockServerOptions = {}): Promise Object.assign(components, workflowPlugin.instructions.get(type).components), + {}, + ); + + return ( + + + {children} + + + ); +} diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionLink.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionLink.tsx index 5220a0f72..922d804a5 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionLink.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/ExecutionLink.tsx @@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { useActionContext, useRecord } from '@nocobase/client'; -import { getWorkflowExecutionsPath } from './constant'; + +import { getWorkflowExecutionsPath } from './utils'; export const ExecutionLink = () => { const { t } = useTranslation(); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/FlowContext.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/FlowContext.tsx similarity index 100% rename from packages/plugins/@nocobase/plugin-workflow/src/client/FlowContext.ts rename to packages/plugins/@nocobase/plugin-workflow/src/client/FlowContext.tsx diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowCanvas.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowCanvas.tsx index 6b654fc6b..2e826a5f3 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowCanvas.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowCanvas.tsx @@ -21,8 +21,7 @@ import { FlowContext, useFlowContext } from './FlowContext'; import { lang } from './locale'; import { executionSchema } from './schemas/executions'; import useStyles from './style'; -import { linkNodes } from './utils'; -import { getWorkflowDetailPath } from './constant'; +import { linkNodes, getWorkflowDetailPath } from './utils'; import { ExecutionStatusColumn } from './components/ExecutionStatus'; function ExecutionResourceProvider({ request, filter = {}, ...others }) { diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowLink.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowLink.tsx index 27829791c..16cb9e888 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowLink.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/WorkflowLink.tsx @@ -2,9 +2,10 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { getWorkflowDetailPath } from './constant'; import { useActionContext, useGetAriaLabelOfAction, useRecord } from '@nocobase/client'; +import { getWorkflowDetailPath } from './utils'; + export const WorkflowLink = () => { const { t } = useTranslation(); const { id } = useRecord(); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/collectionEventTrigger/configuration.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/collectionEventTrigger/configuration.test.ts index e544d178f..c74c1310b 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/collectionEventTrigger/configuration.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/collectionEventTrigger/configuration.test.ts @@ -47,7 +47,9 @@ test.describe('Configuration page to configure the Trigger node', () => { const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName); await collectionTriggerNode.nodeConfigure.click(); await collectionTriggerNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + // await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await collectionTriggerNode.triggerOnDropdown.click(); await page.getByText('After record added', { exact: true }).click(); await collectionTriggerNode.submitButton.click(); @@ -101,7 +103,8 @@ test.describe('Configuration page to configure the Trigger node', () => { const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName); await collectionTriggerNode.nodeConfigure.click(); await collectionTriggerNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await collectionTriggerNode.triggerOnDropdown.click(); await page.getByText('After record updated', { exact: true }).click(); await collectionTriggerNode.submitButton.click(); @@ -157,7 +160,8 @@ test.describe('Configuration page to configure the Trigger node', () => { const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName); await collectionTriggerNode.nodeConfigure.click(); await collectionTriggerNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await collectionTriggerNode.triggerOnDropdown.click(); await page.getByText('After record added or updated', { exact: true }).click(); await collectionTriggerNode.submitButton.click(); @@ -218,7 +222,8 @@ test.describe('Configuration page to configure the Trigger node', () => { const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName); await collectionTriggerNode.nodeConfigure.click(); await collectionTriggerNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await collectionTriggerNode.triggerOnDropdown.click(); await page.getByText('After record added or updated', { exact: true }).click(); // 设置触发器过滤条件 @@ -285,7 +290,8 @@ test.describe('Configuration page to configure the Trigger node', () => { const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName); await collectionTriggerNode.nodeConfigure.click(); await collectionTriggerNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await collectionTriggerNode.triggerOnDropdown.click(); await page.getByText('After record updated', { exact: true }).click(); // 设置触发器过滤条件 @@ -723,7 +729,12 @@ test.describe('Configuration page copy to new version', () => { // 3、预期结果:新版本工作流配置内容同旧版本一样 const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName); await collectionTriggerNode.nodeConfigure.click(); - await expect(page.getByRole('button', { name: triggerNodeCollectionDisplayName })).toBeVisible(); + // await expect(page.getByRole('button', { name: `Main / ${triggerNodeCollectionDisplayName}` })).toBeVisible(); + await expect( + page + .getByLabel('block-item-DataSourceCollectionCascader-workflows-Collection') + .getByText(`Main / ${triggerNodeCollectionDisplayName}`), + ).toBeVisible(); // 4、后置处理:删除工作流 await apiDeleteWorkflow(workflowId); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/createRecordNode/CreateRecord.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/createRecordNode/CreateRecord.test.ts index 6c85b964e..21cdb3de6 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/createRecordNode/CreateRecord.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/createRecordNode/CreateRecord.test.ts @@ -73,7 +73,8 @@ test('Collection event add data trigger, single row text fields for common table const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText(); await createRecordNode.nodeConfigure.click(); await createRecordNode.collectionDropDown.click(); - await page.getByText(createNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: createNodeCollectionDisplayName }).click(); // 设置字段 await createRecordNode.addFieldsButton.click(); await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).click(); @@ -172,7 +173,8 @@ test('Collection event add data trigger, normal table single line text field, se const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText(); await createRecordNode.nodeConfigure.click(); await createRecordNode.collectionDropDown.click(); - await page.getByText(createNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: createNodeCollectionDisplayName }).click(); // 设置字段 await createRecordNode.addFieldsButton.click(); await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).click(); @@ -273,7 +275,8 @@ test('Collection event add data trigger, normal table integer field, set constan const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText(); await createRecordNode.nodeConfigure.click(); await createRecordNode.collectionDropDown.click(); - await page.getByText(createNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: createNodeCollectionDisplayName }).click(); // 设置字段 await createRecordNode.addFieldsButton.click(); await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).click(); @@ -372,7 +375,8 @@ test('Collection event add data trigger, normal table integer field, set trigger const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText(); await createRecordNode.nodeConfigure.click(); await createRecordNode.collectionDropDown.click(); - await page.getByText(createNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: createNodeCollectionDisplayName }).click(); // 设置字段 await createRecordNode.addFieldsButton.click(); await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).click(); @@ -473,7 +477,8 @@ test('Collection event add data trigger, normal table numeric field, set constan const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText(); await createRecordNode.nodeConfigure.click(); await createRecordNode.collectionDropDown.click(); - await page.getByText(createNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: createNodeCollectionDisplayName }).click(); // 设置字段 await createRecordNode.addFieldsButton.click(); await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).click(); @@ -572,7 +577,8 @@ test('Collection event add data trigger, normal table numeric field, set trigger const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText(); await createRecordNode.nodeConfigure.click(); await createRecordNode.collectionDropDown.click(); - await page.getByText(createNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: createNodeCollectionDisplayName }).click(); // 设置字段 await createRecordNode.addFieldsButton.click(); await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).click(); @@ -673,7 +679,8 @@ test('Collection event add data trigger, normal table dropdown radio field, set const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText(); await createRecordNode.nodeConfigure.click(); await createRecordNode.collectionDropDown.click(); - await page.getByText(createNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: createNodeCollectionDisplayName }).click(); // 设置字段 await createRecordNode.addFieldsButton.click(); await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).click(); @@ -769,7 +776,8 @@ test('Collection event add data trigger, normal table dropdown radio field, set const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText(); await createRecordNode.nodeConfigure.click(); await createRecordNode.collectionDropDown.click(); - await page.getByText(createNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: createNodeCollectionDisplayName }).click(); // 设置字段 await createRecordNode.addFieldsButton.click(); await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).click(); @@ -870,7 +878,8 @@ test('Collection event add data trigger, normal table dropdown radio fields, set const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText(); await createRecordNode.nodeConfigure.click(); await createRecordNode.collectionDropDown.click(); - await page.getByText(createNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: createNodeCollectionDisplayName }).click(); // 设置字段 await createRecordNode.addFieldsButton.click(); await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).click(); @@ -967,7 +976,8 @@ test('Collection event add data trigger, normal table dropdown radio fields, set const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText(); await createRecordNode.nodeConfigure.click(); await createRecordNode.collectionDropDown.click(); - await page.getByText(createNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: createNodeCollectionDisplayName }).click(); // 设置字段 await createRecordNode.addFieldsButton.click(); await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).click(); @@ -1068,7 +1078,8 @@ test('Collection event add data trigger, normal table date field, set constant d const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText(); await createRecordNode.nodeConfigure.click(); await createRecordNode.collectionDropDown.click(); - await page.getByText(createNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: createNodeCollectionDisplayName }).click(); // 设置字段 await createRecordNode.addFieldsButton.click(); await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).click(); @@ -1169,7 +1180,8 @@ test('Collection event add data trigger, normal table date field, set trigger no const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText(); await createRecordNode.nodeConfigure.click(); await createRecordNode.collectionDropDown.click(); - await page.getByText(createNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: createNodeCollectionDisplayName }).click(); // 设置字段 await createRecordNode.addFieldsButton.click(); await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).click(); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/deleteRecordNode/DeleteRecord.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/deleteRecordNode/DeleteRecord.test.ts index e40cb2755..c43bb7018 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/deleteRecordNode/DeleteRecord.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/deleteRecordNode/DeleteRecord.test.ts @@ -78,7 +78,8 @@ test('Collection event add data trigger, filter single line text field not null, const deleteRecordNode = new DeleteRecordNode(page, deleteRecordNodeName); await deleteRecordNode.nodeConfigure.click(); await deleteRecordNode.collectionDropDown.click(); - await page.getByText(deleteNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: deleteNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page.getByLabel('block-item-Filter-workflows-Filter').getByRole('button', { name: 'Select field' }).click(); @@ -172,7 +173,8 @@ test('Collection event add data trigger, filter single line text field is trigge const deleteRecordNode = new DeleteRecordNode(page, deleteRecordNodeName); await deleteRecordNode.nodeConfigure.click(); await deleteRecordNode.collectionDropDown.click(); - await page.getByText(deleteNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: deleteNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page.getByLabel('block-item-Filter-workflows-Filter').getByRole('button', { name: 'Select field' }).click(); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/queryRecordNode/QueryRecord.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/queryRecordNode/QueryRecord.test.ts index f7c2aa531..2ef724a9a 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/queryRecordNode/QueryRecord.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/queryRecordNode/QueryRecord.test.ts @@ -61,7 +61,8 @@ test('Collection event add data trigger, no filter no sort query common table 1 const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText(); await queryRecordNode.nodeConfigure.click(); await queryRecordNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await queryRecordNode.submitButton.click(); // 2、测试步骤:添加数据触发工作流 @@ -138,7 +139,8 @@ test('Collection event add data trigger, no filtering and no sorting, query comm const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText(); await queryRecordNode.nodeConfigure.click(); await queryRecordNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await queryRecordNode.allowMultipleDataBoxesForResults.check(); await expect(queryRecordNode.pageNumberEditBox).toHaveValue('1'); await expect(queryRecordNode.pageSizeEditBox).toHaveValue('20'); @@ -216,7 +218,8 @@ test('Collection event add data trigger, no filter ID ascending, query common ta const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText(); await queryRecordNode.nodeConfigure.click(); await queryRecordNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await queryRecordNode.allowMultipleDataBoxesForResults.check(); // 设置排序条件 await queryRecordNode.addSortFieldsButton.click(); @@ -324,7 +327,8 @@ test('Collection event add data trigger, no filter ID descending, query common t const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText(); await queryRecordNode.nodeConfigure.click(); await queryRecordNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await queryRecordNode.allowMultipleDataBoxesForResults.check(); // 设置排序条件 await queryRecordNode.addSortFieldsButton.click(); @@ -433,7 +437,8 @@ test('Collection event add data trigger, no filtering and no sorting, query mult const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText(); await queryRecordNode.nodeConfigure.click(); await queryRecordNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await queryRecordNode.allowMultipleDataBoxesForResults.check(); await expect(queryRecordNode.pageNumberEditBox).toHaveValue('1'); await expect(queryRecordNode.pageSizeEditBox).toHaveValue('20'); @@ -536,7 +541,8 @@ test('Collection event add data trigger, no filtering and no sorting, query the const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText(); await queryRecordNode.nodeConfigure.click(); await queryRecordNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); // await queryRecordNode.allowMultipleDataBoxesForResults.check(); await expect(queryRecordNode.pageNumberEditBox).toHaveValue('1'); await expect(queryRecordNode.pageSizeEditBox).toHaveValue('20'); @@ -641,7 +647,8 @@ test('Collection event add data trigger, no filtering and no sorting, query the const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText(); await queryRecordNode.nodeConfigure.click(); await queryRecordNode.collectionDropDown.click(); - await page.getByRole('option', { name: triggerNodeCollectionDisplayName }).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: triggerNodeCollectionDisplayName }).click(); await queryRecordNode.allowMultipleDataBoxesForResults.check(); await expect(queryRecordNode.pageNumberEditBox).toHaveValue('1'); await expect(queryRecordNode.pageSizeEditBox).toHaveValue('20'); @@ -769,7 +776,8 @@ test('Collection event add data trigger, filter to meet all conditions (status_s const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText(); await queryRecordNode.nodeConfigure.click(); await queryRecordNode.collectionDropDown.click(); - await page.getByText(queryNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: queryNodeCollectionDisplayName }).click(); // await queryRecordNode.allowMultipleDataBoxesForResults.check(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -884,7 +892,8 @@ test('Collection event add data trigger, filter to satisfy any condition (status const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText(); await queryRecordNode.nodeConfigure.click(); await queryRecordNode.collectionDropDown.click(); - await page.getByText(queryNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: queryNodeCollectionDisplayName }).click(); await queryRecordNode.allowMultipleDataBoxesForResults.check(); // 设置过滤条件 await page.getByTestId('filter-select-all-or-any').click(); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/updateRecordNode/updateInABatch.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/updateRecordNode/updateInABatch.test.ts index e683c4366..ebbcd1c2e 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/updateRecordNode/updateInABatch.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/updateRecordNode/updateInABatch.test.ts @@ -86,7 +86,8 @@ test('Collection event add data trigger, filter single line text field not empty const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -240,7 +241,8 @@ test('Collection event add data trigger, filter single line text field not empty const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -401,7 +403,8 @@ test('Collection event add data trigger, filter multi-line text field not empty, const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -555,7 +558,8 @@ test('Collection event add data trigger, filter multiline text field not empty, const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -710,7 +714,8 @@ test('Collection event add data trigger, filter integer field not null, common t const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -860,7 +865,8 @@ test('Collection event add data trigger, filter integer field not empty, common const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -1014,7 +1020,8 @@ test('Collection event add data trigger, filter numeric field not null, common t const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -1163,7 +1170,8 @@ test('Collection event add data trigger, filter numeric field not empty, common const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -1315,7 +1323,8 @@ test('Collection event add data trigger, filter dropdown radio field not null, c const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -1464,7 +1473,8 @@ test('Collection event add data trigger, filter dropdown radio field not empty, const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -1619,7 +1629,8 @@ test('Collection event add data trigger, filter dropdown radio fields not null, const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -1772,7 +1783,8 @@ test('Collection event add data trigger, filter dropdown radio fields not empty, const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -1931,7 +1943,8 @@ test('Collection event add data trigger, filter date field not null, common tabl const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page @@ -2086,7 +2099,8 @@ test('Collection event add data trigger, filter date field not empty, common tab const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); await page diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/updateRecordNode/updateOneByOne.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/updateRecordNode/updateOneByOne.test.ts index d0a6a3ef9..9fb74ace6 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/updateRecordNode/updateOneByOne.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/__e2e__/updateRecordNode/updateOneByOne.test.ts @@ -86,7 +86,8 @@ test('Collection event add data trigger, filter single line text field not empty const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -241,7 +242,8 @@ test('Collection event add data trigger, filter single line text field not empty const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -403,7 +405,8 @@ test('Collection event add data trigger, filter multi-line text field not empty, const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -558,7 +561,8 @@ test('Collection event add data trigger, filter multiline text field not empty, const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -714,7 +718,8 @@ test('Collection event add data trigger, filter integer field not null, common t const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -865,7 +870,8 @@ test('Collection event add data trigger, filter integer field not empty, common const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -1020,7 +1026,8 @@ test('Collection event add data trigger, filter numeric field not null, common t const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -1170,7 +1177,8 @@ test('Collection event add data trigger, filter numeric field not empty, common const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -1323,7 +1331,8 @@ test('Collection event add data trigger, filter dropdown radio field not null, c const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -1473,7 +1482,8 @@ test('Collection event add data trigger, filter dropdown radio field not empty, const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -1629,7 +1639,8 @@ test('Collection event add data trigger, filter dropdown radio fields not null, const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -1783,7 +1794,8 @@ test('Collection event add data trigger, filter dropdown radio fields not empty, const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -1943,7 +1955,8 @@ test('Collection event add data trigger, filter date field not null, common tabl const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); @@ -2099,7 +2112,8 @@ test('Collection event add data trigger, filter date field not empty, common tab const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText(); await updateRecordNode.nodeConfigure.click(); await updateRecordNode.collectionDropDown.click(); - await page.getByText(updateNodeCollectionDisplayName).click(); + await page.getByRole('menuitemcheckbox', { name: 'Main right' }).click(); + await page.getByRole('menuitemcheckbox', { name: updateNodeCollectionDisplayName }).click(); await updateRecordNode.articleByArticleUpdateModeRadio.click(); // 设置过滤条件 await page.getByText('Add condition', { exact: true }).click(); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionBlockInitializer.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionBlockInitializer.tsx index 72bc3b079..3359b905d 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionBlockInitializer.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionBlockInitializer.tsx @@ -1,9 +1,11 @@ import React from 'react'; +import { uid } from '@formily/shared'; import { CollectionProvider_deprecated, SchemaInitializerItem, SchemaInitializerItemType, + parseCollectionName, useCollectionManager_deprecated, useRecordCollectionDataSourceItems, useSchemaInitializer, @@ -13,23 +15,29 @@ import { import { traverseSchema } from '../utils'; -function InnerCollectionBlockInitializer({ collection, dataSource, ...props }) { +function InnerCollectionBlockInitializer({ collection, dataPath, ...props }) { const { insert } = useSchemaInitializer(); const { getTemplateSchemaByMode } = useSchemaTemplateManager(); const { getCollection } = useCollectionManager_deprecated(); const items = useRecordCollectionDataSourceItems('FormItem') as SchemaInitializerItemType[]; - const resolvedCollection = getCollection(collection); + let resolvedCollection; + if (typeof collection === 'string') { + const [dataSourceName, collectionName] = parseCollectionName(collection); + resolvedCollection = getCollection(collectionName, dataSourceName); + } else { + resolvedCollection = collection; + } async function onConfirm({ item }) { const template = item.template ? await getTemplateSchemaByMode(item) : null; const result = { type: 'void', - name: resolvedCollection.name, + name: uid(), title: resolvedCollection.title, 'x-decorator': 'DetailsBlockProvider', 'x-decorator-props': { collection, - dataSource, + dataPath, }, 'x-component': 'CardItem', 'x-component-props': { @@ -66,11 +74,20 @@ function InnerCollectionBlockInitializer({ collection, dataSource, ...props }) { return ; } -export function CollectionBlockInitializer() { +export function CollectionBlockInitializer(props) { const itemConfig = useSchemaInitializerItem(); + const sourceCollection = props?.collection ?? itemConfig.collection; + let dataSource, collection; + if (typeof sourceCollection === 'string') { + const parsed = parseCollectionName(sourceCollection); + dataSource = parsed[0]; + collection = parsed[1]; + } else { + collection = sourceCollection; + } return ( - - + + ); } diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionFieldset.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionFieldset.tsx index e98aaf13c..3c9d7bd3a 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionFieldset.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/CollectionFieldset.tsx @@ -6,6 +6,7 @@ import { SchemaComponent, Variable, css, + parseCollectionName, useCollectionManager_deprecated, useCompile, useToken, @@ -21,7 +22,8 @@ function AssociationInput(props) { const { path } = useField(); const fieldName = path.segments[path.segments.length - 1] as string; const { values: config } = useForm(); - const fields = getCollectionFields(config?.collection); + const [dataSourceName, collectionName] = parseCollectionName(config?.collection); + const fields = getCollectionFields(collectionName, dataSourceName); const { type } = fields.find((item) => item.name === fieldName); const value = Array.isArray(props.value) ? props.value.join(',') : props.value; @@ -39,11 +41,11 @@ const CollectionFieldSet = observer( const { t } = useTranslation(); const compile = useCompile(); const form = useForm(); - const { getCollection, getCollectionFields } = useCollectionManager_deprecated(); + const { getCollectionFields } = useCollectionManager_deprecated(); const scope = useWorkflowVariableOptions(); const { values: config } = form; - const collectionName = config?.collection; - const collectionFields = getCollectionFields(collectionName).filter((field) => field.uiSchema); + const [dataSourceName, collectionName] = parseCollectionName(config?.collection); + const collectionFields = getCollectionFields(collectionName, dataSourceName).filter((field) => field.uiSchema); const fields = filter ? collectionFields.filter(filter.bind(config)) : collectionFields; const unassignedFields = useMemo(() => fields.filter((field) => !value || !(field.name in value)), [fields, value]); @@ -79,7 +81,7 @@ const CollectionFieldSet = observer( `} > {fields.length ? ( - + {fields .filter((field) => value && field.name in value) .map((field) => { diff --git a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/DetailsBlockProvider.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/components/DetailsBlockProvider.tsx similarity index 71% rename from packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/DetailsBlockProvider.tsx rename to packages/plugins/@nocobase/plugin-workflow/src/client/components/DetailsBlockProvider.tsx index 434c2fb89..b378de71c 100644 --- a/packages/plugins/@nocobase/plugin-workflow-manual/src/client/instruction/DetailsBlockProvider.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/DetailsBlockProvider.tsx @@ -1,19 +1,21 @@ +import React, { useMemo, useRef } from 'react'; import { createForm } from '@formily/core'; import { useField } from '@formily/react'; +import { get } from 'lodash'; import { BlockRequestContext_deprecated, CollectionProvider_deprecated, FormBlockContext, RecordProvider, + parseCollectionName, useAPIClient, useAssociationNames, useBlockRequestContext, } from '@nocobase/client'; -import { useFlowContext } from '@nocobase/plugin-workflow/client'; -import { parse } from '@nocobase/utils/client'; -import React, { useMemo, useRef } from 'react'; -function useFlowContextData(dataSource) { +import { useFlowContext } from '../FlowContext'; + +function useFlowContextData(dataPath) { const { execution, nodes } = useFlowContext(); const nodesKeyMap = useMemo(() => { @@ -31,16 +33,24 @@ function useFlowContextData(dataSource) { [execution?.context, execution?.jobs, nodesKeyMap], ); - const result = useMemo(() => parse(dataSource)(data), [data, dataSource]); + const result = useMemo(() => get(data, dataPath), [data, dataPath]); return result; } -export function DetailsBlockProvider(props) { +export function DetailsBlockProvider({ collection, dataPath, children }) { const field = useField(); const formBlockRef = useRef(null); const { getAssociationAppends } = useAssociationNames(); const { appends, updateAssociationValues } = getAssociationAppends(); - const values = useFlowContextData(props.dataSource); + const values = useFlowContextData(dataPath); + let dataSourceName, resolvedCollection; + if (typeof collection === 'string') { + const parsed = parseCollectionName(collection); + dataSourceName = parsed[0]; + resolvedCollection = parsed[1]; + } else { + resolvedCollection = collection; + } const form = useMemo( () => @@ -61,11 +71,11 @@ export function DetailsBlockProvider(props) { }, }; const api = useAPIClient(); - const resource = api.resource(props.collection); + const resource = api.resource(resolvedCollection); const __parent = useBlockRequestContext(); return ( - + - {props.children} + {children} diff --git a/packages/plugins/@nocobase/plugin-workflow/src/client/components/FieldsSelect.tsx b/packages/plugins/@nocobase/plugin-workflow/src/client/components/FieldsSelect.tsx index 358766af8..c8b684e96 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/client/components/FieldsSelect.tsx +++ b/packages/plugins/@nocobase/plugin-workflow/src/client/components/FieldsSelect.tsx @@ -2,7 +2,7 @@ import { observer, useForm } from '@formily/react'; import { Select } from 'antd'; import React from 'react'; -import { useCollectionManager_deprecated, useCompile } from '@nocobase/client'; +import { parseCollectionName, useCollectionManager_deprecated, useCompile } from '@nocobase/client'; function defaultFilter() { return true; @@ -14,7 +14,8 @@ export const FieldsSelect = observer( const compile = useCompile(); const { getCollectionFields } = useCollectionManager_deprecated(); const { values } = useForm(); - const fields = getCollectionFields(values?.collection); + const [dataSourceName, collectionName] = parseCollectionName(values?.collection); + const fields = getCollectionFields(collectionName, dataSourceName); return (