@steedos-labs/plugin-workflow 3.0.21 → 3.0.23
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/APPROVAL_COMMENTS_OPERATIONS.md +673 -0
- package/APPROVAL_COMMENTS_UPGRADE.md +854 -0
- package/main/default/manager/handlers_manager.js +11 -12
- package/main/default/manager/uuflow_manager.js +4 -1
- package/main/default/manager/workflow_manager.js +12 -0
- package/main/default/pages/instance_tasks_detail.page.amis.json +16 -0
- package/main/default/pages/page_instance_print.page.amis.json +1 -1
- package/main/default/routes/api_files.router.js +15 -1
- package/main/default/routes/api_workflow_chart.router.js +41 -9
- package/main/default/routes/api_workflow_next_step.router.js +1 -1
- package/main/default/routes/api_workflow_next_step_users.router.js +7 -2
- package/main/default/routes/flow_form_design.ejs +21 -1
- package/main/default/triggers/amis_form_design.trigger.js +6 -4
- package/main/default/utils/designerManager.js +1 -2
- package/package.json +1 -1
- package/public/workflow/index.css +3 -0
- package/src/rests/approvalCommentsConsole.js +460 -0
- package/src/rests/index.js +5 -1
- package/src/rests/integrationTestApprovalComments.js +96 -0
- package/src/rests/migrateApprovalCommentsField.js +536 -0
- package/src/rests/rollbackApprovalCommentsField.js +319 -0
- package/src/util/templateConverter.js +8 -2
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* @Author: Copilot
|
|
3
|
+
* @Date: 2026-01-28
|
|
4
|
+
* @Description: Rollback approval comments field transformations
|
|
5
|
+
*
|
|
6
|
+
* This endpoint reverts fields that were transformed by the transform endpoint
|
|
7
|
+
* back to their original input type format.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { MongoClient } = require('mongodb');
|
|
11
|
+
|
|
12
|
+
// MongoDB connection
|
|
13
|
+
let client = null;
|
|
14
|
+
let db = null;
|
|
15
|
+
|
|
16
|
+
async function connectToMongoDB() {
|
|
17
|
+
if (db) {
|
|
18
|
+
return db;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const mongoUrl = process.env.MONGO_URL || 'mongodb://127.0.0.1:27017/steedos';
|
|
22
|
+
try {
|
|
23
|
+
client = new MongoClient(mongoUrl);
|
|
24
|
+
await client.connect();
|
|
25
|
+
|
|
26
|
+
const dbName = mongoUrl.split('/').pop().split('?')[0];
|
|
27
|
+
db = client.db(dbName);
|
|
28
|
+
return db;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error('Failed to connect to MongoDB:', error.message);
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function getCollection(name) {
|
|
36
|
+
const database = await connectToMongoDB();
|
|
37
|
+
return database.collection(name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Process fields array and return bulk operations for rollback
|
|
42
|
+
* Implements field-level error handling for silent failure
|
|
43
|
+
*/
|
|
44
|
+
function processFieldsArrayForRollback(fields, pathPrefix, formId, formName) {
|
|
45
|
+
const updates = [];
|
|
46
|
+
const unsets = [];
|
|
47
|
+
let fieldCount = 0; // Track actual fields processed
|
|
48
|
+
const fieldErrors = []; // Track field-specific errors
|
|
49
|
+
|
|
50
|
+
if (!Array.isArray(fields)) {
|
|
51
|
+
return { updates, unsets, fieldCount, fieldErrors };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (let i = 0; i < fields.length; i++) {
|
|
55
|
+
const field = fields[i];
|
|
56
|
+
|
|
57
|
+
// Only rollback fields that were transformed (Idempotency principle)
|
|
58
|
+
if (field.__approval_comments_transformed === true) {
|
|
59
|
+
try {
|
|
60
|
+
fieldCount++; // Count this field
|
|
61
|
+
|
|
62
|
+
// Zero Loss principle: Restore type with priority to stored original type
|
|
63
|
+
const originalType = field.__approval_comments_original_type || 'input';
|
|
64
|
+
updates.push({
|
|
65
|
+
index: i,
|
|
66
|
+
path: `${pathPrefix}.${i}.type`,
|
|
67
|
+
value: originalType
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Restore original formula if exists
|
|
71
|
+
if (field.__approval_comments_original_formula) {
|
|
72
|
+
updates.push({
|
|
73
|
+
index: i,
|
|
74
|
+
path: `${pathPrefix}.${i}.formula`,
|
|
75
|
+
value: field.__approval_comments_original_formula
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Restore original default_value if exists
|
|
80
|
+
if (field.__approval_comments_original_default_value) {
|
|
81
|
+
updates.push({
|
|
82
|
+
index: i,
|
|
83
|
+
path: `${pathPrefix}.${i}.default_value`,
|
|
84
|
+
value: field.__approval_comments_original_default_value
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Rename __approval_comments_original__amisField back to _amisField if it exists
|
|
89
|
+
if (field.__approval_comments_original__amisField) {
|
|
90
|
+
updates.push({
|
|
91
|
+
index: i,
|
|
92
|
+
path: `${pathPrefix}.${i}._amisField`,
|
|
93
|
+
value: field.__approval_comments_original__amisField
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Remove added properties
|
|
98
|
+
unsets.push(`${pathPrefix}.${i}.__approval_comments_transformed`);
|
|
99
|
+
unsets.push(`${pathPrefix}.${i}.config`);
|
|
100
|
+
unsets.push(`${pathPrefix}.${i}.__approval_comments_original__amisField`);
|
|
101
|
+
unsets.push(`${pathPrefix}.${i}.__approval_comments_original_type`); // Remove backup
|
|
102
|
+
unsets.push(`${pathPrefix}.${i}.__approval_comments_original_formula`); // Remove backup
|
|
103
|
+
unsets.push(`${pathPrefix}.${i}.__approval_comments_original_default_value`); // Remove backup
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// Silent Failure principle: Record error but continue processing
|
|
106
|
+
fieldErrors.push({
|
|
107
|
+
formId: formId,
|
|
108
|
+
formName: formName,
|
|
109
|
+
fieldPath: `${pathPrefix}.${i}`,
|
|
110
|
+
fieldCode: field.code || field.name || 'unknown',
|
|
111
|
+
error: error.message
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { updates, unsets, fieldCount, fieldErrors };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
rest: {
|
|
122
|
+
method: 'GET',
|
|
123
|
+
fullPath: '/api/workflow/rollbackApprovalCommentsField'
|
|
124
|
+
},
|
|
125
|
+
params: {
|
|
126
|
+
batchSize: { type: 'number', optional: true, convert: true },
|
|
127
|
+
dryRun: { type: 'boolean', optional: true, convert: true },
|
|
128
|
+
fid: { type: 'string', optional: true } // Flow ID to rollback single flow
|
|
129
|
+
},
|
|
130
|
+
async handler(ctx) {
|
|
131
|
+
const { user } = ctx.meta;
|
|
132
|
+
|
|
133
|
+
// Check if user exists and is space admin
|
|
134
|
+
if (!user || user.is_space_admin !== true) {
|
|
135
|
+
throw new Error('只有管理员才能执行此操作');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const { batchSize = 500, dryRun = false, fid } = ctx.params;
|
|
139
|
+
|
|
140
|
+
const formsCollection = await getCollection('forms');
|
|
141
|
+
|
|
142
|
+
// Build query filter
|
|
143
|
+
const query = {
|
|
144
|
+
state: 'enabled', // Only process enabled forms
|
|
145
|
+
$or: [
|
|
146
|
+
{ 'current.fields.__approval_comments_transformed': true },
|
|
147
|
+
{ 'historys.fields.__approval_comments_transformed': true }
|
|
148
|
+
]
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// If fid is provided, query flows collection to get form ID
|
|
152
|
+
if (fid) {
|
|
153
|
+
const flowsCollection = await getCollection('flows');
|
|
154
|
+
const flow = await flowsCollection.findOne({ _id: fid });
|
|
155
|
+
|
|
156
|
+
if (!flow) {
|
|
157
|
+
throw new Error(`Flow with ID ${fid} not found`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!flow.form) {
|
|
161
|
+
throw new Error(`Flow ${fid} does not have a form associated`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Add form ID filter
|
|
165
|
+
query._id = flow.form;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Use cursor to find forms with transformed fields
|
|
169
|
+
const cursor = formsCollection.find(query);
|
|
170
|
+
|
|
171
|
+
const results = {
|
|
172
|
+
totalForms: 0,
|
|
173
|
+
totalFields: 0,
|
|
174
|
+
errors: []
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const bulkOperations = [];
|
|
178
|
+
|
|
179
|
+
// Process forms one by one using cursor
|
|
180
|
+
while (await cursor.hasNext()) {
|
|
181
|
+
const form = await cursor.next();
|
|
182
|
+
try {
|
|
183
|
+
let formUpdated = false;
|
|
184
|
+
const formUpdate = {};
|
|
185
|
+
const formUnset = {};
|
|
186
|
+
let formFieldCount = 0; // Track actual fields rolled back
|
|
187
|
+
|
|
188
|
+
// Process current.fields
|
|
189
|
+
if (form.current && Array.isArray(form.current.fields)) {
|
|
190
|
+
const { updates, unsets, fieldCount, fieldErrors } = processFieldsArrayForRollback(
|
|
191
|
+
form.current.fields,
|
|
192
|
+
'current.fields',
|
|
193
|
+
form._id,
|
|
194
|
+
form.name
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Add field-level errors to results
|
|
198
|
+
if (fieldErrors && fieldErrors.length > 0) {
|
|
199
|
+
results.errors.push(...fieldErrors);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (updates.length > 0 || unsets.length > 0) {
|
|
203
|
+
formUpdated = true;
|
|
204
|
+
formFieldCount += fieldCount; // Add field count, not operation count
|
|
205
|
+
|
|
206
|
+
// Build $set updates
|
|
207
|
+
for (const update of updates) {
|
|
208
|
+
formUpdate[update.path] = update.value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Build $unset
|
|
212
|
+
for (const path of unsets) {
|
|
213
|
+
formUnset[path] = '';
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Process historys[].fields (Full Coverage principle)
|
|
219
|
+
if (Array.isArray(form.historys)) {
|
|
220
|
+
for (let h = 0; h < form.historys.length; h++) {
|
|
221
|
+
const history = form.historys[h];
|
|
222
|
+
if (Array.isArray(history.fields)) {
|
|
223
|
+
const { updates, unsets, fieldCount, fieldErrors } = processFieldsArrayForRollback(
|
|
224
|
+
history.fields,
|
|
225
|
+
`historys.${h}.fields`,
|
|
226
|
+
form._id,
|
|
227
|
+
form.name
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Add field-level errors to results
|
|
231
|
+
if (fieldErrors && fieldErrors.length > 0) {
|
|
232
|
+
results.errors.push(...fieldErrors);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (updates.length > 0 || unsets.length > 0) {
|
|
236
|
+
formUpdated = true;
|
|
237
|
+
formFieldCount += fieldCount; // Add field count, not operation count
|
|
238
|
+
|
|
239
|
+
// Build $set updates
|
|
240
|
+
for (const update of updates) {
|
|
241
|
+
formUpdate[update.path] = update.value;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Build $unset
|
|
245
|
+
for (const path of unsets) {
|
|
246
|
+
formUnset[path] = '';
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Restore root amis_schema from backup if exists
|
|
254
|
+
if (form.__approval_comments_original__amis_schema) {
|
|
255
|
+
formUpdated = true;
|
|
256
|
+
|
|
257
|
+
// Restore original amis_schema
|
|
258
|
+
formUpdate.amis_schema = form.__approval_comments_original__amis_schema;
|
|
259
|
+
|
|
260
|
+
// Remove backup
|
|
261
|
+
formUnset.__approval_comments_original__amis_schema = '';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (formUpdated) {
|
|
265
|
+
results.totalForms++;
|
|
266
|
+
results.totalFields += formFieldCount; // Add actual field count
|
|
267
|
+
|
|
268
|
+
if (!dryRun) {
|
|
269
|
+
const updateDoc = {};
|
|
270
|
+
if (Object.keys(formUpdate).length > 0) {
|
|
271
|
+
updateDoc.$set = formUpdate;
|
|
272
|
+
}
|
|
273
|
+
if (Object.keys(formUnset).length > 0) {
|
|
274
|
+
updateDoc.$unset = formUnset;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
bulkOperations.push({
|
|
278
|
+
updateOne: {
|
|
279
|
+
filter: { _id: form._id },
|
|
280
|
+
update: updateDoc
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// Execute bulk write in batches
|
|
285
|
+
if (bulkOperations.length >= batchSize) {
|
|
286
|
+
await formsCollection.bulkWrite(bulkOperations, { ordered: false });
|
|
287
|
+
bulkOperations.length = 0; // Clear array
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error(`Error processing form ${form._id}:`, error);
|
|
293
|
+
results.errors.push({
|
|
294
|
+
formId: form._id,
|
|
295
|
+
formName: form.name,
|
|
296
|
+
error: error.message
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Execute remaining bulk operations
|
|
302
|
+
if (!dryRun && bulkOperations.length > 0) {
|
|
303
|
+
try {
|
|
304
|
+
await formsCollection.bulkWrite(bulkOperations, { ordered: false });
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error('Error executing final bulk write:', error);
|
|
307
|
+
results.errors.push({
|
|
308
|
+
error: 'Final bulk write failed: ' + error.message
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
...results,
|
|
315
|
+
dryRun,
|
|
316
|
+
message: dryRun ? 'Dry run completed - no changes made' : 'Rollback completed'
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
};
|
|
@@ -290,15 +290,21 @@ function convertTemplate(content, fields, isPrint) {
|
|
|
290
290
|
newContent = newContent.replace(/\{\{afFieldLabelText\s+name=["'](.+?)["']\}\}/g, '$1');
|
|
291
291
|
|
|
292
292
|
// --- Sub-phase 2d: Convert instanceSignText ---
|
|
293
|
-
newContent = newContent.replace(/\{\{>\s*instanceSignText\s+([
|
|
293
|
+
newContent = newContent.replace(/\{\{>\s*instanceSignText\s+([\s\S]+?)\}\}/g, (match, args) => {
|
|
294
294
|
const argMap = parseArgs(args);
|
|
295
295
|
const fieldName = argMap.name;
|
|
296
296
|
if (!fieldName) return match;
|
|
297
297
|
|
|
298
298
|
const fieldDef = findFieldDef(fields, fieldName);
|
|
299
299
|
let stepName = "";
|
|
300
|
+
|
|
301
|
+
let formula = argMap.field_formula;
|
|
300
302
|
if (fieldDef && fieldDef.formula) {
|
|
301
|
-
|
|
303
|
+
formula = fieldDef.formula;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (formula) {
|
|
307
|
+
const formulaMatch = formula.match(/signature\.traces\.([^}]+)/);
|
|
302
308
|
if (formulaMatch) {
|
|
303
309
|
stepName = formulaMatch[1].replace('}', '');
|
|
304
310
|
}
|