mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-12-02 12:18:15 +08:00
feat(plugin-workflow): support multiple data source in workflow (#3739)
* feat(plugin-workflow): support multiple data source in workflow * fix(plugin-workflow): fix test cases * test(plugin-workflow-sql): debug test case * fix(plugin-workflow): fix collection trigger creation without config * test(plugin-workflow-sql): debug test case * fix: workflow e2e test * chore(ci): disable console intercept in vitest * chore(ci): disable console intercept in vitest * chore(ci): disable console intercept in vitest * chore(ci): disable console intercept in vitest * test(plugin-workflow-sql): debug test case * test: approval e2e * fix: remove pro-plugins from packages * refactor(plugin-workflow): support pass collection from props to CollectionBlockInitializer * test(plugin-workflow): add test case * fix(plugin-workflow): disable modification of executed workflow * fix: e2ePageObjectModel * fix: load data source when data source load failed (#3793) * chore: console.log * fix(subTable): fix sorting rule setting (#3795) * fix: through collection support search (#3800) * fix(client): visible -> useVisible * fix(client): fix action designer error occured in custom form (#3801) * fix(client): fix action designer error occured in custom form * fix(client): fix from the source * chore(module): remove submodule * fix(plugin-workflow): fix client cycling import * fix(plugin-workflow): fix collection event name * fix(plugin-workflow): fix undefined ref --------- Co-authored-by: hongboji <j414562100@qq.com> Co-authored-by: ChengLei Shao <chareice@live.com> Co-authored-by: Zeke Zhang <958414905@qq.com> Co-authored-by: katherinehhh <shunai.tang@hand-china.com> Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
f2828cd8b0
commit
d691e4c7e6
@ -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') {
|
||||
|
@ -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 (
|
||||
<RemoteSelect
|
||||
manual={false}
|
||||
placeholder={t('Select workflow', { ns: 'workflow' })}
|
||||
fieldNames={{
|
||||
label: 'title',
|
||||
value: 'key',
|
||||
}}
|
||||
service={{
|
||||
resource: 'workflows',
|
||||
action: 'list',
|
||||
params: {
|
||||
filter: {
|
||||
type: workflowTypes,
|
||||
enabled: true,
|
||||
'config.collection': workflowCollection,
|
||||
<DataSourceProvider dataSource="main">
|
||||
<RemoteSelect
|
||||
manual={false}
|
||||
placeholder={t('Select workflow', { ns: 'workflow' })}
|
||||
fieldNames={{
|
||||
label: 'title',
|
||||
value: 'key',
|
||||
}}
|
||||
service={{
|
||||
resource: 'workflows',
|
||||
action: 'list',
|
||||
params: {
|
||||
filter: {
|
||||
type: workflowTypes,
|
||||
enabled: true,
|
||||
'config.collection': workflowCollection,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
optionFilter={optionFilter}
|
||||
optionRender={({ label, data }) => {
|
||||
const typeOption = workflowPlugin.getTriggersOptions().find((item) => item.value === data.type);
|
||||
return typeOption ? (
|
||||
<Flex justify="space-between">
|
||||
<span>{label}</span>
|
||||
<Tag color={typeOption.color}>{compile(typeOption.label)}</Tag>
|
||||
</Flex>
|
||||
) : (
|
||||
label
|
||||
);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
}}
|
||||
optionFilter={optionFilter}
|
||||
optionRender={({ label, data }) => {
|
||||
const typeOption = workflowPlugin.getTriggersOptions().find((item) => item.value === data.type);
|
||||
return typeOption ? (
|
||||
<Flex justify="space-between">
|
||||
<span>{label}</span>
|
||||
<Tag color={typeOption.color}>{compile(typeOption.label)}</Tag>
|
||||
</Flex>
|
||||
) : (
|
||||
label
|
||||
);
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</DataSourceProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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' }),
|
||||
|
@ -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<TreeSelectProps & AppendsTreeSelectProp
|
||||
loadData: propsLoadData,
|
||||
...restProps
|
||||
} = props;
|
||||
const { getCollectionFields } = useCollectionManager_deprecated();
|
||||
const compile = useCompile();
|
||||
const { t } = useTranslation();
|
||||
const [optionsMap, setOptionsMap] = useState({});
|
||||
const baseCollection = useCollection({ collection });
|
||||
const collectionString = useCollection({ collection });
|
||||
const [dataSourceName, collectionName] = parseCollectionName(collectionString);
|
||||
const { getCollectionFields } = useCollectionManager_deprecated(dataSourceName);
|
||||
const treeData = Object.values(optionsMap);
|
||||
const value: string | DefaultOptionType[] = useMemo(() => {
|
||||
if (props.multiple) {
|
||||
@ -111,7 +118,7 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
|
||||
},
|
||||
[propsLoadData],
|
||||
);
|
||||
|
||||
// NOTE:
|
||||
useEffect(() => {
|
||||
const parentNode = rootOption
|
||||
? {
|
||||
@ -123,17 +130,19 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
|
||||
isLeaf: false,
|
||||
}
|
||||
: null;
|
||||
const treeData =
|
||||
const tData =
|
||||
propsLoadData === null
|
||||
? []
|
||||
: getCollectionFieldOptions.call({ compile, getCollectionFields, filter }, baseCollection, parentNode);
|
||||
const map = treeData.reduce((result, item) => 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]))) {
|
||||
|
@ -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}`;
|
||||
}
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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);
|
||||
|
@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -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 } = (<Application>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) {
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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 <Cascader {...props} value={p} options={options} onChange={onSelectChange} loadData={loadData as any} />;
|
||||
return (
|
||||
<Cascader
|
||||
{...props}
|
||||
value={p}
|
||||
options={options}
|
||||
changeOnSelect
|
||||
onChange={onSelectChange}
|
||||
loadData={loadData as any}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 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`
|
||||
|
@ -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();
|
||||
|
@ -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 } = <typeof FlowNodeModel>node.constructor;
|
||||
const [dataSourceName, collectionName] = parseCollectionName(collection);
|
||||
const { collectionManager } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName);
|
||||
const repo = associated
|
||||
? database.getRepository<HasManyRepository | BelongsToManyRepository>(
|
||||
? 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;
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 (
|
||||
<GeneralSchemaDesigner title={compile(title)}>
|
||||
<SchemaSettingsBlockTitleItem />
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
'x-component': 'Grid',
|
||||
}}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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,
|
||||
},
|
||||
]
|
||||
|
@ -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 (
|
||||
<AddCustomFormFieldButtonContext.Provider
|
||||
value={{
|
||||
onAddField(item) {
|
||||
const fieldInterface: Record<string, any> = 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,
|
||||
}}
|
||||
>
|
||||
<SchemaInitializerItems {...props} items={items} />
|
||||
<ActionContextProvider value={{ visible: Boolean(interfaceOptions) }}>
|
||||
<ActionContextProvider
|
||||
value={{
|
||||
visible: Boolean(interfaceOptions),
|
||||
setVisible(v) {
|
||||
if (!v) {
|
||||
setInterface(null);
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
{interfaceOptions ? (
|
||||
<SchemaComponent
|
||||
schema={{
|
||||
type: 'void',
|
||||
name: 'drawer',
|
||||
title: '{{t("Configure field")}}',
|
||||
'x-decorator': 'Form',
|
||||
'x-decorator': 'FormV2',
|
||||
'x-decorator-props': {
|
||||
form,
|
||||
},
|
||||
'x-component': 'Action.Drawer',
|
||||
properties: {
|
||||
...interfaceOptions.properties,
|
||||
@ -266,7 +284,7 @@ const CustomItemsComponent = (props) => {
|
||||
'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);
|
||||
},
|
||||
|
@ -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;
|
||||
})
|
||||
|
@ -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' });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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({
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
@ -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(),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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</1> (Commercial plugin).": "SQL 执行的结果可在 <1>JSON 解析节点</1> 中使用(商业插件)。"
|
||||
}
|
||||
|
@ -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 } = (<typeof FlowNodeModel>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
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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<MockServe
|
||||
}
|
||||
}
|
||||
}
|
||||
return createMockServer({
|
||||
const app = await createMockServer({
|
||||
...others,
|
||||
plugins: [
|
||||
[
|
||||
@ -49,6 +52,30 @@ export async function getApp(options: MockServerOptions = {}): Promise<MockServe
|
||||
...plugins,
|
||||
],
|
||||
});
|
||||
|
||||
await app.dataSourceManager.add(
|
||||
new SequelizeDataSource({
|
||||
name: 'another',
|
||||
collectionManager: {
|
||||
database: mockDatabase({
|
||||
tablePrefix: `t${uid(5)}`,
|
||||
}),
|
||||
},
|
||||
resourceManager: {},
|
||||
}),
|
||||
);
|
||||
const another = app.dataSourceManager.dataSources.get('another');
|
||||
// @ts-ignore
|
||||
const anotherDB = another.collectionManager.db;
|
||||
|
||||
await anotherDB.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
await anotherDB.sync();
|
||||
|
||||
another.acl.allow('*', '*');
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
export default class WorkflowTestPlugin extends Plugin {
|
||||
|
@ -21,10 +21,9 @@ import { ExecutionStatusOptionsMap, JobStatusOptions } from './constants';
|
||||
import { FlowContext, useFlowContext } from './FlowContext';
|
||||
import { lang, NAMESPACE } from './locale';
|
||||
import useStyles from './style';
|
||||
import { linkNodes } from './utils';
|
||||
import { linkNodes, getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
|
||||
import { DownOutlined, ExclamationCircleFilled, StopOutlined } from '@ant-design/icons';
|
||||
import { StatusButton } from './components/StatusButton';
|
||||
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
function attachJobs(nodes, jobs: any[] = []): void {
|
||||
|
@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
|
||||
import { SchemaComponentOptions, usePlugin } from '@nocobase/client';
|
||||
|
||||
import PluginWorkflowClient, { FlowContext } from '.';
|
||||
|
||||
export function ExecutionContextProvider({ children, workflow, execution, nodes }) {
|
||||
const workflowPlugin = usePlugin(PluginWorkflowClient);
|
||||
const triggerComponents = workflowPlugin.triggers.get(workflow.type).components;
|
||||
const nodeComponents = nodes.reduce(
|
||||
(components, { type }) => Object.assign(components, workflowPlugin.instructions.get(type).components),
|
||||
{},
|
||||
);
|
||||
|
||||
return (
|
||||
<FlowContext.Provider
|
||||
value={{
|
||||
workflow,
|
||||
nodes,
|
||||
execution,
|
||||
}}
|
||||
>
|
||||
<SchemaComponentOptions
|
||||
components={{
|
||||
...triggerComponents,
|
||||
...nodeComponents,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SchemaComponentOptions>
|
||||
</FlowContext.Provider>
|
||||
);
|
||||
}
|
@ -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();
|
||||
|
@ -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 }) {
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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 <SchemaInitializerItem {...props} onClick={onConfirm} items={items} />;
|
||||
}
|
||||
|
||||
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 (
|
||||
<CollectionProvider_deprecated collection={itemConfig.collection}>
|
||||
<InnerCollectionBlockInitializer {...itemConfig} />
|
||||
<CollectionProvider_deprecated dataSource={dataSource} collection={collection}>
|
||||
<InnerCollectionBlockInitializer {...itemConfig} {...props} />
|
||||
</CollectionProvider_deprecated>
|
||||
);
|
||||
}
|
||||
|
@ -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 ? (
|
||||
<CollectionProvider_deprecated collection={getCollection(collectionName)}>
|
||||
<CollectionProvider_deprecated name={collectionName} dataSource={dataSourceName}>
|
||||
{fields
|
||||
.filter((field) => value && field.name in value)
|
||||
.map((field) => {
|
||||
|
@ -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 (
|
||||
<CollectionProvider_deprecated collection={props.collection}>
|
||||
<CollectionProvider_deprecated dataSource={dataSourceName} collection={resolvedCollection}>
|
||||
<RecordProvider record={values} parent={null}>
|
||||
<BlockRequestContext_deprecated.Provider value={{ block: 'form', field, service, resource, __parent }}>
|
||||
<FormBlockContext.Provider
|
||||
@ -78,7 +88,7 @@ export function DetailsBlockProvider(props) {
|
||||
formBlockRef,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
{children}
|
||||
</FormBlockContext.Provider>
|
||||
</BlockRequestContext_deprecated.Provider>
|
||||
</RecordProvider>
|
@ -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 (
|
||||
<Select
|
||||
|
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { useFieldSchema } from '@formily/react';
|
||||
|
||||
import {
|
||||
GeneralSchemaDesigner,
|
||||
SchemaSettingsBlockTitleItem,
|
||||
SchemaSettingsDivider,
|
||||
SchemaSettingsRemove,
|
||||
useCompile,
|
||||
} from '@nocobase/client';
|
||||
|
||||
export function SimpleDesigner() {
|
||||
const schema = useFieldSchema();
|
||||
const compile = useCompile();
|
||||
return (
|
||||
<GeneralSchemaDesigner title={compile(schema.title)}>
|
||||
<SchemaSettingsBlockTitleItem />
|
||||
<SchemaSettingsDivider />
|
||||
<SchemaSettingsRemove
|
||||
removeParentsIfNoChildren
|
||||
breakRemoveOn={{
|
||||
'x-component': 'Grid',
|
||||
}}
|
||||
/>
|
||||
</GeneralSchemaDesigner>
|
||||
);
|
||||
}
|
@ -3,10 +3,12 @@ import { css, SchemaInitializerItem, useSchemaInitializer, useSchemaInitializerI
|
||||
import { parse } from '@nocobase/utils/client';
|
||||
import React from 'react';
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
import { SimpleDesigner } from './SimpleDesigner';
|
||||
|
||||
export const ValueBlock: (() => JSX.Element) & {
|
||||
Initializer: () => JSX.Element;
|
||||
Result: (props) => JSX.Element;
|
||||
Designer: () => JSX.Element;
|
||||
} = () => {
|
||||
return null;
|
||||
};
|
||||
@ -27,7 +29,7 @@ function Initializer() {
|
||||
'x-component-props': {
|
||||
title: node.title ?? `#${node.id}`,
|
||||
},
|
||||
'x-designer': 'SimpleDesigner',
|
||||
'x-designer': 'ValueBlock.Designer',
|
||||
properties: {
|
||||
result: {
|
||||
type: 'void',
|
||||
@ -71,3 +73,4 @@ function Result({ dataSource }) {
|
||||
|
||||
ValueBlock.Initializer = Initializer;
|
||||
ValueBlock.Result = Result;
|
||||
ValueBlock.Designer = SimpleDesigner;
|
||||
|
@ -1,6 +1,8 @@
|
||||
export * from './CollectionBlockInitializer';
|
||||
export * from './DetailsBlockProvider';
|
||||
export * from './FieldsSelect';
|
||||
export * from './FilterDynamicComponent';
|
||||
export * from './RadioWithTooltip';
|
||||
export * from './CheckboxGroupWithTooltip';
|
||||
export * from './ValueBlock';
|
||||
export * from './SimpleDesigner';
|
||||
|
@ -1,2 +0,0 @@
|
||||
export const getWorkflowDetailPath = (id: string | number) => `/admin/workflow/workflows/${id}`;
|
||||
export const getWorkflowExecutionsPath = (id: string | number) => `/admin/workflow/executions/${id}`;
|
@ -0,0 +1,3 @@
|
||||
export * from './useGetAriaLabelOfAddButton';
|
||||
export * from './useTriggerWorkflowActionProps';
|
||||
export * from './useWorkflowExecuted';
|
@ -0,0 +1,11 @@
|
||||
import { useFlowContext } from '../FlowContext';
|
||||
|
||||
export function useWorkflowExecuted() {
|
||||
const { workflow } = useFlowContext();
|
||||
return Boolean(workflow?.executed);
|
||||
}
|
||||
|
||||
export function useWorkflowAnyExecuted() {
|
||||
const { workflow } = useFlowContext();
|
||||
return Boolean(workflow?.allExecuted);
|
||||
}
|
@ -1,16 +1,3 @@
|
||||
export * from './Branch';
|
||||
export * from './FlowContext';
|
||||
export * from './constants';
|
||||
export * from './nodes';
|
||||
export { Trigger, useTrigger } from './triggers';
|
||||
export * from './variable';
|
||||
export * from './components';
|
||||
export * from './utils';
|
||||
export * from './hooks/useGetAriaLabelOfAddButton';
|
||||
export { default as useStyles } from './style';
|
||||
export * from './variable';
|
||||
export * from './hooks/useTriggerWorkflowActionProps';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { Plugin } from '@nocobase/client';
|
||||
@ -30,7 +17,7 @@ import QueryInstruction from './nodes/query';
|
||||
import CreateInstruction from './nodes/create';
|
||||
import UpdateInstruction from './nodes/update';
|
||||
import DestroyInstruction from './nodes/destroy';
|
||||
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
|
||||
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
|
||||
import { NAMESPACE } from './locale';
|
||||
import { customizeSubmitToWorkflowActionSettings } from './settings/customizeSubmitToWorkflowActionSettings';
|
||||
|
||||
@ -114,3 +101,16 @@ export default class PluginWorkflowClient extends Plugin {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export * from './Branch';
|
||||
export * from './FlowContext';
|
||||
export * from './constants';
|
||||
export * from './nodes';
|
||||
export { Trigger, useTrigger } from './triggers';
|
||||
export * from './variable';
|
||||
export * from './components';
|
||||
export * from './utils';
|
||||
export * from './hooks';
|
||||
export { default as useStyles } from './style';
|
||||
export * from './variable';
|
||||
export * from './ExecutionContextProvider';
|
||||
|
@ -18,7 +18,22 @@ export default class extends Instruction {
|
||||
group = 'collection';
|
||||
description = `{{t("Add new record to a collection. You can use variables from upstream nodes to assign values to fields.", { ns: "${NAMESPACE}" })}}`;
|
||||
fieldset = {
|
||||
collection,
|
||||
collection: {
|
||||
...collection,
|
||||
'x-reactions': [
|
||||
...collection['x-reactions'],
|
||||
{
|
||||
target: 'params',
|
||||
effects: ['onFieldValueChange'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$self.value}}',
|
||||
value: '{{Object.create({})}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// multiple: {
|
||||
// type: 'boolean',
|
||||
// title: '多条数据',
|
||||
@ -84,7 +99,7 @@ export default class extends Instruction {
|
||||
title: node.title ?? `#${node.id}`,
|
||||
Component: CollectionBlockInitializer,
|
||||
collection: node.config.collection,
|
||||
dataSource: `{{$jobsMapByNodeKey.${node.key}}}`,
|
||||
dataPath: `$jobsMapByNodeKey.${node.key}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,29 @@ export default class extends Instruction {
|
||||
group = 'collection';
|
||||
description = `{{t("Delete records of a collection. Could use variables in workflow context as filter. All records match the filter will be deleted.", { ns: "${NAMESPACE}" })}}`;
|
||||
fieldset = {
|
||||
collection,
|
||||
collection: {
|
||||
...collection,
|
||||
'x-reactions': [
|
||||
...collection['x-reactions'],
|
||||
{
|
||||
target: 'params',
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'params',
|
||||
effects: ['onFieldValueChange'],
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{Object.create({})}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
@ -22,7 +22,22 @@ export default class extends Instruction {
|
||||
group = 'collection';
|
||||
description = `{{t("Query records from a collection. You can use variables from upstream nodes as query conditions.", { ns: "${NAMESPACE}" })}}`;
|
||||
fieldset = {
|
||||
collection,
|
||||
collection: {
|
||||
...collection,
|
||||
'x-reactions': [
|
||||
...collection['x-reactions'],
|
||||
{
|
||||
target: 'params',
|
||||
effects: ['onFieldValueChange'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$self.value}}',
|
||||
value: '{{Object.create({})}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
multiple: {
|
||||
type: 'boolean',
|
||||
'x-decorator': 'FormItem',
|
||||
@ -130,7 +145,7 @@ export default class extends Instruction {
|
||||
title: node.title ?? `#${node.id}`,
|
||||
Component: CollectionBlockInitializer,
|
||||
collection: node.config.collection,
|
||||
dataSource: `{{$jobsMapByNodeKey.${node.key}}}`,
|
||||
dataPath: `$jobsMapByNodeKey.${node.key}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,38 @@ export default class extends Instruction {
|
||||
group = 'collection';
|
||||
description = `{{t("Update records of a collection. You can use variables from upstream nodes as query conditions and field values.", { ns: "${NAMESPACE}" })}}`;
|
||||
fieldset = {
|
||||
collection,
|
||||
collection: {
|
||||
...collection,
|
||||
'x-reactions': [
|
||||
...collection['x-reactions'],
|
||||
{
|
||||
target: 'params',
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$self.value}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'params.filter',
|
||||
effects: ['onFieldValueChange'],
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{Object.create({})}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'params.values',
|
||||
effects: ['onFieldValueChange'],
|
||||
fulfill: {
|
||||
state: {
|
||||
value: '{{Object.create({})}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
params: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { useForm } from '@formily/react';
|
||||
import { css, useCollectionFilterOptions } from '@nocobase/client';
|
||||
import { css, parseCollectionName, useCollectionFilterOptions } from '@nocobase/client';
|
||||
import { NAMESPACE } from '../locale';
|
||||
|
||||
export const collection = {
|
||||
@ -8,15 +8,13 @@ export const collection = {
|
||||
required: true,
|
||||
'x-reactions': [],
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'CollectionSelect',
|
||||
'x-component-props': {
|
||||
className: 'auto-width',
|
||||
},
|
||||
'x-component': 'DataSourceCollectionCascader',
|
||||
};
|
||||
|
||||
export const values = {
|
||||
type: 'object',
|
||||
title: '{{t("Fields values")}}',
|
||||
description: `{{t("Unassigned fields will be set to default values, and those without default values will be set to null.", { ns: "${NAMESPACE}" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {
|
||||
labelAlign: 'left',
|
||||
@ -25,7 +23,6 @@ export const values = {
|
||||
`,
|
||||
},
|
||||
'x-component': 'CollectionFieldset',
|
||||
description: `{{t("Unassigned fields will be set to default values, and those without default values will be set to null.", { ns: "${NAMESPACE}" })}}`,
|
||||
};
|
||||
|
||||
export const filter = {
|
||||
@ -36,7 +33,8 @@ export const filter = {
|
||||
'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`
|
||||
|
@ -1,12 +1,14 @@
|
||||
import React from 'react';
|
||||
import { ISchema } from '@formily/react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useActionContext, useRecord, useResourceActionContext, useResourceContext } from '@nocobase/client';
|
||||
import { ExecutionStatusOptions } from '../constants';
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { message } from 'antd';
|
||||
import { getWorkflowDetailPath } from '../constant';
|
||||
|
||||
import { useActionContext, useRecord, useResourceActionContext, useResourceContext } from '@nocobase/client';
|
||||
|
||||
import { ExecutionStatusOptions } from '../constants';
|
||||
import { NAMESPACE } from '../locale';
|
||||
import { getWorkflowDetailPath } from '../utils';
|
||||
|
||||
export const executionCollection = {
|
||||
name: 'execution-executions',
|
||||
|
@ -9,6 +9,7 @@ import { FieldsSelect } from '../components/FieldsSelect';
|
||||
import { NAMESPACE, lang } from '../locale';
|
||||
import { appends, collection, filter } from '../schemas/collection';
|
||||
import { getCollectionFieldOptions } from '../variable';
|
||||
import { useWorkflowAnyExecuted } from '../hooks';
|
||||
import { Trigger } from '.';
|
||||
|
||||
const COLLECTION_TRIGGER_MODE = {
|
||||
@ -31,6 +32,12 @@ export default class extends Trigger {
|
||||
fieldset = {
|
||||
collection: {
|
||||
...collection,
|
||||
'x-disabled': '{{ useWorkflowAnyExecuted() }}',
|
||||
'x-component-props': {
|
||||
dataSourceFilter(item) {
|
||||
return item.options.key === 'main' || item.options.isDBInstance;
|
||||
},
|
||||
},
|
||||
['x-reactions']: [
|
||||
...collection['x-reactions'],
|
||||
{
|
||||
@ -147,6 +154,7 @@ export default class extends Trigger {
|
||||
};
|
||||
scope = {
|
||||
useCollectionDataSource,
|
||||
useWorkflowAnyExecuted,
|
||||
};
|
||||
components = {
|
||||
FieldsSelect,
|
||||
@ -192,7 +200,7 @@ export default class extends Trigger {
|
||||
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
||||
Component: CollectionBlockInitializer,
|
||||
collection: config.collection,
|
||||
dataSource: '{{$context.data}}',
|
||||
dataPath: '$context.data',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,8 @@ function dateFieldFilter(field) {
|
||||
return !field.hidden && (field.uiSchema ? field.type === 'date' : false);
|
||||
}
|
||||
|
||||
export function OnField({ value, onChange }) {
|
||||
export function OnField({ value: propsValue, onChange }) {
|
||||
const value = propsValue ?? {};
|
||||
const { t } = useTranslation();
|
||||
const [dir, setDir] = useState(value.offset ? value.offset / Math.abs(value.offset) : 0);
|
||||
|
||||
|
@ -68,14 +68,21 @@ const ModeFieldsets = {
|
||||
[SCHEDULE_MODE.DATE_FIELD]: {
|
||||
collection: {
|
||||
...collection,
|
||||
'x-component-props': {
|
||||
dataSourceFilter(item) {
|
||||
return item.options.key === 'main' || item.options.isDBInstance;
|
||||
},
|
||||
},
|
||||
'x-reactions': [
|
||||
...collection['x-reactions'],
|
||||
{
|
||||
// only full path works
|
||||
target: 'startsOn',
|
||||
effects: ['onFieldValueChange'],
|
||||
fulfill: {
|
||||
state: {
|
||||
visible: '{{!!$self.value}}',
|
||||
value: '{{Object.create({})}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -79,7 +79,7 @@ export default class extends Trigger {
|
||||
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
||||
Component: CollectionBlockInitializer,
|
||||
collection: config.collection,
|
||||
dataSource: '{{$context.data}}',
|
||||
dataPath: '$context.data',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -43,3 +43,11 @@ export function traverseSchema(schema, fn) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorkflowDetailPath(id: string | number) {
|
||||
return `/admin/workflow/workflows/${id}`;
|
||||
}
|
||||
|
||||
export function getWorkflowExecutionsPath(id: string | number) {
|
||||
return `/admin/workflow/executions/${id}`;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Variable, useCompile, usePlugin } from '@nocobase/client';
|
||||
import { Variable, parseCollectionName, useCompile, usePlugin } from '@nocobase/client';
|
||||
|
||||
import { useFlowContext } from './FlowContext';
|
||||
import { NAMESPACE, lang } from './locale';
|
||||
@ -250,22 +250,23 @@ export function useWorkflowVariableOptions(options: UseVariableOptions = {}) {
|
||||
}
|
||||
|
||||
function getNormalizedFields(collectionName, { compile, getCollectionFields }) {
|
||||
const fields = getCollectionFields(collectionName);
|
||||
const [dataSourceName, collection] = parseCollectionName(collectionName);
|
||||
const fields = getCollectionFields(collection, dataSourceName);
|
||||
const foreignKeyFields: any[] = [];
|
||||
const otherFields: any[] = [];
|
||||
const result: any[] = [];
|
||||
fields.forEach((field) => {
|
||||
if (field.isForeignKey) {
|
||||
foreignKeyFields.push(field);
|
||||
} else {
|
||||
otherFields.push(field);
|
||||
result.push(field);
|
||||
}
|
||||
});
|
||||
for (let i = otherFields.length - 1; i >= 0; i--) {
|
||||
const field = otherFields[i];
|
||||
for (let i = result.length - 1; i >= 0; i--) {
|
||||
const field = result[i];
|
||||
if (field.type === 'belongsTo') {
|
||||
const foreignKeyField = foreignKeyFields.find((f) => f.name === field.foreignKey);
|
||||
if (foreignKeyField) {
|
||||
otherFields.splice(i, 0, {
|
||||
result.splice(i, 0, {
|
||||
...field,
|
||||
...foreignKeyField,
|
||||
uiSchema: {
|
||||
@ -274,7 +275,7 @@ function getNormalizedFields(collectionName, { compile, getCollectionFields }) {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
otherFields.splice(i, 0, {
|
||||
result.splice(i, 0, {
|
||||
...field,
|
||||
name: field.foreignKey,
|
||||
type: 'bigInt',
|
||||
@ -288,8 +289,8 @@ function getNormalizedFields(collectionName, { compile, getCollectionFields }) {
|
||||
}
|
||||
} else if (field.type === 'context' && field.collectionName === 'users') {
|
||||
const belongsToField =
|
||||
otherFields.find((f) => f.type === 'belongsTo' && f.target === 'users' && f.foreignKey === field.name) ?? {};
|
||||
otherFields.splice(i, 0, {
|
||||
result.find((f) => f.type === 'belongsTo' && f.target === 'users' && f.foreignKey === field.name) ?? {};
|
||||
result.splice(i, 0, {
|
||||
...field,
|
||||
type: field.dataType,
|
||||
interface: belongsToField.interface,
|
||||
@ -301,13 +302,15 @@ function getNormalizedFields(collectionName, { compile, getCollectionFields }) {
|
||||
}
|
||||
}
|
||||
|
||||
return otherFields.filter((field) => field.interface && !field.hidden);
|
||||
return result.filter((field) => field.interface && !field.hidden);
|
||||
}
|
||||
|
||||
function loadChildren(option) {
|
||||
const appends = getNextAppends(option.field, option.appends);
|
||||
const result = getCollectionFieldOptions({
|
||||
collection: option.field.target,
|
||||
collection: `${
|
||||
option.field.dataSourceKey && option.field.dataSourceKey !== 'main' ? `${option.field.dataSourceKey}:` : ''
|
||||
}${option.field.target}`,
|
||||
types: option.types,
|
||||
appends,
|
||||
depth: option.depth - 1,
|
||||
@ -336,8 +339,7 @@ export function getCollectionFieldOptions(options): VariableOption[] {
|
||||
getCollectionFields,
|
||||
fieldNames = defaultFieldNames,
|
||||
} = options;
|
||||
const normalizedFields = getNormalizedFields(collection, { compile, getCollectionFields });
|
||||
const computedFields = fields ?? normalizedFields;
|
||||
const computedFields = fields ?? getNormalizedFields(collection, { compile, getCollectionFields });
|
||||
const boundLoadChildren = loadChildren.bind({ compile, getCollectionFields, fieldNames });
|
||||
|
||||
const result: VariableOption[] = filterTypedFields({
|
||||
|
@ -126,4 +126,35 @@ describe('workflow > instructions > create', () => {
|
||||
expect(job.result.posts[0].id).toBe(post.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple data source', () => {
|
||||
it('create one', async () => {
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'create',
|
||||
config: {
|
||||
collection: 'another:posts',
|
||||
params: {
|
||||
values: {
|
||||
title: '{{$context.data.title}}',
|
||||
published: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result.title).toBe(post.title);
|
||||
|
||||
const AnotherPostRepo = app.dataSourceManager.dataSources.get('another').collectionManager.getRepository('posts');
|
||||
const p2s = await AnotherPostRepo.find();
|
||||
expect(p2s.length).toBe(1);
|
||||
expect(p2s[0].title).toBe(post.title);
|
||||
expect(p2s[0].published).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -55,4 +55,37 @@ describe('workflow > instructions > destroy', () => {
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple data source', () => {
|
||||
it('destroy one', 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: 'destroy',
|
||||
config: {
|
||||
collection: 'another:posts',
|
||||
params: {
|
||||
filter: {
|
||||
// @ts-ignore
|
||||
id: post.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
const [job] = await execution.getJobs();
|
||||
expect(job.result).toBe(1);
|
||||
|
||||
const p2s = await AnotherPostRepo.find();
|
||||
expect(p2s.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -381,4 +381,35 @@ describe('workflow > instructions > query', () => {
|
||||
expect(job.result).toMatchObject([]);
|
||||
});
|
||||
});
|
||||
|
||||
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: 'query',
|
||||
config: {
|
||||
collection: 'another:posts',
|
||||
params: {
|
||||
filter: {
|
||||
// @ts-ignore
|
||||
id: post.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.title).toBe('t1');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { Application } from '@nocobase/server';
|
||||
import Database from '@nocobase/database';
|
||||
import { MockDatabase } from '@nocobase/database';
|
||||
import { getApp, sleep } from '@nocobase/plugin-workflow-test';
|
||||
import { MockServer } from '@nocobase/test';
|
||||
|
||||
import type { WorkflowModel as WorkflowModelType } from '../../types';
|
||||
import { EXECUTION_STATUS } from '../../constants';
|
||||
|
||||
describe('workflow > instructions > update', () => {
|
||||
let app: Application;
|
||||
let db: Database;
|
||||
let app: MockServer;
|
||||
let db: MockDatabase;
|
||||
let PostRepo;
|
||||
let WorkflowModel;
|
||||
let workflow: WorkflowModelType;
|
||||
@ -60,6 +62,8 @@ describe('workflow > instructions > update', () => {
|
||||
});
|
||||
|
||||
it('params: from job of node', async () => {
|
||||
const p1 = await PostRepo.create({ values: { title: 't1' }, hooks: false });
|
||||
|
||||
const n1 = await workflow.createNode({
|
||||
type: 'query',
|
||||
config: {
|
||||
@ -98,6 +102,9 @@ describe('workflow > instructions > update', () => {
|
||||
// should get from db
|
||||
const post = await PostRepo.findById(id);
|
||||
expect(post.title).toBe('changed');
|
||||
|
||||
await p1.reload();
|
||||
expect(p1.title).toBe('t1');
|
||||
});
|
||||
});
|
||||
|
||||
@ -186,4 +193,40 @@ describe('workflow > instructions > update', () => {
|
||||
expect(w2Exes.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple data source', () => {
|
||||
it('update one', 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: 'update',
|
||||
config: {
|
||||
collection: 'another:posts',
|
||||
params: {
|
||||
filter: {
|
||||
// @ts-ignore
|
||||
id: post.id,
|
||||
},
|
||||
values: {
|
||||
title: 't2',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const [execution] = await workflow.getExecutions();
|
||||
expect(execution.status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
|
||||
const p2s = await AnotherPostRepo.find();
|
||||
expect(p2s.length).toBe(1);
|
||||
expect(p2s[0].title).toBe('t2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,12 @@
|
||||
import Database from '@nocobase/database';
|
||||
import { MockDatabase } from '@nocobase/database';
|
||||
import { MockServer } from '@nocobase/test';
|
||||
import { getApp, sleep } from '@nocobase/plugin-workflow-test';
|
||||
import { Application } from '@nocobase/server';
|
||||
|
||||
import { EXECUTION_STATUS } from '../../constants';
|
||||
|
||||
describe('workflow > triggers > collection', () => {
|
||||
let app: Application;
|
||||
let db: Database;
|
||||
let app: MockServer;
|
||||
let db: MockDatabase;
|
||||
let CategoryRepo;
|
||||
let PostRepo;
|
||||
let CommentRepo;
|
||||
@ -14,7 +15,7 @@ describe('workflow > triggers > collection', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await getApp({
|
||||
plugins: ['error-handler', 'collection-manager'],
|
||||
plugins: ['error-handler', 'collection-manager', 'users', 'auth'],
|
||||
});
|
||||
|
||||
db = app.db;
|
||||
@ -28,6 +29,16 @@ describe('workflow > triggers > collection', () => {
|
||||
afterEach(() => app.destroy());
|
||||
|
||||
describe('toggle', () => {
|
||||
it('create without config should ok', async () => {
|
||||
const workflow = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {},
|
||||
});
|
||||
|
||||
expect(workflow).toBeDefined();
|
||||
});
|
||||
|
||||
it('when collection change', async () => {
|
||||
const workflow = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
@ -507,4 +518,97 @@ describe('workflow > triggers > collection', () => {
|
||||
expect(executions[0].status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
});
|
||||
});
|
||||
|
||||
describe('multiple data source', () => {
|
||||
let anotherDB: MockDatabase;
|
||||
beforeEach(async () => {
|
||||
// @ts-ignore
|
||||
anotherDB = app.dataSourceManager.dataSources.get('another').collectionManager.db;
|
||||
});
|
||||
|
||||
it('collection trigger on another', async () => {
|
||||
const workflow = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {
|
||||
mode: 1,
|
||||
collection: 'another:posts',
|
||||
},
|
||||
});
|
||||
|
||||
const post = await PostRepo.create({ values: { title: 't1' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const e1s = await workflow.getExecutions();
|
||||
expect(e1s.length).toBe(0);
|
||||
|
||||
const AnotherPostRepo = anotherDB.getRepository('posts');
|
||||
const anotherPost = await AnotherPostRepo.create({ values: { title: 't2' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const e2s = await workflow.getExecutions();
|
||||
expect(e2s.length).toBe(1);
|
||||
expect(e2s[0].status).toBe(EXECUTION_STATUS.RESOLVED);
|
||||
expect(e2s[0].context.data.title).toBe('t2');
|
||||
|
||||
const p1s = await PostRepo.find();
|
||||
expect(p1s.length).toBe(1);
|
||||
|
||||
const p2s = await AnotherPostRepo.find();
|
||||
expect(p2s.length).toBe(1);
|
||||
});
|
||||
|
||||
it('revisiond workflow should only trigger on enabled version', async () => {
|
||||
const w1 = await WorkflowModel.create({
|
||||
enabled: true,
|
||||
type: 'collection',
|
||||
config: {
|
||||
mode: 1,
|
||||
collection: 'another:posts',
|
||||
},
|
||||
});
|
||||
|
||||
const AnotherPostRepo = anotherDB.getRepository('posts');
|
||||
const p1 = await AnotherPostRepo.create({ values: { title: 't2' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const e1s = await w1.getExecutions();
|
||||
expect(e1s.length).toBe(1);
|
||||
|
||||
const user = await app.db.getRepository('users').findOne();
|
||||
const agent = app.agent().login(user);
|
||||
|
||||
const { body } = await agent.resource('workflows').revision({
|
||||
filterByTk: w1.id,
|
||||
filter: {
|
||||
key: w1.key,
|
||||
},
|
||||
});
|
||||
const w2 = await WorkflowModel.findByPk(body.data.id);
|
||||
await w2.update({ enabled: true });
|
||||
expect(w2.enabled).toBe(true);
|
||||
console.log('w2', w2.toJSON());
|
||||
|
||||
await w1.reload();
|
||||
expect(w1.enabled).toBe(false);
|
||||
|
||||
const p2 = await AnotherPostRepo.create({ values: { title: 't2' } });
|
||||
|
||||
await sleep(500);
|
||||
|
||||
const e2s = await w1.getExecutions({ order: [['createdAt', 'ASC']] });
|
||||
expect(e2s.length).toBe(1);
|
||||
|
||||
const ExecutionRepo = app.db.getRepository('executions');
|
||||
const e3s = await ExecutionRepo.find({
|
||||
filter: {
|
||||
workflowId: w2.id,
|
||||
},
|
||||
});
|
||||
expect(e3s.length).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { parseCollectionName } from '@nocobase/data-source-manager';
|
||||
|
||||
import { JOB_STATUS } from '../constants';
|
||||
import { toJSON } from '../utils';
|
||||
import type Processor from '../Processor';
|
||||
@ -7,8 +9,11 @@ import { Instruction } from '.';
|
||||
export class CreateInstruction extends Instruction {
|
||||
async run(node: FlowNodeModel, input, processor: Processor) {
|
||||
const { collection, params: { appends = [], ...params } = {} } = node.config;
|
||||
const [dataSourceName, collectionName] = parseCollectionName(collection);
|
||||
|
||||
const { repository, model } = (<typeof FlowNodeModel>node.constructor).database.getCollection(collection);
|
||||
const { repository, model } = this.workflow.app.dataSourceManager.dataSources
|
||||
.get(dataSourceName)
|
||||
.collectionManager.getCollection(collectionName);
|
||||
const options = processor.getParsedValue(params, node.id);
|
||||
|
||||
const created = await repository.create({
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { parseCollectionName } from '@nocobase/data-source-manager';
|
||||
|
||||
import { Instruction } from '.';
|
||||
import type Processor from '../Processor';
|
||||
import { JOB_STATUS } from '../constants';
|
||||
@ -7,9 +9,13 @@ export class DestroyInstruction extends Instruction {
|
||||
async run(node: FlowNodeModel, input, processor: Processor) {
|
||||
const { collection, params = {} } = node.config;
|
||||
|
||||
const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
|
||||
const [dataSourceName, collectionName] = parseCollectionName(collection);
|
||||
|
||||
const { repository } = this.workflow.app.dataSourceManager.dataSources
|
||||
.get(dataSourceName)
|
||||
.collectionManager.getCollection(collectionName);
|
||||
const options = processor.getParsedValue(params, node.id);
|
||||
const result = await repo.destroy({
|
||||
const result = await repository.destroy({
|
||||
...options,
|
||||
context: {
|
||||
stack: Array.from(new Set((processor.execution.context.stack ?? []).concat(processor.execution.id))),
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { DEFAULT_PAGE, DEFAULT_PER_PAGE, utils } from '@nocobase/actions';
|
||||
import { parseCollectionName } from '@nocobase/data-source-manager';
|
||||
|
||||
import type Processor from '../Processor';
|
||||
import { JOB_STATUS } from '../constants';
|
||||
@ -10,7 +11,11 @@ export class QueryInstruction extends Instruction {
|
||||
async run(node: FlowNodeModel, input, processor: Processor) {
|
||||
const { collection, multiple, params = {}, failOnEmpty = false } = node.config;
|
||||
|
||||
const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
|
||||
const [dataSourceName, collectionName] = parseCollectionName(collection);
|
||||
|
||||
const { repository } = this.workflow.app.dataSourceManager.dataSources
|
||||
.get(dataSourceName)
|
||||
.collectionManager.getCollection(collectionName);
|
||||
const {
|
||||
page = DEFAULT_PAGE,
|
||||
pageSize = DEFAULT_PER_PAGE,
|
||||
@ -26,7 +31,7 @@ export class QueryInstruction extends Instruction {
|
||||
}, new Set()),
|
||||
)
|
||||
: options.appends;
|
||||
const result = await (multiple ? repo.find : repo.findOne).call(repo, {
|
||||
const result = await (multiple ? repository.find : repository.findOne).call(repository, {
|
||||
...options,
|
||||
...utils.pageArgsToLimitArgs(page, pageSize),
|
||||
sort: sort
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { parseCollectionName } from '@nocobase/data-source-manager';
|
||||
|
||||
import type Processor from '../Processor';
|
||||
import { JOB_STATUS } from '../constants';
|
||||
import type { FlowNodeModel } from '../types';
|
||||
@ -7,9 +9,13 @@ export class UpdateInstruction extends Instruction {
|
||||
async run(node: FlowNodeModel, input, processor: Processor) {
|
||||
const { collection, params = {} } = node.config;
|
||||
|
||||
const repo = (<typeof FlowNodeModel>node.constructor).database.getRepository(collection);
|
||||
const [dataSourceName, collectionName] = parseCollectionName(collection);
|
||||
|
||||
const { repository } = this.workflow.app.dataSourceManager.dataSources
|
||||
.get(dataSourceName)
|
||||
.collectionManager.getCollection(collectionName);
|
||||
const options = processor.getParsedValue(params, node.id);
|
||||
const result = await repo.update({
|
||||
const result = await repository.update({
|
||||
...options,
|
||||
context: {
|
||||
stack: Array.from(new Set((processor.execution.context.stack ?? []).concat(processor.execution.id))),
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Collection, Model, Transactionable } from '@nocobase/database';
|
||||
import { Model, Transactionable } from '@nocobase/database';
|
||||
import Trigger from '.';
|
||||
import { toJSON } from '../utils';
|
||||
import type { WorkflowModel } from '../types';
|
||||
import { parseCollectionName, ICollection } from '@nocobase/data-source-manager';
|
||||
|
||||
export interface CollectionChangeTriggerConfig {
|
||||
collection: string;
|
||||
@ -25,18 +26,21 @@ function getHookId(workflow, type) {
|
||||
return `${type}#${workflow.id}`;
|
||||
}
|
||||
|
||||
function getFieldRawName(collection: Collection, name: string) {
|
||||
function getFieldRawName(collection: ICollection, name: string) {
|
||||
const field = collection.getField(name);
|
||||
if (field && field.type === 'belongsTo') {
|
||||
return field.foreignKey;
|
||||
if (field && field.options.type === 'belongsTo') {
|
||||
return field.options.foreignKey;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
// async function, should return promise
|
||||
async function handler(this: CollectionTrigger, workflow: WorkflowModel, data: Model, options) {
|
||||
const { collection: collectionName, condition, changed, mode, appends } = workflow.config;
|
||||
const collection = (<typeof Model>data.constructor).database.getCollection(collectionName);
|
||||
const { condition, changed, mode, appends } = workflow.config;
|
||||
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
|
||||
const collection = this.workflow.app.dataSourceManager?.dataSources
|
||||
.get(dataSourceName)
|
||||
.collectionManager.getCollection(collectionName);
|
||||
const { transaction, context } = options;
|
||||
const { repository, model } = collection;
|
||||
|
||||
@ -45,7 +49,9 @@ async function handler(this: CollectionTrigger, workflow: WorkflowModel, data: M
|
||||
changed &&
|
||||
changed.length &&
|
||||
changed
|
||||
.filter((name) => !['linkTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(collection.getField(name).type))
|
||||
.filter(
|
||||
(name) => !['linkTo', 'hasOne', 'hasMany', 'belongsToMany'].includes(collection.getField(name).options.type),
|
||||
)
|
||||
.every((name) => !data.changedWithAssociations(getFieldRawName(collection, name)))
|
||||
) {
|
||||
return;
|
||||
@ -102,16 +108,20 @@ export default class CollectionTrigger extends Trigger {
|
||||
events = new Map();
|
||||
|
||||
on(workflow: WorkflowModel) {
|
||||
const { db } = this.workflow.app;
|
||||
const { collection, mode } = workflow.config;
|
||||
const Collection = db.getCollection(collection);
|
||||
if (!Collection) {
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
const [dataSourceName, collectionName] = parseCollectionName(collection);
|
||||
// @ts-ignore
|
||||
const { db } = this.workflow.app.dataSourceManager?.dataSources.get(dataSourceName)?.collectionManager ?? {};
|
||||
if (!db || !db.getCollection(collectionName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, type] of MODE_BITMAP_EVENTS.entries()) {
|
||||
const event = `${collection}.${type}`;
|
||||
const name = getHookId(workflow, event);
|
||||
const event = `${collectionName}.${type}`;
|
||||
const name = getHookId(workflow, `${collection}.${type}`);
|
||||
if (mode & key) {
|
||||
if (!this.events.has(name)) {
|
||||
const listener = handler.bind(this, workflow);
|
||||
@ -129,19 +139,23 @@ export default class CollectionTrigger extends Trigger {
|
||||
}
|
||||
|
||||
off(workflow: WorkflowModel) {
|
||||
const { db } = this.workflow.app;
|
||||
const { collection, mode } = workflow.config;
|
||||
const Collection = db.getCollection(collection);
|
||||
if (!Collection) {
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
const [dataSourceName, collectionName] = parseCollectionName(collection);
|
||||
// @ts-ignore
|
||||
const { db } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName)?.collectionManager ?? {};
|
||||
if (!db || !db.getCollection(collectionName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, type] of MODE_BITMAP_EVENTS.entries()) {
|
||||
const event = `${collection}.${type}`;
|
||||
const name = getHookId(workflow, event);
|
||||
const name = getHookId(workflow, `${collection}.${type}`);
|
||||
if (mode & key) {
|
||||
const listener = this.events.get(name);
|
||||
if (listener) {
|
||||
db.off(event, listener);
|
||||
db.off(`${collectionName}.${type}`, listener);
|
||||
this.events.delete(name);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import parser from 'cron-parser';
|
||||
import type Plugin from '../../Plugin';
|
||||
import type { WorkflowModel } from '../../types';
|
||||
import { parseDateWithoutMs, SCHEDULE_MODE } from './utils';
|
||||
import { parseCollectionName } from '@nocobase/data-source-manager';
|
||||
|
||||
export type ScheduleOnField = {
|
||||
field: string;
|
||||
@ -334,7 +335,9 @@ export default class ScheduleTrigger {
|
||||
this.inspect([workflow]);
|
||||
|
||||
const { collection } = workflow.config;
|
||||
const event = `${collection}.afterSaveWithAssociations`;
|
||||
const [dataSourceName, collectionName] = parseCollectionName(collection);
|
||||
const event = `${collectionName}.afterSaveWithAssociations`;
|
||||
const eventKey = `${collection}.afterSaveWithAssociations`;
|
||||
const name = getHookId(workflow, event);
|
||||
if (this.events.has(name)) {
|
||||
return;
|
||||
@ -346,7 +349,8 @@ export default class ScheduleTrigger {
|
||||
};
|
||||
|
||||
this.events.set(name, listener);
|
||||
this.workflow.app.db.on(event, listener);
|
||||
// @ts-ignore
|
||||
this.workflow.app.dataSourceManager.dataSources.get(dataSourceName).collectionManager.db.on(event, listener);
|
||||
}
|
||||
|
||||
off(workflow: WorkflowModel) {
|
||||
@ -358,12 +362,16 @@ export default class ScheduleTrigger {
|
||||
}
|
||||
|
||||
const { collection } = workflow.config;
|
||||
const event = `${collection}.afterSaveWithAssociations`;
|
||||
const [dataSourceName, collectionName] = parseCollectionName(collection);
|
||||
const event = `${collectionName}.afterSaveWithAssociations`;
|
||||
const eventKey = `${collection}.afterSaveWithAssociations`;
|
||||
const name = getHookId(workflow, event);
|
||||
if (this.events.has(name)) {
|
||||
if (this.events.has(eventKey)) {
|
||||
const listener = this.events.get(name);
|
||||
this.events.delete(name);
|
||||
this.workflow.app.db.off(event, listener);
|
||||
// @ts-ignore
|
||||
const { db } = this.workflow.app.dataSourceManager.dataSources.get(dataSourceName).collectionManager;
|
||||
db.off(event, listener);
|
||||
this.events.delete(eventKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user