@steedos-labs/plugin-workflow 3.0.14 → 3.0.16

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.
@@ -0,0 +1,335 @@
1
+ const _ = require('lodash');
2
+
3
+ // Helper to find field definition
4
+ function findFieldDef(fields, fieldName) {
5
+ if (!fields) return null;
6
+
7
+ // Handle array fields (e.g. 'line_items.$.description')
8
+ const cleanName = fieldName.replace(/\.\$\./g, '.').replace(/\.\$$/, '');
9
+
10
+ // Direct match
11
+ if (fields[cleanName]) return fields[cleanName];
12
+
13
+ // Search in nested fields
14
+ for (const key in fields) {
15
+ if (fields[key].name === cleanName) return fields[key];
16
+ if (fields[key].code === cleanName) return fields[key];
17
+ // If field is inside a grid/object
18
+ if (key.includes('.') && key.endsWith(cleanName)) return fields[key];
19
+ }
20
+ return null;
21
+ }
22
+
23
+ function getFieldLabel(fieldDef, fieldName) {
24
+ if (fieldDef && fieldDef.label) {
25
+ return fieldDef.label;
26
+ }
27
+ // Fallback: try to derive from name or just return name
28
+ return fieldName;
29
+ }
30
+
31
+ function fieldToAmis(fieldName, fieldDef, schema, isColumn = false, isPrint = false) {
32
+ // Handle section type special case
33
+ if (fieldDef && fieldDef.type === 'section') {
34
+ const displayName = fieldDef.name || fieldDef.code || '';
35
+ return `<span class="antd-TplField"><span><div class="font-bold">${displayName}<span class="antd-Form-star">*</span><pre class="font-normal"></pre></div></span></span>`;
36
+ }
37
+
38
+ let finalName = fieldName;
39
+ let processedFormula = null;
40
+ if (fieldDef) {
41
+ if (fieldDef.code) {
42
+ finalName = fieldDef.code;
43
+ } else if (fieldDef.name) {
44
+ finalName = fieldDef.name;
45
+ }
46
+
47
+ // Print Template Simplification
48
+ if (isPrint && !isColumn) {
49
+ const type = fieldDef.type;
50
+ if (['text', 'input', 'textarea', 'url', 'email', 'number', 'currency', 'percent', 'password', 'date', 'datetime', 'boolean', 'select', 'lookup', 'master_detail', 'radio', 'checkbox'].includes(type) || fieldDef.formula) {
51
+ return `{{${finalName}}}`;
52
+ }
53
+ }
54
+ }
55
+
56
+ const config = {
57
+ name: finalName,
58
+ id: `u:${Math.random().toString(36).substr(2, 9)}`
59
+ };
60
+
61
+ if (isColumn) {
62
+ if (schema && schema.atts && schema.atts.label) {
63
+ config.label = schema.atts.label;
64
+ } else {
65
+ config.label = getFieldLabel(fieldDef, fieldName);
66
+ }
67
+ } else {
68
+ config.label = false;
69
+ }
70
+
71
+ // Type mapping
72
+ let type = 'input-text'; // default
73
+ if (fieldDef) {
74
+ switch (fieldDef.type) {
75
+ case 'text':
76
+ case 'input':
77
+ type = 'input-text';
78
+ break;
79
+ case 'textarea': type = 'textarea'; break;
80
+ case 'number': type = 'input-number'; break;
81
+ case 'date': type = 'input-date'; break;
82
+ case 'datetime': type = 'input-datetime'; break;
83
+ case 'boolean': type = 'checkbox'; break;
84
+ case 'radio': type = 'radios'; break;
85
+ case 'checkbox': type = 'checkboxes'; break;
86
+ case 'select': type = 'select'; break;
87
+ case 'lookup': type = 'select'; break; // Simplified for now
88
+ case 'table':
89
+ type = 'steedos-input-table';
90
+ config.columnsTogglable = false;
91
+ if (!isColumn) {
92
+ const editableCondition = "${record.step.permissions['" + config.name + "'] == 'editable'}";
93
+ config.addableOn = editableCondition;
94
+ config.editableOn = editableCondition;
95
+ config.removableOn = editableCondition;
96
+ }
97
+ if (fieldDef.fields) {
98
+ config.fields = fieldDef.fields.map(f => {
99
+ return fieldToAmis(f.name || f.code, f, null, true);
100
+ });
101
+ }
102
+ break;
103
+ }
104
+
105
+ if (fieldDef.options) {
106
+ let options = fieldDef.options;
107
+ if (typeof options === 'string') {
108
+ options = options.split('\n').map(o => o.trim()).filter(o => o);
109
+ }
110
+
111
+ if (Array.isArray(options)) {
112
+ config.options = options.map(opt => {
113
+ if (typeof opt === 'string') {
114
+ return { label: opt, value: opt };
115
+ }
116
+ if (typeof opt === 'object') {
117
+ return {
118
+ label: opt.label || opt.name || opt.value,
119
+ value: opt.value || opt.code || opt.name
120
+ };
121
+ }
122
+ return opt;
123
+ });
124
+ } else {
125
+ config.options = options;
126
+ }
127
+ }
128
+ if (fieldDef.required) {
129
+ config.required = true;
130
+ }
131
+ if (fieldDef.precision) {
132
+ config.precision = fieldDef.precision;
133
+ }
134
+
135
+ if (fieldDef.formula && (type === 'input-text' || type === 'input-number' || type === 'textarea')) {
136
+ let formula = fieldDef.formula;
137
+ const replaceCallback = (match, p1) => {
138
+ const cleanP1 = p1.replace(/(/g, '_').replace(/)/g, '');
139
+ if (type === 'input-number') {
140
+ return `(${cleanP1} || 0)`;
141
+ } else {
142
+ return `(${cleanP1} || "")`;
143
+ }
144
+ };
145
+ formula = formula.replace(/\{([^}]+)\}/g, replaceCallback);
146
+
147
+ processedFormula = "${" + formula + "}";
148
+
149
+ if (isColumn) {
150
+ config.value = processedFormula;
151
+ }
152
+ }
153
+ }
154
+
155
+ config.type = type;
156
+
157
+ if (!isColumn) {
158
+ if (processedFormula) {
159
+ config.static = true;
160
+ const formulaComp = {
161
+ name: config.name,
162
+ id: config.id,
163
+ label: false,
164
+ formula: processedFormula,
165
+ type: "formula"
166
+ };
167
+ return [config, formulaComp];
168
+ } else {
169
+ config.staticOn = "${record.step.permissions['" + config.name + "'] != 'editable'}";
170
+ }
171
+ }
172
+
173
+ return config;
174
+ }
175
+
176
+ function parseArgs(args) {
177
+ const argMap = {};
178
+ const re = /(\w+)\s*=\s*(["'])(.*?)\2|(\w+)\s*=\s*([^"'\s]+)/g;
179
+ let m;
180
+ while ((m = re.exec(args)) !== null) {
181
+ if (m[1]) argMap[m[1]] = m[3];
182
+ else argMap[m[4]] = m[5];
183
+ }
184
+ return argMap;
185
+ }
186
+
187
+ /**
188
+ * 转换模板内容
189
+ * @param {string} content 原始Blaze模板
190
+ * @param {object} fields 表单字段定义
191
+ * @param {boolean} isPrint 是否为打印模板
192
+ */
193
+ function convertTemplate(content, fields, isPrint) {
194
+ if (!content) return content;
195
+ let newContent = content;
196
+
197
+ // Phase 1: Blaze -> Liquid
198
+ newContent = newContent.replace(/\{\{![\s\S]*?\}\}/g, '');
199
+ newContent = newContent.replace(/\{\{#if\s+([^}]+)\}\}/g, '{% if $1 %}');
200
+ newContent = newContent.replace(/\{\{else\}\}/g, '{% else %}');
201
+ newContent = newContent.replace(/\{\{\/if\}\}/g, '{% endif %}');
202
+ newContent = newContent.replace(/\{\{#unless\s+([^}]+)\}\}/g, '{% unless $1 %}');
203
+ newContent = newContent.replace(/\{\{\/unless\}\}/g, '{% endunless %}');
204
+ newContent = newContent.replace(/\{\{#each\s+([^}]+)\}\}/g, '{% for item in $1 %}');
205
+ newContent = newContent.replace(/\{\{\/each\}\}/g, '{% endfor %}');
206
+ newContent = newContent.replace(/\{\{\{\s*([^}]+)\s*\}\}\}/g, '{{ $1 | raw }}');
207
+
208
+ // Remove table-page-footer from all templates
209
+ newContent = newContent.replace(/<table[^>]*class="[^"]*table-page-footer[\s\S]*?<\/table>/g, '');
210
+
211
+ // Phase 2: afFormGroup -> AMIS
212
+ // Remove table-page-footer (Retaining table-page-title based on requirement)
213
+ if (isPrint) {
214
+ newContent = newContent.replace(/<tr([^>]*)style=(["'])([^"']*?height:\s*0px[^"']*?)\2([^>]*)>/gi, '<tr$1style=$2$3; display: none;$2$4>');
215
+
216
+ if (!newContent.includes('border: 2px solid black;')) {
217
+ const tableCSS = `
218
+ <style>
219
+ .form-table {
220
+ border-collapse: collapse;
221
+ border: 2px solid black;
222
+ }
223
+ .form-table th,
224
+ .form-table td {
225
+ border: 1px solid black;
226
+ padding: 4px 6px;
227
+ }
228
+ </style>
229
+ `;
230
+ if (newContent.includes('<style>')) {
231
+ newContent = newContent.replace('<style>', tableCSS + '\n<style>');
232
+ } else {
233
+ newContent = tableCSS + '\n' + newContent;
234
+ }
235
+ }
236
+ }
237
+
238
+ // --- Sub-phase 2a: Convert TD wrapped afFormGroup tags ---
239
+ newContent = newContent.replace(/<td([^>]*)>(\s*)\{\{>\s*afFormGroup\s+([^}]+)\}\}(\s*)<\/td>/g, (match, tdAttrs, space1, args, space2) => {
240
+ const argMap = parseArgs(args);
241
+ const fieldName = argMap.name;
242
+ if (!fieldName) return match;
243
+
244
+ const fieldDef = findFieldDef(fields, fieldName);
245
+ const amisSchema = fieldToAmis(fieldName, fieldDef, { atts: argMap }, false, isPrint);
246
+
247
+ let contentInner = '';
248
+ if (typeof amisSchema === 'string') {
249
+ contentInner = amisSchema;
250
+ } else if (Array.isArray(amisSchema)) {
251
+ contentInner = amisSchema.map(s => `{% amis %}\n${JSON.stringify(s, null, 2)}\n{% endamis %}`).join('\n ');
252
+ } else {
253
+ contentInner = `{% amis %}\n${JSON.stringify(amisSchema, null, 2)}\n{% endamis %}`;
254
+ }
255
+
256
+ if (fieldDef && fieldDef.type === 'section') {
257
+ let newAttrs = tdAttrs || ' ';
258
+ if (newAttrs.includes('style="')) {
259
+ newAttrs = newAttrs.replace('style="', 'style="background: rgb(241, 241, 241); ');
260
+ } else {
261
+ newAttrs = newAttrs + ' style="background: rgb(241, 241, 241);"';
262
+ }
263
+ return `<td${newAttrs}>${space1}${contentInner}${space2}</td>`;
264
+ }
265
+
266
+ return `<td${tdAttrs}>${space1}${contentInner}${space2}</td>`;
267
+ });
268
+
269
+ // --- Sub-phase 2b: Convert remaining raw afFormGroup tags ---
270
+ newContent = newContent.replace(/\{\{>\s*afFormGroup\s+([^}]+)\}\}/g, (match, args) => {
271
+ const argMap = parseArgs(args);
272
+ const fieldName = argMap.name;
273
+ if (!fieldName) return match;
274
+
275
+ const fieldDef = findFieldDef(fields, fieldName);
276
+ const amisSchema = fieldToAmis(fieldName, fieldDef, { atts: argMap }, false, isPrint);
277
+
278
+ if (typeof amisSchema === 'string') {
279
+ return amisSchema;
280
+ }
281
+ if (Array.isArray(amisSchema)) {
282
+ return amisSchema.map(s => `{% amis %}\n${JSON.stringify(s, null, 2)}\n{% endamis %}`).join('\n ');
283
+ }
284
+
285
+ return `{% amis %}\n${JSON.stringify(amisSchema, null, 2)}\n{% endamis %}`;
286
+ });
287
+
288
+ // --- Sub-phase 2c: Convert afFieldLabelText ---
289
+ newContent = newContent.replace(/\{\{afFieldLabelText\s+name=["'](.+?)["']\}\}/g, '$1');
290
+
291
+ // --- Sub-phase 2d: Convert instanceSignText ---
292
+ newContent = newContent.replace(/\{\{>\s*instanceSignText\s+([^}]+)\}\}/g, (match, args) => {
293
+ const argMap = parseArgs(args);
294
+ const fieldName = argMap.name;
295
+ if (!fieldName) return match;
296
+
297
+ const fieldDef = findFieldDef(fields, fieldName);
298
+ let stepName = "";
299
+ if (fieldDef && fieldDef.formula) {
300
+ const formulaMatch = fieldDef.formula.match(/signature\.traces\.([^}]+)/);
301
+ if (formulaMatch) {
302
+ stepName = formulaMatch[1].replace('}', '');
303
+ }
304
+ }
305
+
306
+ const randomSuffix = Math.random().toString(36).substr(2, 6);
307
+ const amisSchema = {
308
+ "type": "sfield-approvalcomments",
309
+ "config": {
310
+ "type": "approval_comments",
311
+ "label": false,
312
+ "amis": {
313
+ "mode": "horizontal",
314
+ "name": fieldName,
315
+ "id": `u:${fieldName}`
316
+ },
317
+ "name": `confirm_${randomSuffix}`,
318
+ "steps": [
319
+ {
320
+ "name": stepName
321
+ }
322
+ ]
323
+ },
324
+ "id": `u:sfield-approvalcomments-${randomSuffix}`
325
+ };
326
+
327
+ return `{% amis %}\n${JSON.stringify(amisSchema, null, 2)}\n{% endamis %}`;
328
+ });
329
+
330
+ return newContent;
331
+ }
332
+
333
+ module.exports = {
334
+ convertTemplate
335
+ };