@nocobase/plugin-workflow-action-trigger 0.20.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +9 -0
  3. package/README.zh-CN.md +9 -0
  4. package/client.d.ts +2 -0
  5. package/client.js +1 -0
  6. package/dist/client/ActionTrigger.d.ts +54 -0
  7. package/dist/client/index.d.ts +4 -0
  8. package/dist/client/index.js +1 -0
  9. package/dist/externalVersion.js +13 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +39 -0
  12. package/dist/locale/en-US.json +9 -0
  13. package/dist/locale/index.d.ts +3 -0
  14. package/dist/locale/index.js +39 -0
  15. package/dist/locale/ko_KR.json +9 -0
  16. package/dist/locale/zh-CN.json +10 -0
  17. package/dist/server/ActionTrigger.d.ts +9 -0
  18. package/dist/server/ActionTrigger.js +130 -0
  19. package/dist/server/Plugin.d.ts +4 -0
  20. package/dist/server/Plugin.js +41 -0
  21. package/dist/server/index.d.ts +1 -0
  22. package/dist/server/index.js +33 -0
  23. package/dist/server/migrations/20240227172623-change-name.d.ts +6 -0
  24. package/dist/server/migrations/20240227172623-change-name.js +42 -0
  25. package/package.json +28 -0
  26. package/server.d.ts +2 -0
  27. package/server.js +1 -0
  28. package/src/client/ActionTrigger.tsx +132 -0
  29. package/src/client/__e2e__/configuration.test.ts +661 -0
  30. package/src/client/__e2e__/workflowCRUD.test.ts +178 -0
  31. package/src/client/index.ts +100 -0
  32. package/src/index.ts +2 -0
  33. package/src/locale/en-US.json +9 -0
  34. package/src/locale/index.ts +12 -0
  35. package/src/locale/ko_KR.json +9 -0
  36. package/src/locale/zh-CN.json +10 -0
  37. package/src/server/ActionTrigger.ts +132 -0
  38. package/src/server/Plugin.ts +11 -0
  39. package/src/server/__tests__/trigger.test.ts +503 -0
  40. package/src/server/index.ts +1 -0
  41. package/src/server/migrations/20240227172623-change-name.ts +22 -0
