@nocobase/plugin-workflow-manual 0.17.0-alpha.4
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.
- package/LICENSE +661 -0
- package/README.md +9 -0
- package/README.zh-CN.md +9 -0
- package/client.d.ts +2 -0
- package/client.js +1 -0
- package/dist/client/WorkflowTodo.d.ts +5 -0
- package/dist/client/WorkflowTodoBlockInitializer.d.ts +2 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +13 -0
- package/dist/client/instruction/AssigneesSelect.d.ts +6 -0
- package/dist/client/instruction/DetailsBlockProvider.d.ts +2 -0
- package/dist/client/instruction/FormBlockInitializer.d.ts +2 -0
- package/dist/client/instruction/FormBlockProvider.d.ts +2 -0
- package/dist/client/instruction/ModeConfig.d.ts +5 -0
- package/dist/client/instruction/SchemaConfig.d.ts +49 -0
- package/dist/client/instruction/forms/create.d.ts +3 -0
- package/dist/client/instruction/forms/custom.d.ts +5 -0
- package/dist/client/instruction/forms/update.d.ts +3 -0
- package/dist/client/instruction/index.d.ts +83 -0
- package/dist/client/instruction/utils.d.ts +1 -0
- package/dist/externalVersion.js +18 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +39 -0
- package/dist/locale/en-US.json +30 -0
- package/dist/locale/index.d.ts +3 -0
- package/dist/locale/index.js +39 -0
- package/dist/locale/zh-CN.json +30 -0
- package/dist/server/ManualInstruction.d.ts +28 -0
- package/dist/server/ManualInstruction.js +150 -0
- package/dist/server/Plugin.d.ts +6 -0
- package/dist/server/Plugin.js +73 -0
- package/dist/server/actions.d.ts +2 -0
- package/dist/server/actions.js +94 -0
- package/dist/server/collecions/jobs.d.ts +19 -0
- package/dist/server/collecions/jobs.js +39 -0
- package/dist/server/collecions/users.d.ts +15 -0
- package/dist/server/collecions/users.js +37 -0
- package/dist/server/collecions/users_jobs.d.ts +3 -0
- package/dist/server/collecions/users_jobs.js +70 -0
- package/dist/server/forms/create.d.ts +5 -0
- package/dist/server/forms/create.js +41 -0
- package/dist/server/forms/index.d.ts +6 -0
- package/dist/server/forms/index.js +38 -0
- package/dist/server/forms/update.d.ts +6 -0
- package/dist/server/forms/update.js +41 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +33 -0
- package/package.json +33 -0
- package/server.d.ts +2 -0
- package/server.js +1 -0
- package/src/client/WorkflowTodo.tsx +618 -0
- package/src/client/WorkflowTodoBlockInitializer.tsx +30 -0
- package/src/client/index.ts +44 -0
- package/src/client/instruction/AssigneesSelect.tsx +39 -0
- package/src/client/instruction/DetailsBlockProvider.tsx +85 -0
- package/src/client/instruction/FormBlockInitializer.tsx +72 -0
- package/src/client/instruction/FormBlockProvider.tsx +84 -0
- package/src/client/instruction/ModeConfig.tsx +85 -0
- package/src/client/instruction/SchemaConfig.tsx +538 -0
- package/src/client/instruction/forms/create.tsx +112 -0
- package/src/client/instruction/forms/custom.tsx +403 -0
- package/src/client/instruction/forms/update.tsx +150 -0
- package/src/client/instruction/index.tsx +157 -0
- package/src/client/instruction/utils.ts +19 -0
- package/src/index.ts +2 -0
- package/src/locale/en-US.json +30 -0
- package/src/locale/index.ts +12 -0
- package/src/locale/zh-CN.json +30 -0
- package/src/server/ManualInstruction.ts +151 -0
- package/src/server/Plugin.ts +48 -0
- package/src/server/__tests__/collections/categories.ts +15 -0
- package/src/server/__tests__/collections/comments.ts +24 -0
- package/src/server/__tests__/collections/posts.ts +40 -0
- package/src/server/__tests__/collections/replies.ts +9 -0
- package/src/server/__tests__/collections/tags.ts +15 -0
- package/src/server/__tests__/instruction.test.ts +1154 -0
- package/src/server/actions.ts +100 -0
- package/src/server/collecions/jobs.ts +17 -0
- package/src/server/collecions/users.ts +15 -0
- package/src/server/collecions/users_jobs.ts +50 -0
- package/src/server/forms/create.ts +23 -0
- package/src/server/forms/index.ts +13 -0
- package/src/server/forms/update.ts +23 -0
- package/src/server/index.ts +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function findSchema(schema, filter, onlyLeaf = false) {
|
|
2
|
+
const result = [];
|
|
3
|
+
|
|
4
|
+
if (!schema) {
|
|
5
|
+
return result;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (filter(schema) && (!onlyLeaf || !schema.properties)) {
|
|
9
|
+
result.push(schema);
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (schema.properties) {
|
|
14
|
+
Object.keys(schema.properties).forEach((key) => {
|
|
15
|
+
result.push(...findSchema(schema.properties[key], filter));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Manual": "Manual",
|
|
3
|
+
"Could be used for manually submitting data, and determine whether to continue or exit. Workflow will generate a todo item for assigned user when it reaches a manual node, and continue processing after user submits the form.": "Could be used for manually submitting data, and determine whether to continue or exit. Workflow will generate a todo item for assigned user when it reaches a manual node, and continue processing after user submits the form.",
|
|
4
|
+
"Values preset in this form will override user submitted ones when continue or reject.": "Values preset in this form will override user submitted ones when continue or reject.",
|
|
5
|
+
"Assignee": "Assignee",
|
|
6
|
+
"Assignees": "Assignees",
|
|
7
|
+
"User interface": "User interface",
|
|
8
|
+
"Configure user interface": "Configure user interface",
|
|
9
|
+
"View user interface": "View user interface",
|
|
10
|
+
"Separately": "Separately",
|
|
11
|
+
"Each user has own task": "Each user has own task",
|
|
12
|
+
"Collaboratively": "Collaboratively",
|
|
13
|
+
"Everyone shares one task": "Everyone shares one task",
|
|
14
|
+
"Negotiation": "Negotiation",
|
|
15
|
+
"All pass": "All pass",
|
|
16
|
+
"Everyone should pass": "Everyone should pass",
|
|
17
|
+
"Any pass": "Any pass",
|
|
18
|
+
"Anyone pass": "Anyone pass",
|
|
19
|
+
"Field name existed in form": "Field name existed in form",
|
|
20
|
+
"Continue the process": "Continue the process",
|
|
21
|
+
"Terminate the process": "Terminate the process",
|
|
22
|
+
"Save temporarily": "Save temporarily",
|
|
23
|
+
"Custom form": "Custom form",
|
|
24
|
+
"Data record": "Data record",
|
|
25
|
+
"Create record form": "Create record form",
|
|
26
|
+
"Update record form": "Update record form",
|
|
27
|
+
"Filter settings": "Filter settings",
|
|
28
|
+
"Workflow todos": "Workflow todos",
|
|
29
|
+
"Task": "Task"
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
|
|
3
|
+
export const NAMESPACE = 'workflow-manual';
|
|
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,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Manual": "人工处理",
|
|
3
|
+
"Could be used for manually submitting data, and determine whether to continue or exit. Workflow will generate a todo item for assigned user when it reaches a manual node, and continue processing after user submits the form.": "可用于人工提交数据,并决定是否继续或退出流程。工作流在执行到人工节点时会为被指派的用户生成待办事项,直到用户提交对应表单后继续处理该流程。",
|
|
4
|
+
"Values preset in this form will override user submitted ones when continue or reject.": "表单中预设的字段值会在用户提交继续或拒绝时覆盖相应字段的值。",
|
|
5
|
+
"Assignee": "负责人",
|
|
6
|
+
"Assignees": "负责人",
|
|
7
|
+
"User interface": "操作界面",
|
|
8
|
+
"Configure user interface": "配置界面",
|
|
9
|
+
"View user interface": "查看界面",
|
|
10
|
+
"Separately": "分别处理",
|
|
11
|
+
"Each user has own task": "每个人处理各自的任务",
|
|
12
|
+
"Collaboratively": "协作处理",
|
|
13
|
+
"Everyone shares one task": "所有人共享同一个任务",
|
|
14
|
+
"Negotiation": "协商机制",
|
|
15
|
+
"All pass": "全部通过",
|
|
16
|
+
"Everyone should pass": "每个人通过才通过",
|
|
17
|
+
"Any pass": "任意通过",
|
|
18
|
+
"Anyone pass": "任何一人通过即通过",
|
|
19
|
+
"Field name existed in form": "表单中已有对应标识的字段",
|
|
20
|
+
"Continue the process": "继续流程",
|
|
21
|
+
"Terminate the process": "终止流程",
|
|
22
|
+
"Save temporarily": "暂存",
|
|
23
|
+
"Custom form": "自定义表单",
|
|
24
|
+
"Data record": "数据记录",
|
|
25
|
+
"Create record form": "新增数据表单",
|
|
26
|
+
"Update record form": "更新数据表单",
|
|
27
|
+
"Filter settings": "筛选设置",
|
|
28
|
+
"Workflow todos": "工作流待办",
|
|
29
|
+
"Task": "任务"
|
|
30
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Registry } from '@nocobase/utils';
|
|
2
|
+
import WorkflowPlugin, { Processor, JOB_STATUS, Instruction } from '@nocobase/plugin-workflow';
|
|
3
|
+
|
|
4
|
+
import initFormTypes, { FormHandler } from './forms';
|
|
5
|
+
|
|
6
|
+
type FormType = {
|
|
7
|
+
type: 'custom' | 'create' | 'update';
|
|
8
|
+
actions: number[];
|
|
9
|
+
options: {
|
|
10
|
+
[key: string]: any;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export interface ManualConfig {
|
|
15
|
+
schema: { [key: string]: any };
|
|
16
|
+
forms: { [key: string]: FormType };
|
|
17
|
+
assignees?: (number | string)[];
|
|
18
|
+
mode?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const MULTIPLE_ASSIGNED_MODE = {
|
|
22
|
+
SINGLE: Symbol('single'),
|
|
23
|
+
ALL: Symbol('all'),
|
|
24
|
+
ANY: Symbol('any'),
|
|
25
|
+
ALL_PERCENTAGE: Symbol('all percentage'),
|
|
26
|
+
ANY_PERCENTAGE: Symbol('any percentage'),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const Modes = {
|
|
30
|
+
[MULTIPLE_ASSIGNED_MODE.SINGLE]: {
|
|
31
|
+
getStatus(distribution, assignees) {
|
|
32
|
+
const done = distribution.find((item) => item.status !== JOB_STATUS.PENDING && item.count > 0);
|
|
33
|
+
return done ? done.status : null;
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
[MULTIPLE_ASSIGNED_MODE.ALL]: {
|
|
37
|
+
getStatus(distribution, assignees) {
|
|
38
|
+
const resolved = distribution.find((item) => item.status === JOB_STATUS.RESOLVED);
|
|
39
|
+
if (resolved && resolved.count === assignees.length) {
|
|
40
|
+
return JOB_STATUS.RESOLVED;
|
|
41
|
+
}
|
|
42
|
+
const rejected = distribution.find((item) => item.status < JOB_STATUS.PENDING);
|
|
43
|
+
if (rejected && rejected.count) {
|
|
44
|
+
return rejected.status;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return null;
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
[MULTIPLE_ASSIGNED_MODE.ANY]: {
|
|
51
|
+
getStatus(distribution, assignees) {
|
|
52
|
+
const resolved = distribution.find((item) => item.status === JOB_STATUS.RESOLVED);
|
|
53
|
+
if (resolved && resolved.count) {
|
|
54
|
+
return JOB_STATUS.RESOLVED;
|
|
55
|
+
}
|
|
56
|
+
const rejectedCount = distribution.reduce(
|
|
57
|
+
(count, item) => (item.status < JOB_STATUS.PENDING ? count + item.count : count),
|
|
58
|
+
0,
|
|
59
|
+
);
|
|
60
|
+
// NOTE: all failures are considered as rejected for now
|
|
61
|
+
if (rejectedCount === assignees.length) {
|
|
62
|
+
return JOB_STATUS.REJECTED;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function getMode(mode) {
|
|
71
|
+
switch (true) {
|
|
72
|
+
case mode === 1:
|
|
73
|
+
return Modes[MULTIPLE_ASSIGNED_MODE.ALL];
|
|
74
|
+
case mode === -1:
|
|
75
|
+
return Modes[MULTIPLE_ASSIGNED_MODE.ANY];
|
|
76
|
+
case mode > 0:
|
|
77
|
+
return Modes[MULTIPLE_ASSIGNED_MODE.ALL_PERCENTAGE];
|
|
78
|
+
case mode < 0:
|
|
79
|
+
return Modes[MULTIPLE_ASSIGNED_MODE.ANY_PERCENTAGE];
|
|
80
|
+
default:
|
|
81
|
+
return Modes[MULTIPLE_ASSIGNED_MODE.SINGLE];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export default class extends Instruction {
|
|
86
|
+
formTypes = new Registry<FormHandler>();
|
|
87
|
+
|
|
88
|
+
constructor(public plugin: WorkflowPlugin) {
|
|
89
|
+
super(plugin);
|
|
90
|
+
|
|
91
|
+
initFormTypes(this);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async run(node, prevJob, processor: Processor) {
|
|
95
|
+
const { mode, ...config } = node.config as ManualConfig;
|
|
96
|
+
const assignees = [...new Set(processor.getParsedValue(config.assignees, node.id) || [])];
|
|
97
|
+
|
|
98
|
+
const job = await processor.saveJob({
|
|
99
|
+
status: JOB_STATUS.PENDING,
|
|
100
|
+
result: mode ? [] : null,
|
|
101
|
+
nodeId: node.id,
|
|
102
|
+
upstreamId: prevJob?.id ?? null,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// NOTE: batch create users jobs
|
|
106
|
+
const UserJobModel = processor.options.plugin.db.getModel('users_jobs');
|
|
107
|
+
await UserJobModel.bulkCreate(
|
|
108
|
+
assignees.map((userId) => ({
|
|
109
|
+
userId,
|
|
110
|
+
jobId: job.id,
|
|
111
|
+
nodeId: node.id,
|
|
112
|
+
executionId: job.executionId,
|
|
113
|
+
workflowId: node.workflowId,
|
|
114
|
+
status: JOB_STATUS.PENDING,
|
|
115
|
+
})),
|
|
116
|
+
{
|
|
117
|
+
transaction: processor.transaction,
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return job;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async resume(node, job, processor: Processor) {
|
|
125
|
+
// NOTE: check all users jobs related if all done then continue as parallel
|
|
126
|
+
const { assignees = [], mode } = node.config as ManualConfig;
|
|
127
|
+
|
|
128
|
+
const UserJobModel = processor.options.plugin.db.getModel('users_jobs');
|
|
129
|
+
const distribution = await UserJobModel.count({
|
|
130
|
+
where: {
|
|
131
|
+
jobId: job.id,
|
|
132
|
+
},
|
|
133
|
+
group: ['status'],
|
|
134
|
+
transaction: processor.transaction,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const submitted = distribution.reduce(
|
|
138
|
+
(count, item) => (item.status !== JOB_STATUS.PENDING ? count + item.count : count),
|
|
139
|
+
0,
|
|
140
|
+
);
|
|
141
|
+
const status = job.status || (getMode(mode).getStatus(distribution, assignees) ?? JOB_STATUS.PENDING);
|
|
142
|
+
const result = mode ? (submitted || 0) / assignees.length : job.latestUserJob?.result ?? job.result;
|
|
143
|
+
processor.logger.debug(`manual resume job and next status: ${status}`);
|
|
144
|
+
job.set({
|
|
145
|
+
status,
|
|
146
|
+
result,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return job;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Plugin } from '@nocobase/server';
|
|
2
|
+
import actions from '@nocobase/actions';
|
|
3
|
+
import { HandlerType } from '@nocobase/resourcer';
|
|
4
|
+
import WorkflowPlugin, { JOB_STATUS } from '@nocobase/plugin-workflow';
|
|
5
|
+
|
|
6
|
+
import jobsCollection from './collecions/jobs';
|
|
7
|
+
import usersCollection from './collecions/users';
|
|
8
|
+
import usersJobsCollection from './collecions/users_jobs';
|
|
9
|
+
import { submit } from './actions';
|
|
10
|
+
|
|
11
|
+
import ManualInstruction from './ManualInstruction';
|
|
12
|
+
|
|
13
|
+
export default class extends Plugin {
|
|
14
|
+
workflow: WorkflowPlugin;
|
|
15
|
+
|
|
16
|
+
async load() {
|
|
17
|
+
this.app.db.collection(usersJobsCollection);
|
|
18
|
+
this.app.db.extendCollection(usersCollection);
|
|
19
|
+
this.app.db.extendCollection(jobsCollection);
|
|
20
|
+
|
|
21
|
+
this.app.resource({
|
|
22
|
+
name: 'users_jobs',
|
|
23
|
+
actions: {
|
|
24
|
+
list: {
|
|
25
|
+
filter: {
|
|
26
|
+
$or: [
|
|
27
|
+
{
|
|
28
|
+
'workflow.enabled': true,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
'workflow.enabled': false,
|
|
32
|
+
status: {
|
|
33
|
+
$ne: JOB_STATUS.PENDING,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
handler: actions.list as HandlerType,
|
|
39
|
+
},
|
|
40
|
+
submit,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const workflowPlugin = this.app.getPlugin('workflow') as WorkflowPlugin;
|
|
45
|
+
this.workflow = workflowPlugin;
|
|
46
|
+
workflowPlugin.instructions.register('manual', new ManualInstruction(workflowPlugin));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CollectionOptions } from '@nocobase/database';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'comments',
|
|
5
|
+
fields: [
|
|
6
|
+
{
|
|
7
|
+
type: 'belongsTo',
|
|
8
|
+
name: 'post',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
type: 'text',
|
|
12
|
+
name: 'content',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
type: 'integer',
|
|
16
|
+
name: 'status',
|
|
17
|
+
defaultValue: 0,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: 'hasMany',
|
|
21
|
+
name: 'replies',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
} as CollectionOptions;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { CollectionOptions } from '@nocobase/database';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
name: 'posts',
|
|
5
|
+
createdBy: true,
|
|
6
|
+
updatedBy: true,
|
|
7
|
+
fields: [
|
|
8
|
+
{
|
|
9
|
+
type: 'string',
|
|
10
|
+
name: 'title',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
type: 'boolean',
|
|
14
|
+
name: 'published',
|
|
15
|
+
defaultValue: false,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: 'integer',
|
|
19
|
+
name: 'read',
|
|
20
|
+
defaultValue: 0,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
type: 'belongsTo',
|
|
24
|
+
name: 'category',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'hasMany',
|
|
28
|
+
name: 'comments',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: 'belongsToMany',
|
|
32
|
+
name: 'tags',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'integer',
|
|
36
|
+
name: 'read',
|
|
37
|
+
defaultValue: 0,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
} as CollectionOptions;
|