@steedos-labs/plugin-workflow 3.0.84 → 3.0.86
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-CUTUedfe.js +1006 -0
- package/designer/dist/index.html +1 -1
- package/main/default/client/flow2_render.client.js +1 -1
- package/main/default/manager/instance_number_rules.js +246 -2
- package/main/default/manager/uuflow_manager.js +85 -10
- package/main/default/objects/instances/listviews/monitor.listview.yml +1 -1
- package/main/default/pages/flow_selector.page.amis.json +2 -2
- package/main/default/routes/api_auto_number.router.js +11 -178
- package/main/default/routes/api_workflow_chart.router.js +26 -11
- package/main/default/routes/api_workflow_export.router.js +2 -1
- package/main/default/routes/api_workflow_instance_forward.router.js +13 -1
- package/main/default/routes/buildFormDesignSystemPrompt.js +4 -3
- package/main/default/server/ejs/export_instances.ejs +12 -4
- package/main/default/test/test_getApproveValues.js +221 -0
- package/package.json +3 -2
- package/public/amis-renderer/amis-renderer.css +1 -1
- package/public/amis-renderer/amis-renderer.js +1 -1
- package/designer/dist/assets/index-DpFVuLlI.js +0 -1004
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
const express = require("express");
|
|
2
2
|
const router = express.Router();
|
|
3
|
-
const
|
|
3
|
+
const autoNumberManager = require("../manager/instance_number_rules");
|
|
4
|
+
const { instanceNumberBuilder } = autoNumberManager;
|
|
4
5
|
const objectql = require('@steedos/objectql');
|
|
5
6
|
const _ = require('lodash');
|
|
6
7
|
const UUFlowManager = require('../manager/uuflow_manager');
|
|
7
8
|
const { requireAuthentication } = require("@steedos/auth");
|
|
8
|
-
const { getCollection } = require('../utils/collection');
|
|
9
9
|
|
|
10
10
|
// 获取用户当前的 approve
|
|
11
11
|
const getUserApprove = ({ instance, userId }) => {
|
|
@@ -39,192 +39,25 @@ const getUserApprove = ({ instance, userId }) => {
|
|
|
39
39
|
return currentApprove;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
// ===== 新设计器 autoNumber 字段:基于字段配置直接生成编号 =====
|
|
43
|
-
|
|
44
|
-
// 序号补零
|
|
45
|
-
const padNumber = (num, length) => {
|
|
46
|
-
if (length <= 0) return String(num);
|
|
47
|
-
const str = String(num);
|
|
48
|
-
return str.length >= length ? str : '0'.repeat(length - str.length) + str;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// 根据日期格式生成日期片段
|
|
52
|
-
const formatDatePart = (dateFormat, yyyy, mm, dd) => {
|
|
53
|
-
const mmStr = mm < 10 ? '0' + mm : String(mm);
|
|
54
|
-
const ddStr = dd < 10 ? '0' + dd : String(dd);
|
|
55
|
-
switch (dateFormat) {
|
|
56
|
-
case 'YYYY': return String(yyyy);
|
|
57
|
-
case 'YYYYMM': return String(yyyy) + mmStr;
|
|
58
|
-
case 'YYYYMMDD': return String(yyyy) + mmStr + ddStr;
|
|
59
|
-
default: return ''; // 'none' 或 undefined
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
// 根据重置周期判断是否需要重置序号
|
|
64
|
-
const shouldResetNumber = (resetCycle, storedRule, yyyy, mm, dd) => {
|
|
65
|
-
switch (resetCycle) {
|
|
66
|
-
case 'yearly': return yyyy !== storedRule.year;
|
|
67
|
-
case 'monthly': return yyyy !== storedRule.year || mm !== storedRule.month;
|
|
68
|
-
case 'daily': return yyyy !== storedRule.year || mm !== storedRule.month || dd !== storedRule.day;
|
|
69
|
-
default: return false; // 'none' - 永不重置
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
|
|
73
42
|
/**
|
|
74
43
|
* 基于字段配置生成自动编号(新设计器 autoNumber 类型字段)
|
|
75
44
|
* 同样使用 instance_number_rules 集合做序号持久化,如果尚未创建规则记录则自动创建
|
|
76
45
|
*/
|
|
77
46
|
const generateAutoNumberFromFieldConfig = async (spaceId, fieldConfig) => {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const ruleName = fieldConfig.auto_number_name;
|
|
81
|
-
const prefix = fieldConfig.autoNumberPrefix || '';
|
|
82
|
-
const suffix = fieldConfig.autoNumberSuffix || '';
|
|
83
|
-
const paddingLen = fieldConfig.autoNumberPadding != null ? fieldConfig.autoNumberPadding : 4;
|
|
84
|
-
const dateFormat = fieldConfig.autoNumberDateFormat || 'YYYYMM';
|
|
85
|
-
const resetCycle = fieldConfig.autoNumberResetCycle || 'monthly';
|
|
86
|
-
|
|
87
|
-
const now = new Date();
|
|
88
|
-
const yyyy = now.getFullYear();
|
|
89
|
-
const mm = now.getMonth() + 1;
|
|
90
|
-
const dd = now.getDate();
|
|
91
|
-
|
|
92
|
-
let numberRules = await db.findOne({ space: spaceId, name: ruleName });
|
|
93
|
-
|
|
94
|
-
if (!numberRules) {
|
|
95
|
-
// 首次使用该字段,自动创建规则记录
|
|
96
|
-
await db.insertOne({
|
|
97
|
-
space: spaceId,
|
|
98
|
-
name: ruleName,
|
|
99
|
-
year: yyyy,
|
|
100
|
-
month: mm,
|
|
101
|
-
day: dd,
|
|
102
|
-
first_number: 1,
|
|
103
|
-
number: 0,
|
|
104
|
-
rules: '' // 新类型字段不使用 rules 模板
|
|
105
|
-
});
|
|
106
|
-
numberRules = await db.findOne({ space: spaceId, name: ruleName });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
let nextNumber = (numberRules.number || 0) + 1;
|
|
110
|
-
|
|
111
|
-
if (shouldResetNumber(resetCycle, numberRules, yyyy, mm, dd)) {
|
|
112
|
-
nextNumber = numberRules.first_number || 1;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const paddedNumber = padNumber(nextNumber, paddingLen);
|
|
116
|
-
const datePart = formatDatePart(dateFormat, yyyy, mm, dd);
|
|
117
|
-
const autoNumber = prefix + datePart + paddedNumber + suffix;
|
|
118
|
-
|
|
119
|
-
await db.updateOne(
|
|
120
|
-
{ _id: numberRules._id },
|
|
121
|
-
{ $set: { year: yyyy, month: mm, day: dd, number: nextNumber } }
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
return autoNumber;
|
|
47
|
+
return autoNumberManager.generateAutoNumberFromFieldConfig(spaceId, fieldConfig);
|
|
125
48
|
};
|
|
126
49
|
|
|
127
50
|
// 获取可编辑的自动编号字段
|
|
128
|
-
const getEditableAutoNumberFields = async (step, formId, formVersion) => {
|
|
51
|
+
const getEditableAutoNumberFields = async (step, formId, formVersion, spaceId) => {
|
|
129
52
|
const permissions = step.permissions || {};
|
|
130
53
|
const form = await UUFlowManager.getForm(formId);
|
|
131
54
|
const formV = await UUFlowManager.getFormVersion(form, formVersion);
|
|
132
|
-
|
|
133
|
-
const autoNumberFields = [];
|
|
134
|
-
const pushedFieldKeys = new Set();
|
|
135
|
-
|
|
136
|
-
const getFieldKey = (field) => field?.code || field?.name;
|
|
137
|
-
const isAutoNumberTypeField = (field) => {
|
|
138
|
-
const fieldType = (field?.type || '').toString().toLowerCase();
|
|
139
|
-
return fieldType === 'autonumber' || field?.type === 'autoNumber';
|
|
140
|
-
};
|
|
141
|
-
const canGenerateAutoNumber = (field) => {
|
|
142
|
-
const fieldKey = getFieldKey(field);
|
|
143
|
-
if (!fieldKey) return false;
|
|
144
|
-
const permission = permissions[fieldKey];
|
|
145
|
-
if (permission === 'editable') return true;
|
|
146
|
-
return false;
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
// 检查字段是否是自动编号字段(通过 formula 判断)
|
|
150
|
-
const isAutoNumberField = (field) => {
|
|
151
|
-
if (!field) return false;
|
|
152
|
-
const hasLegacyAutoNumberFormula = field.default_value && typeof field.default_value === 'string' && field.default_value.trim().startsWith('auto_number(');
|
|
153
|
-
return hasLegacyAutoNumberFormula || isAutoNumberTypeField(field);
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
// 从 formula 中提取自动编号规则名称
|
|
157
|
-
const extractAutoNumberName = (formula) => {
|
|
158
|
-
// 匹配 auto_number("xxx") 或 auto_number('xxx') 或 auto_number(xxx)
|
|
159
|
-
const match = formula.match(/auto_number\s*\(\s*['"]?([^'"()]+?)['"]?\s*\)/);
|
|
160
|
-
return match ? match[1].trim() : null;
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const getAutoNumberRuleName = (field) => {
|
|
164
|
-
// 1) 兼容旧字段公式:default_value = auto_number(...)
|
|
165
|
-
if (field?.default_value && typeof field.default_value === 'string') {
|
|
166
|
-
const byFormula = extractAutoNumberName(field.default_value);
|
|
167
|
-
if (byFormula) return byFormula;
|
|
168
|
-
}
|
|
169
55
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
field?.autoNumberRule ||
|
|
176
|
-
field?.auto_number_rule_name ||
|
|
177
|
-
field?.autoNumberRuleName;
|
|
178
|
-
if (fromProps && typeof fromProps === 'string') {
|
|
179
|
-
return fromProps.trim();
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// 3) 对于新设计器 autoNumber 字段,默认回退到字段标识(code/name)
|
|
183
|
-
return getFieldKey(field);
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const tryPushAutoNumberField = (field) => {
|
|
187
|
-
if (!isAutoNumberField(field) || !canGenerateAutoNumber(field)) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const fieldKey = getFieldKey(field);
|
|
191
|
-
const autoNumberName = getAutoNumberRuleName(field);
|
|
192
|
-
if (!fieldKey || !autoNumberName || pushedFieldKeys.has(fieldKey)) {
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
pushedFieldKeys.add(fieldKey);
|
|
196
|
-
|
|
197
|
-
const fieldInfo = {
|
|
198
|
-
code: fieldKey,
|
|
199
|
-
auto_number_name: autoNumberName,
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// 新设计器 autoNumber 字段,携带配置属性用于直接生成编号
|
|
203
|
-
if (isAutoNumberTypeField(field)) {
|
|
204
|
-
fieldInfo.isNewType = true;
|
|
205
|
-
fieldInfo.autoNumberPrefix = field.autoNumberPrefix;
|
|
206
|
-
fieldInfo.autoNumberSuffix = field.autoNumberSuffix;
|
|
207
|
-
fieldInfo.autoNumberPadding = field.autoNumberPadding;
|
|
208
|
-
fieldInfo.autoNumberDateFormat = field.autoNumberDateFormat;
|
|
209
|
-
fieldInfo.autoNumberResetCycle = field.autoNumberResetCycle;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
autoNumberFields.push(fieldInfo);
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
for (const field of formV.fields || []) {
|
|
216
|
-
if (field.type === "section") {
|
|
217
|
-
// 处理 section 中的字段
|
|
218
|
-
for (const sectionField of field.fields || []) {
|
|
219
|
-
tryPushAutoNumberField(sectionField);
|
|
220
|
-
}
|
|
221
|
-
} else {
|
|
222
|
-
// 处理普通字段
|
|
223
|
-
tryPushAutoNumberField(field);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return autoNumberFields;
|
|
56
|
+
return autoNumberManager.collectAutoNumberFields(formV.fields || [], permissions, {
|
|
57
|
+
spaceId,
|
|
58
|
+
formId,
|
|
59
|
+
formName: form.name,
|
|
60
|
+
});
|
|
228
61
|
};
|
|
229
62
|
|
|
230
63
|
router.post('/api/workflow/v2/auto_number', requireAuthentication, async function (req, res) {
|
|
@@ -319,7 +152,7 @@ router.post('/api/workflow/v2/auto_number', requireAuthentication, async functio
|
|
|
319
152
|
}
|
|
320
153
|
|
|
321
154
|
// 获取可编辑的自动编号字段
|
|
322
|
-
const autoNumberFields = await getEditableAutoNumberFields(step, instance.form, instance.form_version);
|
|
155
|
+
const autoNumberFields = await getEditableAutoNumberFields(step, instance.form, instance.form_version, instance.space);
|
|
323
156
|
if (autoNumberFields.length === 0) {
|
|
324
157
|
return res.status(200).send({
|
|
325
158
|
success: true,
|
|
@@ -379,4 +212,4 @@ router.post('/api/workflow/v2/auto_number', requireAuthentication, async functio
|
|
|
379
212
|
});
|
|
380
213
|
|
|
381
214
|
|
|
382
|
-
exports.default = router;
|
|
215
|
+
exports.default = router;
|
|
@@ -40,6 +40,15 @@ const FlowversionAPI = {
|
|
|
40
40
|
return str.replace(/\"/g, """).replace(/\n/g, "<br/>");
|
|
41
41
|
},
|
|
42
42
|
|
|
43
|
+
getSafeEdgeLabel: function (str) {
|
|
44
|
+
// Mermaid edge label (A-->|text|B) 对部分符号(如括号/竖线)解析不稳定,先做保守清洗。
|
|
45
|
+
return FlowversionAPI.replaceErrorSymbol(String(str || ''))
|
|
46
|
+
.replace(/[|]/g, '|')
|
|
47
|
+
.replace(/[()()\[\]{}<>]/g, ' ')
|
|
48
|
+
.replace(/\s+/g, ' ')
|
|
49
|
+
.trim();
|
|
50
|
+
},
|
|
51
|
+
|
|
43
52
|
getStepHandlerName: async function (step, insId, instance) {
|
|
44
53
|
const db = {
|
|
45
54
|
users: await getCollection('users'),
|
|
@@ -113,12 +122,10 @@ const FlowversionAPI = {
|
|
|
113
122
|
|
|
114
123
|
getStepLabel: function (stepName, stepHandlerName) {
|
|
115
124
|
// 返回sstepName与stepHandlerName结合的步骤显示名称
|
|
125
|
+
// 注意:必须单行输出,避免 replaceErrorSymbol 把模板字符串的换行替换成 <br/> 撑高节点
|
|
116
126
|
let nodeStr = "";
|
|
117
127
|
if (stepName) {
|
|
118
|
-
nodeStr = `<div class='graph-node'
|
|
119
|
-
<div class='step-name'>${stepName}</div>
|
|
120
|
-
<div class='step-handler-name'>${stepHandlerName}</div>
|
|
121
|
-
</div>`;
|
|
128
|
+
nodeStr = `<div class='graph-node'><div class='step-name'>${stepName}</div><div class='step-handler-name'>${stepHandlerName}</div></div>`;
|
|
122
129
|
// 把特殊字符清空或替换,以避免mermaidAPI出现异常
|
|
123
130
|
nodeStr = FlowversionAPI.replaceErrorSymbol(nodeStr);
|
|
124
131
|
}
|
|
@@ -196,8 +203,12 @@ const FlowversionAPI = {
|
|
|
196
203
|
let toStep = steps.find(s => s._id === line.to_step);
|
|
197
204
|
let toStepName = await FlowversionAPI.getStepName(toStep, cachedStepNames, instance_id, instance);
|
|
198
205
|
if (step.step_type === "condition" && line.name) {
|
|
199
|
-
let lineName = FlowversionAPI.
|
|
200
|
-
|
|
206
|
+
let lineName = FlowversionAPI.getSafeEdgeLabel(line.name);
|
|
207
|
+
if (lineName) {
|
|
208
|
+
nodes.push(` ${step._id}("${stepName}")-->|${lineName}|${line.to_step}("${toStepName}")`);
|
|
209
|
+
} else {
|
|
210
|
+
nodes.push(` ${step._id}("${stepName}")-->${line.to_step}("${toStepName}")`);
|
|
211
|
+
}
|
|
201
212
|
} else {
|
|
202
213
|
nodes.push(` ${step._id}("${stepName}")-->${line.to_step}("${toStepName}")`);
|
|
203
214
|
}
|
|
@@ -294,12 +305,10 @@ const FlowversionAPI = {
|
|
|
294
305
|
|
|
295
306
|
getTraceName: function (traceName, approveHandlerName) {
|
|
296
307
|
// 返回trace节点名称
|
|
308
|
+
// 注意:必须单行输出,避免 replaceErrorSymbol 把模板字符串的换行替换成 <br/> 撑高节点
|
|
297
309
|
let nodeStr = "";
|
|
298
310
|
if (traceName) {
|
|
299
|
-
nodeStr = `<div class='graph-node'
|
|
300
|
-
<div class='trace-name'>${traceName}</div>
|
|
301
|
-
<div class='trace-handler-name'>${approveHandlerName}</div>
|
|
302
|
-
</div>`;
|
|
311
|
+
nodeStr = `<div class='graph-node'><div class='trace-name'>${traceName}</div><div class='trace-handler-name'>${approveHandlerName}</div></div>`;
|
|
303
312
|
nodeStr = FlowversionAPI.replaceErrorSymbol(nodeStr);
|
|
304
313
|
}
|
|
305
314
|
return nodeStr;
|
|
@@ -871,6 +880,12 @@ const FlowversionAPI = {
|
|
|
871
880
|
.graph-node {
|
|
872
881
|
padding: 4px 8px;
|
|
873
882
|
}
|
|
883
|
+
.node foreignObject > div {
|
|
884
|
+
height: 100% !important;
|
|
885
|
+
display: flex !important;
|
|
886
|
+
align-items: center;
|
|
887
|
+
justify-content: center;
|
|
888
|
+
}
|
|
874
889
|
.graph-node .step-name {
|
|
875
890
|
font-weight: 500;
|
|
876
891
|
margin-bottom: 2px;
|
|
@@ -1054,4 +1069,4 @@ router.get('/api/workflow/chart/traces', requireAuthentication, async function (
|
|
|
1054
1069
|
router.get('/api/workflow/chart/traces_expand', requireAuthentication, async function (req, res) {
|
|
1055
1070
|
await FlowversionAPI.sendHtmlResponse(req, res, "traces_expand");
|
|
1056
1071
|
});
|
|
1057
|
-
exports.default = router;
|
|
1072
|
+
exports.default = router;
|
|
@@ -113,7 +113,8 @@ router.get('/api/workflow/export/instances', requireAuthentication, async functi
|
|
|
113
113
|
const str = fs.readFileSync(path.resolve(__dirname, '../server/ejs/export_instances.ejs'), 'utf8');
|
|
114
114
|
const template = ejs.compile(str);
|
|
115
115
|
let lang = 'en';
|
|
116
|
-
|
|
116
|
+
const locale = String(userSession.locale || userSession.language || '').toLowerCase().replace('_', '-');
|
|
117
|
+
if (locale.startsWith('zh')) {
|
|
117
118
|
lang = 'zh-CN';
|
|
118
119
|
}
|
|
119
120
|
const utcOffset = timezoneoffset / -60;
|
|
@@ -192,7 +192,7 @@ router.post("/api/workflow/v2/instance/forward", requireAuthentication, async fu
|
|
|
192
192
|
|
|
193
193
|
// Process fields
|
|
194
194
|
for (const field of fields) {
|
|
195
|
-
const exists_field = _.find(old_fields, f => f.
|
|
195
|
+
const exists_field = _.find(old_fields, f => f.code == field.code);
|
|
196
196
|
if (exists_field) common_fields.push(field);
|
|
197
197
|
|
|
198
198
|
const select_input_field = _.find(old_fields, f =>
|
|
@@ -241,6 +241,10 @@ router.post("/api/workflow/v2/instance/forward", requireAuthentication, async fu
|
|
|
241
241
|
old_v = new_multiSelected.join(",");
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
if (f.type == "lookup" && _.isObject(old_v) && old_v._id) {
|
|
245
|
+
old_v = old_v._id;
|
|
246
|
+
}
|
|
247
|
+
|
|
244
248
|
new_values[key] = old_v;
|
|
245
249
|
}
|
|
246
250
|
}
|
|
@@ -281,6 +285,10 @@ router.post("/api/workflow/v2/instance/forward", requireAuthentication, async fu
|
|
|
281
285
|
old_v = new_multiSelected.join(",");
|
|
282
286
|
}
|
|
283
287
|
|
|
288
|
+
if (f.type == "lookup" && _.isObject(old_v) && old_v._id) {
|
|
289
|
+
old_v = old_v._id;
|
|
290
|
+
}
|
|
291
|
+
|
|
284
292
|
new_table_row_values[key] = old_v;
|
|
285
293
|
}
|
|
286
294
|
}
|
|
@@ -324,6 +332,10 @@ router.post("/api/workflow/v2/instance/forward", requireAuthentication, async fu
|
|
|
324
332
|
old_v = new_multiSelected.join(",");
|
|
325
333
|
}
|
|
326
334
|
|
|
335
|
+
if (field.type == "lookup" && _.isObject(old_v) && old_v._id) {
|
|
336
|
+
old_v = old_v._id;
|
|
337
|
+
}
|
|
338
|
+
|
|
327
339
|
new_values[key] = old_v;
|
|
328
340
|
}
|
|
329
341
|
}
|
|
@@ -208,13 +208,14 @@ function buildFormDesignSystemPrompt({ chineseFieldNames = false, isUpgrade = fa
|
|
|
208
208
|
|
|
209
209
|
- autoNumber 类型(自动编号):
|
|
210
210
|
- 自动编号字段始终为只读(readonly: true),运行时由系统自动生成唯一编号
|
|
211
|
+
- auto_number_name: 字符串,可选,编号规则名称;留空时系统按表单和字段自动生成
|
|
211
212
|
- autoNumberPrefix: 字符串,编号前缀,如 "SN-"、"HT-",默认 ""
|
|
212
213
|
- autoNumberSuffix: 字符串,编号后缀,默认 ""
|
|
213
214
|
- autoNumberPadding: 数字,序号位数(补零),默认 4,如 4 → 0001
|
|
214
215
|
- autoNumberDateFormat: 字符串,日期片段格式,可选值:"none"(无日期) | "YYYY"(年) | "YYYYMM"(年月,默认) | "YYYYMMDD"(年月日)
|
|
215
216
|
- autoNumberResetCycle: 字符串,序号重置周期,可选值:"none"(不重置) | "yearly"(每年) | "monthly"(每月,默认) | "daily"(每天)
|
|
216
217
|
- 生成的编号格式示例:前缀 + 日期 + 序号 + 后缀,如 "SN-202603-0001"
|
|
217
|
-
- 示例:{ "type": "autoNumber", "label": "合同编号", "name": "contract_no", "readonly": true, "autoNumberPrefix": "HT-", "autoNumberDateFormat": "YYYYMM", "autoNumberPadding": 4, "autoNumberResetCycle": "monthly" }
|
|
218
|
+
- 示例:{ "type": "autoNumber", "label": "合同编号", "name": "contract_no", "readonly": true, "auto_number_name": "合同编号", "autoNumberPrefix": "HT-", "autoNumberDateFormat": "YYYYMM", "autoNumberPadding": 4, "autoNumberResetCycle": "monthly" }
|
|
218
219
|
|
|
219
220
|
- 动态显隐条件(任何字段都可设置):${chineseFieldNames ? `
|
|
220
221
|
- visibilityExpression: 字符串,条件表达式,控制字段显示/隐藏。当表达式结果为 true 时字段可见,否则隐藏。
|
|
@@ -438,11 +439,11 @@ ${formulaRule}
|
|
|
438
439
|
- 迁移规则:
|
|
439
440
|
- 遇到字段的 default_value 包含 \`auto_number(...)\` 时,将该字段的 type 改为 "autoNumber",设置 readonly: true
|
|
440
441
|
- 删除原有的 default_value 属性
|
|
441
|
-
- 如果旧版编号规则名称中能推断出前缀、日期格式等信息,相应设置 autoNumberPrefix、autoNumberDateFormat 等属性;否则使用默认值
|
|
442
|
+
- 如果旧版编号规则名称中能推断出前缀、日期格式等信息,相应设置 auto_number_name、autoNumberPrefix、autoNumberDateFormat 等属性;否则使用默认值
|
|
442
443
|
- 默认配置:autoNumberPrefix: ""、autoNumberPadding: 4、autoNumberDateFormat: "YYYYMM"、autoNumberResetCycle: "monthly"
|
|
443
444
|
- 迁移示例:
|
|
444
445
|
旧版字段:{ "name": "合同编号", "type": "text", "default_value": "auto_number('contract_no')" }
|
|
445
|
-
新版字段:{ "name": "contract_no", "label": "合同编号", "type": "autoNumber", "readonly": true, "autoNumberPrefix": "", "autoNumberPadding": 4, "autoNumberDateFormat": "YYYYMM", "autoNumberResetCycle": "monthly" }
|
|
446
|
+
新版字段:{ "name": "contract_no", "label": "合同编号", "type": "autoNumber", "readonly": true, "auto_number_name": "contract_no", "autoNumberPrefix": "", "autoNumberPadding": 4, "autoNumberDateFormat": "YYYYMM", "autoNumberResetCycle": "monthly" }
|
|
446
447
|
- migrationLog 记录格式:"字段 <name>: default_value auto_number(...) 转换为 autoNumber 类型"
|
|
447
448
|
|
|
448
449
|
### 旧版 HTML 字段类型迁移
|
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
|
|
8
8
|
<Worksheet ss:Name="<%= form_name %>" >
|
|
9
9
|
<Table>
|
|
10
|
+
<%
|
|
11
|
+
var getFieldDisplayName = function(field) {
|
|
12
|
+
if (!field) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
return field.label || field.title || field.name || field.code || '';
|
|
16
|
+
};
|
|
17
|
+
%>
|
|
10
18
|
<!-- 编号 -->
|
|
11
19
|
<Column ss:Width="35"/>
|
|
12
20
|
<!-- 名称 -->
|
|
@@ -52,13 +60,13 @@
|
|
|
52
60
|
<Cell><Data ss:Type="String"><%= t('export.current_step_start_date',{},lang) %></Data></Cell>
|
|
53
61
|
<% _.each(fields, function(field) { %>
|
|
54
62
|
<% if (field.type != "table" && field.type != "section") { %>
|
|
55
|
-
<Cell><Data ss:Type="String"><%= field
|
|
63
|
+
<Cell><Data ss:Type="String"><%= getFieldDisplayName(field) %></Data></Cell>
|
|
56
64
|
<% } %>
|
|
57
65
|
|
|
58
66
|
<% if (field.type == "section" && field.fields) {
|
|
59
67
|
_.each(field.fields, function(sec) {
|
|
60
68
|
%>
|
|
61
|
-
<Cell><Data ss:Type="String"><%= sec
|
|
69
|
+
<Cell><Data ss:Type="String"><%= getFieldDisplayName(sec) %></Data></Cell>
|
|
62
70
|
<% }) %>
|
|
63
71
|
<% } %>
|
|
64
72
|
<% }) %>
|
|
@@ -259,7 +267,7 @@
|
|
|
259
267
|
</Worksheet>
|
|
260
268
|
|
|
261
269
|
<% _.each(table_fields, function(table_field) { %>
|
|
262
|
-
<Worksheet ss:Name="<%= table_field
|
|
270
|
+
<Worksheet ss:Name="<%= getFieldDisplayName(table_field) %>" >
|
|
263
271
|
<Table>
|
|
264
272
|
<!-- 编号 -->
|
|
265
273
|
<Column ss:Width="35"/>
|
|
@@ -296,7 +304,7 @@
|
|
|
296
304
|
<Cell><Data ss:Type="String"><%= t('export.current_step_name',{},lang) %></Data></Cell>
|
|
297
305
|
<Cell><Data ss:Type="String"><%= t('export.current_step_start_date',{},lang) %></Data></Cell>
|
|
298
306
|
<% _.each(table_field.fields, function(field) { %>
|
|
299
|
-
|
|
307
|
+
<Cell><Data ss:Type="String"><%= getFieldDisplayName(field) %></Data></Cell>
|
|
300
308
|
<% }) %>
|
|
301
309
|
</Row>
|
|
302
310
|
<% _.each(ins_to_xls, function(ins) { %>
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 回归测试:UUFlowManager.getApproveValues 对公式子字段的兜底识别。
|
|
3
|
+
*
|
|
4
|
+
* 背景:
|
|
5
|
+
* - issue: 子表保存后刷新,公式子字段计算结果丢失(线上现象)
|
|
6
|
+
* - PR #781 在过滤规则中加 `c.formula ||` 放权
|
|
7
|
+
* - 但部分历史表单设计器保存时丢失了字段的 `formula` 属性,
|
|
8
|
+
* 公式表达式被转存到 `default_value`,形如 "${...}"
|
|
9
|
+
* - 因此除了 `c.formula` 之外,还需把 `default_value` 形如 "${...}" 的字段视为公式字段
|
|
10
|
+
*
|
|
11
|
+
* 用法:
|
|
12
|
+
* node main/default/test/test_getApproveValues.js
|
|
13
|
+
* 或 yarn workspace @steedos-labs/plugin-workflow test:approve-values
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const assert = require('assert');
|
|
17
|
+
|
|
18
|
+
// 注意:require uuflow_manager.js 会顺带加载 @steedos/objectql 等运行时依赖;
|
|
19
|
+
// 本机已验证可独立加载,无需启动完整 server。
|
|
20
|
+
const UUFlowManager = require('../manager/uuflow_manager.js');
|
|
21
|
+
|
|
22
|
+
// Monkey-patch DB 读取函数,注入测试用 form schema
|
|
23
|
+
const fakeForms = {};
|
|
24
|
+
UUFlowManager.getForm = async function (form_id) {
|
|
25
|
+
if (!fakeForms[form_id]) throw new Error(`fake form not found: ${form_id}`);
|
|
26
|
+
return fakeForms[form_id];
|
|
27
|
+
};
|
|
28
|
+
UUFlowManager.getFormVersion = function (form, form_version) {
|
|
29
|
+
if (form.current && form.current._id === form_version) return form.current;
|
|
30
|
+
const h = (form.historys || []).find(f => f._id === form_version);
|
|
31
|
+
if (!h) throw new Error(`fake form version not found: ${form_version}`);
|
|
32
|
+
return h;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let passed = 0;
|
|
36
|
+
let failed = 0;
|
|
37
|
+
|
|
38
|
+
async function run(name, fn) {
|
|
39
|
+
try {
|
|
40
|
+
await fn();
|
|
41
|
+
console.log(` ✓ ${name}`);
|
|
42
|
+
passed++;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
console.log(` ✗ ${name}`);
|
|
45
|
+
console.log(` ${e.message}`);
|
|
46
|
+
if (e.actual !== undefined) {
|
|
47
|
+
console.log(` actual: ${JSON.stringify(e.actual)}`);
|
|
48
|
+
console.log(` expected: ${JSON.stringify(e.expected)}`);
|
|
49
|
+
}
|
|
50
|
+
failed++;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
(async () => {
|
|
55
|
+
console.log('UUFlowManager.getApproveValues 公式子字段兜底识别');
|
|
56
|
+
|
|
57
|
+
// 通用:父表 readonly,前两个子字段 editable,第三个(公式)无显式权限
|
|
58
|
+
const permissions = {
|
|
59
|
+
'表格': 'readable',
|
|
60
|
+
'加油量': 'editable',
|
|
61
|
+
'单价': 'editable',
|
|
62
|
+
'金额': 'readable',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const submittedRow = {
|
|
66
|
+
_id: 'row1',
|
|
67
|
+
'加油量': '10',
|
|
68
|
+
'单价': '8',
|
|
69
|
+
'金额': '80', // 客户端计算出的公式值
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
await run('公式子字段带 formula 属性(本地新版 schema)→ 保留金额', async () => {
|
|
73
|
+
fakeForms['F1'] = {
|
|
74
|
+
current: {
|
|
75
|
+
_id: 'V1',
|
|
76
|
+
fields: [{
|
|
77
|
+
type: 'table',
|
|
78
|
+
code: '表格',
|
|
79
|
+
fields: [
|
|
80
|
+
{ type: 'number', code: '加油量' },
|
|
81
|
+
{ type: 'number', code: '单价' },
|
|
82
|
+
{ type: 'number', code: '金额', formula: '{加油量}*{单价}' },
|
|
83
|
+
],
|
|
84
|
+
}],
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
const approve_values = { '表格': [Object.assign({}, submittedRow)] };
|
|
88
|
+
const out = await UUFlowManager.getApproveValues(approve_values, permissions, 'F1', 'V1');
|
|
89
|
+
assert.deepStrictEqual(out['表格'][0], submittedRow);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await run('公式子字段无 formula、default_value 为 ${...}(线上老 schema)→ 保留金额', async () => {
|
|
93
|
+
fakeForms['F2'] = {
|
|
94
|
+
current: {
|
|
95
|
+
_id: 'V2',
|
|
96
|
+
fields: [{
|
|
97
|
+
type: 'table',
|
|
98
|
+
code: '表格',
|
|
99
|
+
fields: [
|
|
100
|
+
{ type: 'number', code: '加油量', default_value: '0.00' },
|
|
101
|
+
{ type: 'number', code: '单价', default_value: '0.00' },
|
|
102
|
+
{ type: 'number', code: '金额', default_value: '${加油量*单价}' },
|
|
103
|
+
],
|
|
104
|
+
}],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const approve_values = { '表格': [Object.assign({}, submittedRow)] };
|
|
108
|
+
const out = await UUFlowManager.getApproveValues(approve_values, permissions, 'F2', 'V2');
|
|
109
|
+
assert.deepStrictEqual(out['表格'][0], submittedRow,
|
|
110
|
+
'老 schema 公式子字段应被识别保留,否则保存后刷新公式值会丢失');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await run('普通子字段 default_value 是纯字符串 → 不当作公式,无 editable 权限则删除', async () => {
|
|
114
|
+
fakeForms['F3'] = {
|
|
115
|
+
current: {
|
|
116
|
+
_id: 'V3',
|
|
117
|
+
fields: [{
|
|
118
|
+
type: 'table',
|
|
119
|
+
code: '表格',
|
|
120
|
+
fields: [
|
|
121
|
+
{ type: 'text', code: '加油量' },
|
|
122
|
+
{ type: 'text', code: '单价' },
|
|
123
|
+
{ type: 'text', code: '备注', default_value: '默认备注' },
|
|
124
|
+
],
|
|
125
|
+
}],
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
const perms = { '表格': 'readable', '加油量': 'editable', '单价': 'editable', '备注': 'readable' };
|
|
129
|
+
const row = { _id: 'r', '加油量': '1', '单价': '2', '备注': '客户端写入' };
|
|
130
|
+
const approve_values = { '表格': [Object.assign({}, row)] };
|
|
131
|
+
const out = await UUFlowManager.getApproveValues(approve_values, perms, 'F3', 'V3');
|
|
132
|
+
assert.strictEqual(out['表格'][0]['备注'], undefined, '普通字段不应被当作公式保留');
|
|
133
|
+
assert.strictEqual(out['表格'][0]['加油量'], '1');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await run('default_value 形如 "${"... 但未完整闭合 → 不当作公式', async () => {
|
|
137
|
+
fakeForms['F4'] = {
|
|
138
|
+
current: {
|
|
139
|
+
_id: 'V4',
|
|
140
|
+
fields: [{
|
|
141
|
+
type: 'table',
|
|
142
|
+
code: '表格',
|
|
143
|
+
fields: [
|
|
144
|
+
{ type: 'text', code: '加油量' },
|
|
145
|
+
{ type: 'text', code: '怪字段', default_value: '${unclosed' },
|
|
146
|
+
],
|
|
147
|
+
}],
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
const perms = { '表格': 'readable', '加油量': 'editable', '怪字段': 'readable' };
|
|
151
|
+
const row = { _id: 'r', '加油量': '1', '怪字段': 'x' };
|
|
152
|
+
const approve_values = { '表格': [Object.assign({}, row)] };
|
|
153
|
+
const out = await UUFlowManager.getApproveValues(approve_values, perms, 'F4', 'V4');
|
|
154
|
+
assert.strictEqual(out['表格'][0]['怪字段'], undefined);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await run('顶层公式字段无 formula、default_value=${...} → 保留', async () => {
|
|
158
|
+
fakeForms['F5'] = {
|
|
159
|
+
current: {
|
|
160
|
+
_id: 'V5',
|
|
161
|
+
fields: [
|
|
162
|
+
{ type: 'number', code: '数量' },
|
|
163
|
+
{ type: 'number', code: '小计', default_value: '${数量*10}' },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
const perms = { '数量': 'editable', '小计': 'readable' };
|
|
168
|
+
const approve_values = { '数量': '5', '小计': '50' };
|
|
169
|
+
const out = await UUFlowManager.getApproveValues(approve_values, perms, 'F5', 'V5');
|
|
170
|
+
assert.strictEqual(out['小计'], '50', '顶层公式字段应同样被兜底识别');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await run('section 内公式子字段无 formula、default_value=${...} → 保留', async () => {
|
|
174
|
+
fakeForms['F6'] = {
|
|
175
|
+
current: {
|
|
176
|
+
_id: 'V6',
|
|
177
|
+
fields: [{
|
|
178
|
+
type: 'section',
|
|
179
|
+
code: 'section1',
|
|
180
|
+
fields: [
|
|
181
|
+
{ type: 'number', code: '数量' },
|
|
182
|
+
{ type: 'number', code: '小计', default_value: '${数量*10}' },
|
|
183
|
+
],
|
|
184
|
+
}],
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
const perms = { '数量': 'editable', '小计': 'readable' };
|
|
188
|
+
const approve_values = { '数量': '5', '小计': '50' };
|
|
189
|
+
const out = await UUFlowManager.getApproveValues(approve_values, perms, 'F6', 'V6');
|
|
190
|
+
assert.strictEqual(out['小计'], '50', 'section 内公式字段应同样被兜底识别');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
await run('permissions 为 null → 直接返回 {}', async () => {
|
|
194
|
+
const out = await UUFlowManager.getApproveValues({ x: 1 }, null, 'F1', 'V1');
|
|
195
|
+
assert.deepStrictEqual(out, {});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
await run('historys 版本中的老 schema 同样生效', async () => {
|
|
199
|
+
fakeForms['F7'] = {
|
|
200
|
+
current: { _id: 'V7_NEW', fields: [] },
|
|
201
|
+
historys: [{
|
|
202
|
+
_id: 'V7_OLD',
|
|
203
|
+
fields: [{
|
|
204
|
+
type: 'table',
|
|
205
|
+
code: '表格',
|
|
206
|
+
fields: [
|
|
207
|
+
{ type: 'number', code: '加油量', default_value: '0.00' },
|
|
208
|
+
{ type: 'number', code: '单价', default_value: '0.00' },
|
|
209
|
+
{ type: 'number', code: '金额', default_value: '${加油量*单价}' },
|
|
210
|
+
],
|
|
211
|
+
}],
|
|
212
|
+
}],
|
|
213
|
+
};
|
|
214
|
+
const approve_values = { '表格': [Object.assign({}, submittedRow)] };
|
|
215
|
+
const out = await UUFlowManager.getApproveValues(approve_values, permissions, 'F7', 'V7_OLD');
|
|
216
|
+
assert.deepStrictEqual(out['表格'][0], submittedRow);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
220
|
+
process.exit(failed === 0 ? 0 : 1);
|
|
221
|
+
})();
|