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...]')
|
.arguments('[paths...]')
|
||||||
.allowUnknownOption()
|
.allowUnknownOption()
|
||||||
.action(async (paths, opts) => {
|
.action(async (paths, opts) => {
|
||||||
|
process.argv.push('--disable-console-intercept');
|
||||||
if (name === 'test:server') {
|
if (name === 'test:server') {
|
||||||
process.env.TEST_ENV = 'server-side';
|
process.env.TEST_ENV = 'server-side';
|
||||||
} else if (name === 'test:client') {
|
} else if (name === 'test:client') {
|
||||||
|
@ -11,7 +11,11 @@ import { usePlugin } from '../../../application/hooks';
|
|||||||
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
|
import { SchemaSettingOptions, SchemaSettings } from '../../../application/schema-settings';
|
||||||
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
import { useSchemaToolbar } from '../../../application/schema-toolbar';
|
||||||
import { useFormBlockContext } from '../../../block-provider';
|
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 { FlagProvider } from '../../../flag-provider';
|
||||||
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
|
import { SaveMode } from '../../../modules/actions/submit/createSubmitActionSettings';
|
||||||
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
import { SchemaSettingOpenModeSchemaItems } from '../../../schema-items';
|
||||||
@ -28,6 +32,7 @@ import {
|
|||||||
import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
import { DefaultValueProvider } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
|
||||||
import { useLinkageAction } from './hooks';
|
import { useLinkageAction } from './hooks';
|
||||||
import { requestSettingsSchema } from './utils';
|
import { requestSettingsSchema } from './utils';
|
||||||
|
import { DataSourceProvider, useDataSourceKey } from '../../../data-source';
|
||||||
|
|
||||||
const MenuGroup = (props) => {
|
const MenuGroup = (props) => {
|
||||||
return props.children;
|
return props.children;
|
||||||
@ -327,7 +332,8 @@ function WorkflowSelect({ actionType, direct = false, ...props }) {
|
|||||||
const { setValuesIn } = useForm();
|
const { setValuesIn } = useForm();
|
||||||
const baseCollection = useCollection_deprecated();
|
const baseCollection = useCollection_deprecated();
|
||||||
const { getCollection } = useCollectionManager_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 compile = useCompile();
|
||||||
|
|
||||||
const workflowPlugin = usePlugin('workflow') as any;
|
const workflowPlugin = usePlugin('workflow') as any;
|
||||||
@ -351,11 +357,11 @@ function WorkflowSelect({ actionType, direct = false, ...props }) {
|
|||||||
const path = paths[i];
|
const path = paths[i];
|
||||||
const associationField = collection.fields.find((f) => f.name === path);
|
const associationField = collection.fields.find((f) => f.name === path);
|
||||||
if (associationField) {
|
if (associationField) {
|
||||||
collection = getCollection(associationField.target);
|
collection = getCollection(associationField.target, dataSourceKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setWorkflowCollection(collection.name);
|
setWorkflowCollection(joinCollectionName(dataSourceKey, collection.name));
|
||||||
setValuesIn(`group[${index}].workflowKey`, null);
|
setValuesIn(`group[${index}].workflowKey`, null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -375,38 +381,40 @@ function WorkflowSelect({ actionType, direct = false, ...props }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RemoteSelect
|
<DataSourceProvider dataSource="main">
|
||||||
manual={false}
|
<RemoteSelect
|
||||||
placeholder={t('Select workflow', { ns: 'workflow' })}
|
manual={false}
|
||||||
fieldNames={{
|
placeholder={t('Select workflow', { ns: 'workflow' })}
|
||||||
label: 'title',
|
fieldNames={{
|
||||||
value: 'key',
|
label: 'title',
|
||||||
}}
|
value: 'key',
|
||||||
service={{
|
}}
|
||||||
resource: 'workflows',
|
service={{
|
||||||
action: 'list',
|
resource: 'workflows',
|
||||||
params: {
|
action: 'list',
|
||||||
filter: {
|
params: {
|
||||||
type: workflowTypes,
|
filter: {
|
||||||
enabled: true,
|
type: workflowTypes,
|
||||||
'config.collection': workflowCollection,
|
enabled: true,
|
||||||
|
'config.collection': workflowCollection,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}}
|
||||||
}}
|
optionFilter={optionFilter}
|
||||||
optionFilter={optionFilter}
|
optionRender={({ label, data }) => {
|
||||||
optionRender={({ label, data }) => {
|
const typeOption = workflowPlugin.getTriggersOptions().find((item) => item.value === data.type);
|
||||||
const typeOption = workflowPlugin.getTriggersOptions().find((item) => item.value === data.type);
|
return typeOption ? (
|
||||||
return typeOption ? (
|
<Flex justify="space-between">
|
||||||
<Flex justify="space-between">
|
<span>{label}</span>
|
||||||
<span>{label}</span>
|
<Tag color={typeOption.color}>{compile(typeOption.label)}</Tag>
|
||||||
<Tag color={typeOption.color}>{compile(typeOption.label)}</Tag>
|
</Flex>
|
||||||
</Flex>
|
) : (
|
||||||
) : (
|
label
|
||||||
label
|
);
|
||||||
);
|
}}
|
||||||
}}
|
{...props}
|
||||||
{...props}
|
/>
|
||||||
/>
|
</DataSourceProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -414,7 +422,7 @@ export function WorkflowConfig() {
|
|||||||
const { dn } = useDesignable();
|
const { dn } = useDesignable();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const fieldSchema = useFieldSchema();
|
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'.
|
// TODO(refactor): should refactor for getting certain action type, better from 'x-action'.
|
||||||
const formBlock = useFormBlockContext();
|
const formBlock = useFormBlockContext();
|
||||||
const actionType = formBlock?.type || fieldSchema['x-action'];
|
const actionType = formBlock?.type || fieldSchema['x-action'];
|
||||||
@ -483,7 +491,9 @@ export function WorkflowConfig() {
|
|||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
placeholder: t('Select context', { ns: 'workflow' }),
|
placeholder: t('Select context', { ns: 'workflow' }),
|
||||||
popupMatchSelectWidth: false,
|
popupMatchSelectWidth: false,
|
||||||
collection,
|
collection: `${
|
||||||
|
collection.dataSource && collection.dataSource !== 'main' ? `${collection.dataSource}:` : ''
|
||||||
|
}${collection.name}`,
|
||||||
filter: '{{ fieldFilter }}',
|
filter: '{{ fieldFilter }}',
|
||||||
rootOption: {
|
rootOption: {
|
||||||
label: t('Full form data', { ns: 'workflow' }),
|
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 type { DefaultOptionType, TreeSelectProps } from 'rc-tree-select/es/TreeSelect';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { CollectionFieldOptions_deprecated, useCollectionManager_deprecated, useCompile } from '../../..';
|
import {
|
||||||
|
CollectionFieldOptions_deprecated,
|
||||||
|
parseCollectionName,
|
||||||
|
useCollectionManager_deprecated,
|
||||||
|
useCompile,
|
||||||
|
} from '../../..';
|
||||||
|
|
||||||
export type AppendsTreeSelectProps = {
|
export type AppendsTreeSelectProps = {
|
||||||
value: string[] | string;
|
value: string[] | string;
|
||||||
@ -27,7 +32,7 @@ function usePropsCollection({ collection }) {
|
|||||||
|
|
||||||
type CallScope = {
|
type CallScope = {
|
||||||
compile?(value: string): string;
|
compile?(value: string): string;
|
||||||
getCollectionFields?(name: any): CollectionFieldOptions_deprecated[];
|
getCollectionFields?(name: any, dataSource?: string): CollectionFieldOptions_deprecated[];
|
||||||
filter(field): boolean;
|
filter(field): boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -52,7 +57,8 @@ function trueFilter(field) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getCollectionFieldOptions(this: CallScope, collection, parentNode?): TreeOptionType[] {
|
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);
|
const boundLoadChildren = loadChildren.bind(this);
|
||||||
return fields.filter(this.filter).map((field) => {
|
return fields.filter(this.filter).map((field) => {
|
||||||
const key = parentNode ? `${parentNode.value ? `${parentNode.value}.` : ''}${field.name}` : field.name;
|
const key = parentNode ? `${parentNode.value ? `${parentNode.value}.` : ''}${field.name}` : field.name;
|
||||||
@ -84,11 +90,12 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
|
|||||||
loadData: propsLoadData,
|
loadData: propsLoadData,
|
||||||
...restProps
|
...restProps
|
||||||
} = props;
|
} = props;
|
||||||
const { getCollectionFields } = useCollectionManager_deprecated();
|
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [optionsMap, setOptionsMap] = useState({});
|
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 treeData = Object.values(optionsMap);
|
||||||
const value: string | DefaultOptionType[] = useMemo(() => {
|
const value: string | DefaultOptionType[] = useMemo(() => {
|
||||||
if (props.multiple) {
|
if (props.multiple) {
|
||||||
@ -111,7 +118,7 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
|
|||||||
},
|
},
|
||||||
[propsLoadData],
|
[propsLoadData],
|
||||||
);
|
);
|
||||||
|
// NOTE:
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const parentNode = rootOption
|
const parentNode = rootOption
|
||||||
? {
|
? {
|
||||||
@ -123,17 +130,19 @@ export const AppendsTreeSelect: React.FC<TreeSelectProps & AppendsTreeSelectProp
|
|||||||
isLeaf: false,
|
isLeaf: false,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
const treeData =
|
const tData =
|
||||||
propsLoadData === null
|
propsLoadData === null
|
||||||
? []
|
? []
|
||||||
: getCollectionFieldOptions.call({ compile, getCollectionFields, filter }, baseCollection, parentNode);
|
: getCollectionFieldOptions.call({ compile, getCollectionFields, filter }, collectionString, parentNode);
|
||||||
const map = treeData.reduce((result, item) => Object.assign(result, { [item.value]: item }), {});
|
|
||||||
|
const map = tData.reduce((result, item) => Object.assign(result, { [item.value]: item }), {});
|
||||||
if (parentNode) {
|
if (parentNode) {
|
||||||
map[parentNode.value] = parentNode;
|
map[parentNode.value] = parentNode;
|
||||||
}
|
}
|
||||||
setOptionsMap(map);
|
setOptionsMap(map);
|
||||||
}, [collection, baseCollection, rootOption, filter, propsLoadData]);
|
}, [collectionString, rootOption, filter, propsLoadData]);
|
||||||
|
|
||||||
|
// NOTE: preload options in value
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const arr = (props.multiple ? propsValue : propsValue ? [propsValue] : []) as string[];
|
const arr = (props.multiple ? propsValue : propsValue ? [propsValue] : []) as string[];
|
||||||
if (!arr?.length || arr.every((v) => Boolean(optionsMap[v]))) {
|
if (!arr?.length || arr.every((v) => Boolean(optionsMap[v]))) {
|
||||||
|
@ -7,3 +7,10 @@ export function parseCollectionName(collection: string) {
|
|||||||
const dataSourceName = dataSourceCollection[0] ?? 'main';
|
const dataSourceName = dataSourceCollection[0] ?? 'main';
|
||||||
return [dataSourceName, collectionName];
|
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",
|
"sqlite3": "^5.0.8",
|
||||||
"supertest": "^6.1.6",
|
"supertest": "^6.1.6",
|
||||||
"vite": "^5.0.0",
|
"vite": "^5.0.0",
|
||||||
"vitest": "^1.0.0",
|
"vitest": "^1.4.0",
|
||||||
"vitest-dom": "^0.1.1",
|
"vitest-dom": "^0.1.1",
|
||||||
"ws": "^8.13.0"
|
"ws": "^8.13.0"
|
||||||
},
|
},
|
||||||
|
@ -101,18 +101,13 @@ const parseString = (() => {
|
|||||||
value = value();
|
value = value();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof value === 'object' && value !== null) {
|
// Accommodate numbers as values.
|
||||||
|
if (str.startsWith('{{') && str.endsWith('}}')) {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accommodate numbers as values.
|
if (value instanceof Date) {
|
||||||
if (matches.length === 1 && str.startsWith('{{') && str.endsWith('}}')) {
|
value = value.toISOString();
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accommodate numbers as values.
|
|
||||||
if (matches.length === 1 && str.startsWith('{{') && str.endsWith('}}')) {
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.replace(match, value == null ? '' : value);
|
return result.replace(match, value == null ? '' : value);
|
||||||
|
@ -6,7 +6,12 @@ import {
|
|||||||
useCollectionManager_deprecated,
|
useCollectionManager_deprecated,
|
||||||
useCompile,
|
useCompile,
|
||||||
} from '@nocobase/client';
|
} 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';
|
import { NAMESPACE, useLang } from '../locale';
|
||||||
|
|
||||||
export default class extends Trigger {
|
export default class extends Trigger {
|
||||||
@ -17,10 +22,8 @@ export default class extends Trigger {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'CollectionSelect',
|
'x-component': 'DataSourceCollectionCascader',
|
||||||
'x-component-props': {
|
'x-disabled': '{{ useWorkflowAnyExecuted() }}',
|
||||||
className: 'auto-width',
|
|
||||||
},
|
|
||||||
title: `{{t("Collection", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Collection", { ns: "${NAMESPACE}" })}}`,
|
||||||
description: `{{t("Which collection record belongs to.", { ns: "${NAMESPACE}" })}}`,
|
description: `{{t("Which collection record belongs to.", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-reactions': [
|
'x-reactions': [
|
||||||
@ -63,6 +66,7 @@ export default class extends Trigger {
|
|||||||
};
|
};
|
||||||
scope = {
|
scope = {
|
||||||
useCollectionDataSource,
|
useCollectionDataSource,
|
||||||
|
useWorkflowAnyExecuted,
|
||||||
};
|
};
|
||||||
isActionTriggerable = (config, context) => {
|
isActionTriggerable = (config, context) => {
|
||||||
return ['create', 'update', 'customize:update', 'customize:triggerWorkflows'].includes(context.action);
|
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}" })}}`,
|
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
||||||
Component: CollectionBlockInitializer,
|
Component: CollectionBlockInitializer,
|
||||||
collection: config.collection,
|
collection: config.collection,
|
||||||
dataSource: '{{$context.data}}',
|
dataPath: '$context.data',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
apiUpdateWorkflowTrigger,
|
apiUpdateWorkflowTrigger,
|
||||||
appendJsonCollectionName,
|
appendJsonCollectionName,
|
||||||
generalWithNoRelationalFields,
|
generalWithNoRelationalFields,
|
||||||
|
apiGetDataSourceCount,
|
||||||
} from '@nocobase/plugin-workflow-test/e2e';
|
} from '@nocobase/plugin-workflow-test/e2e';
|
||||||
import { expect, test } from '@nocobase/test/e2e';
|
import { expect, test } from '@nocobase/test/e2e';
|
||||||
import { dayjs } from '@nocobase/utils';
|
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);
|
const formEventTriggerNode = new FormEventTriggerNode(page, workFlowName, triggerNodeCollectionName);
|
||||||
await formEventTriggerNode.nodeConfigure.click();
|
await formEventTriggerNode.nodeConfigure.click();
|
||||||
await formEventTriggerNode.collectionDropDown.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();
|
await formEventTriggerNode.submitButton.click();
|
||||||
|
|
||||||
//配置录入数据区块
|
//配置录入数据区块
|
||||||
@ -60,6 +62,10 @@ test.describe('Configuration page to configure the Trigger node', () => {
|
|||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
await page.getByLabel('schema-initializer-Grid-page:addBlock').hover();
|
await page.getByLabel('schema-initializer-Grid-page:addBlock').hover();
|
||||||
await page.getByRole('menuitem', { name: 'table Table' }).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();
|
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);
|
const formEventTriggerNode = new FormEventTriggerNode(page, workFlowName, triggerNodeCollectionName);
|
||||||
await formEventTriggerNode.nodeConfigure.click();
|
await formEventTriggerNode.nodeConfigure.click();
|
||||||
await formEventTriggerNode.collectionDropDown.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();
|
await formEventTriggerNode.submitButton.click();
|
||||||
|
|
||||||
//配置录入数据区块
|
//配置录入数据区块
|
||||||
@ -152,6 +159,10 @@ test.describe('Configuration page to configure the Trigger node', () => {
|
|||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
await page.getByLabel('schema-initializer-Grid-page:addBlock').hover();
|
await page.getByLabel('schema-initializer-Grid-page:addBlock').hover();
|
||||||
await page.getByRole('menuitem', { name: 'table Table' }).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();
|
await page.getByRole('menuitem', { name: `${triggerNodeCollectionDisplayName}` }).click();
|
||||||
|
|
||||||
// 移开鼠标,关闭菜单
|
// 移开鼠标,关闭菜单
|
||||||
@ -649,8 +660,11 @@ test.describe('Configuration page copy to new version', () => {
|
|||||||
// 3、预期结果:新版本工作流配置内容同旧版本一样
|
// 3、预期结果:新版本工作流配置内容同旧版本一样
|
||||||
const formEventTriggerNode = new FormEventTriggerNode(page, workFlowName, triggerNodeCollectionName);
|
const formEventTriggerNode = new FormEventTriggerNode(page, workFlowName, triggerNodeCollectionName);
|
||||||
await formEventTriggerNode.nodeConfigure.click();
|
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、后置处理:删除工作流
|
// 4、后置处理:删除工作流
|
||||||
await apiDeleteWorkflow(workflowId);
|
await apiDeleteWorkflow(workflowId);
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { get } from 'lodash';
|
import { get } from 'lodash';
|
||||||
import { BelongsTo, HasOne } from 'sequelize';
|
import { BelongsTo, HasOne } from 'sequelize';
|
||||||
import { Model, modelAssociationByKey } from '@nocobase/database';
|
import { Model, modelAssociationByKey } from '@nocobase/database';
|
||||||
import { DefaultContext } from '@nocobase/server';
|
import Application, { DefaultContext } from '@nocobase/server';
|
||||||
import { ActionContext } from '@nocobase/resourcer';
|
import { Context as ActionContext, Next } from '@nocobase/actions';
|
||||||
import { Next } from '@nocobase/actions';
|
|
||||||
|
|
||||||
import WorkflowPlugin, { Trigger, WorkflowModel, toJSON } from '@nocobase/plugin-workflow';
|
import WorkflowPlugin, { Trigger, WorkflowModel, toJSON } from '@nocobase/plugin-workflow';
|
||||||
|
import { parseCollectionName } from '@nocobase/data-source-manager';
|
||||||
|
|
||||||
interface Context extends ActionContext, DefaultContext {}
|
interface Context extends ActionContext, DefaultContext {}
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ export default class extends Trigger {
|
|||||||
constructor(workflow: WorkflowPlugin) {
|
constructor(workflow: WorkflowPlugin) {
|
||||||
super(workflow);
|
super(workflow);
|
||||||
|
|
||||||
workflow.app.resourcer.use(this.middleware);
|
workflow.app.use(this.middleware, { after: 'dataSource' });
|
||||||
}
|
}
|
||||||
|
|
||||||
async triggerAction(context: Context, next: Next) {
|
async triggerAction(context: Context, next: Next) {
|
||||||
@ -55,6 +55,7 @@ export default class extends Trigger {
|
|||||||
|
|
||||||
private async trigger(context: Context) {
|
private async trigger(context: Context) {
|
||||||
const { triggerWorkflows = '', values } = context.action.params;
|
const { triggerWorkflows = '', values } = context.action.params;
|
||||||
|
const dataSourceHeader = context.get('x-data-source') || 'main';
|
||||||
|
|
||||||
const { currentUser, currentRole } = context.state;
|
const { currentUser, currentRole } = context.state;
|
||||||
const { model: UserModel } = this.workflow.db.getCollection('users');
|
const { model: UserModel } = this.workflow.db.getCollection('users');
|
||||||
@ -79,12 +80,16 @@ export default class extends Trigger {
|
|||||||
const asyncGroup = [];
|
const asyncGroup = [];
|
||||||
for (const workflow of workflows) {
|
for (const workflow of workflows) {
|
||||||
const { collection, appends = [] } = workflow.config;
|
const { collection, appends = [] } = workflow.config;
|
||||||
|
const [dataSourceName, collectionName] = parseCollectionName(collection);
|
||||||
const trigger = triggers.find((trigger) => trigger[0] == workflow.key);
|
const trigger = triggers.find((trigger) => trigger[0] == workflow.key);
|
||||||
const event = [workflow];
|
const event = [workflow];
|
||||||
if (context.action.resourceName !== 'workflows') {
|
if (context.action.resourceName !== 'workflows') {
|
||||||
if (!context.body) {
|
if (!context.body) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (dataSourceName !== dataSourceHeader) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const { body: data } = context;
|
const { body: data } = context;
|
||||||
for (const row of Array.isArray(data) ? data : [data]) {
|
for (const row of Array.isArray(data) ? data : [data]) {
|
||||||
let payload = row;
|
let payload = row;
|
||||||
@ -101,7 +106,7 @@ export default class extends Trigger {
|
|||||||
}
|
}
|
||||||
const model = payload.constructor;
|
const model = payload.constructor;
|
||||||
if (payload instanceof Model) {
|
if (payload instanceof Model) {
|
||||||
if (collection !== model.collection.name) {
|
if (collectionName !== model.collection.name) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (appends.length) {
|
if (appends.length) {
|
||||||
@ -115,7 +120,9 @@ export default class extends Trigger {
|
|||||||
event.push({ data: toJSON(payload), ...userInfo });
|
event.push({ data: toJSON(payload), ...userInfo });
|
||||||
}
|
}
|
||||||
} else {
|
} 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;
|
let data = trigger[1] ? get(values, trigger[1]) : values;
|
||||||
const pk = get(data, model.primaryKeyAttribute);
|
const pk = get(data, model.primaryKeyAttribute);
|
||||||
if (appends.length && pk != null) {
|
if (appends.length && pk != null) {
|
||||||
|
@ -12,7 +12,7 @@ describe('workflow > action-trigger', () => {
|
|||||||
let PostRepo;
|
let PostRepo;
|
||||||
let CommentRepo;
|
let CommentRepo;
|
||||||
let WorkflowModel;
|
let WorkflowModel;
|
||||||
let UserModel;
|
let UserRepo;
|
||||||
let users;
|
let users;
|
||||||
let userAgents;
|
let userAgents;
|
||||||
|
|
||||||
@ -26,12 +26,14 @@ describe('workflow > action-trigger', () => {
|
|||||||
WorkflowModel = db.getCollection('workflows').model;
|
WorkflowModel = db.getCollection('workflows').model;
|
||||||
PostRepo = db.getCollection('posts').repository;
|
PostRepo = db.getCollection('posts').repository;
|
||||||
CommentRepo = db.getCollection('comments').repository;
|
CommentRepo = db.getCollection('comments').repository;
|
||||||
UserModel = db.getCollection('users').model;
|
UserRepo = db.getCollection('users').repository;
|
||||||
|
|
||||||
users = await UserModel.bulkCreate([
|
users = await UserRepo.create({
|
||||||
{ id: 2, nickname: 'a' },
|
values: [
|
||||||
{ id: 3, nickname: 'b' },
|
{ id: 2, nickname: 'a', roles: [{ name: 'root' }] },
|
||||||
]);
|
{ id: 3, nickname: 'b' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
userAgents = users.map((user) => app.agent().login(user));
|
userAgents = users.map((user) => app.agent().login(user));
|
||||||
});
|
});
|
||||||
@ -569,4 +571,49 @@ describe('workflow > action-trigger', () => {
|
|||||||
expect(e3s[0].status).toBe(EXECUTION_STATUS.RESOLVED);
|
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,
|
SchemaComponentContext,
|
||||||
SchemaInitializerItemType,
|
SchemaInitializerItemType,
|
||||||
css,
|
css,
|
||||||
|
joinCollectionName,
|
||||||
|
parseCollectionName,
|
||||||
useCollectionDataSource,
|
useCollectionDataSource,
|
||||||
useCollectionFilterOptions,
|
useCollectionFilterOptions,
|
||||||
useCollectionManager_deprecated,
|
useCollectionManager_deprecated,
|
||||||
@ -116,25 +118,34 @@ function AssociatedConfig({ value, onChange, ...props }): JSX.Element {
|
|||||||
// need to get:
|
// need to get:
|
||||||
// * source collection (from node.config)
|
// * source collection (from node.config)
|
||||||
// * target collection (from field name)
|
// * 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);
|
const primaryKeyField = collection.fields.find((f) => f.primaryKey);
|
||||||
|
|
||||||
setValuesIn('collection', target);
|
setValuesIn('collection', `${dataSourceKey}:${target}`);
|
||||||
|
|
||||||
onChange({
|
onChange({
|
||||||
name,
|
name,
|
||||||
// primary key data path
|
// primary key data path
|
||||||
associatedKey: `{{${path.slice(0, -1).join('.')}.${primaryKeyField.name}}}`,
|
associatedKey: `{{${path.slice(0, -1).join('.')}.${primaryKeyField.name}}}`,
|
||||||
// data associated collection name
|
// data associated collection name
|
||||||
associatedCollection: collectionName,
|
associatedCollection: joinCollectionName(dataSourceKey, collectionName),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[onChange],
|
[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:
|
// based on collection:
|
||||||
@ -217,7 +228,7 @@ export default class extends Instruction {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'CollectionSelect',
|
'x-component': 'DataSourceCollectionCascader',
|
||||||
title: `{{t("Data of collection", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Data of collection", { ns: "${NAMESPACE}" })}}`,
|
||||||
'x-reactions': [
|
'x-reactions': [
|
||||||
{
|
{
|
||||||
@ -333,7 +344,8 @@ export default class extends Instruction {
|
|||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
useProps() {
|
useProps() {
|
||||||
const { values } = useForm();
|
const { values } = useForm();
|
||||||
const options = useCollectionFilterOptions(values?.collection);
|
const [dataSourceName, collectionName] = parseCollectionName(values?.collection);
|
||||||
|
const options = useCollectionFilterOptions(collectionName, dataSourceName);
|
||||||
return {
|
return {
|
||||||
options,
|
options,
|
||||||
className: css`
|
className: css`
|
||||||
|
@ -80,7 +80,8 @@ test.describe('no filter', () => {
|
|||||||
const aggregateRecordNodeId = await aggregateRecordNode.node.locator('.workflow-node-id').innerText();
|
const aggregateRecordNodeId = await aggregateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.submitButton.click();
|
await aggregateRecordNode.submitButton.click();
|
||||||
@ -179,7 +180,8 @@ test.describe('no filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.sumRadio.click();
|
await aggregateRecordNode.sumRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.submitButton.click();
|
await aggregateRecordNode.submitButton.click();
|
||||||
@ -281,7 +283,8 @@ test.describe('no filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.avgRadio.click();
|
await aggregateRecordNode.avgRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.submitButton.click();
|
await aggregateRecordNode.submitButton.click();
|
||||||
@ -385,7 +388,8 @@ test.describe('no filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.minRadio.click();
|
await aggregateRecordNode.minRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.submitButton.click();
|
await aggregateRecordNode.submitButton.click();
|
||||||
@ -488,7 +492,8 @@ test.describe('no filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.maxRadio.click();
|
await aggregateRecordNode.maxRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.submitButton.click();
|
await aggregateRecordNode.submitButton.click();
|
||||||
@ -590,7 +595,8 @@ test.describe('no filter', () => {
|
|||||||
const aggregateRecordNodeId = await aggregateRecordNode.node.locator('.workflow-node-id').innerText();
|
const aggregateRecordNodeId = await aggregateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.distinctCheckBox.click();
|
await aggregateRecordNode.distinctCheckBox.click();
|
||||||
@ -690,7 +696,8 @@ test.describe('no filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.sumRadio.click();
|
await aggregateRecordNode.sumRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.distinctCheckBox.click();
|
await aggregateRecordNode.distinctCheckBox.click();
|
||||||
@ -795,7 +802,8 @@ test.describe('no filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.avgRadio.click();
|
await aggregateRecordNode.avgRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.distinctCheckBox.click();
|
await aggregateRecordNode.distinctCheckBox.click();
|
||||||
@ -902,7 +910,8 @@ test.describe('no filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.minRadio.click();
|
await aggregateRecordNode.minRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.distinctCheckBox.click();
|
await aggregateRecordNode.distinctCheckBox.click();
|
||||||
@ -1006,7 +1015,8 @@ test.describe('no filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.maxRadio.click();
|
await aggregateRecordNode.maxRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.distinctCheckBox.click();
|
await aggregateRecordNode.distinctCheckBox.click();
|
||||||
@ -1111,7 +1121,8 @@ test.describe('filter', () => {
|
|||||||
const aggregateRecordNodeId = await aggregateRecordNode.node.locator('.workflow-node-id').innerText();
|
const aggregateRecordNodeId = await aggregateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
// 过滤条件
|
// 过滤条件
|
||||||
@ -1217,7 +1228,8 @@ test.describe('filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.sumRadio.click();
|
await aggregateRecordNode.sumRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
// 过滤条件
|
// 过滤条件
|
||||||
@ -1327,7 +1339,8 @@ test.describe('filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.avgRadio.click();
|
await aggregateRecordNode.avgRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
// 过滤条件
|
// 过滤条件
|
||||||
@ -1442,7 +1455,8 @@ test.describe('filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.minRadio.click();
|
await aggregateRecordNode.minRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
// 过滤条件
|
// 过滤条件
|
||||||
@ -1553,7 +1567,8 @@ test.describe('filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.maxRadio.click();
|
await aggregateRecordNode.maxRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).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();
|
const aggregateRecordNodeId = await aggregateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.distinctCheckBox.click();
|
await aggregateRecordNode.distinctCheckBox.click();
|
||||||
@ -1770,7 +1786,8 @@ test.describe('filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.sumRadio.click();
|
await aggregateRecordNode.sumRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.distinctCheckBox.click();
|
await aggregateRecordNode.distinctCheckBox.click();
|
||||||
@ -1885,7 +1902,8 @@ test.describe('filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.avgRadio.click();
|
await aggregateRecordNode.avgRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.distinctCheckBox.click();
|
await aggregateRecordNode.distinctCheckBox.click();
|
||||||
@ -2009,7 +2027,8 @@ test.describe('filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.minRadio.click();
|
await aggregateRecordNode.minRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.distinctCheckBox.click();
|
await aggregateRecordNode.distinctCheckBox.click();
|
||||||
@ -2121,7 +2140,8 @@ test.describe('filter', () => {
|
|||||||
await aggregateRecordNode.nodeConfigure.click();
|
await aggregateRecordNode.nodeConfigure.click();
|
||||||
await aggregateRecordNode.maxRadio.click();
|
await aggregateRecordNode.maxRadio.click();
|
||||||
await aggregateRecordNode.collectionDropDown.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 aggregateRecordNode.aggregatedFieldDropDown.click();
|
||||||
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
await page.getByRole('option', { name: aggregateNodeFieldDisplayName }).click();
|
||||||
await aggregateRecordNode.distinctCheckBox.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';
|
import { Processor, Instruction, JOB_STATUS, FlowNodeModel } from '@nocobase/plugin-workflow';
|
||||||
|
|
||||||
const aggregators = {
|
const aggregators = {
|
||||||
@ -13,13 +14,14 @@ export default class extends Instruction {
|
|||||||
async run(node: FlowNodeModel, input, processor: Processor) {
|
async run(node: FlowNodeModel, input, processor: Processor) {
|
||||||
const { aggregator, associated, collection, association = {}, params = {} } = node.config;
|
const { aggregator, associated, collection, association = {}, params = {} } = node.config;
|
||||||
const options = processor.getParsedValue(params, node.id);
|
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
|
const repo = associated
|
||||||
? database.getRepository<HasManyRepository | BelongsToManyRepository>(
|
? collectionManager.getRepository(
|
||||||
`${association?.associatedCollection}.${association.name}`,
|
`${association?.associatedCollection}.${association.name}`,
|
||||||
processor.getParsedValue(association?.associatedKey, node.id),
|
processor.getParsedValue(association?.associatedKey, node.id),
|
||||||
)
|
)
|
||||||
: database.getRepository(collection);
|
: collectionManager.getRepository(collectionName);
|
||||||
|
|
||||||
if (!options.dataType && aggregator === 'avg') {
|
if (!options.dataType && aggregator === 'avg') {
|
||||||
options.dataType = DataTypes.DOUBLE;
|
options.dataType = DataTypes.DOUBLE;
|
||||||
|
@ -3,6 +3,7 @@ import { Application } from '@nocobase/server';
|
|||||||
import { getApp, sleep } from '@nocobase/plugin-workflow-test';
|
import { getApp, sleep } from '@nocobase/plugin-workflow-test';
|
||||||
|
|
||||||
import Plugin from '..';
|
import Plugin from '..';
|
||||||
|
import { EXECUTION_STATUS } from '@nocobase/plugin-workflow';
|
||||||
|
|
||||||
describe('workflow > instructions > aggregate', () => {
|
describe('workflow > instructions > aggregate', () => {
|
||||||
let app: Application;
|
let app: Application;
|
||||||
@ -295,4 +296,33 @@ describe('workflow > instructions > aggregate', () => {
|
|||||||
expect(j3.result).toBe(1);
|
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,
|
linkNodes,
|
||||||
useAvailableUpstreams,
|
useAvailableUpstreams,
|
||||||
useFlowContext,
|
useFlowContext,
|
||||||
|
DetailsBlockProvider,
|
||||||
} from '@nocobase/plugin-workflow/client';
|
} from '@nocobase/plugin-workflow/client';
|
||||||
|
|
||||||
import { NAMESPACE, useLang } from '../locale';
|
import { NAMESPACE, useLang } from '../locale';
|
||||||
import { DetailsBlockProvider } from './instruction/DetailsBlockProvider';
|
|
||||||
import { FormBlockProvider } from './instruction/FormBlockProvider';
|
import { FormBlockProvider } from './instruction/FormBlockProvider';
|
||||||
import { ManualFormType, manualFormTypes } from './instruction/SchemaConfig';
|
import { ManualFormType, manualFormTypes } from './instruction/SchemaConfig';
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
apiUpdateWorkflowTrigger,
|
apiUpdateWorkflowTrigger,
|
||||||
appendJsonCollectionName,
|
appendJsonCollectionName,
|
||||||
generalWithNoRelationalFields,
|
generalWithNoRelationalFields,
|
||||||
|
apiGetDataSourceCount,
|
||||||
} from '@nocobase/plugin-workflow-test/e2e';
|
} from '@nocobase/plugin-workflow-test/e2e';
|
||||||
import { expect, test } from '@nocobase/test/e2e';
|
import { expect, test } from '@nocobase/test/e2e';
|
||||||
import { dayjs } from '@nocobase/utils';
|
import { dayjs } from '@nocobase/utils';
|
||||||
@ -81,6 +82,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -216,6 +221,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -351,6 +360,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -486,6 +499,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -621,6 +638,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -756,6 +777,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -891,6 +916,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1027,6 +1056,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1162,6 +1195,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1297,6 +1334,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1440,6 +1481,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1574,6 +1619,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1715,6 +1764,10 @@ test.describe('field data entry', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1865,6 +1918,10 @@ test.describe('action button', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -2000,6 +2057,10 @@ test.describe('action button', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -2141,6 +2202,10 @@ test.describe('action button', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
apiUpdateWorkflowTrigger,
|
apiUpdateWorkflowTrigger,
|
||||||
appendJsonCollectionName,
|
appendJsonCollectionName,
|
||||||
generalWithNoRelationalFields,
|
generalWithNoRelationalFields,
|
||||||
|
apiGetDataSourceCount,
|
||||||
} from '@nocobase/plugin-workflow-test/e2e';
|
} from '@nocobase/plugin-workflow-test/e2e';
|
||||||
import { expect, test } from '@nocobase/test/e2e';
|
import { expect, test } from '@nocobase/test/e2e';
|
||||||
import { dayjs } from '@nocobase/utils';
|
import { dayjs } from '@nocobase/utils';
|
||||||
@ -97,6 +98,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -249,6 +254,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -401,6 +410,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -553,6 +566,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -705,6 +722,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -857,6 +878,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1025,6 +1050,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1193,6 +1222,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1361,6 +1394,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1530,6 +1567,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1707,6 +1748,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -1875,6 +1920,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -2049,6 +2098,10 @@ test.describe('field data update', () => {
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.updateRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
|
@ -8,6 +8,7 @@ import {
|
|||||||
apiUpdateWorkflowTrigger,
|
apiUpdateWorkflowTrigger,
|
||||||
appendJsonCollectionName,
|
appendJsonCollectionName,
|
||||||
generalWithNoRelationalFields,
|
generalWithNoRelationalFields,
|
||||||
|
apiGetDataSourceCount,
|
||||||
} from '@nocobase/plugin-workflow-test/e2e';
|
} from '@nocobase/plugin-workflow-test/e2e';
|
||||||
import { expect, test } from '@nocobase/test/e2e';
|
import { expect, test } from '@nocobase/test/e2e';
|
||||||
import { dayjs } from '@nocobase/utils';
|
import { dayjs } from '@nocobase/utils';
|
||||||
@ -69,6 +70,10 @@ test('filter task node', async ({ page, mockPage, mockCollections, mockRecords }
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
@ -179,6 +184,10 @@ test('filter workflow name', async ({ page, mockPage, mockCollections, mockRecor
|
|||||||
await manualNode.configureUserInterfaceButton.click();
|
await manualNode.configureUserInterfaceButton.click();
|
||||||
await manualNode.addBlockButton.hover();
|
await manualNode.addBlockButton.hover();
|
||||||
await manualNode.createRecordFormMenu.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.getByRole('menuitem', { name: manualNodeCollectionDisplayName }).click();
|
||||||
await page.mouse.move(300, 0, { steps: 100 });
|
await page.mouse.move(300, 0, { steps: 100 });
|
||||||
await page
|
await page
|
||||||
|
@ -35,6 +35,8 @@ import {
|
|||||||
} from '@nocobase/client';
|
} from '@nocobase/client';
|
||||||
import WorkflowPlugin, {
|
import WorkflowPlugin, {
|
||||||
JOB_STATUS,
|
JOB_STATUS,
|
||||||
|
DetailsBlockProvider,
|
||||||
|
SimpleDesigner,
|
||||||
useAvailableUpstreams,
|
useAvailableUpstreams,
|
||||||
useFlowContext,
|
useFlowContext,
|
||||||
useNodeContext,
|
useNodeContext,
|
||||||
@ -44,7 +46,6 @@ import WorkflowPlugin, {
|
|||||||
import { Registry, lodash } from '@nocobase/utils/client';
|
import { Registry, lodash } from '@nocobase/utils/client';
|
||||||
|
|
||||||
import { NAMESPACE, useLang } from '../../locale';
|
import { NAMESPACE, useLang } from '../../locale';
|
||||||
import { DetailsBlockProvider } from './DetailsBlockProvider';
|
|
||||||
import { FormBlockProvider } from './FormBlockProvider';
|
import { FormBlockProvider } from './FormBlockProvider';
|
||||||
import createRecordForm from './forms/create';
|
import createRecordForm from './forms/create';
|
||||||
import customRecordForm from './forms/custom';
|
import customRecordForm from './forms/custom';
|
||||||
@ -104,24 +105,6 @@ const blockTypeNames = {
|
|||||||
record: `{{t("Data record", { ns: "${NAMESPACE}" })}}`,
|
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
|
* @deprecated
|
||||||
*/
|
*/
|
||||||
@ -153,7 +136,7 @@ export const addBlockButton_deprecated = new CompatibleSchemaInitializer({
|
|||||||
{
|
{
|
||||||
name: 'nodes',
|
name: 'nodes',
|
||||||
type: 'subMenu',
|
type: 'subMenu',
|
||||||
title: `{{t("Node result", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Node result", { ns: "workflow" })}}`,
|
||||||
children: nodeBlockInitializers,
|
children: nodeBlockInitializers,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -3,7 +3,7 @@ import React, { useContext, useMemo, useState } from 'react';
|
|||||||
import { ArrayTable } from '@formily/antd-v5';
|
import { ArrayTable } from '@formily/antd-v5';
|
||||||
import { Field, createForm } from '@formily/core';
|
import { Field, createForm } from '@formily/core';
|
||||||
import { useField, useFieldSchema, useForm } from '@formily/react';
|
import { useField, useFieldSchema, useForm } from '@formily/react';
|
||||||
import lodash from 'lodash';
|
import { cloneDeep, pick, set } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ActionContextProvider,
|
ActionContextProvider,
|
||||||
@ -161,7 +161,7 @@ function getOptions(interfaces) {
|
|||||||
const schema = interfaces[type];
|
const schema = interfaces[type];
|
||||||
const { group = 'others' } = schema;
|
const { group = 'others' } = schema;
|
||||||
fields[group] = fields[group] || {};
|
fields[group] = fields[group] || {};
|
||||||
lodash.set(fields, [group, type], schema);
|
set(fields, [group, type], schema);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Object.keys(GroupLabels)
|
return Object.keys(GroupLabels)
|
||||||
@ -208,33 +208,51 @@ const CustomItemsComponent = (props) => {
|
|||||||
const items = useCommonInterfaceInitializers();
|
const items = useCommonInterfaceInitializers();
|
||||||
const collection = useCollection_deprecated();
|
const collection = useCollection_deprecated();
|
||||||
const { setCollectionFields } = useContext(FormBlockContext);
|
const { setCollectionFields } = useContext(FormBlockContext);
|
||||||
|
const form = useMemo(() => createForm(), [interfaceOptions]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AddCustomFormFieldButtonContext.Provider
|
<AddCustomFormFieldButtonContext.Provider
|
||||||
value={{
|
value={{
|
||||||
onAddField(item) {
|
onAddField(item) {
|
||||||
|
const fieldInterface: Record<string, any> = pick(item, [
|
||||||
|
'name',
|
||||||
|
'group',
|
||||||
|
'title',
|
||||||
|
'default',
|
||||||
|
'validateSchema',
|
||||||
|
]);
|
||||||
const {
|
const {
|
||||||
properties: { unique, type, ...properties },
|
properties: { unique, type, layout, autoIncrement, ...properties },
|
||||||
...options
|
} = item;
|
||||||
} = lodash.cloneDeep(item);
|
fieldInterface.properties = properties;
|
||||||
delete properties.name['x-disabled'];
|
const result = cloneDeep(fieldInterface);
|
||||||
setInterface({
|
delete result.properties.name['x-disabled'];
|
||||||
...options,
|
setInterface(result);
|
||||||
properties,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
setCallback,
|
setCallback,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SchemaInitializerItems {...props} items={items} />
|
<SchemaInitializerItems {...props} items={items} />
|
||||||
<ActionContextProvider value={{ visible: Boolean(interfaceOptions) }}>
|
<ActionContextProvider
|
||||||
|
value={{
|
||||||
|
visible: Boolean(interfaceOptions),
|
||||||
|
setVisible(v) {
|
||||||
|
if (!v) {
|
||||||
|
setInterface(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
{interfaceOptions ? (
|
{interfaceOptions ? (
|
||||||
<SchemaComponent
|
<SchemaComponent
|
||||||
schema={{
|
schema={{
|
||||||
type: 'void',
|
type: 'void',
|
||||||
name: 'drawer',
|
name: 'drawer',
|
||||||
title: '{{t("Configure field")}}',
|
title: '{{t("Configure field")}}',
|
||||||
'x-decorator': 'Form',
|
'x-decorator': 'FormV2',
|
||||||
|
'x-decorator-props': {
|
||||||
|
form,
|
||||||
|
},
|
||||||
'x-component': 'Action.Drawer',
|
'x-component': 'Action.Drawer',
|
||||||
properties: {
|
properties: {
|
||||||
...interfaceOptions.properties,
|
...interfaceOptions.properties,
|
||||||
@ -266,7 +284,7 @@ const CustomItemsComponent = (props) => {
|
|||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
type: 'primary',
|
type: 'primary',
|
||||||
useAction() {
|
useAction() {
|
||||||
const { values, query } = useForm();
|
const { values, query, reset } = useForm();
|
||||||
const messages = [useLang('Field name existed in form')];
|
const messages = [useLang('Field name existed in form')];
|
||||||
return {
|
return {
|
||||||
async run() {
|
async run() {
|
||||||
@ -301,6 +319,7 @@ const CustomItemsComponent = (props) => {
|
|||||||
'x-toolbar': 'FormItemSchemaToolbar',
|
'x-toolbar': 'FormItemSchemaToolbar',
|
||||||
'x-settings': 'fieldSettings:FormItem',
|
'x-settings': 'fieldSettings:FormItem',
|
||||||
});
|
});
|
||||||
|
reset();
|
||||||
setCallback(null);
|
setCallback(null);
|
||||||
setInterface(null);
|
setInterface(null);
|
||||||
},
|
},
|
||||||
|
@ -138,7 +138,7 @@ export default class extends Instruction {
|
|||||||
title: form.title ?? formKey,
|
title: form.title ?? formKey,
|
||||||
Component: CollectionBlockInitializer,
|
Component: CollectionBlockInitializer,
|
||||||
collection: form.collection,
|
collection: form.collection,
|
||||||
dataSource: `{{$jobsMapByNodeKey.${node.key}.${formKey}}}`,
|
dataPath: `$jobsMapByNodeKey.${node.key}.${formKey}`,
|
||||||
} as SchemaInitializerItemType)
|
} as SchemaInitializerItemType)
|
||||||
: null;
|
: 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', () => {
|
describe('use result of submitted form in manual node', () => {
|
||||||
it('result should be available and correct', async () => {
|
it('result should be available and correct', async () => {
|
||||||
const n1 = await workflow.createNode({
|
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 { Processor } from '@nocobase/plugin-workflow';
|
||||||
import ManualInstruction from '../ManualInstruction';
|
import ManualInstruction from '../ManualInstruction';
|
||||||
|
|
||||||
export default async function (this: ManualInstruction, instance, { collection }, processor: Processor) {
|
export default async function (
|
||||||
const repo = this.workflow.db.getRepository(collection);
|
this: ManualInstruction,
|
||||||
|
instance,
|
||||||
|
{ dataSource = 'main', collection },
|
||||||
|
processor: Processor,
|
||||||
|
) {
|
||||||
|
const repo = this.workflow.app.dataSourceManager.dataSources
|
||||||
|
.get(dataSource)
|
||||||
|
.collectionManager.getRepository(collection);
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
throw new Error(`collection ${collection} for create data on manual node not found`);
|
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: {
|
context: {
|
||||||
executionId: processor.execution.id,
|
executionId: processor.execution.id,
|
||||||
},
|
},
|
||||||
// transaction: processor.transaction,
|
transaction: processor.transaction,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
import { Processor } from '@nocobase/plugin-workflow';
|
import { Processor } from '@nocobase/plugin-workflow';
|
||||||
import ManualInstruction from '../ManualInstruction';
|
import ManualInstruction from '../ManualInstruction';
|
||||||
|
|
||||||
export default async function (this: ManualInstruction, instance, { collection, filter = {} }, processor: Processor) {
|
export default async function (
|
||||||
const repo = this.workflow.db.getRepository(collection);
|
this: ManualInstruction,
|
||||||
|
instance,
|
||||||
|
{ dataSource = 'main', collection, filter = {} },
|
||||||
|
processor: Processor,
|
||||||
|
) {
|
||||||
|
const repo = this.workflow.app.dataSourceManager.dataSources
|
||||||
|
.get(dataSource)
|
||||||
|
.collectionManager.getRepository(collection);
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
throw new Error(`collection ${collection} for update data on manual node not found`);
|
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: {
|
context: {
|
||||||
executionId: processor.execution.id,
|
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';
|
group = 'collection';
|
||||||
description = `{{t("Execute a SQL statement in database.", { ns: "${NAMESPACE}" })}}`;
|
description = `{{t("Execute a SQL statement in database.", { ns: "${NAMESPACE}" })}}`;
|
||||||
fieldset = {
|
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: {
|
sql: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"SQL action": "SQL 操作",
|
"SQL action": "SQL 操作",
|
||||||
"Execute a SQL statement in database.": "在数据库中执行一个 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> 中使用(商业插件)。"
|
"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 {
|
export default class extends Instruction {
|
||||||
async run(node: FlowNodeModel, input, processor: Processor) {
|
async run(node: FlowNodeModel, input, processor: Processor) {
|
||||||
const { sequelize } = (<typeof FlowNodeModel>node.constructor).database;
|
// @ts-ignore
|
||||||
const sql = processor.getParsedValue(node.config.sql ?? '', node.id).trim();
|
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) {
|
if (!sql) {
|
||||||
return {
|
return {
|
||||||
status: JOB_STATUS.RESOLVED,
|
status: JOB_STATUS.RESOLVED,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await sequelize.query(sql, {
|
// @ts-ignore
|
||||||
|
const result = await db.sequelize.query(sql, {
|
||||||
transaction: processor.transaction,
|
transaction: processor.transaction,
|
||||||
// plain: true,
|
// plain: true,
|
||||||
// model: db.getCollection(node.config.collection).model
|
// model: db.getCollection(node.config.collection).model
|
||||||
|
@ -92,6 +92,26 @@ describe('workflow > instructions > sql', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('sql with variables', () => {
|
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 () => {
|
it('update', async () => {
|
||||||
const queryInterface = db.sequelize.getQueryInterface();
|
const queryInterface = db.sequelize.getQueryInterface();
|
||||||
const n1 = await workflow.createNode({
|
const n1 = await workflow.createNode({
|
||||||
@ -195,4 +215,35 @@ describe('workflow > instructions > sql', () => {
|
|||||||
expect(job.status).toBe(JOB_STATUS.RESOLVED);
|
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;
|
nodeTitle: Locator;
|
||||||
nodeConfigure: Locator;
|
nodeConfigure: Locator;
|
||||||
collectionDropDown: Locator;
|
collectionDropDown: Locator;
|
||||||
checkWthdrawable: Locator;
|
dataBlocksInitiationRadio: Locator;
|
||||||
configureUserInterfaceButton: Locator;
|
dataBlocksAndGlobalApprovalBlocksInitiationRadio: Locator;
|
||||||
|
allowedToBeWithdrawnCheckbox: Locator;
|
||||||
|
goToconfigureButton: Locator;
|
||||||
addBlockButton: Locator;
|
addBlockButton: Locator;
|
||||||
addApplyFormMenu: Locator;
|
addApplyFormMenu: Locator;
|
||||||
configureFieldsButton: Locator;
|
configureFieldsButton: Locator;
|
||||||
configureActionsButton: Locator;
|
configureActionsButton: Locator;
|
||||||
saveDraftSwitch: Locator;
|
saveDraftSwitch: Locator;
|
||||||
|
submitButton: Locator;
|
||||||
|
cancelButton: Locator;
|
||||||
addNodeButton: Locator;
|
addNodeButton: Locator;
|
||||||
constructor(page: Page, triggerName: string, collectionName: string) {
|
constructor(page: Page, triggerName: string, collectionName: string) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.node = page.getByText('TriggeraConfigure');
|
this.node = page.getByText('TriggeraConfigure');
|
||||||
this.nodeTitle = page.locator('textarea').filter({ hasText: triggerName });
|
this.nodeTitle = page.locator('textarea').filter({ hasText: triggerName });
|
||||||
this.nodeConfigure = page.getByRole('button', { name: 'Configure' });
|
this.nodeConfigure = page.getByRole('button', { name: 'Configure' });
|
||||||
this.collectionDropDown = page.getByRole('button', { name: 'Select collection' });
|
this.collectionDropDown = page
|
||||||
this.checkWthdrawable = page.getByLabel('Withdrawable');
|
.getByLabel('block-item-DataSourceCollectionCascader-workflows-Collection')
|
||||||
this.configureUserInterfaceButton = page.getByRole('button', { name: 'Configure user interface' });
|
.locator('.ant-select-selection-search-input');
|
||||||
this.addBlockButton = page.getByRole('button', { name: 'Add block' });
|
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.addApplyFormMenu = page.getByRole('menuitem', { name: 'Apply form' });
|
||||||
this.configureFieldsButton = page.getByTestId('configure-fields-button-of-form-item-' + collectionName);
|
this.configureFieldsButton = page.getByLabel(`schema-initializer-Grid-form:configureFields-${collectionName}`);
|
||||||
this.configureActionsButton = page.getByTestId(
|
this.configureActionsButton = page.getByLabel(
|
||||||
'approval-trigger-configure-form-actions-add-action-button-' + collectionName,
|
`schema-initializer-ActionBar-ApprovalApplyAddActionButton-${collectionName}`,
|
||||||
);
|
);
|
||||||
this.saveDraftSwitch = page.getByRole('menuitem', { name: 'Save draft' }).getByRole('switch');
|
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;
|
readonly page: Page;
|
||||||
node: Locator;
|
node: Locator;
|
||||||
nodeTitle: Locator;
|
nodeTitle: Locator;
|
||||||
nodeConfigure: Locator;
|
nodeConfigure: Locator;
|
||||||
|
addAssigneesButton: Locator;
|
||||||
assigneesDropDown: Locator;
|
assigneesDropDown: Locator;
|
||||||
checkReturnable: Locator;
|
OrRadio: Locator;
|
||||||
configureUserInterfaceButton: Locator;
|
AndRadio: Locator;
|
||||||
|
votingRadio: Locator;
|
||||||
|
parallellyRadio: Locator;
|
||||||
|
sequentiallyRadio: Locator;
|
||||||
|
goToconfigureButton: Locator;
|
||||||
addBlockButton: Locator;
|
addBlockButton: Locator;
|
||||||
addApplyFormMenu: Locator;
|
addDetailsMenu: Locator;
|
||||||
configureFieldsButton: Locator;
|
detailsConfigureFieldsButton: Locator;
|
||||||
configureActionsButton: Locator;
|
addActionsMenu: Locator;
|
||||||
saveDraftSwitch: Locator;
|
actionsConfigureFieldsButton: Locator;
|
||||||
|
actionsConfigureActionsButton: Locator;
|
||||||
|
addApproveButton: Locator;
|
||||||
|
addRejectButton: Locator;
|
||||||
|
addReturnButton: Locator;
|
||||||
|
addNodeResult: Locator;
|
||||||
|
submitButton: Locator;
|
||||||
|
cancelButton: Locator;
|
||||||
addNodeButton: Locator;
|
addNodeButton: Locator;
|
||||||
constructor(page: Page, nodeName: string, collectionName: string) {
|
constructor(page: Page, nodeName: string, collectionName: string) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.node = page.getByText('TriggeraConfigure');
|
this.node = page.getByLabel(`Approval-${nodeName}`, { exact: true });
|
||||||
this.nodeTitle = page.locator('textarea').filter({ hasText: nodeName });
|
this.nodeTitle = page.getByLabel(`Approval-${nodeName}`, { exact: true }).getByRole('textbox');
|
||||||
this.nodeConfigure = page.getByRole('button', { name: 'Configure' });
|
this.nodeConfigure = page
|
||||||
this.assigneesDropDown = page.getByLabel('Search');
|
.getByLabel(`Approval-${nodeName}`, { exact: true })
|
||||||
this.checkReturnable = page.getByLabel('Returnable');
|
.getByRole('button', { name: 'Configure' });
|
||||||
this.configureUserInterfaceButton = page.getByRole('button', { name: 'Configure user interface' });
|
this.addAssigneesButton = page.getByRole('button', { name: 'plus Add assignee' });
|
||||||
this.addBlockButton = page.getByTestId('add-block-button-in-workflow-workflows');
|
this.assigneesDropDown = page.getByTestId('select-single');
|
||||||
this.addApplyFormMenu = page.getByRole('menuitem', { name: 'Apply form' });
|
this.OrRadio = page.getByLabel('Or', { exact: true });
|
||||||
this.configureFieldsButton = page.getByTestId('configure-fields-button-of-form-item-' + collectionName);
|
this.AndRadio = page.getByLabel('And', { exact: true });
|
||||||
this.configureActionsButton = page.getByTestId(
|
this.votingRadio = page.getByLabel('Voting', { exact: true });
|
||||||
'approval-trigger-configure-form-actions-add-action-button-' + collectionName,
|
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 });
|
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.RrpeatModeDropdown = page.getByLabel('block-item-RepeatField-workflows-Repeat mode');
|
||||||
|
|
||||||
this.dataTableTimeFieldOptions = page.getByLabel('Based on date field of collection');
|
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.startTimeDropdown = page.getByLabel('block-item-OnField-workflows-Starts on');
|
||||||
this.submitButton = page.getByLabel('action-Action-Submit-workflows');
|
this.submitButton = page.getByLabel('action-Action-Submit-workflows');
|
||||||
this.cancelButton = page.getByLabel('action-Action-Cancel-workflows');
|
this.cancelButton = page.getByLabel('action-Action-Cancel-workflows');
|
||||||
@ -187,7 +229,10 @@ export class CollectionTriggerNode {
|
|||||||
this.node = page.getByLabel(`Trigger-${triggerName}`);
|
this.node = page.getByLabel(`Trigger-${triggerName}`);
|
||||||
this.nodeTitle = page.getByLabel(`Trigger-${triggerName}`).getByRole('textbox');
|
this.nodeTitle = page.getByLabel(`Trigger-${triggerName}`).getByRole('textbox');
|
||||||
this.nodeConfigure = page.getByLabel(`Trigger-${triggerName}`).getByRole('button', { name: 'Configure' });
|
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
|
this.triggerOnDropdown = page
|
||||||
.getByLabel('block-item-Select-workflows-Trigger on')
|
.getByLabel('block-item-Select-workflows-Trigger on')
|
||||||
.getByRole('button', { name: 'Trigger on' });
|
.getByRole('button', { name: 'Trigger on' });
|
||||||
@ -212,7 +257,9 @@ export class FormEventTriggerNode {
|
|||||||
this.node = page.getByLabel(`Trigger-${triggerName}`);
|
this.node = page.getByLabel(`Trigger-${triggerName}`);
|
||||||
this.nodeTitle = page.getByLabel(`Trigger-${triggerName}`).getByRole('textbox');
|
this.nodeTitle = page.getByLabel(`Trigger-${triggerName}`).getByRole('textbox');
|
||||||
this.nodeConfigure = page.getByLabel(`Trigger-${triggerName}`).getByRole('button', { name: 'Configure' });
|
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.relationalDataDropdown = page.getByTestId('select-field-Preload associations');
|
||||||
this.submitButton = page.getByLabel('action-Action-Submit-workflows');
|
this.submitButton = page.getByLabel('action-Action-Submit-workflows');
|
||||||
this.cancelButton = page.getByLabel('action-Action-Cancel-workflows');
|
this.cancelButton = page.getByLabel('action-Action-Cancel-workflows');
|
||||||
@ -269,7 +316,9 @@ export class QueryRecordNode {
|
|||||||
this.nodeConfigure = page
|
this.nodeConfigure = page
|
||||||
.getByLabel(`Query record-${nodeName}`, { exact: true })
|
.getByLabel(`Query record-${nodeName}`, { exact: true })
|
||||||
.getByRole('button', { name: 'Configure' });
|
.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.allowMultipleDataBoxesForResults = page.getByLabel('Allow multiple records as');
|
||||||
this.addSortFieldsButton = page.getByRole('button', { name: 'plus Add sort field' });
|
this.addSortFieldsButton = page.getByRole('button', { name: 'plus Add sort field' });
|
||||||
this.pageNumberEditBox = page.getByLabel('variable-constant');
|
this.pageNumberEditBox = page.getByLabel('variable-constant');
|
||||||
@ -299,7 +348,9 @@ export class CreateRecordNode {
|
|||||||
this.nodeConfigure = page
|
this.nodeConfigure = page
|
||||||
.getByLabel(`Create record-${nodeName}`, { exact: true })
|
.getByLabel(`Create record-${nodeName}`, { exact: true })
|
||||||
.getByRole('button', { name: 'Configure' });
|
.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.addFieldsButton = page.getByRole('button', { name: 'plus Add field' });
|
||||||
this.submitButton = page.getByLabel('action-Action-Submit-workflows');
|
this.submitButton = page.getByLabel('action-Action-Submit-workflows');
|
||||||
this.cancelButton = page.getByLabel('action-Action-Cancel-workflows');
|
this.cancelButton = page.getByLabel('action-Action-Cancel-workflows');
|
||||||
@ -326,7 +377,9 @@ export class UpdateRecordNode {
|
|||||||
this.nodeConfigure = page
|
this.nodeConfigure = page
|
||||||
.getByLabel(`Update record-${nodeName}`, { exact: true })
|
.getByLabel(`Update record-${nodeName}`, { exact: true })
|
||||||
.getByRole('button', { name: 'Configure' });
|
.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
|
this.batchUpdateModeRadio = page
|
||||||
.getByLabel('block-item-IndividualHooksRadioWithTooltip-workflows-Update mode')
|
.getByLabel('block-item-IndividualHooksRadioWithTooltip-workflows-Update mode')
|
||||||
.getByLabel('Update in a batch');
|
.getByLabel('Update in a batch');
|
||||||
@ -356,7 +409,9 @@ export class DeleteRecordNode {
|
|||||||
this.nodeConfigure = page
|
this.nodeConfigure = page
|
||||||
.getByLabel(`Delete record-${nodeName}`, { exact: true })
|
.getByLabel(`Delete record-${nodeName}`, { exact: true })
|
||||||
.getByRole('button', { name: 'Configure' });
|
.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.submitButton = page.getByLabel('action-Action-Submit-workflows');
|
||||||
this.cancelButton = page.getByLabel('action-Action-Cancel-workflows');
|
this.cancelButton = page.getByLabel('action-Action-Cancel-workflows');
|
||||||
this.addNodeButton = page.getByLabel(`add-button-delete-${nodeName}`, { exact: true });
|
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.minRadio = page.getByLabel('MIN', { exact: true });
|
||||||
this.dataTableDataRadio = page.getByLabel('Data of collection');
|
this.dataTableDataRadio = page.getByLabel('Data of collection');
|
||||||
this.linkedDataTableDataRadio = page.getByLabel('Data of associated collection');
|
this.linkedDataTableDataRadio = page.getByLabel('Data of associated collection');
|
||||||
this.collectionDropDown = page.getByRole('button', { name: 'Select collection' });
|
this.collectionDropDown = page
|
||||||
// this.aggregatedFieldDropDown = page.getByLabel('block-item-FieldsSelect-workflows-Field to aggregate').getByRole('textbox').getByRole('combobox');
|
.getByLabel('block-item-DataSourceCollectionCascader-workflows-Data of collection')
|
||||||
this.aggregatedFieldDropDown = page.locator(
|
.locator('.ant-select-selection-search-input');
|
||||||
'input.ant-select-selection-search-input[role="combobox"][aria-haspopup="listbox"]',
|
this.aggregatedFieldDropDown = page
|
||||||
);
|
.getByLabel('block-item-FieldsSelect-workflows-Field to aggregate')
|
||||||
|
.locator('.ant-select-selection-search-input');
|
||||||
this.distinctCheckBox = page
|
this.distinctCheckBox = page
|
||||||
.getByLabel('block-item-Checkbox-workflows-Distinct')
|
.getByLabel('block-item-Checkbox-workflows-Distinct')
|
||||||
.locator('input.ant-checkbox-input[type="checkbox"]');
|
.locator('input.ant-checkbox-input[type="checkbox"]');
|
||||||
@ -572,7 +628,7 @@ export default module.exports = {
|
|||||||
WorkflowManagement,
|
WorkflowManagement,
|
||||||
WorkflowListRecords,
|
WorkflowListRecords,
|
||||||
ApprovalTriggerNode,
|
ApprovalTriggerNode,
|
||||||
ApprovalNode,
|
ApprovalPassthroughModeNode,
|
||||||
ScheduleTriggerNode,
|
ScheduleTriggerNode,
|
||||||
CollectionTriggerNode,
|
CollectionTriggerNode,
|
||||||
FormEventTriggerNode,
|
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 PORT = process.env.APP_PORT || 20000;
|
||||||
const APP_BASE_URL = process.env.APP_BASE_URL || `http://localhost:${PORT}`;
|
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();
|
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) => {
|
const getStorageItem = (key: string, storageState: any) => {
|
||||||
return storageState.origins
|
return storageState.origins
|
||||||
.find((item) => item.origin === APP_BASE_URL)
|
.find((item) => item.origin === APP_BASE_URL)
|
||||||
@ -767,6 +917,16 @@ function getHeaders(storageState: any) {
|
|||||||
return headers;
|
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 = {
|
export default module.exports = {
|
||||||
apiCreateWorkflow,
|
apiCreateWorkflow,
|
||||||
apiUpdateWorkflow,
|
apiUpdateWorkflow,
|
||||||
@ -783,4 +943,8 @@ export default module.exports = {
|
|||||||
apiCreateRecordTriggerFormEvent,
|
apiCreateRecordTriggerFormEvent,
|
||||||
apiSubmitRecordTriggerFormEvent,
|
apiSubmitRecordTriggerFormEvent,
|
||||||
apiFilterList,
|
apiFilterList,
|
||||||
|
apiGetDataSourceCount,
|
||||||
|
apiCreateRecordTriggerActionEvent,
|
||||||
|
apiApplyApprovalEvent,
|
||||||
|
userLogin,
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import { ApplicationOptions, Plugin } from '@nocobase/server';
|
import { ApplicationOptions, Plugin } from '@nocobase/server';
|
||||||
import { MockServer, createMockServer } from '@nocobase/test';
|
import { MockServer, createMockServer, mockDatabase } from '@nocobase/test';
|
||||||
|
|
||||||
import functions from './functions';
|
import functions from './functions';
|
||||||
import triggers from './triggers';
|
import triggers from './triggers';
|
||||||
import instructions from './instructions';
|
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 {
|
export interface MockServerOptions extends ApplicationOptions {
|
||||||
collectionsPath?: string;
|
collectionsPath?: string;
|
||||||
@ -33,7 +36,7 @@ export async function getApp(options: MockServerOptions = {}): Promise<MockServe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return createMockServer({
|
const app = await createMockServer({
|
||||||
...others,
|
...others,
|
||||||
plugins: [
|
plugins: [
|
||||||
[
|
[
|
||||||
@ -49,6 +52,30 @@ export async function getApp(options: MockServerOptions = {}): Promise<MockServe
|
|||||||
...plugins,
|
...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 {
|
export default class WorkflowTestPlugin extends Plugin {
|
||||||
|
@ -21,10 +21,9 @@ import { ExecutionStatusOptionsMap, JobStatusOptions } from './constants';
|
|||||||
import { FlowContext, useFlowContext } from './FlowContext';
|
import { FlowContext, useFlowContext } from './FlowContext';
|
||||||
import { lang, NAMESPACE } from './locale';
|
import { lang, NAMESPACE } from './locale';
|
||||||
import useStyles from './style';
|
import useStyles from './style';
|
||||||
import { linkNodes } from './utils';
|
import { linkNodes, getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
|
||||||
import { DownOutlined, ExclamationCircleFilled, StopOutlined } from '@ant-design/icons';
|
import { DownOutlined, ExclamationCircleFilled, StopOutlined } from '@ant-design/icons';
|
||||||
import { StatusButton } from './components/StatusButton';
|
import { StatusButton } from './components/StatusButton';
|
||||||
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
function attachJobs(nodes, jobs: any[] = []): void {
|
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 { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { useActionContext, useRecord } from '@nocobase/client';
|
import { useActionContext, useRecord } from '@nocobase/client';
|
||||||
import { getWorkflowExecutionsPath } from './constant';
|
|
||||||
|
import { getWorkflowExecutionsPath } from './utils';
|
||||||
|
|
||||||
export const ExecutionLink = () => {
|
export const ExecutionLink = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -21,8 +21,7 @@ import { FlowContext, useFlowContext } from './FlowContext';
|
|||||||
import { lang } from './locale';
|
import { lang } from './locale';
|
||||||
import { executionSchema } from './schemas/executions';
|
import { executionSchema } from './schemas/executions';
|
||||||
import useStyles from './style';
|
import useStyles from './style';
|
||||||
import { linkNodes } from './utils';
|
import { linkNodes, getWorkflowDetailPath } from './utils';
|
||||||
import { getWorkflowDetailPath } from './constant';
|
|
||||||
import { ExecutionStatusColumn } from './components/ExecutionStatus';
|
import { ExecutionStatusColumn } from './components/ExecutionStatus';
|
||||||
|
|
||||||
function ExecutionResourceProvider({ request, filter = {}, ...others }) {
|
function ExecutionResourceProvider({ request, filter = {}, ...others }) {
|
||||||
|
@ -2,9 +2,10 @@ import React from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
import { getWorkflowDetailPath } from './constant';
|
|
||||||
import { useActionContext, useGetAriaLabelOfAction, useRecord } from '@nocobase/client';
|
import { useActionContext, useGetAriaLabelOfAction, useRecord } from '@nocobase/client';
|
||||||
|
|
||||||
|
import { getWorkflowDetailPath } from './utils';
|
||||||
|
|
||||||
export const WorkflowLink = () => {
|
export const WorkflowLink = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { id } = useRecord();
|
const { id } = useRecord();
|
||||||
|
@ -47,7 +47,9 @@ test.describe('Configuration page to configure the Trigger node', () => {
|
|||||||
const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName);
|
const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName);
|
||||||
await collectionTriggerNode.nodeConfigure.click();
|
await collectionTriggerNode.nodeConfigure.click();
|
||||||
await collectionTriggerNode.collectionDropDown.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 collectionTriggerNode.triggerOnDropdown.click();
|
||||||
await page.getByText('After record added', { exact: true }).click();
|
await page.getByText('After record added', { exact: true }).click();
|
||||||
await collectionTriggerNode.submitButton.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);
|
const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName);
|
||||||
await collectionTriggerNode.nodeConfigure.click();
|
await collectionTriggerNode.nodeConfigure.click();
|
||||||
await collectionTriggerNode.collectionDropDown.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 collectionTriggerNode.triggerOnDropdown.click();
|
||||||
await page.getByText('After record updated', { exact: true }).click();
|
await page.getByText('After record updated', { exact: true }).click();
|
||||||
await collectionTriggerNode.submitButton.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);
|
const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName);
|
||||||
await collectionTriggerNode.nodeConfigure.click();
|
await collectionTriggerNode.nodeConfigure.click();
|
||||||
await collectionTriggerNode.collectionDropDown.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 collectionTriggerNode.triggerOnDropdown.click();
|
||||||
await page.getByText('After record added or updated', { exact: true }).click();
|
await page.getByText('After record added or updated', { exact: true }).click();
|
||||||
await collectionTriggerNode.submitButton.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);
|
const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName);
|
||||||
await collectionTriggerNode.nodeConfigure.click();
|
await collectionTriggerNode.nodeConfigure.click();
|
||||||
await collectionTriggerNode.collectionDropDown.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 collectionTriggerNode.triggerOnDropdown.click();
|
||||||
await page.getByText('After record added or updated', { exact: true }).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);
|
const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName);
|
||||||
await collectionTriggerNode.nodeConfigure.click();
|
await collectionTriggerNode.nodeConfigure.click();
|
||||||
await collectionTriggerNode.collectionDropDown.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 collectionTriggerNode.triggerOnDropdown.click();
|
||||||
await page.getByText('After record updated', { exact: true }).click();
|
await page.getByText('After record updated', { exact: true }).click();
|
||||||
// 设置触发器过滤条件
|
// 设置触发器过滤条件
|
||||||
@ -723,7 +729,12 @@ test.describe('Configuration page copy to new version', () => {
|
|||||||
// 3、预期结果:新版本工作流配置内容同旧版本一样
|
// 3、预期结果:新版本工作流配置内容同旧版本一样
|
||||||
const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName);
|
const collectionTriggerNode = new CollectionTriggerNode(page, workFlowName, triggerNodeCollectionName);
|
||||||
await collectionTriggerNode.nodeConfigure.click();
|
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、后置处理:删除工作流
|
// 4、后置处理:删除工作流
|
||||||
await apiDeleteWorkflow(workflowId);
|
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();
|
const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await createRecordNode.nodeConfigure.click();
|
await createRecordNode.nodeConfigure.click();
|
||||||
await createRecordNode.collectionDropDown.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 createRecordNode.addFieldsButton.click();
|
||||||
await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).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();
|
const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await createRecordNode.nodeConfigure.click();
|
await createRecordNode.nodeConfigure.click();
|
||||||
await createRecordNode.collectionDropDown.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 createRecordNode.addFieldsButton.click();
|
||||||
await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).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();
|
const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await createRecordNode.nodeConfigure.click();
|
await createRecordNode.nodeConfigure.click();
|
||||||
await createRecordNode.collectionDropDown.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 createRecordNode.addFieldsButton.click();
|
||||||
await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).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();
|
const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await createRecordNode.nodeConfigure.click();
|
await createRecordNode.nodeConfigure.click();
|
||||||
await createRecordNode.collectionDropDown.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 createRecordNode.addFieldsButton.click();
|
||||||
await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).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();
|
const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await createRecordNode.nodeConfigure.click();
|
await createRecordNode.nodeConfigure.click();
|
||||||
await createRecordNode.collectionDropDown.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 createRecordNode.addFieldsButton.click();
|
||||||
await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).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();
|
const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await createRecordNode.nodeConfigure.click();
|
await createRecordNode.nodeConfigure.click();
|
||||||
await createRecordNode.collectionDropDown.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 createRecordNode.addFieldsButton.click();
|
||||||
await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).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();
|
const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await createRecordNode.nodeConfigure.click();
|
await createRecordNode.nodeConfigure.click();
|
||||||
await createRecordNode.collectionDropDown.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 createRecordNode.addFieldsButton.click();
|
||||||
await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).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();
|
const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await createRecordNode.nodeConfigure.click();
|
await createRecordNode.nodeConfigure.click();
|
||||||
await createRecordNode.collectionDropDown.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 createRecordNode.addFieldsButton.click();
|
||||||
await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).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();
|
const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await createRecordNode.nodeConfigure.click();
|
await createRecordNode.nodeConfigure.click();
|
||||||
await createRecordNode.collectionDropDown.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 createRecordNode.addFieldsButton.click();
|
||||||
await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).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();
|
const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await createRecordNode.nodeConfigure.click();
|
await createRecordNode.nodeConfigure.click();
|
||||||
await createRecordNode.collectionDropDown.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 createRecordNode.addFieldsButton.click();
|
||||||
await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).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();
|
const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await createRecordNode.nodeConfigure.click();
|
await createRecordNode.nodeConfigure.click();
|
||||||
await createRecordNode.collectionDropDown.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 createRecordNode.addFieldsButton.click();
|
||||||
await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).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();
|
const createRecordNodeId = await createRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await createRecordNode.nodeConfigure.click();
|
await createRecordNode.nodeConfigure.click();
|
||||||
await createRecordNode.collectionDropDown.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 createRecordNode.addFieldsButton.click();
|
||||||
await page.getByRole('menuitem', { name: createNodeFieldDisplayName }).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);
|
const deleteRecordNode = new DeleteRecordNode(page, deleteRecordNodeName);
|
||||||
await deleteRecordNode.nodeConfigure.click();
|
await deleteRecordNode.nodeConfigure.click();
|
||||||
await deleteRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page.getByLabel('block-item-Filter-workflows-Filter').getByRole('button', { name: 'Select field' }).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);
|
const deleteRecordNode = new DeleteRecordNode(page, deleteRecordNodeName);
|
||||||
await deleteRecordNode.nodeConfigure.click();
|
await deleteRecordNode.nodeConfigure.click();
|
||||||
await deleteRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page.getByLabel('block-item-Filter-workflows-Filter').getByRole('button', { name: 'Select field' }).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();
|
const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await queryRecordNode.nodeConfigure.click();
|
await queryRecordNode.nodeConfigure.click();
|
||||||
await queryRecordNode.collectionDropDown.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();
|
await queryRecordNode.submitButton.click();
|
||||||
|
|
||||||
// 2、测试步骤:添加数据触发工作流
|
// 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();
|
const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await queryRecordNode.nodeConfigure.click();
|
await queryRecordNode.nodeConfigure.click();
|
||||||
await queryRecordNode.collectionDropDown.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.allowMultipleDataBoxesForResults.check();
|
||||||
await expect(queryRecordNode.pageNumberEditBox).toHaveValue('1');
|
await expect(queryRecordNode.pageNumberEditBox).toHaveValue('1');
|
||||||
await expect(queryRecordNode.pageSizeEditBox).toHaveValue('20');
|
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();
|
const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await queryRecordNode.nodeConfigure.click();
|
await queryRecordNode.nodeConfigure.click();
|
||||||
await queryRecordNode.collectionDropDown.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.allowMultipleDataBoxesForResults.check();
|
||||||
// 设置排序条件
|
// 设置排序条件
|
||||||
await queryRecordNode.addSortFieldsButton.click();
|
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();
|
const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await queryRecordNode.nodeConfigure.click();
|
await queryRecordNode.nodeConfigure.click();
|
||||||
await queryRecordNode.collectionDropDown.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.allowMultipleDataBoxesForResults.check();
|
||||||
// 设置排序条件
|
// 设置排序条件
|
||||||
await queryRecordNode.addSortFieldsButton.click();
|
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();
|
const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await queryRecordNode.nodeConfigure.click();
|
await queryRecordNode.nodeConfigure.click();
|
||||||
await queryRecordNode.collectionDropDown.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.allowMultipleDataBoxesForResults.check();
|
||||||
await expect(queryRecordNode.pageNumberEditBox).toHaveValue('1');
|
await expect(queryRecordNode.pageNumberEditBox).toHaveValue('1');
|
||||||
await expect(queryRecordNode.pageSizeEditBox).toHaveValue('20');
|
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();
|
const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await queryRecordNode.nodeConfigure.click();
|
await queryRecordNode.nodeConfigure.click();
|
||||||
await queryRecordNode.collectionDropDown.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.allowMultipleDataBoxesForResults.check();
|
||||||
await expect(queryRecordNode.pageNumberEditBox).toHaveValue('1');
|
await expect(queryRecordNode.pageNumberEditBox).toHaveValue('1');
|
||||||
await expect(queryRecordNode.pageSizeEditBox).toHaveValue('20');
|
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();
|
const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await queryRecordNode.nodeConfigure.click();
|
await queryRecordNode.nodeConfigure.click();
|
||||||
await queryRecordNode.collectionDropDown.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.allowMultipleDataBoxesForResults.check();
|
||||||
await expect(queryRecordNode.pageNumberEditBox).toHaveValue('1');
|
await expect(queryRecordNode.pageNumberEditBox).toHaveValue('1');
|
||||||
await expect(queryRecordNode.pageSizeEditBox).toHaveValue('20');
|
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();
|
const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await queryRecordNode.nodeConfigure.click();
|
await queryRecordNode.nodeConfigure.click();
|
||||||
await queryRecordNode.collectionDropDown.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 queryRecordNode.allowMultipleDataBoxesForResults.check();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).click();
|
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();
|
const queryRecordNodeId = await queryRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await queryRecordNode.nodeConfigure.click();
|
await queryRecordNode.nodeConfigure.click();
|
||||||
await queryRecordNode.collectionDropDown.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 queryRecordNode.allowMultipleDataBoxesForResults.check();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByTestId('filter-select-all-or-any').click();
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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.getByText('Add condition', { exact: true }).click();
|
||||||
await page
|
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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).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();
|
const updateRecordNodeId = await updateRecordNode.node.locator('.workflow-node-id').innerText();
|
||||||
await updateRecordNode.nodeConfigure.click();
|
await updateRecordNode.nodeConfigure.click();
|
||||||
await updateRecordNode.collectionDropDown.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 updateRecordNode.articleByArticleUpdateModeRadio.click();
|
||||||
// 设置过滤条件
|
// 设置过滤条件
|
||||||
await page.getByText('Add condition', { exact: true }).click();
|
await page.getByText('Add condition', { exact: true }).click();
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { uid } from '@formily/shared';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CollectionProvider_deprecated,
|
CollectionProvider_deprecated,
|
||||||
SchemaInitializerItem,
|
SchemaInitializerItem,
|
||||||
SchemaInitializerItemType,
|
SchemaInitializerItemType,
|
||||||
|
parseCollectionName,
|
||||||
useCollectionManager_deprecated,
|
useCollectionManager_deprecated,
|
||||||
useRecordCollectionDataSourceItems,
|
useRecordCollectionDataSourceItems,
|
||||||
useSchemaInitializer,
|
useSchemaInitializer,
|
||||||
@ -13,23 +15,29 @@ import {
|
|||||||
|
|
||||||
import { traverseSchema } from '../utils';
|
import { traverseSchema } from '../utils';
|
||||||
|
|
||||||
function InnerCollectionBlockInitializer({ collection, dataSource, ...props }) {
|
function InnerCollectionBlockInitializer({ collection, dataPath, ...props }) {
|
||||||
const { insert } = useSchemaInitializer();
|
const { insert } = useSchemaInitializer();
|
||||||
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
const { getTemplateSchemaByMode } = useSchemaTemplateManager();
|
||||||
const { getCollection } = useCollectionManager_deprecated();
|
const { getCollection } = useCollectionManager_deprecated();
|
||||||
const items = useRecordCollectionDataSourceItems('FormItem') as SchemaInitializerItemType[];
|
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 }) {
|
async function onConfirm({ item }) {
|
||||||
const template = item.template ? await getTemplateSchemaByMode(item) : null;
|
const template = item.template ? await getTemplateSchemaByMode(item) : null;
|
||||||
const result = {
|
const result = {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
name: resolvedCollection.name,
|
name: uid(),
|
||||||
title: resolvedCollection.title,
|
title: resolvedCollection.title,
|
||||||
'x-decorator': 'DetailsBlockProvider',
|
'x-decorator': 'DetailsBlockProvider',
|
||||||
'x-decorator-props': {
|
'x-decorator-props': {
|
||||||
collection,
|
collection,
|
||||||
dataSource,
|
dataPath,
|
||||||
},
|
},
|
||||||
'x-component': 'CardItem',
|
'x-component': 'CardItem',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
@ -66,11 +74,20 @@ function InnerCollectionBlockInitializer({ collection, dataSource, ...props }) {
|
|||||||
return <SchemaInitializerItem {...props} onClick={onConfirm} items={items} />;
|
return <SchemaInitializerItem {...props} onClick={onConfirm} items={items} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CollectionBlockInitializer() {
|
export function CollectionBlockInitializer(props) {
|
||||||
const itemConfig = useSchemaInitializerItem();
|
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 (
|
return (
|
||||||
<CollectionProvider_deprecated collection={itemConfig.collection}>
|
<CollectionProvider_deprecated dataSource={dataSource} collection={collection}>
|
||||||
<InnerCollectionBlockInitializer {...itemConfig} />
|
<InnerCollectionBlockInitializer {...itemConfig} {...props} />
|
||||||
</CollectionProvider_deprecated>
|
</CollectionProvider_deprecated>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
SchemaComponent,
|
SchemaComponent,
|
||||||
Variable,
|
Variable,
|
||||||
css,
|
css,
|
||||||
|
parseCollectionName,
|
||||||
useCollectionManager_deprecated,
|
useCollectionManager_deprecated,
|
||||||
useCompile,
|
useCompile,
|
||||||
useToken,
|
useToken,
|
||||||
@ -21,7 +22,8 @@ function AssociationInput(props) {
|
|||||||
const { path } = useField();
|
const { path } = useField();
|
||||||
const fieldName = path.segments[path.segments.length - 1] as string;
|
const fieldName = path.segments[path.segments.length - 1] as string;
|
||||||
const { values: config } = useForm();
|
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 { type } = fields.find((item) => item.name === fieldName);
|
||||||
|
|
||||||
const value = Array.isArray(props.value) ? props.value.join(',') : props.value;
|
const value = Array.isArray(props.value) ? props.value.join(',') : props.value;
|
||||||
@ -39,11 +41,11 @@ const CollectionFieldSet = observer(
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const form = useForm();
|
const form = useForm();
|
||||||
const { getCollection, getCollectionFields } = useCollectionManager_deprecated();
|
const { getCollectionFields } = useCollectionManager_deprecated();
|
||||||
const scope = useWorkflowVariableOptions();
|
const scope = useWorkflowVariableOptions();
|
||||||
const { values: config } = form;
|
const { values: config } = form;
|
||||||
const collectionName = config?.collection;
|
const [dataSourceName, collectionName] = parseCollectionName(config?.collection);
|
||||||
const collectionFields = getCollectionFields(collectionName).filter((field) => field.uiSchema);
|
const collectionFields = getCollectionFields(collectionName, dataSourceName).filter((field) => field.uiSchema);
|
||||||
const fields = filter ? collectionFields.filter(filter.bind(config)) : collectionFields;
|
const fields = filter ? collectionFields.filter(filter.bind(config)) : collectionFields;
|
||||||
|
|
||||||
const unassignedFields = useMemo(() => fields.filter((field) => !value || !(field.name in value)), [fields, value]);
|
const unassignedFields = useMemo(() => fields.filter((field) => !value || !(field.name in value)), [fields, value]);
|
||||||
@ -79,7 +81,7 @@ const CollectionFieldSet = observer(
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{fields.length ? (
|
{fields.length ? (
|
||||||
<CollectionProvider_deprecated collection={getCollection(collectionName)}>
|
<CollectionProvider_deprecated name={collectionName} dataSource={dataSourceName}>
|
||||||
{fields
|
{fields
|
||||||
.filter((field) => value && field.name in value)
|
.filter((field) => value && field.name in value)
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
|
import React, { useMemo, useRef } from 'react';
|
||||||
import { createForm } from '@formily/core';
|
import { createForm } from '@formily/core';
|
||||||
import { useField } from '@formily/react';
|
import { useField } from '@formily/react';
|
||||||
|
import { get } from 'lodash';
|
||||||
import {
|
import {
|
||||||
BlockRequestContext_deprecated,
|
BlockRequestContext_deprecated,
|
||||||
CollectionProvider_deprecated,
|
CollectionProvider_deprecated,
|
||||||
FormBlockContext,
|
FormBlockContext,
|
||||||
RecordProvider,
|
RecordProvider,
|
||||||
|
parseCollectionName,
|
||||||
useAPIClient,
|
useAPIClient,
|
||||||
useAssociationNames,
|
useAssociationNames,
|
||||||
useBlockRequestContext,
|
useBlockRequestContext,
|
||||||
} from '@nocobase/client';
|
} 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 { execution, nodes } = useFlowContext();
|
||||||
|
|
||||||
const nodesKeyMap = useMemo(() => {
|
const nodesKeyMap = useMemo(() => {
|
||||||
@ -31,16 +33,24 @@ function useFlowContextData(dataSource) {
|
|||||||
[execution?.context, execution?.jobs, nodesKeyMap],
|
[execution?.context, execution?.jobs, nodesKeyMap],
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = useMemo(() => parse(dataSource)(data), [data, dataSource]);
|
const result = useMemo(() => get(data, dataPath), [data, dataPath]);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DetailsBlockProvider(props) {
|
export function DetailsBlockProvider({ collection, dataPath, children }) {
|
||||||
const field = useField();
|
const field = useField();
|
||||||
const formBlockRef = useRef(null);
|
const formBlockRef = useRef(null);
|
||||||
const { getAssociationAppends } = useAssociationNames();
|
const { getAssociationAppends } = useAssociationNames();
|
||||||
const { appends, updateAssociationValues } = getAssociationAppends();
|
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(
|
const form = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@ -61,11 +71,11 @@ export function DetailsBlockProvider(props) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const api = useAPIClient();
|
const api = useAPIClient();
|
||||||
const resource = api.resource(props.collection);
|
const resource = api.resource(resolvedCollection);
|
||||||
const __parent = useBlockRequestContext();
|
const __parent = useBlockRequestContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollectionProvider_deprecated collection={props.collection}>
|
<CollectionProvider_deprecated dataSource={dataSourceName} collection={resolvedCollection}>
|
||||||
<RecordProvider record={values} parent={null}>
|
<RecordProvider record={values} parent={null}>
|
||||||
<BlockRequestContext_deprecated.Provider value={{ block: 'form', field, service, resource, __parent }}>
|
<BlockRequestContext_deprecated.Provider value={{ block: 'form', field, service, resource, __parent }}>
|
||||||
<FormBlockContext.Provider
|
<FormBlockContext.Provider
|
||||||
@ -78,7 +88,7 @@ export function DetailsBlockProvider(props) {
|
|||||||
formBlockRef,
|
formBlockRef,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{children}
|
||||||
</FormBlockContext.Provider>
|
</FormBlockContext.Provider>
|
||||||
</BlockRequestContext_deprecated.Provider>
|
</BlockRequestContext_deprecated.Provider>
|
||||||
</RecordProvider>
|
</RecordProvider>
|
@ -2,7 +2,7 @@ import { observer, useForm } from '@formily/react';
|
|||||||
import { Select } from 'antd';
|
import { Select } from 'antd';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useCollectionManager_deprecated, useCompile } from '@nocobase/client';
|
import { parseCollectionName, useCollectionManager_deprecated, useCompile } from '@nocobase/client';
|
||||||
|
|
||||||
function defaultFilter() {
|
function defaultFilter() {
|
||||||
return true;
|
return true;
|
||||||
@ -14,7 +14,8 @@ export const FieldsSelect = observer(
|
|||||||
const compile = useCompile();
|
const compile = useCompile();
|
||||||
const { getCollectionFields } = useCollectionManager_deprecated();
|
const { getCollectionFields } = useCollectionManager_deprecated();
|
||||||
const { values } = useForm();
|
const { values } = useForm();
|
||||||
const fields = getCollectionFields(values?.collection);
|
const [dataSourceName, collectionName] = parseCollectionName(values?.collection);
|
||||||
|
const fields = getCollectionFields(collectionName, dataSourceName);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<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 { parse } from '@nocobase/utils/client';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useFlowContext } from '../FlowContext';
|
import { useFlowContext } from '../FlowContext';
|
||||||
|
import { SimpleDesigner } from './SimpleDesigner';
|
||||||
|
|
||||||
export const ValueBlock: (() => JSX.Element) & {
|
export const ValueBlock: (() => JSX.Element) & {
|
||||||
Initializer: () => JSX.Element;
|
Initializer: () => JSX.Element;
|
||||||
Result: (props) => JSX.Element;
|
Result: (props) => JSX.Element;
|
||||||
|
Designer: () => JSX.Element;
|
||||||
} = () => {
|
} = () => {
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@ -27,7 +29,7 @@ function Initializer() {
|
|||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
title: node.title ?? `#${node.id}`,
|
title: node.title ?? `#${node.id}`,
|
||||||
},
|
},
|
||||||
'x-designer': 'SimpleDesigner',
|
'x-designer': 'ValueBlock.Designer',
|
||||||
properties: {
|
properties: {
|
||||||
result: {
|
result: {
|
||||||
type: 'void',
|
type: 'void',
|
||||||
@ -71,3 +73,4 @@ function Result({ dataSource }) {
|
|||||||
|
|
||||||
ValueBlock.Initializer = Initializer;
|
ValueBlock.Initializer = Initializer;
|
||||||
ValueBlock.Result = Result;
|
ValueBlock.Result = Result;
|
||||||
|
ValueBlock.Designer = SimpleDesigner;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
export * from './CollectionBlockInitializer';
|
export * from './CollectionBlockInitializer';
|
||||||
|
export * from './DetailsBlockProvider';
|
||||||
export * from './FieldsSelect';
|
export * from './FieldsSelect';
|
||||||
export * from './FilterDynamicComponent';
|
export * from './FilterDynamicComponent';
|
||||||
export * from './RadioWithTooltip';
|
export * from './RadioWithTooltip';
|
||||||
export * from './CheckboxGroupWithTooltip';
|
export * from './CheckboxGroupWithTooltip';
|
||||||
export * from './ValueBlock';
|
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 React from 'react';
|
||||||
|
|
||||||
import { Plugin } from '@nocobase/client';
|
import { Plugin } from '@nocobase/client';
|
||||||
@ -30,7 +17,7 @@ import QueryInstruction from './nodes/query';
|
|||||||
import CreateInstruction from './nodes/create';
|
import CreateInstruction from './nodes/create';
|
||||||
import UpdateInstruction from './nodes/update';
|
import UpdateInstruction from './nodes/update';
|
||||||
import DestroyInstruction from './nodes/destroy';
|
import DestroyInstruction from './nodes/destroy';
|
||||||
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './constant';
|
import { getWorkflowDetailPath, getWorkflowExecutionsPath } from './utils';
|
||||||
import { NAMESPACE } from './locale';
|
import { NAMESPACE } from './locale';
|
||||||
import { customizeSubmitToWorkflowActionSettings } from './settings/customizeSubmitToWorkflowActionSettings';
|
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';
|
group = 'collection';
|
||||||
description = `{{t("Add new record to a collection. You can use variables from upstream nodes to assign values to fields.", { ns: "${NAMESPACE}" })}}`;
|
description = `{{t("Add new record to a collection. You can use variables from upstream nodes to assign values to fields.", { ns: "${NAMESPACE}" })}}`;
|
||||||
fieldset = {
|
fieldset = {
|
||||||
collection,
|
collection: {
|
||||||
|
...collection,
|
||||||
|
'x-reactions': [
|
||||||
|
...collection['x-reactions'],
|
||||||
|
{
|
||||||
|
target: 'params',
|
||||||
|
effects: ['onFieldValueChange'],
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
visible: '{{!!$self.value}}',
|
||||||
|
value: '{{Object.create({})}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
// multiple: {
|
// multiple: {
|
||||||
// type: 'boolean',
|
// type: 'boolean',
|
||||||
// title: '多条数据',
|
// title: '多条数据',
|
||||||
@ -84,7 +99,7 @@ export default class extends Instruction {
|
|||||||
title: node.title ?? `#${node.id}`,
|
title: node.title ?? `#${node.id}`,
|
||||||
Component: CollectionBlockInitializer,
|
Component: CollectionBlockInitializer,
|
||||||
collection: node.config.collection,
|
collection: node.config.collection,
|
||||||
dataSource: `{{$jobsMapByNodeKey.${node.key}}}`,
|
dataPath: `$jobsMapByNodeKey.${node.key}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,29 @@ export default class extends Instruction {
|
|||||||
group = 'collection';
|
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}" })}}`;
|
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 = {
|
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: {
|
params: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -22,7 +22,22 @@ export default class extends Instruction {
|
|||||||
group = 'collection';
|
group = 'collection';
|
||||||
description = `{{t("Query records from a collection. You can use variables from upstream nodes as query conditions.", { ns: "${NAMESPACE}" })}}`;
|
description = `{{t("Query records from a collection. You can use variables from upstream nodes as query conditions.", { ns: "${NAMESPACE}" })}}`;
|
||||||
fieldset = {
|
fieldset = {
|
||||||
collection,
|
collection: {
|
||||||
|
...collection,
|
||||||
|
'x-reactions': [
|
||||||
|
...collection['x-reactions'],
|
||||||
|
{
|
||||||
|
target: 'params',
|
||||||
|
effects: ['onFieldValueChange'],
|
||||||
|
fulfill: {
|
||||||
|
state: {
|
||||||
|
visible: '{{!!$self.value}}',
|
||||||
|
value: '{{Object.create({})}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
multiple: {
|
multiple: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
@ -130,7 +145,7 @@ export default class extends Instruction {
|
|||||||
title: node.title ?? `#${node.id}`,
|
title: node.title ?? `#${node.id}`,
|
||||||
Component: CollectionBlockInitializer,
|
Component: CollectionBlockInitializer,
|
||||||
collection: node.config.collection,
|
collection: node.config.collection,
|
||||||
dataSource: `{{$jobsMapByNodeKey.${node.key}}}`,
|
dataPath: `$jobsMapByNodeKey.${node.key}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,38 @@ export default class extends Instruction {
|
|||||||
group = 'collection';
|
group = 'collection';
|
||||||
description = `{{t("Update records of a collection. You can use variables from upstream nodes as query conditions and field values.", { ns: "${NAMESPACE}" })}}`;
|
description = `{{t("Update records of a collection. You can use variables from upstream nodes as query conditions and field values.", { ns: "${NAMESPACE}" })}}`;
|
||||||
fieldset = {
|
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: {
|
params: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useForm } from '@formily/react';
|
import { useForm } from '@formily/react';
|
||||||
import { css, useCollectionFilterOptions } from '@nocobase/client';
|
import { css, parseCollectionName, useCollectionFilterOptions } from '@nocobase/client';
|
||||||
import { NAMESPACE } from '../locale';
|
import { NAMESPACE } from '../locale';
|
||||||
|
|
||||||
export const collection = {
|
export const collection = {
|
||||||
@ -8,15 +8,13 @@ export const collection = {
|
|||||||
required: true,
|
required: true,
|
||||||
'x-reactions': [],
|
'x-reactions': [],
|
||||||
'x-decorator': 'FormItem',
|
'x-decorator': 'FormItem',
|
||||||
'x-component': 'CollectionSelect',
|
'x-component': 'DataSourceCollectionCascader',
|
||||||
'x-component-props': {
|
|
||||||
className: 'auto-width',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const values = {
|
export const values = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
title: '{{t("Fields values")}}',
|
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': 'FormItem',
|
||||||
'x-decorator-props': {
|
'x-decorator-props': {
|
||||||
labelAlign: 'left',
|
labelAlign: 'left',
|
||||||
@ -25,7 +23,6 @@ export const values = {
|
|||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
'x-component': 'CollectionFieldset',
|
'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 = {
|
export const filter = {
|
||||||
@ -36,7 +33,8 @@ export const filter = {
|
|||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
useProps() {
|
useProps() {
|
||||||
const { values } = useForm();
|
const { values } = useForm();
|
||||||
const options = useCollectionFilterOptions(values?.collection);
|
const [dataSourceName, collectionName] = parseCollectionName(values?.collection);
|
||||||
|
const options = useCollectionFilterOptions(collectionName, dataSourceName);
|
||||||
return {
|
return {
|
||||||
options,
|
options,
|
||||||
className: css`
|
className: css`
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ISchema } from '@formily/react';
|
import { ISchema } from '@formily/react';
|
||||||
import { Link } from 'react-router-dom';
|
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 { useTranslation } from 'react-i18next';
|
||||||
import { message } from 'antd';
|
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 = {
|
export const executionCollection = {
|
||||||
name: 'execution-executions',
|
name: 'execution-executions',
|
||||||
|
@ -9,6 +9,7 @@ import { FieldsSelect } from '../components/FieldsSelect';
|
|||||||
import { NAMESPACE, lang } from '../locale';
|
import { NAMESPACE, lang } from '../locale';
|
||||||
import { appends, collection, filter } from '../schemas/collection';
|
import { appends, collection, filter } from '../schemas/collection';
|
||||||
import { getCollectionFieldOptions } from '../variable';
|
import { getCollectionFieldOptions } from '../variable';
|
||||||
|
import { useWorkflowAnyExecuted } from '../hooks';
|
||||||
import { Trigger } from '.';
|
import { Trigger } from '.';
|
||||||
|
|
||||||
const COLLECTION_TRIGGER_MODE = {
|
const COLLECTION_TRIGGER_MODE = {
|
||||||
@ -31,6 +32,12 @@ export default class extends Trigger {
|
|||||||
fieldset = {
|
fieldset = {
|
||||||
collection: {
|
collection: {
|
||||||
...collection,
|
...collection,
|
||||||
|
'x-disabled': '{{ useWorkflowAnyExecuted() }}',
|
||||||
|
'x-component-props': {
|
||||||
|
dataSourceFilter(item) {
|
||||||
|
return item.options.key === 'main' || item.options.isDBInstance;
|
||||||
|
},
|
||||||
|
},
|
||||||
['x-reactions']: [
|
['x-reactions']: [
|
||||||
...collection['x-reactions'],
|
...collection['x-reactions'],
|
||||||
{
|
{
|
||||||
@ -147,6 +154,7 @@ export default class extends Trigger {
|
|||||||
};
|
};
|
||||||
scope = {
|
scope = {
|
||||||
useCollectionDataSource,
|
useCollectionDataSource,
|
||||||
|
useWorkflowAnyExecuted,
|
||||||
};
|
};
|
||||||
components = {
|
components = {
|
||||||
FieldsSelect,
|
FieldsSelect,
|
||||||
@ -192,7 +200,7 @@ export default class extends Trigger {
|
|||||||
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
||||||
Component: CollectionBlockInitializer,
|
Component: CollectionBlockInitializer,
|
||||||
collection: config.collection,
|
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);
|
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 { t } = useTranslation();
|
||||||
const [dir, setDir] = useState(value.offset ? value.offset / Math.abs(value.offset) : 0);
|
const [dir, setDir] = useState(value.offset ? value.offset / Math.abs(value.offset) : 0);
|
||||||
|
|
||||||
|
@ -68,14 +68,21 @@ const ModeFieldsets = {
|
|||||||
[SCHEDULE_MODE.DATE_FIELD]: {
|
[SCHEDULE_MODE.DATE_FIELD]: {
|
||||||
collection: {
|
collection: {
|
||||||
...collection,
|
...collection,
|
||||||
|
'x-component-props': {
|
||||||
|
dataSourceFilter(item) {
|
||||||
|
return item.options.key === 'main' || item.options.isDBInstance;
|
||||||
|
},
|
||||||
|
},
|
||||||
'x-reactions': [
|
'x-reactions': [
|
||||||
...collection['x-reactions'],
|
...collection['x-reactions'],
|
||||||
{
|
{
|
||||||
// only full path works
|
// only full path works
|
||||||
target: 'startsOn',
|
target: 'startsOn',
|
||||||
|
effects: ['onFieldValueChange'],
|
||||||
fulfill: {
|
fulfill: {
|
||||||
state: {
|
state: {
|
||||||
visible: '{{!!$self.value}}',
|
visible: '{{!!$self.value}}',
|
||||||
|
value: '{{Object.create({})}}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -79,7 +79,7 @@ export default class extends Trigger {
|
|||||||
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
title: `{{t("Trigger data", { ns: "${NAMESPACE}" })}}`,
|
||||||
Component: CollectionBlockInitializer,
|
Component: CollectionBlockInitializer,
|
||||||
collection: config.collection,
|
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 React from 'react';
|
||||||
|
|
||||||
import { Variable, useCompile, usePlugin } from '@nocobase/client';
|
import { Variable, parseCollectionName, useCompile, usePlugin } from '@nocobase/client';
|
||||||
|
|
||||||
import { useFlowContext } from './FlowContext';
|
import { useFlowContext } from './FlowContext';
|
||||||
import { NAMESPACE, lang } from './locale';
|
import { NAMESPACE, lang } from './locale';
|
||||||
@ -250,22 +250,23 @@ export function useWorkflowVariableOptions(options: UseVariableOptions = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getNormalizedFields(collectionName, { compile, getCollectionFields }) {
|
function getNormalizedFields(collectionName, { compile, getCollectionFields }) {
|
||||||
const fields = getCollectionFields(collectionName);
|
const [dataSourceName, collection] = parseCollectionName(collectionName);
|
||||||
|
const fields = getCollectionFields(collection, dataSourceName);
|
||||||
const foreignKeyFields: any[] = [];
|
const foreignKeyFields: any[] = [];
|
||||||
const otherFields: any[] = [];
|
const result: any[] = [];
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
if (field.isForeignKey) {
|
if (field.isForeignKey) {
|
||||||
foreignKeyFields.push(field);
|
foreignKeyFields.push(field);
|
||||||
} else {
|
} else {
|
||||||
otherFields.push(field);
|
result.push(field);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
for (let i = otherFields.length - 1; i >= 0; i--) {
|
for (let i = result.length - 1; i >= 0; i--) {
|
||||||
const field = otherFields[i];
|
const field = result[i];
|
||||||
if (field.type === 'belongsTo') {
|
if (field.type === 'belongsTo') {
|
||||||
const foreignKeyField = foreignKeyFields.find((f) => f.name === field.foreignKey);
|
const foreignKeyField = foreignKeyFields.find((f) => f.name === field.foreignKey);
|
||||||
if (foreignKeyField) {
|
if (foreignKeyField) {
|
||||||
otherFields.splice(i, 0, {
|
result.splice(i, 0, {
|
||||||
...field,
|
...field,
|
||||||
...foreignKeyField,
|
...foreignKeyField,
|
||||||
uiSchema: {
|
uiSchema: {
|
||||||
@ -274,7 +275,7 @@ function getNormalizedFields(collectionName, { compile, getCollectionFields }) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
otherFields.splice(i, 0, {
|
result.splice(i, 0, {
|
||||||
...field,
|
...field,
|
||||||
name: field.foreignKey,
|
name: field.foreignKey,
|
||||||
type: 'bigInt',
|
type: 'bigInt',
|
||||||
@ -288,8 +289,8 @@ function getNormalizedFields(collectionName, { compile, getCollectionFields }) {
|
|||||||
}
|
}
|
||||||
} else if (field.type === 'context' && field.collectionName === 'users') {
|
} else if (field.type === 'context' && field.collectionName === 'users') {
|
||||||
const belongsToField =
|
const belongsToField =
|
||||||
otherFields.find((f) => f.type === 'belongsTo' && f.target === 'users' && f.foreignKey === field.name) ?? {};
|
result.find((f) => f.type === 'belongsTo' && f.target === 'users' && f.foreignKey === field.name) ?? {};
|
||||||
otherFields.splice(i, 0, {
|
result.splice(i, 0, {
|
||||||
...field,
|
...field,
|
||||||
type: field.dataType,
|
type: field.dataType,
|
||||||
interface: belongsToField.interface,
|
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) {
|
function loadChildren(option) {
|
||||||
const appends = getNextAppends(option.field, option.appends);
|
const appends = getNextAppends(option.field, option.appends);
|
||||||
const result = getCollectionFieldOptions({
|
const result = getCollectionFieldOptions({
|
||||||
collection: option.field.target,
|
collection: `${
|
||||||
|
option.field.dataSourceKey && option.field.dataSourceKey !== 'main' ? `${option.field.dataSourceKey}:` : ''
|
||||||
|
}${option.field.target}`,
|
||||||
types: option.types,
|
types: option.types,
|
||||||
appends,
|
appends,
|
||||||
depth: option.depth - 1,
|
depth: option.depth - 1,
|
||||||
@ -336,8 +339,7 @@ export function getCollectionFieldOptions(options): VariableOption[] {
|
|||||||
getCollectionFields,
|
getCollectionFields,
|
||||||
fieldNames = defaultFieldNames,
|
fieldNames = defaultFieldNames,
|
||||||
} = options;
|
} = options;
|
||||||
const normalizedFields = getNormalizedFields(collection, { compile, getCollectionFields });
|
const computedFields = fields ?? getNormalizedFields(collection, { compile, getCollectionFields });
|
||||||
const computedFields = fields ?? normalizedFields;
|
|
||||||
const boundLoadChildren = loadChildren.bind({ compile, getCollectionFields, fieldNames });
|
const boundLoadChildren = loadChildren.bind({ compile, getCollectionFields, fieldNames });
|
||||||
|
|
||||||
const result: VariableOption[] = filterTypedFields({
|
const result: VariableOption[] = filterTypedFields({
|
||||||
|
@ -126,4 +126,35 @@ describe('workflow > instructions > create', () => {
|
|||||||
expect(job.result.posts[0].id).toBe(post.id);
|
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);
|
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([]);
|
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 { MockDatabase } from '@nocobase/database';
|
||||||
import Database from '@nocobase/database';
|
|
||||||
import { getApp, sleep } from '@nocobase/plugin-workflow-test';
|
import { getApp, sleep } from '@nocobase/plugin-workflow-test';
|
||||||
|
import { MockServer } from '@nocobase/test';
|
||||||
|
|
||||||
import type { WorkflowModel as WorkflowModelType } from '../../types';
|
import type { WorkflowModel as WorkflowModelType } from '../../types';
|
||||||
|
import { EXECUTION_STATUS } from '../../constants';
|
||||||
|
|
||||||
describe('workflow > instructions > update', () => {
|
describe('workflow > instructions > update', () => {
|
||||||
let app: Application;
|
let app: MockServer;
|
||||||
let db: Database;
|
let db: MockDatabase;
|
||||||
let PostRepo;
|
let PostRepo;
|
||||||
let WorkflowModel;
|
let WorkflowModel;
|
||||||
let workflow: WorkflowModelType;
|
let workflow: WorkflowModelType;
|
||||||
@ -60,6 +62,8 @@ describe('workflow > instructions > update', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('params: from job of node', async () => {
|
it('params: from job of node', async () => {
|
||||||
|
const p1 = await PostRepo.create({ values: { title: 't1' }, hooks: false });
|
||||||
|
|
||||||
const n1 = await workflow.createNode({
|
const n1 = await workflow.createNode({
|
||||||
type: 'query',
|
type: 'query',
|
||||||
config: {
|
config: {
|
||||||
@ -98,6 +102,9 @@ describe('workflow > instructions > update', () => {
|
|||||||
// should get from db
|
// should get from db
|
||||||
const post = await PostRepo.findById(id);
|
const post = await PostRepo.findById(id);
|
||||||
expect(post.title).toBe('changed');
|
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);
|
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 { getApp, sleep } from '@nocobase/plugin-workflow-test';
|
||||||
import { Application } from '@nocobase/server';
|
|
||||||
import { EXECUTION_STATUS } from '../../constants';
|
import { EXECUTION_STATUS } from '../../constants';
|
||||||
|
|
||||||
describe('workflow > triggers > collection', () => {
|
describe('workflow > triggers > collection', () => {
|
||||||
let app: Application;
|
let app: MockServer;
|
||||||
let db: Database;
|
let db: MockDatabase;
|
||||||
let CategoryRepo;
|
let CategoryRepo;
|
||||||
let PostRepo;
|
let PostRepo;
|
||||||
let CommentRepo;
|
let CommentRepo;
|
||||||
@ -14,7 +15,7 @@ describe('workflow > triggers > collection', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
app = await getApp({
|
app = await getApp({
|
||||||
plugins: ['error-handler', 'collection-manager'],
|
plugins: ['error-handler', 'collection-manager', 'users', 'auth'],
|
||||||
});
|
});
|
||||||
|
|
||||||
db = app.db;
|
db = app.db;
|
||||||
@ -28,6 +29,16 @@ describe('workflow > triggers > collection', () => {
|
|||||||
afterEach(() => app.destroy());
|
afterEach(() => app.destroy());
|
||||||
|
|
||||||
describe('toggle', () => {
|
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 () => {
|
it('when collection change', async () => {
|
||||||
const workflow = await WorkflowModel.create({
|
const workflow = await WorkflowModel.create({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@ -507,4 +518,97 @@ describe('workflow > triggers > collection', () => {
|
|||||||
expect(executions[0].status).toBe(EXECUTION_STATUS.RESOLVED);
|
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 { JOB_STATUS } from '../constants';
|
||||||
import { toJSON } from '../utils';
|
import { toJSON } from '../utils';
|
||||||
import type Processor from '../Processor';
|
import type Processor from '../Processor';
|
||||||
@ -7,8 +9,11 @@ import { Instruction } from '.';
|
|||||||
export class CreateInstruction extends Instruction {
|
export class CreateInstruction extends Instruction {
|
||||||
async run(node: FlowNodeModel, input, processor: Processor) {
|
async run(node: FlowNodeModel, input, processor: Processor) {
|
||||||
const { collection, params: { appends = [], ...params } = {} } = node.config;
|
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 options = processor.getParsedValue(params, node.id);
|
||||||
|
|
||||||
const created = await repository.create({
|
const created = await repository.create({
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { parseCollectionName } from '@nocobase/data-source-manager';
|
||||||
|
|
||||||
import { Instruction } from '.';
|
import { Instruction } from '.';
|
||||||
import type Processor from '../Processor';
|
import type Processor from '../Processor';
|
||||||
import { JOB_STATUS } from '../constants';
|
import { JOB_STATUS } from '../constants';
|
||||||
@ -7,9 +9,13 @@ export class DestroyInstruction extends Instruction {
|
|||||||
async run(node: FlowNodeModel, input, processor: Processor) {
|
async run(node: FlowNodeModel, input, processor: Processor) {
|
||||||
const { collection, params = {} } = node.config;
|
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 options = processor.getParsedValue(params, node.id);
|
||||||
const result = await repo.destroy({
|
const result = await repository.destroy({
|
||||||
...options,
|
...options,
|
||||||
context: {
|
context: {
|
||||||
stack: Array.from(new Set((processor.execution.context.stack ?? []).concat(processor.execution.id))),
|
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 { DEFAULT_PAGE, DEFAULT_PER_PAGE, utils } from '@nocobase/actions';
|
||||||
|
import { parseCollectionName } from '@nocobase/data-source-manager';
|
||||||
|
|
||||||
import type Processor from '../Processor';
|
import type Processor from '../Processor';
|
||||||
import { JOB_STATUS } from '../constants';
|
import { JOB_STATUS } from '../constants';
|
||||||
@ -10,7 +11,11 @@ export class QueryInstruction extends Instruction {
|
|||||||
async run(node: FlowNodeModel, input, processor: Processor) {
|
async run(node: FlowNodeModel, input, processor: Processor) {
|
||||||
const { collection, multiple, params = {}, failOnEmpty = false } = node.config;
|
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 {
|
const {
|
||||||
page = DEFAULT_PAGE,
|
page = DEFAULT_PAGE,
|
||||||
pageSize = DEFAULT_PER_PAGE,
|
pageSize = DEFAULT_PER_PAGE,
|
||||||
@ -26,7 +31,7 @@ export class QueryInstruction extends Instruction {
|
|||||||
}, new Set()),
|
}, new Set()),
|
||||||
)
|
)
|
||||||
: options.appends;
|
: options.appends;
|
||||||
const result = await (multiple ? repo.find : repo.findOne).call(repo, {
|
const result = await (multiple ? repository.find : repository.findOne).call(repository, {
|
||||||
...options,
|
...options,
|
||||||
...utils.pageArgsToLimitArgs(page, pageSize),
|
...utils.pageArgsToLimitArgs(page, pageSize),
|
||||||
sort: sort
|
sort: sort
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { parseCollectionName } from '@nocobase/data-source-manager';
|
||||||
|
|
||||||
import type Processor from '../Processor';
|
import type Processor from '../Processor';
|
||||||
import { JOB_STATUS } from '../constants';
|
import { JOB_STATUS } from '../constants';
|
||||||
import type { FlowNodeModel } from '../types';
|
import type { FlowNodeModel } from '../types';
|
||||||
@ -7,9 +9,13 @@ export class UpdateInstruction extends Instruction {
|
|||||||
async run(node: FlowNodeModel, input, processor: Processor) {
|
async run(node: FlowNodeModel, input, processor: Processor) {
|
||||||
const { collection, params = {} } = node.config;
|
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 options = processor.getParsedValue(params, node.id);
|
||||||
const result = await repo.update({
|
const result = await repository.update({
|
||||||
...options,
|
...options,
|
||||||
context: {
|
context: {
|
||||||
stack: Array.from(new Set((processor.execution.context.stack ?? []).concat(processor.execution.id))),
|
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 Trigger from '.';
|
||||||
import { toJSON } from '../utils';
|
import { toJSON } from '../utils';
|
||||||
import type { WorkflowModel } from '../types';
|
import type { WorkflowModel } from '../types';
|
||||||
|
import { parseCollectionName, ICollection } from '@nocobase/data-source-manager';
|
||||||
|
|
||||||
export interface CollectionChangeTriggerConfig {
|
export interface CollectionChangeTriggerConfig {
|
||||||
collection: string;
|
collection: string;
|
||||||
@ -25,18 +26,21 @@ function getHookId(workflow, type) {
|
|||||||
return `${type}#${workflow.id}`;
|
return `${type}#${workflow.id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFieldRawName(collection: Collection, name: string) {
|
function getFieldRawName(collection: ICollection, name: string) {
|
||||||
const field = collection.getField(name);
|
const field = collection.getField(name);
|
||||||
if (field && field.type === 'belongsTo') {
|
if (field && field.options.type === 'belongsTo') {
|
||||||
return field.foreignKey;
|
return field.options.foreignKey;
|
||||||
}
|
}
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// async function, should return promise
|
// async function, should return promise
|
||||||
async function handler(this: CollectionTrigger, workflow: WorkflowModel, data: Model, options) {
|
async function handler(this: CollectionTrigger, workflow: WorkflowModel, data: Model, options) {
|
||||||
const { collection: collectionName, condition, changed, mode, appends } = workflow.config;
|
const { condition, changed, mode, appends } = workflow.config;
|
||||||
const collection = (<typeof Model>data.constructor).database.getCollection(collectionName);
|
const [dataSourceName, collectionName] = parseCollectionName(workflow.config.collection);
|
||||||
|
const collection = this.workflow.app.dataSourceManager?.dataSources
|
||||||
|
.get(dataSourceName)
|
||||||
|
.collectionManager.getCollection(collectionName);
|
||||||
const { transaction, context } = options;
|
const { transaction, context } = options;
|
||||||
const { repository, model } = collection;
|
const { repository, model } = collection;
|
||||||
|
|
||||||
@ -45,7 +49,9 @@ async function handler(this: CollectionTrigger, workflow: WorkflowModel, data: M
|
|||||||
changed &&
|
changed &&
|
||||||
changed.length &&
|
changed.length &&
|
||||||
changed
|
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)))
|
.every((name) => !data.changedWithAssociations(getFieldRawName(collection, name)))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@ -102,16 +108,20 @@ export default class CollectionTrigger extends Trigger {
|
|||||||
events = new Map();
|
events = new Map();
|
||||||
|
|
||||||
on(workflow: WorkflowModel) {
|
on(workflow: WorkflowModel) {
|
||||||
const { db } = this.workflow.app;
|
|
||||||
const { collection, mode } = workflow.config;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [key, type] of MODE_BITMAP_EVENTS.entries()) {
|
for (const [key, type] of MODE_BITMAP_EVENTS.entries()) {
|
||||||
const event = `${collection}.${type}`;
|
const event = `${collectionName}.${type}`;
|
||||||
const name = getHookId(workflow, event);
|
const name = getHookId(workflow, `${collection}.${type}`);
|
||||||
if (mode & key) {
|
if (mode & key) {
|
||||||
if (!this.events.has(name)) {
|
if (!this.events.has(name)) {
|
||||||
const listener = handler.bind(this, workflow);
|
const listener = handler.bind(this, workflow);
|
||||||
@ -129,19 +139,23 @@ export default class CollectionTrigger extends Trigger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
off(workflow: WorkflowModel) {
|
off(workflow: WorkflowModel) {
|
||||||
const { db } = this.workflow.app;
|
|
||||||
const { collection, mode } = workflow.config;
|
const { collection, mode } = workflow.config;
|
||||||
const Collection = db.getCollection(collection);
|
if (!collection) {
|
||||||
if (!Collection) {
|
|
||||||
return;
|
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()) {
|
for (const [key, type] of MODE_BITMAP_EVENTS.entries()) {
|
||||||
const event = `${collection}.${type}`;
|
const name = getHookId(workflow, `${collection}.${type}`);
|
||||||
const name = getHookId(workflow, event);
|
|
||||||
if (mode & key) {
|
if (mode & key) {
|
||||||
const listener = this.events.get(name);
|
const listener = this.events.get(name);
|
||||||
if (listener) {
|
if (listener) {
|
||||||
db.off(event, listener);
|
db.off(`${collectionName}.${type}`, listener);
|
||||||
this.events.delete(name);
|
this.events.delete(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import parser from 'cron-parser';
|
|||||||
import type Plugin from '../../Plugin';
|
import type Plugin from '../../Plugin';
|
||||||
import type { WorkflowModel } from '../../types';
|
import type { WorkflowModel } from '../../types';
|
||||||
import { parseDateWithoutMs, SCHEDULE_MODE } from './utils';
|
import { parseDateWithoutMs, SCHEDULE_MODE } from './utils';
|
||||||
|
import { parseCollectionName } from '@nocobase/data-source-manager';
|
||||||
|
|
||||||
export type ScheduleOnField = {
|
export type ScheduleOnField = {
|
||||||
field: string;
|
field: string;
|
||||||
@ -334,7 +335,9 @@ export default class ScheduleTrigger {
|
|||||||
this.inspect([workflow]);
|
this.inspect([workflow]);
|
||||||
|
|
||||||
const { collection } = workflow.config;
|
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);
|
const name = getHookId(workflow, event);
|
||||||
if (this.events.has(name)) {
|
if (this.events.has(name)) {
|
||||||
return;
|
return;
|
||||||
@ -346,7 +349,8 @@ export default class ScheduleTrigger {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this.events.set(name, listener);
|
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) {
|
off(workflow: WorkflowModel) {
|
||||||
@ -358,12 +362,16 @@ export default class ScheduleTrigger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { collection } = workflow.config;
|
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);
|
const name = getHookId(workflow, event);
|
||||||
if (this.events.has(name)) {
|
if (this.events.has(eventKey)) {
|
||||||
const listener = this.events.get(name);
|
const listener = this.events.get(name);
|
||||||
this.events.delete(name);
|
// @ts-ignore
|
||||||
this.workflow.app.db.off(event, listener);
|
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