@steedos-labs/plugin-workflow 3.0.54 → 3.0.56
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/designer/dist/amis-renderer/amis-renderer.css +1 -1
- package/designer/dist/amis-renderer/amis-renderer.js +1 -1
- package/designer/dist/assets/{index-DXnimQAi.js → index-B9naSD1C.js} +173 -173
- package/designer/dist/assets/index-DEEcIiu0.css +1 -0
- package/designer/dist/index.html +2 -2
- package/main/default/manager/push_manager.js +93 -113
- package/main/default/manager/uuflow_manager.js +151 -47
- package/main/default/objects/categories/buttons/badge_recalc.button.yml +44 -0
- package/main/default/objects/instance_tasks/listviews/inbox.listview.yml +1 -1
- package/main/default/objects/instance_tasks/listviews/outbox.listview.yml +1 -2
- package/main/default/objects/instances/buttons/instance_export.button.js +10 -0
- package/main/default/objects/instances/buttons/instance_export.button.yml +83 -0
- package/main/default/objects/instances/listviews/completed.listview.yml +1 -2
- package/main/default/objects/instances/listviews/draft.listview.yml +1 -2
- package/main/default/objects/instances/listviews/monitor.listview.yml +1 -1
- package/main/default/objects/instances/listviews/pending.listview.yml +1 -2
- package/main/default/pages/page_instance_print.page.amis.json +6 -2
- package/main/default/routes/api_workflow_ai_form_design.router.js +9 -3
- package/main/default/routes/api_workflow_ai_form_design_stream.router.js +9 -3
- package/main/default/routes/api_workflow_export.router.js +97 -152
- package/main/default/routes/api_workflow_instance_forward.router.js +15 -1
- package/main/default/routes/api_workflow_instance_permissions.router.js +2 -2
- package/main/default/routes/api_workflow_nav.router.js +7 -1
- package/main/default/routes/api_workflow_next_step.router.js +1 -1
- package/main/default/services/instance.service.js +1 -1
- package/main/default/utils/business_hours.js +210 -0
- package/main/default/utils/business_timeout.js +211 -0
- package/package.json +1 -1
- package/package.service.js +9 -1
- package/public/amis-renderer/amis-renderer.css +1 -1
- package/public/amis-renderer/amis-renderer.js +1 -1
- package/public/workflow/index.css +7 -2
- package/src/rests/badgeRecalcConsole.js +593 -0
- package/src/rests/badgeRecalcExecute.js +308 -0
- package/src/rests/index.js +2 -0
- package/src/timeout_auto_submit.js +81 -0
- package/designer/dist/assets/index-xR8ApdWL.css +0 -1
|
@@ -527,7 +527,10 @@ function buildSystemPrompt() {
|
|
|
527
527
|
- reference_to: 字符串,关联对象的 API 名称,如 "contracts"、"accounts"
|
|
528
528
|
- lookupLabelField: 字符串,用于搜索和显示的名称字段,默认 "name"
|
|
529
529
|
- pickerMultiple: 布尔值,是否允许多选,默认 false
|
|
530
|
-
- lookupFilters: 字符串,
|
|
530
|
+
- lookupFilters: 字符串,DevExpress JSON 格式的过滤表达式,支持 {fieldName} 占位符引用其他表单字段值(运行时自动替换为实际值)
|
|
531
|
+
- 静态过滤示例:'[["enablestate","=","2"],"and",["hidden","<>",true]]'
|
|
532
|
+
- 动态过滤示例(引用其他字段值):'[["company_id","=","{company_id}"],"and",["enablestate","=","2"]]' — 当表单中 company_id 字段值变化时,{company_id} 会被自动替换为该字段的实际值
|
|
533
|
+
- 格式规则:eq → "=",ne → "<>",gt → ">",ge → ">=",lt → "<",le → "<=",contains → "contains",startswith → "startswith"。布尔值不加引号(true/false)
|
|
531
534
|
- lookupDisplayFields: 数组,关联数据后额外展示的字段,格式 [{ "field": "字段API名", "label": "显示标签" }]
|
|
532
535
|
- lookupFillRules: 数组,填充规则,选择关联记录后将源字段值自动填充到表单其他字段。格式 [{ "sourceField": "关联对象的字段API名", "targetField": "当前表单中的目标字段name" }]
|
|
533
536
|
- 示例:选择合同后自动填充合同金额到表单的金额字段:lookupFillRules: [{ "sourceField": "amount", "targetField": "contract_amount" }]
|
|
@@ -612,6 +615,8 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
612
615
|
7. 如果用户只要求修改字段,则 events 保持原样不变
|
|
613
616
|
8. 返回完整的 fields 数组和 events 对象
|
|
614
617
|
9. ⚠️ **type 与 formula 属性的严格对应**:只要字段包含公式表达式(formula 属性),其 type 就**必须**是 "formula",绝对不能是 "number"、"text" 或其他类型。"number" 类型仅用于用户手工输入数字的字段(无公式)。错误示例:{ type: "number", formula: "\${a + b}" },正确示例:{ type: "formula", formula: "\${a + b}", valueType: "number", precision: 2 }。grid/table 的 children 子字段同样适用此规则
|
|
618
|
+
10. ⚠️ **升级场景禁止自动设置配色**:当用户提供了 instance_template、form_script、flow_events 等旧版数据进行升级转换时,section(分组)、grid(网格)、table(子表)字段**不要自动添加** colorScheme、sectionColor、titleColor 等配色属性。配色属性只有在用户明确要求设置颜色/配色/主题色时才添加。升级的目标是忠实还原旧版表单的结构和逻辑,不要擅自美化
|
|
619
|
+
11. **原样保留字段的 is_wide 属性**:is_wide 用于控制字段是否占满整行宽度。输出时必须保持每个字段的 is_wide 与输入完全一致:输入为 true 则输出 true,输入为 false 则输出 false,输入中没有该属性则不要添加。禁止擅自修改或统一设置 is_wide 的值
|
|
615
620
|
|
|
616
621
|
## 额外待转换的数据(升级场景)
|
|
617
622
|
|
|
@@ -749,15 +754,16 @@ if (field.name === 'quantity' || field.name === 'unit_price') {
|
|
|
749
754
|
|
|
750
755
|
### 旧公式字段跨对象引用迁移
|
|
751
756
|
- 旧版表单中,公式字段可能通过 \`{applicant.organization.name}\`、\`{applicant.name}\` 等语法引用成员的关联属性。这种跨对象引用在新版中不再通过公式实现,而是通过**成员字段的 pickerFillRules(填充规则)**来替代。
|
|
757
|
+
- ⚠️ **重要:applicant(申请人)是系统内置字段**,name 固定为 \`__applicant\`,不要创建新的 member 字段。在 fields 数组中输出 { "name": "__applicant", "label": "申请人", "type": "member", "pickerFillRules": [...] } 即可覆盖系统默认配置。该字段在拟稿状态下可编辑(用户可更换申请人),pickerFillRules 会在选人后自动触发填充。
|
|
752
758
|
- 迁移方式:
|
|
753
759
|
1. 将引用跨对象属性的公式字段改为**普通文本字段**(type: "text")作为目标字段
|
|
754
|
-
2.
|
|
760
|
+
2. 在 \`__applicant\` 字段上配置 **pickerFillRules**,使用点号表示法的 sourceField(如 "organization.name"),targetField 指向该文本字段
|
|
755
761
|
3. 如果 targetField 字段应为只读展示(即旧版是纯展示的公式字段),设置 readonly: true
|
|
756
762
|
- 迁移示例:
|
|
757
763
|
旧版公式字段:\`{applicant.organization.name}\` 用于显示申请人所属部门名称
|
|
758
764
|
新版迁移方式:
|
|
759
765
|
1. 创建文本字段 { "name": "dept_name", "label": "所属部门", "type": "text", "readonly": true }
|
|
760
|
-
2.
|
|
766
|
+
2. 输出 __applicant 字段:{ "name": "__applicant", "label": "申请人", "type": "member", "pickerFillRules": [{ "sourceField": "organization.name", "targetField": "dept_name" }] }
|
|
761
767
|
- 常见映射:
|
|
762
768
|
- \`{applicant.organization.name}\` → sourceField: "organization.name"
|
|
763
769
|
- \`{applicant.organization.fullname}\` → sourceField: "organization.fullname"
|
|
@@ -11,8 +11,8 @@ const express = require('express');
|
|
|
11
11
|
const router = express.Router();
|
|
12
12
|
const { requireAuthentication } = require("@steedos/auth");
|
|
13
13
|
const WorkflowManager = require('../manager/workflow_manager');
|
|
14
|
+
const { getCollection } = require('../utils/collection');
|
|
14
15
|
const _ = require('underscore');
|
|
15
|
-
const Fiber = function(fun){console.log('TODO Fiber...')}
|
|
16
16
|
const moment = require('moment')
|
|
17
17
|
const fs = require('fs')
|
|
18
18
|
const path = require('path')
|
|
@@ -35,160 +35,105 @@ const path = require('path')
|
|
|
35
35
|
*/
|
|
36
36
|
router.get('/api/workflow/export/instances', requireAuthentication, async function (req, res) {
|
|
37
37
|
try {
|
|
38
|
-
|
|
39
|
-
const spaceId = userSession.spaceId;
|
|
38
|
+
const userSession = req.user;
|
|
40
39
|
const userId = userSession.userId;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
fields = form.current.fields;
|
|
67
|
-
table_fields = new Array;
|
|
68
|
-
_.each(form.current.fields, function (field) {
|
|
69
|
-
if (field.type === "table") {
|
|
70
|
-
return table_fields.push(field);
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
ins_to_xls = new Array;
|
|
74
|
-
start_date = null;
|
|
75
|
-
end_date = null;
|
|
76
|
-
now = new Date;
|
|
77
|
-
selector = {
|
|
78
|
-
space: space_id,
|
|
79
|
-
flow: flow_id
|
|
80
|
-
};
|
|
81
|
-
selector.state = {
|
|
82
|
-
$in: ["pending", "completed"]
|
|
83
|
-
};
|
|
84
|
-
uid = userId;
|
|
85
|
-
space = db.spaces.findOne({_id: space_id});
|
|
86
|
-
if (!space) {
|
|
87
|
-
selector.state = "none";
|
|
88
|
-
}
|
|
89
|
-
if (!space.admins.includes(uid)) {
|
|
90
|
-
flow_ids = WorkflowManager.getMyAdminOrMonitorFlows(space_id, uid);
|
|
91
|
-
if (!flow_ids.includes(selector.flow)) {
|
|
92
|
-
selector.$or = [
|
|
93
|
-
{
|
|
94
|
-
submitter: uid
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
applicant: uid
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
inbox_users: uid
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
outbox_users: uid
|
|
104
|
-
}
|
|
105
|
-
];
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
// 0-本月
|
|
109
|
-
if (type === 0) {
|
|
110
|
-
start_date = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
111
|
-
selector.submit_date = {
|
|
112
|
-
$gte: start_date
|
|
113
|
-
};
|
|
114
|
-
ins_to_xls = db.instances.find(selector, {
|
|
115
|
-
sort: {
|
|
116
|
-
submit_date: 1
|
|
117
|
-
}
|
|
118
|
-
}).fetch();
|
|
119
|
-
// 1-上月
|
|
120
|
-
} else if (type === 1) {
|
|
121
|
-
last_month_date = new Date(new Date(now.getFullYear(), now.getMonth(), 1) - 1000 * 60 * 60 * 24);
|
|
122
|
-
start_date = new Date(last_month_date.getFullYear(), last_month_date.getMonth(), 1);
|
|
123
|
-
end_date = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
124
|
-
selector.submit_date = {
|
|
125
|
-
$gte: start_date,
|
|
126
|
-
$lte: end_date
|
|
127
|
-
};
|
|
128
|
-
ins_to_xls = db.instances.find(selector, {
|
|
129
|
-
sort: {
|
|
130
|
-
submit_date: 1
|
|
131
|
-
}
|
|
132
|
-
}).fetch();
|
|
133
|
-
// 2-整个年度
|
|
134
|
-
} else if (type === 2) {
|
|
135
|
-
start_date = new Date(now.getFullYear(), 0, 1);
|
|
136
|
-
selector.submit_date = {
|
|
137
|
-
$gte: start_date
|
|
138
|
-
};
|
|
139
|
-
ins_to_xls = db.instances.find(selector, {
|
|
140
|
-
sort: {
|
|
141
|
-
submit_date: 1
|
|
142
|
-
}
|
|
143
|
-
}).fetch();
|
|
144
|
-
// 3-所有
|
|
145
|
-
} else if (type === 3) {
|
|
146
|
-
ins_to_xls = db.instances.find(selector, {
|
|
147
|
-
sort: {
|
|
148
|
-
submit_date: 1
|
|
149
|
-
}
|
|
150
|
-
}).fetch();
|
|
151
|
-
}
|
|
152
|
-
ejs = require('ejs');
|
|
153
|
-
str = fs.readFileSync(path.resolve(__dirname, '../server/ejs/export_instances.ejs'), 'utf8');
|
|
154
|
-
// 检测是否有语法错误
|
|
155
|
-
// ejsLint = require('ejs-lint')
|
|
156
|
-
// if ejsLint.lint
|
|
157
|
-
// error_obj = ejsLint.lint(str, {})
|
|
158
|
-
// else
|
|
159
|
-
// error_obj = ejsLint(str, {})
|
|
160
|
-
// if error_obj
|
|
161
|
-
// console.error "===/api/workflow/export:"
|
|
162
|
-
// console.error error_obj
|
|
163
|
-
template = ejs.compile(str);
|
|
164
|
-
lang = 'en';
|
|
165
|
-
if (userSession.locale === 'zh-cn') {
|
|
166
|
-
lang = 'zh-CN';
|
|
167
|
-
}
|
|
168
|
-
utcOffset = timezoneoffset / -60;
|
|
169
|
-
formatDate = function (date, formater) {
|
|
170
|
-
return moment(date).utcOffset(utcOffset).format(formater);
|
|
171
|
-
};
|
|
172
|
-
ret = template({
|
|
173
|
-
lang: lang,
|
|
174
|
-
formatDate: formatDate,
|
|
175
|
-
form_name: form_name,
|
|
176
|
-
fields: fields,
|
|
177
|
-
table_fields: table_fields,
|
|
178
|
-
ins_to_xls: ins_to_xls
|
|
179
|
-
});
|
|
180
|
-
fileName = "SteedOSWorkflow_" + moment().format('YYYYMMDDHHmm') + ".xls";
|
|
181
|
-
res.setHeader("Content-type", "application/octet-stream");
|
|
182
|
-
res.setHeader("Content-Disposition", "attachment;filename=" + encodeURI(fileName));
|
|
183
|
-
return res.end(ret);
|
|
184
|
-
} catch (e) {
|
|
185
|
-
console.error(e);
|
|
186
|
-
res.status(200).send({
|
|
187
|
-
errors: [{ errorMessage: e.message }]
|
|
188
|
-
});
|
|
40
|
+
const query = req.query;
|
|
41
|
+
const space_id = query.space_id;
|
|
42
|
+
const flow_id = query.flow_id;
|
|
43
|
+
const type = parseInt(query.type);
|
|
44
|
+
const timezoneoffset = parseInt(query.timezoneoffset);
|
|
45
|
+
|
|
46
|
+
const flowsCollection = await getCollection('flows');
|
|
47
|
+
const formsCollection = await getCollection('forms');
|
|
48
|
+
const spacesCollection = await getCollection('spaces');
|
|
49
|
+
const instancesCollection = await getCollection('instances');
|
|
50
|
+
|
|
51
|
+
const flow = await flowsCollection.findOne(
|
|
52
|
+
{ _id: flow_id },
|
|
53
|
+
{ projection: { form: 1 } }
|
|
54
|
+
);
|
|
55
|
+
const form = await formsCollection.findOne(
|
|
56
|
+
{ _id: flow.form },
|
|
57
|
+
{ projection: { name: 1, 'current.fields': 1 } }
|
|
58
|
+
);
|
|
59
|
+
const form_name = form.name;
|
|
60
|
+
const fields = form.current.fields;
|
|
61
|
+
const table_fields = [];
|
|
62
|
+
_.each(fields, function (field) {
|
|
63
|
+
if (field.type === "table") {
|
|
64
|
+
table_fields.push(field);
|
|
189
65
|
}
|
|
190
|
-
})
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
let ins_to_xls = [];
|
|
69
|
+
const now = new Date();
|
|
70
|
+
const selector = {
|
|
71
|
+
space: space_id,
|
|
72
|
+
flow: flow_id,
|
|
73
|
+
state: { $in: ["pending", "completed"] }
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const uid = userId;
|
|
77
|
+
const space = await spacesCollection.findOne({ _id: space_id });
|
|
78
|
+
if (!space) {
|
|
79
|
+
selector.state = "none";
|
|
80
|
+
}
|
|
81
|
+
if (space && !space.admins.includes(uid)) {
|
|
82
|
+
const flow_ids = await WorkflowManager.getMyAdminOrMonitorFlows(space_id, uid);
|
|
83
|
+
if (!flow_ids.includes(flow_id)) {
|
|
84
|
+
selector.$or = [
|
|
85
|
+
{ submitter: uid },
|
|
86
|
+
{ applicant: uid },
|
|
87
|
+
{ inbox_users: uid },
|
|
88
|
+
{ outbox_users: uid }
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 0-本月
|
|
94
|
+
if (type === 0) {
|
|
95
|
+
const start_date = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
96
|
+
selector.submit_date = { $gte: start_date };
|
|
97
|
+
// 1-上月
|
|
98
|
+
} else if (type === 1) {
|
|
99
|
+
const last_month_date = new Date(new Date(now.getFullYear(), now.getMonth(), 1) - 1000 * 60 * 60 * 24);
|
|
100
|
+
const start_date = new Date(last_month_date.getFullYear(), last_month_date.getMonth(), 1);
|
|
101
|
+
const end_date = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
102
|
+
selector.submit_date = { $gte: start_date, $lte: end_date };
|
|
103
|
+
// 2-整个年度
|
|
104
|
+
} else if (type === 2) {
|
|
105
|
+
const start_date = new Date(now.getFullYear(), 0, 1);
|
|
106
|
+
selector.submit_date = { $gte: start_date };
|
|
107
|
+
}
|
|
108
|
+
// 3-所有: 不加 submit_date 过滤
|
|
109
|
+
|
|
110
|
+
ins_to_xls = await instancesCollection.find(selector).sort({ submit_date: 1 }).toArray();
|
|
111
|
+
|
|
112
|
+
const ejs = require('ejs');
|
|
113
|
+
const str = fs.readFileSync(path.resolve(__dirname, '../server/ejs/export_instances.ejs'), 'utf8');
|
|
114
|
+
const template = ejs.compile(str);
|
|
115
|
+
let lang = 'en';
|
|
116
|
+
if (userSession.locale === 'zh-cn') {
|
|
117
|
+
lang = 'zh-CN';
|
|
118
|
+
}
|
|
119
|
+
const utcOffset = timezoneoffset / -60;
|
|
120
|
+
const formatDate = function (date, formater) {
|
|
121
|
+
return moment(date).utcOffset(utcOffset).format(formater);
|
|
122
|
+
};
|
|
123
|
+
const ret = template({
|
|
124
|
+
lang: lang,
|
|
125
|
+
formatDate: formatDate,
|
|
126
|
+
form_name: form_name,
|
|
127
|
+
fields: fields,
|
|
128
|
+
table_fields: table_fields,
|
|
129
|
+
ins_to_xls: ins_to_xls
|
|
130
|
+
});
|
|
131
|
+
const fileName = "SteedOSWorkflow_" + moment().format('YYYYMMDDHHmm') + ".xls";
|
|
132
|
+
res.setHeader("Content-type", "application/octet-stream");
|
|
133
|
+
res.setHeader("Content-Disposition", "attachment;filename=" + encodeURI(fileName));
|
|
134
|
+
return res.end(ret);
|
|
191
135
|
} catch (e) {
|
|
136
|
+
console.error(e);
|
|
192
137
|
res.status(200).send({
|
|
193
138
|
errors: [{ errorMessage: e.message }]
|
|
194
139
|
});
|
|
@@ -155,7 +155,18 @@ router.post("/api/workflow/v2/instance/forward", requireAuthentication, async fu
|
|
|
155
155
|
const old_values = ins.values;
|
|
156
156
|
let new_values = {};
|
|
157
157
|
const form = await UUFlowManager.getForm(flow.form, { fields: { historys: 0 } });
|
|
158
|
-
const
|
|
158
|
+
const rawFields = form.current.fields || [];
|
|
159
|
+
|
|
160
|
+
// Normalize fields: ensure every field (and sub-fields) has `code` (fallback to `name`)
|
|
161
|
+
const normalizeFields = (list) => (list || []).map(f => {
|
|
162
|
+
const normalized = f.code ? f : { ...f, code: f.name };
|
|
163
|
+
if (['table', 'section'].includes(normalized.type) && _.isArray(normalized.fields)) {
|
|
164
|
+
normalized.fields = normalizeFields(normalized.fields);
|
|
165
|
+
}
|
|
166
|
+
return normalized;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const fields = normalizeFields(rawFields);
|
|
159
170
|
|
|
160
171
|
const formCollection = await getCollection('forms')
|
|
161
172
|
|
|
@@ -176,6 +187,9 @@ router.post("/api/workflow/v2/instance/forward", requireAuthentication, async fu
|
|
|
176
187
|
}
|
|
177
188
|
}
|
|
178
189
|
|
|
190
|
+
// Normalize old_fields using the same helper
|
|
191
|
+
old_fields = normalizeFields(old_fields);
|
|
192
|
+
|
|
179
193
|
// Process fields
|
|
180
194
|
for (const field of fields) {
|
|
181
195
|
const exists_field = _.find(old_fields, f => f.type == field.type && f.code == field.code);
|
|
@@ -20,8 +20,8 @@ router.get('/api/workflow/v2/instance/:instanceId/permission', requireAuthentica
|
|
|
20
20
|
}
|
|
21
21
|
const user = await db.users.findOne({_id: current_user_id});
|
|
22
22
|
const instance = await db.instances.findOne({_id: instanceId})
|
|
23
|
-
console.log(`user`, user)
|
|
24
|
-
console.log(`instance`, instance)
|
|
23
|
+
// console.log(`user`, user)
|
|
24
|
+
// console.log(`instance`, instance)
|
|
25
25
|
const permission = await WorkflowManager.hasInstancePermissions(user, instance)
|
|
26
26
|
res.status(200).send({
|
|
27
27
|
data: {
|
|
@@ -147,10 +147,16 @@ const getCategoriesMonitor = async (userSession, req, currentUrl) => {
|
|
|
147
147
|
// Only the string 'false' will disable the filtering
|
|
148
148
|
const enableCategoryFilter = process.env.STEEDOS_WORKFLOW_ENABLE_CATEGORY_FILTER !== 'false';
|
|
149
149
|
|
|
150
|
+
// When set to 'false', skip monitor children (category/flow) computation, only show root node
|
|
151
|
+
// Defaults to the value of STEEDOS_WORKFLOW_ENABLE_CATEGORY_FILTER
|
|
152
|
+
const enableMonitorCategoryFilter = process.env.STEEDOS_WORKFLOW_ENABLE_MONITOR_CATEGORY_FILTER
|
|
153
|
+
? process.env.STEEDOS_WORKFLOW_ENABLE_MONITOR_CATEGORY_FILTER !== 'false'
|
|
154
|
+
: enableCategoryFilter;
|
|
155
|
+
|
|
150
156
|
try {
|
|
151
157
|
let flows = [];
|
|
152
158
|
|
|
153
|
-
if (!
|
|
159
|
+
if (!enableMonitorCategoryFilter) {
|
|
154
160
|
// When category filtering is disabled, return empty array (no sub-navigation)
|
|
155
161
|
// Only box-level items will be shown
|
|
156
162
|
// Still need to check permissions for hasFlowsPer (used to control monitor box visibility)
|
|
@@ -70,7 +70,7 @@ const getNextSteps = async (flow, flowVersionId, instance, currentStep, judge, a
|
|
|
70
70
|
}
|
|
71
71
|
switch (currentStep.step_type) {
|
|
72
72
|
case 'condition': //条件
|
|
73
|
-
const stepIds = await UUFlowManager.getNextSteps(instance, flow, currentStep, judge, autoFormDoc);
|
|
73
|
+
const stepIds = await UUFlowManager.getNextSteps(instance, flow, currentStep, judge, autoFormDoc, true);
|
|
74
74
|
nextSteps = getStepsById(flow, flowVersionId, stepIds);
|
|
75
75
|
if (!nextSteps.length)
|
|
76
76
|
throw new Error('未能根据条件找到下一步');
|
|
@@ -156,7 +156,7 @@ module.exports = {
|
|
|
156
156
|
break;
|
|
157
157
|
case 'pending':
|
|
158
158
|
filter.push(['state', '=', 'pending']);
|
|
159
|
-
filter.push([
|
|
159
|
+
filter.push(['submitterOrApplicant', '=', userId]);
|
|
160
160
|
break;
|
|
161
161
|
case 'completed':
|
|
162
162
|
filter.push(['submitter', '=', userId]);
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Business hours calculation utilities.
|
|
4
|
+
* Ported from steedos-platform packages/core/src/holidays/business_hours.ts
|
|
5
|
+
*/
|
|
6
|
+
const moment = require('moment');
|
|
7
|
+
|
|
8
|
+
// BusinessHoursCheckedType enum values
|
|
9
|
+
const BusinessHoursCheckedType = {
|
|
10
|
+
onAm: 1, // 上午班
|
|
11
|
+
onPm: 2, // 下午班
|
|
12
|
+
offDay: 0, // 非工作日,即节假日、周未等
|
|
13
|
+
offAm: -1, // 工作日,下班时间,非午休,早上0点到上午上班时间
|
|
14
|
+
offPm: -2, // 工作日,下班时间,非午休,下午下班时间到第二天早上0点
|
|
15
|
+
offLunch: -3 // 工作日,午休时间
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 把09:33这种字符串解析出来对应的时间值
|
|
20
|
+
* @param {string} str
|
|
21
|
+
* @param {number} digitsForHours
|
|
22
|
+
* @returns {object} { hours, minutes, valueToHours, valueToMinutes }
|
|
23
|
+
*/
|
|
24
|
+
function getStringTimeValue(str, digitsForHours = 2) {
|
|
25
|
+
str = str.trim();
|
|
26
|
+
if (!/^\d{1,2}:\d{1,2}$/.test(str)) {
|
|
27
|
+
throw new Error("getStringTimeValue:Time format error, please enter HH:MM this format of 24 hours time character.");
|
|
28
|
+
}
|
|
29
|
+
const splits = str.split(":");
|
|
30
|
+
const h = parseInt(splits[0]);
|
|
31
|
+
const m = parseInt(splits[1]);
|
|
32
|
+
if (h > 24 || m > 60) {
|
|
33
|
+
throw new Error("getStringTimeValue:Time format error, please enter HH:MM this format of 24 hours time character.");
|
|
34
|
+
} else if (h === 24 && m > 0) {
|
|
35
|
+
throw new Error("getStringTimeValue:Time format error, please enter HH:MM this format of 24 hours time character.");
|
|
36
|
+
}
|
|
37
|
+
const valueToMinutes = h * 60 + m;
|
|
38
|
+
const valueToHours = Number((valueToMinutes / 60).toFixed(digitsForHours));
|
|
39
|
+
return { hours: h, minutes: m, valueToHours, valueToMinutes };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 计算09:00-18:00这种开始时间结束时间代表的每天工作时间长度
|
|
44
|
+
*/
|
|
45
|
+
function computeBusinessHoursPerDay(start, end, lunch_start, lunch_end, digitsForHours = 2) {
|
|
46
|
+
const startValue = getStringTimeValue(start, digitsForHours);
|
|
47
|
+
const endValue = getStringTimeValue(end, digitsForHours);
|
|
48
|
+
const lunchStartValue = getStringTimeValue(lunch_start, digitsForHours);
|
|
49
|
+
const lunchEndValue = getStringTimeValue(lunch_end, digitsForHours);
|
|
50
|
+
if (startValue && endValue && lunchStartValue && lunchEndValue) {
|
|
51
|
+
let computedMinutes = endValue.valueToMinutes - startValue.valueToMinutes;
|
|
52
|
+
const computedAmMinutes = lunchStartValue.valueToMinutes - startValue.valueToMinutes;
|
|
53
|
+
const computedPmMinutes = endValue.valueToMinutes - lunchEndValue.valueToMinutes;
|
|
54
|
+
const computedLunchMinutes = lunchEndValue.valueToMinutes - lunchStartValue.valueToMinutes;
|
|
55
|
+
computedMinutes = computedMinutes - computedLunchMinutes;
|
|
56
|
+
if (computedMinutes <= 0 || computedLunchMinutes <= 0) {
|
|
57
|
+
throw new Error("computeBusinessHoursPerDay:The end or lunch_end time value must be later than the start or lunch_start time.");
|
|
58
|
+
} else if (lunchStartValue.valueToMinutes <= startValue.valueToMinutes || lunchEndValue.valueToMinutes >= endValue.valueToMinutes) {
|
|
59
|
+
throw new Error("computeBusinessHoursPerDay:The lunch time must between the working time.");
|
|
60
|
+
} else {
|
|
61
|
+
const computedHours = Number((computedMinutes / 60).toFixed(digitsForHours));
|
|
62
|
+
const computedAmHours = Number((computedAmMinutes / 60).toFixed(digitsForHours));
|
|
63
|
+
const computedPmHours = Number((computedPmMinutes / 60).toFixed(digitsForHours));
|
|
64
|
+
const computedLunchHours = Number((computedLunchMinutes / 60).toFixed(digitsForHours));
|
|
65
|
+
return {
|
|
66
|
+
computedHours, computedMinutes,
|
|
67
|
+
computedAmHours, computedAmMinutes,
|
|
68
|
+
computedPmHours, computedPmMinutes,
|
|
69
|
+
computedLunchHours, computedLunchMinutes,
|
|
70
|
+
startValue, endValue, lunchStartValue, lunchEndValue
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
throw new Error("computeBusinessHoursPerDay:start or end is not valid.");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 根据businessHours取其对应的每天工作时间长度,并把值缓存到其computedPerDay属性中
|
|
80
|
+
*/
|
|
81
|
+
function getBusinessHoursPerDay(businessHours, digitsForHours = 2) {
|
|
82
|
+
if (businessHours.computedPerDay) {
|
|
83
|
+
return businessHours.computedPerDay;
|
|
84
|
+
}
|
|
85
|
+
const computedPerDay = computeBusinessHoursPerDay(
|
|
86
|
+
businessHours.start, businessHours.end,
|
|
87
|
+
businessHours.lunch_start, businessHours.lunch_end,
|
|
88
|
+
digitsForHours
|
|
89
|
+
);
|
|
90
|
+
businessHours.computedPerDay = computedPerDay;
|
|
91
|
+
return computedPerDay;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 计算某个日期是不是工作日
|
|
96
|
+
*/
|
|
97
|
+
function computeIsBusinessDay(date, holidays, workingDays) {
|
|
98
|
+
const value = moment.utc(date);
|
|
99
|
+
value.hours(0);
|
|
100
|
+
value.minutes(0);
|
|
101
|
+
value.seconds(0);
|
|
102
|
+
value.milliseconds(0);
|
|
103
|
+
const holiday = holidays.find((item) => {
|
|
104
|
+
return item.date && item.date.getTime() === value.toDate().getTime();
|
|
105
|
+
});
|
|
106
|
+
if (holiday) {
|
|
107
|
+
switch (holiday.type) {
|
|
108
|
+
case "adjusted_working_day":
|
|
109
|
+
return true;
|
|
110
|
+
case "adjusted_holiday":
|
|
111
|
+
return false;
|
|
112
|
+
case "public":
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
return workingDays.indexOf(value.day().toString()) > -1;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 计算某个时间点是不是工作时间(只判断时间点,不判断日期)
|
|
122
|
+
*/
|
|
123
|
+
function computeIsBusinessHours(date, businessHours, digitsForHours = 2) {
|
|
124
|
+
const utcOffset = businessHours.utc_offset;
|
|
125
|
+
const businessHoursPerDay = getBusinessHoursPerDay(businessHours, digitsForHours);
|
|
126
|
+
const startMoment = moment.utc(date);
|
|
127
|
+
startMoment.hours(businessHoursPerDay.startValue.hours - utcOffset);
|
|
128
|
+
startMoment.minutes(businessHoursPerDay.startValue.minutes);
|
|
129
|
+
const endMoment = moment.utc(date);
|
|
130
|
+
endMoment.hours(businessHoursPerDay.endValue.hours - utcOffset);
|
|
131
|
+
endMoment.minutes(businessHoursPerDay.endValue.minutes);
|
|
132
|
+
const lunchStartMoment = moment.utc(date);
|
|
133
|
+
lunchStartMoment.hours(businessHoursPerDay.lunchStartValue.hours - utcOffset);
|
|
134
|
+
lunchStartMoment.minutes(businessHoursPerDay.lunchStartValue.minutes);
|
|
135
|
+
const lunchEndMoment = moment.utc(date);
|
|
136
|
+
lunchEndMoment.hours(businessHoursPerDay.lunchEndValue.hours - utcOffset);
|
|
137
|
+
lunchEndMoment.minutes(businessHoursPerDay.lunchEndValue.minutes);
|
|
138
|
+
const startTimeValue = startMoment.toDate().getTime();
|
|
139
|
+
const endTimeValue = endMoment.toDate().getTime();
|
|
140
|
+
const lunchStartTimeValue = lunchStartMoment.toDate().getTime();
|
|
141
|
+
const lunchEndTimeValue = lunchEndMoment.toDate().getTime();
|
|
142
|
+
const dateTimeValue = date.getTime();
|
|
143
|
+
if (dateTimeValue <= endTimeValue && dateTimeValue >= startTimeValue) {
|
|
144
|
+
if (dateTimeValue <= lunchStartTimeValue) {
|
|
145
|
+
return BusinessHoursCheckedType.onAm;
|
|
146
|
+
} else if (dateTimeValue >= lunchEndTimeValue) {
|
|
147
|
+
return BusinessHoursCheckedType.onPm;
|
|
148
|
+
} else {
|
|
149
|
+
return BusinessHoursCheckedType.offLunch;
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
if (dateTimeValue > endTimeValue) {
|
|
153
|
+
return BusinessHoursCheckedType.offPm;
|
|
154
|
+
} else {
|
|
155
|
+
return BusinessHoursCheckedType.offAm;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 计算某个时间点是不是工作时间,包括节假日及非工作时间都要排除
|
|
162
|
+
*/
|
|
163
|
+
function computeIsBusinessDate(date, holidays, businessHours, digitsForHours = 2) {
|
|
164
|
+
if (computeIsBusinessDay(date, holidays, businessHours.working_days)) {
|
|
165
|
+
return computeIsBusinessHours(date, businessHours, digitsForHours);
|
|
166
|
+
} else {
|
|
167
|
+
return BusinessHoursCheckedType.offDay;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 根据来源时间所在日期计算下一个工作日的开始时间
|
|
173
|
+
*/
|
|
174
|
+
function computeNextBusinessDate(source, holidays, businessHours, digitsForHours = 2) {
|
|
175
|
+
const utcOffset = businessHours.utc_offset;
|
|
176
|
+
const businessHoursPerDay = getBusinessHoursPerDay(businessHours, digitsForHours);
|
|
177
|
+
const workingDays = businessHours.working_days;
|
|
178
|
+
const sourceMoment = moment.utc(source);
|
|
179
|
+
sourceMoment.hours(businessHoursPerDay.startValue.hours - utcOffset);
|
|
180
|
+
sourceMoment.minutes(businessHoursPerDay.startValue.minutes);
|
|
181
|
+
let startMoment = null;
|
|
182
|
+
const maxCount = 365;
|
|
183
|
+
for (let i = 0; i < maxCount; i++) {
|
|
184
|
+
sourceMoment.add(1, 'd');
|
|
185
|
+
if (computeIsBusinessDay(sourceMoment.toDate(), holidays, workingDays)) {
|
|
186
|
+
startMoment = sourceMoment;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (startMoment) {
|
|
191
|
+
const start = startMoment.toDate();
|
|
192
|
+
startMoment.hours(businessHoursPerDay.endValue.hours - utcOffset);
|
|
193
|
+
startMoment.minutes(businessHoursPerDay.endValue.minutes);
|
|
194
|
+
const end = startMoment.toDate();
|
|
195
|
+
return { start, end };
|
|
196
|
+
} else {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
BusinessHoursCheckedType,
|
|
203
|
+
getStringTimeValue,
|
|
204
|
+
computeBusinessHoursPerDay,
|
|
205
|
+
getBusinessHoursPerDay,
|
|
206
|
+
computeIsBusinessDay,
|
|
207
|
+
computeIsBusinessHours,
|
|
208
|
+
computeIsBusinessDate,
|
|
209
|
+
computeNextBusinessDate
|
|
210
|
+
};
|