@@ -0,0 +1,178 @@
1
+ import { faker } from '@faker-js/faker';
2
+ import {
3
+ CreateWorkFlow,
4
+ EditWorkFlow,
5
+ FormEventTriggerNode,
6
+ WorkflowListRecords,
7
+ apiCreateRecordTriggerFormEvent,
8
+ apiCreateWorkflow,
9
+ apiDeleteWorkflow,
10
+ apiGetWorkflow,
11
+ apiUpdateWorkflowTrigger,
12
+ appendJsonCollectionName,
13
+ generalWithNoRelationalFields,
14
+ } from '@nocobase/plugin-workflow-test/e2e';
15
+ import { expect, test } from '@nocobase/test/e2e';
16
+ import { dayjs } from '@nocobase/utils';
17
+
18
+ test.describe('Filter', () => {
19
+ test('filter workflow name', async ({ page }) => {
20
+ //添加工作流
21
+ const triggerNodeAppendText = faker.string.alphanumeric(5);
22
+ const workFlowName = faker.string.alphanumeric(5) + triggerNodeAppendText;
23
+ const workflowData = {
24
+ current: true,
25
+ options: { deleteExecutionOnStatus: [] },
26
+ title: workFlowName,
27
+ type: 'action',
28
+ enabled: true,
29
+ };
30
+ const workflow = await apiCreateWorkflow(workflowData);
31
+ const workflowObj = JSON.parse(JSON.stringify(workflow));
32
+ const workflowId = workflowObj.id;
33
+
34
+ // 2、筛选工作流
35
+ await page.goto('/admin/settings/workflow');
36
+ await page.waitForLoadState('networkidle');
37
+ await page.getByLabel('action-Filter.Action-Filter-filter-workflows').click();
38
+ await page.getByRole('textbox').fill(workFlowName);
39
+ await page.getByRole('button', { name: 'Submit' }).click();
40
+
41
+ // 3、预期结果:列表中出现筛选的工作流
42
+ await expect(page.getByText(workFlowName)).toBeAttached();
43
+
44
+ // 4、后置处理:删除工作流
45
+ await apiDeleteWorkflow(workflowId);
46
+ });
47
+ });
48
+
49
+ test.describe('Add new', () => {
50
+ test('add new Action event', async ({ page }) => {
51
+ // 添加工作流
52
+ await page.goto('/admin/settings/workflow');
53
+ await page.waitForLoadState('networkidle');
54
+ await page.getByLabel('action-Action-Add new-workflows').click();
55
+ const createWorkFlow = new CreateWorkFlow(page);
56
+ const workFlowName = faker.string.alphanumeric(5);
57
+ await createWorkFlow.name.fill(workFlowName);
58
+ await createWorkFlow.triggerType.click();
59
+ await page.getByRole('option', { name: 'Action event' }).click();
60
+ await page.getByLabel('action-Action-Submit-workflows').click();
61
+
62
+ // 3、预期结果:列表中出现新建的工作流
63
+ await expect(page.getByText(workFlowName)).toBeVisible();
64
+
65
+ // 4、后置处理:删除工作流
66
+ await page.getByLabel('action-Filter.Action-Filter-filter-workflows').click();
67
+ await page.getByRole('textbox').fill(workFlowName);
68
+ await page.getByRole('button', { name: 'Submit' }).click();
69
+ await page.getByLabel(`action-Action.Link-Delete-workflows-${workFlowName}`).click();
70
+ await page.getByRole('button', { name: 'OK', exact: true }).click();
71
+ await expect(page.getByText(workFlowName)).toBeHidden();
72
+ });
73
+ });
74
+
75
+ test.describe('Sync', () => {});
76
+
77
+ test.describe('Delete', () => {
78
+ test('delete Action event', async ({ page }) => {
79
+ //添加工作流
80
+ const triggerNodeAppendText = faker.string.alphanumeric(5);
81
+ const workFlowName = faker.string.alphanumeric(5) + triggerNodeAppendText;
82
+ const workflowData = {
83
+ current: true,
84
+ options: { deleteExecutionOnStatus: [] },
85
+ title: workFlowName,
86
+ type: 'action',
87
+ enabled: true,
88
+ };
89
+ const workflow = await apiCreateWorkflow(workflowData);
90
+ const workflowObj = JSON.parse(JSON.stringify(workflow));
91
+ const workflowId = workflowObj.id;
92
+
93
+ // 删除工作流
94
+ await page.goto('/admin/settings/workflow');
95
+ await page.waitForLoadState('networkidle');
96
+ await page.getByLabel('action-Filter.Action-Filter-filter-workflows').click();
97
+ await page.getByRole('textbox').fill(workFlowName);
98
+ await page.getByRole('button', { name: 'Submit' }).click();
99
+ await page.getByLabel(`action-Action.Link-Delete-workflows-${workFlowName}`).click();
100
+ await page.getByRole('button', { name: 'OK', exact: true }).click();
101
+
102
+ // 3、预期结果:列表中出现筛选的工作流
103
+ await expect(page.getByText(workFlowName)).toBeHidden();
104
+
105
+ // 4、后置处理:删除工作流
106
+ });
107
+ });
108
+
109
+ test.describe('Edit', () => {
110
+ test('edit Action event name', async ({ page }) => {
111
+ //添加工作流
112
+ const triggerNodeAppendText = faker.string.alphanumeric(5);
113
+ let workFlowName = faker.string.alphanumeric(5) + triggerNodeAppendText;
114
+ const workflowData = {
115
+ current: true,
116
+ options: { deleteExecutionOnStatus: [] },
117
+ title: workFlowName,
118
+ type: 'action',
119
+ enabled: true,
120
+ };
121
+ const workflow = await apiCreateWorkflow(workflowData);
122
+ const workflowObj = JSON.parse(JSON.stringify(workflow));
123
+ const workflowId = workflowObj.id;
124
+
125
+ // 编辑工作流
126
+ await page.goto('/admin/settings/workflow');
127
+ await page.waitForLoadState('networkidle');
128
+ await page.getByLabel(`action-Action.Link-Edit-workflows-${workFlowName}`).click();
129
+ const editWorkFlow = new EditWorkFlow(page, workFlowName);
130
+ workFlowName = faker.string.alphanumeric(5) + triggerNodeAppendText;
131
+ await editWorkFlow.name.fill(workFlowName);
132
+ await page.getByLabel('action-Action-Submit-workflows').click();
133
+ await page.waitForLoadState('networkidle');
134
+ // 3、预期结果:编辑成功,列表中出现编辑后的工作流
135
+ await expect(page.getByText(workFlowName)).toBeAttached();
136
+
137
+ // 4、后置处理:删除工作流
138
+ await apiDeleteWorkflow(workflowId);
139
+ });
140
+ });
141
+
142
+ test.describe('Duplicate', () => {
143
+ test('Duplicate Action event triggers with only unconfigured trigger nodes', async ({ page }) => {
144
+ //添加工作流
145
+ const triggerNodeAppendText = faker.string.alphanumeric(5);
146
+ const workFlowName = faker.string.alphanumeric(5) + triggerNodeAppendText;
147
+ const workflowData = {
148
+ current: true,
149
+ options: { deleteExecutionOnStatus: [] },
150
+ title: workFlowName,
151
+ type: 'action',
152
+ enabled: true,
153
+ };
154
+ const workflow = await apiCreateWorkflow(workflowData);
155
+ const workflowObj = JSON.parse(JSON.stringify(workflow));
156
+ const workflowId = workflowObj.id;
157
+
158
+ // 2、复制工作流
159
+ await page.goto('/admin/settings/workflow');
160
+ await page.waitForLoadState('networkidle');
161
+ await page.getByLabel(`action-Action.Link-Duplicate-workflows-${workFlowName}`).click();
162
+ await page.getByLabel(`action-Action-Submit-workflows-${workFlowName}`).click();
163
+ await page.waitForLoadState('networkidle');
164
+ // 3、预期结果:列表中出现筛选的工作流
165
+ await page.getByLabel('action-Filter.Action-Filter-filter-workflows').click();
166
+ await page.getByRole('textbox').fill(workFlowName);
167
+ await page.getByRole('button', { name: 'Submit', exact: true }).click();
168
+ await expect(page.getByText(`${workFlowName} copy`)).toBeAttached();
169
+
170
+ // 4、后置处理:删除工作流
171
+ await page.getByLabel(`action-Action.Link-Delete-workflows-${workFlowName} copy`).click();
172
+ await page.getByRole('button', { name: 'OK', exact: true }).click();
173
+ await expect(page.getByText(`${workFlowName} copy`)).toBeHidden();
174
+ await apiDeleteWorkflow(workflowId);
175
+ });
176
+ });
177
+
178
+ test.describe('Executed', () => {});
@@ -0,0 +1,100 @@
1
+ import { Plugin, SchemaInitializerItemType } from '@nocobase/client';
2
+ import WorkflowPlugin, {
3
+ useTriggerWorkflowsActionProps,
4
+ useRecordTriggerWorkflowsActionProps,
5
+ } from '@nocobase/plugin-workflow/client';
6
+
7
+ import ActionTrigger from './ActionTrigger';
8
+
9
+ const submitToWorkflowActionInitializer: SchemaInitializerItemType = {
10
+ name: 'submitToWorkflow',
11
+ title: '{{t("Submit to workflow", { ns: "workflow" })}}',
12
+ Component: 'CustomizeActionInitializer',
13
+ schema: {
14
+ title: '{{t("Submit to workflow", { ns: "workflow" })}}',
15
+ 'x-component': 'Action',
16
+ 'x-component-props': {
17
+ useProps: '{{ useTriggerWorkflowsActionProps }}',
18
+ },
19
+ 'x-designer': 'Action.Designer',
20
+ 'x-action-settings': {
21
+ // assignedValues: {},
22
+ skipValidator: false,
23
+ onSuccess: {
24
+ manualClose: true,
25
+ redirecting: false,
26
+ successMessage: '{{t("Submitted successfully")}}',
27
+ },
28
+ triggerWorkflows: [],
29
+ },
30
+ 'x-action': 'customize:triggerWorkflows',
31
+ },
32
+ };
33
+
34
+ const recordTriggerWorkflowActionInitializer: SchemaInitializerItemType = {
35
+ name: 'submitToWorkflow',
36
+ title: '{{t("Submit to workflow", { ns: "workflow" })}}',
37
+ Component: 'CustomizeActionInitializer',
38
+ schema: {
39
+ title: '{{t("Submit to workflow", { ns: "workflow" })}}',
40
+ 'x-component': 'Action',
41
+ 'x-component-props': {
42
+ useProps: '{{ useRecordTriggerWorkflowsActionProps }}',
43
+ },
44
+ 'x-designer': 'Action.Designer',
45
+ 'x-action-settings': {
46
+ // assignedValues: {},
47
+ onSuccess: {
48
+ manualClose: true,
49
+ redirecting: false,
50
+ successMessage: '{{t("Submitted successfully")}}',
51
+ },
52
+ triggerWorkflows: [],
53
+ },
54
+ 'x-action': 'customize:triggerWorkflows',
55
+ },
56
+ };
57
+
58
+ const recordTriggerWorkflowActionLinkInitializer = {
59
+ ...recordTriggerWorkflowActionInitializer,
60
+ schema: {
61
+ ...recordTriggerWorkflowActionInitializer.schema,
62
+ 'x-component': 'Action.Link',
63
+ },
64
+ };
65
+
66
+ export default class extends Plugin {
67
+ async load() {
68
+ const workflow = this.app.pm.get('workflow') as WorkflowPlugin;
69
+ workflow.registerTrigger('action', ActionTrigger);
70
+
71
+ this.app.addScopes({
72
+ useTriggerWorkflowsActionProps,
73
+ useRecordTriggerWorkflowsActionProps,
74
+ });
75
+
76
+ const FormActionInitializers = this.app.schemaInitializerManager.get('FormActionInitializers');
77
+ FormActionInitializers.add('customize.submitToWorkflow', submitToWorkflowActionInitializer);
78
+
79
+ const CreateFormActionInitializers = this.app.schemaInitializerManager.get('CreateFormActionInitializers');
80
+ CreateFormActionInitializers.add('customize.submitToWorkflow', submitToWorkflowActionInitializer);
81
+
82
+ const UpdateFormActionInitializers = this.app.schemaInitializerManager.get('UpdateFormActionInitializers');
83
+ UpdateFormActionInitializers.add('customize.submitToWorkflow', submitToWorkflowActionInitializer);
84
+
85
+ const DetailsActionInitializers = this.app.schemaInitializerManager.get('DetailsActionInitializers');
86
+ DetailsActionInitializers.add('customize.submitToWorkflow', recordTriggerWorkflowActionInitializer);
87
+
88
+ const ReadPrettyFormActionInitializers = this.app.schemaInitializerManager.get('ReadPrettyFormActionInitializers');
89
+ ReadPrettyFormActionInitializers.add('customize.submitToWorkflow', recordTriggerWorkflowActionInitializer);
90
+
91
+ const TableActionColumnInitializers = this.app.schemaInitializerManager.get('TableActionColumnInitializers');
92
+ TableActionColumnInitializers.add('customize.submitToWorkflow', recordTriggerWorkflowActionLinkInitializer);
93
+
94
+ const GridCardItemActionInitializers = this.app.schemaInitializerManager.get('GridCardItemActionInitializers');
95
+ GridCardItemActionInitializers.add('customize.submitToWorkflow', recordTriggerWorkflowActionLinkInitializer);
96
+
97
+ const ListItemActionInitializers = this.app.schemaInitializerManager.get('ListItemActionInitializers');
98
+ ListItemActionInitializers.add('customize.submitToWorkflow', recordTriggerWorkflowActionLinkInitializer);
99
+ }
100
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './server';
2
+ export { default } from './server';
@@ -0,0 +1,9 @@
1
+ {
2
+ "Form event": "Form event",
3
+ "Event triggers when submitted a workflow bound form action.": "Event triggers when submitted a workflow bound form action.",
4
+ "Form data model": "Form data model",
5
+ "Use a collection to match form data.": "Use a collection to match form data.",
6
+ "Associations to use": "Associations to use",
7
+ "User submitted form": "User submitted form",
8
+ "Role of user submitted form": "Role of user submitted form"
9
+ }
@@ -0,0 +1,12 @@
1
+ import { useTranslation } from 'react-i18next';
2
+
3
+ export const NAMESPACE = 'workflow-action-trigger';
4
+
5
+ export function useLang(key: string, options = {}) {
6
+ const { t } = usePluginTranslation(options);
7
+ return t(key);
8
+ }
9
+
10
+ export function usePluginTranslation(options) {
11
+ return useTranslation(NAMESPACE, options);
12
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "Form event": "폼 이벤트",
3
+ "Event triggers when submitted a workflow bound form action.": "작업 흐름에 바인딩된 폼 작업이 제출될 때 이벤트가 트리거됩니다.",
4
+ "Form data model": "폼 데이터 모델",
5
+ "Use a collection to match form data.": "폼 데이터를 일치시키기 위해 데이터 테이블을 사용합니다.",
6
+ "Associations to use": "사용할 관련 데이터",
7
+ "User submitted form": "사용자가 제출한 폼",
8
+ "Role of user submitted form": "사용자 제출 폼의 역할"
9
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "Action event": "操作事件",
3
+ "Triggers after specific operations on data are submitted, such as create, update, delete, etc., or directly submitting a record to the workflow.": "在对数据的特定操作提交后触发,如创建、更新、删除等,或直接提交一条数据至工作流。",
4
+ "Collection": "数据表",
5
+ "Which collection record belongs to.": "数据所属的数据表。",
6
+ "Associations to use": "待使用的关系数据",
7
+ "Trigger data": "触发器数据",
8
+ "User submitted action": "提交操作的用户",
9
+ "Role of user submitted action": "提交操作用户的角色"
10
+ }
@@ -0,0 +1,132 @@
1
+ import { get } from 'lodash';
2
+ import { BelongsTo, HasOne } from 'sequelize';
3
+ import { Model, modelAssociationByKey } from '@nocobase/database';
4
+
5
+ import WorkflowPlugin, { Trigger, WorkflowModel, toJSON } from '@nocobase/plugin-workflow';
6
+
7
+ export default class extends Trigger {
8
+ constructor(workflow: WorkflowPlugin) {
9
+ super(workflow);
10
+
11
+ workflow.app.resourcer.use(this.middleware);
12
+ }
13
+
14
+ async triggerAction(context, next) {
15
+ const { triggerWorkflows } = context.action.params;
16
+
17
+ if (!triggerWorkflows) {
18
+ return context.throw(400);
19
+ }
20
+
21
+ context.status = 202;
22
+ await next();
23
+
24
+ this.trigger(context);
25
+ }
26
+
27
+ middleware = async (context, next) => {
28
+ const {
29
+ resourceName,
30
+ actionName,
31
+ params: { triggerWorkflows },
32
+ } = context.action;
33
+
34
+ if (resourceName === 'workflows' && actionName === 'trigger') {
35
+ return this.triggerAction(context, next);
36
+ }
37
+
38
+ await next();
39
+
40
+ if (!triggerWorkflows) {
41
+ return;
42
+ }
43
+
44
+ if (!['create', 'update'].includes(actionName)) {
45
+ return;
46
+ }
47
+
48
+ return this.trigger(context);
49
+ };
50
+
51
+ private async trigger(context) {
52
+ const { triggerWorkflows = '', values } = context.action.params;
53
+
54
+ const { currentUser, currentRole } = context.state;
55
+ const userInfo = {
56
+ user: toJSON(currentUser),
57
+ roleName: currentRole,
58
+ };
59
+
60
+ const triggers = triggerWorkflows.split(',').map((trigger) => trigger.split('!'));
61
+ const workflowRepo = this.workflow.db.getRepository('workflows');
62
+ const workflows = await workflowRepo.find({
63
+ filter: {
64
+ key: triggers.map((trigger) => trigger[0]),
65
+ current: true,
66
+ type: 'action',
67
+ enabled: true,
68
+ },
69
+ });
70
+ const syncGroup = [];
71
+ const asyncGroup = [];
72
+ for (const workflow of workflows) {
73
+ const trigger = triggers.find((trigger) => trigger[0] == workflow.key);
74
+ const event = [workflow];
75
+ if (context.action.resourceName !== 'workflows') {
76
+ if (!context.body) {
77
+ continue;
78
+ }
79
+ const { body: data } = context;
80
+ for (const row of Array.isArray(data) ? data : [data]) {
81
+ let payload = row;
82
+ if (trigger[1]) {
83
+ const paths = trigger[1].split('.');
84
+ for (const field of paths) {
85
+ if (payload.get(field)) {
86
+ payload = payload.get(field);
87
+ } else {
88
+ const association = <HasOne | BelongsTo>modelAssociationByKey(payload, field);
89
+ payload = await payload[association.accessors.get]();
90
+ }
91
+ }
92
+ }
93
+ const { collection, appends = [] } = workflow.config;
94
+ const model = payload.constructor;
95
+ if (payload instanceof Model) {
96
+ if (collection !== model.collection.name) {
97
+ continue;
98
+ }
99
+ if (appends.length) {
100
+ payload = await model.collection.repository.findOne({
101
+ filterByTk: payload.get(model.primaryKeyAttribute),
102
+ appends,
103
+ });
104
+ }
105
+ }
106
+ // this.workflow.trigger(workflow, { data: toJSON(payload), ...userInfo });
107
+ event.push({ data: toJSON(payload), ...userInfo });
108
+ }
109
+ } else {
110
+ const data = trigger[1] ? get(values, trigger[1]) : values;
111
+ // this.workflow.trigger(workflow, {
112
+ // data,
113
+ // ...userInfo,
114
+ // });
115
+ event.push({ data, ...userInfo });
116
+ }
117
+ (workflow.sync ? syncGroup : asyncGroup).push(event);
118
+ }
119
+
120
+ for (const event of syncGroup) {
121
+ await this.workflow.trigger(event[0], event[1], { httpContext: context });
122
+ }
123
+
124
+ for (const event of asyncGroup) {
125
+ this.workflow.trigger(event[0], event[1]);
126
+ }
127
+ }
128
+
129
+ on(workflow: WorkflowModel) {}
130
+
131
+ off(workflow: WorkflowModel) {}
132
+ }
@@ -0,0 +1,11 @@
1
+ import { Plugin } from '@nocobase/server';
2
+ import WorkflowPlugin from '@nocobase/plugin-workflow';
3
+
4
+ import ActionTrigger from './ActionTrigger';
5
+
6
+ export default class extends Plugin {
7
+ async load() {
8
+ const workflowPlugin = this.app.pm.get(WorkflowPlugin) as WorkflowPlugin;
9
+ workflowPlugin.triggers.register('action', new ActionTrigger(workflowPlugin));
10
+ }
11
+ }