@steedos-labs/plugin-workflow 3.0.15 → 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,222 @@
1
+ # Workflow Template Export/Import
2
+
3
+ ## 概述 Overview
4
+
5
+ 这是一个独立的脚本工具,用于导出和导入工作流模板(表单模板和打印模板),支持从 Blaze 格式转换为 Liquid 格式。
6
+
7
+ This is a standalone script tool for exporting and importing workflow templates (form templates and print templates), supporting conversion from Blaze to Liquid format.
8
+
9
+ ## 安装 Installation
10
+
11
+ 在运行脚本前,需要先安装依赖:
12
+ Before running the script, install dependencies:
13
+
14
+ ```bash
15
+ cd steedos-packages/plugin-workflow
16
+ npm install
17
+ ```
18
+
19
+ 脚本使用原生 MongoDB 驱动连接数据库,连接信息从环境变量 `MONGO_URL` 读取。
20
+ The script uses native MongoDB driver to connect to the database. Connection info is read from `MONGO_URL` environment variable.
21
+
22
+ ## 快速开始 Quick Start
23
+
24
+ ```bash
25
+ cd steedos-packages/plugin-workflow
26
+
27
+ # 导出所有模板 Export all templates
28
+ node run.js export
29
+
30
+ # 导出特定流程 Export specific flow
31
+ node run.js export <flowId>
32
+
33
+ # 转换 .blaze 文件为 .liquid (手动或使用 Copilot)
34
+ # Convert .blaze files to .liquid (manually or with Copilot)
35
+
36
+ # 导入转换后的模板 Import converted templates
37
+ node run.js import <flowId>
38
+ ```
39
+
40
+ ## 目录结构 Directory Structure
41
+
42
+ ```
43
+ flows-template/
44
+ ├── {flowId}.instance_template.blaze # 导出的表单模板 (Blaze 格式)
45
+ ├── {flowId}.print_template.blaze # 导出的打印模板 (Blaze 格式)
46
+ ├── {flowId}.fields.json # 字段定义
47
+ ├── {flowId}.instance_template.liquid # 转换后的表单模板 (Liquid 格式,用于导入)
48
+ └── {flowId}.print_template.liquid # 转换后的打印模板 (Liquid 格式,用于导入)
49
+ ```
50
+
51
+ ## 命令使用 Commands
52
+
53
+ ### 导出模板 Export Templates
54
+
55
+ ```bash
56
+ # 导出所有包含模板的流程
57
+ # Export all flows with templates
58
+ node run.js export
59
+
60
+ # 导出特定流程
61
+ # Export specific flow
62
+ node run.js export <flowId>
63
+
64
+ # 导出到自定义目录
65
+ # Export to custom directory
66
+ node run.js export <flowId> /path/to/output
67
+ ```
68
+
69
+ **示例 Example:**
70
+ ```bash
71
+ node run.js export abc123
72
+ ```
73
+
74
+ **输出 Output:**
75
+ ```
76
+ Found 1 flow(s) to export.
77
+
78
+ Exporting flow: Leave Request (abc123)
79
+ ✓ abc123.instance_template.blaze
80
+ ✓ abc123.print_template.blaze
81
+ ✓ abc123.fields.json
82
+ Exported 3 file(s)
83
+
84
+ ✅ Export completed. Files saved to: /path/to/flows-template
85
+ ```
86
+
87
+ ### 导入模板 Import Templates
88
+
89
+ ```bash
90
+ # 导入流程的所有模板
91
+ # Import both templates for a flow
92
+ node run.js import <flowId>
93
+
94
+ # 仅导入表单模板
95
+ # Import only instance_template
96
+ node run.js import <flowId> instance_template
97
+
98
+ # 仅导入打印模板
99
+ # Import only print_template
100
+ node run.js import <flowId> print_template
101
+
102
+ # 从自定义目录导入
103
+ # Import from custom directory
104
+ node run.js import <flowId> instance_template /path/to/input
105
+ ```
106
+
107
+ **示例 Example:**
108
+ ```bash
109
+ node run.js import abc123
110
+ ```
111
+
112
+ **输出 Output:**
113
+ ```
114
+ Importing templates for flow: Leave Request (abc123)
115
+ ℹ Backing up old instance_template to instance_template_backup
116
+ ✓ Imported instance_template from abc123.instance_template.liquid
117
+ ℹ Backing up old print_template to print_template_backup
118
+ ✓ Imported print_template from abc123.print_template.liquid
119
+
120
+ ✅ Import completed. Imported 2 template(s).
121
+ - instance_template: updated (old value backed up)
122
+ - print_template: updated (old value backed up)
123
+ ```
124
+
125
+ ## 工作流程 Workflow
126
+
127
+ 1. **导出 Export**: 运行导出命令,将模板保存为 `.blaze` 文件
128
+ 2. **转换 Convert**: 使用 Copilot 或其他工具将 `.blaze` 文件转换为 `.liquid` 格式
129
+ 3. **导入 Import**: 运行导入命令,将转换后的 `.liquid` 模板加载回数据库
130
+ 4. **备份 Backup**: 导入时自动备份旧模板到 `{模板名}_backup` 字段
131
+
132
+ ## 备份字段 Backup Fields
133
+
134
+ 导入模板时,旧值会自动备份到:
135
+ When importing templates, old values are backed up to:
136
+
137
+ - `instance_template_backup`: 表单模板备份 (Form template backup)
138
+ - `print_template_backup`: 打印模板备份 (Print template backup)
139
+
140
+ 这些备份字段在界面中隐藏,但可以通过程序访问。
141
+ These backup fields are hidden in the UI but can be accessed programmatically.
142
+
143
+ ## 字段定义 Field Definitions
144
+
145
+ `.fields.json` 文件包含表单的字段定义(来自 `current.fields`),为模板转换工具(如 Copilot)提供字段结构和类型的上下文信息。
146
+
147
+ The `.fields.json` file contains field definitions from the form's `current.fields`, providing context about field structure and types for conversion tools like Copilot.
148
+
149
+ **示例 Example:**
150
+ ```json
151
+ [
152
+ {
153
+ "_id": "field1",
154
+ "code": "userName",
155
+ "name": "User Name",
156
+ "type": "text",
157
+ "is_required": true
158
+ },
159
+ {
160
+ "_id": "field2",
161
+ "code": "requestDate",
162
+ "name": "Request Date",
163
+ "type": "date",
164
+ "is_required": true
165
+ }
166
+ ]
167
+ ```
168
+
169
+ ## 数据库配置 Database Configuration
170
+
171
+ 脚本使用环境变量配置的 MongoDB 连接(通过 `.env` 文件中的 `MONGO_URL`)。
172
+
173
+ The script uses the MongoDB connection configured via environment variables (through `MONGO_URL` in `.env` file).
174
+
175
+ ## 错误处理 Error Handling
176
+
177
+ 脚本提供清晰的错误信息:
178
+ The script provides clear error messages:
179
+
180
+ - `Flow not found: abc123` - 验证流程 ID 是否存在 (Verify the flow ID exists)
181
+ - `No template files found for flow abc123` - 确保 `.liquid` 文件存在 (Ensure `.liquid` files exist)
182
+ - `Permission denied reading file` - 检查文件权限 (Check file permissions)
183
+ - `No flows found with templates` - 没有包含模板的流程 (No flows have templates)
184
+
185
+ ## 使用提示 Tips
186
+
187
+ 1. 导入模板前务必备份数据库 (Always backup your database before importing)
188
+ 2. 旧模板值会自动备份到 `{模板名}_backup` 字段 (Old values are automatically backed up)
189
+ 3. 先在开发环境测试导入 (Test imports in development environment first)
190
+ 4. 使用字段定义了解模板结构 (Use field definitions to understand template structure)
191
+ 5. 导入前仔细检查转换后的模板 (Review converted templates carefully before importing)
192
+ 6. `flows-template` 目录自动被 git 忽略 (The directory is automatically ignored by git)
193
+
194
+ ## 完整示例 Complete Example
195
+
196
+ ```bash
197
+ # 1. 导出模板 Export templates
198
+ cd steedos-packages/plugin-workflow
199
+ node run.js export abc123
200
+
201
+ # 2. 查看导出的文件 View exported files
202
+ ls -la flows-template/
203
+ # abc123.instance_template.blaze
204
+ # abc123.print_template.blaze
205
+ # abc123.fields.json
206
+
207
+ # 3. 转换模板 Convert templates
208
+ # 使用 Copilot 或手动将 .blaze 文件转换为 .liquid
209
+ # Use Copilot or manually convert .blaze files to .liquid
210
+ #
211
+ # 示例转换 Example conversion:
212
+ # {{#if userName}} → {% if userName %}
213
+ # {{userName}} → {{ userName }}
214
+ # {{/if}} → {% endif %}
215
+
216
+ # 4. 导入转换后的模板 Import converted templates
217
+ node run.js import abc123
218
+
219
+ # 5. 验证 Verify
220
+ # 检查数据库中的 flows 记录,确认模板已更新且备份已创建
221
+ # Check flows record in database to confirm templates are updated and backups are created
222
+ ```
@@ -0,0 +1,51 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const converter = require('./src/util/templateConverter');
4
+
5
+ const convertDir = path.join(__dirname, 'flows-template/export');
6
+
7
+ if (!fs.existsSync(convertDir)) {
8
+ console.error(`Directory not found: ${convertDir}`);
9
+ process.exit(1);
10
+ }
11
+
12
+ console.log('==========================================');
13
+ console.log('开始转换模板 (Blaze -> Liquid + AMIS)');
14
+ console.log('==========================================');
15
+
16
+ const blazeFiles = fs.readdirSync(convertDir).filter(f => f.endsWith('.blaze'));
17
+ const fieldsCache = {}; // Cache fields.json content
18
+
19
+ blazeFiles.forEach(file => {
20
+ const content = fs.readFileSync(path.join(convertDir, file), 'utf8');
21
+ const flowId = file.split('.')[0];
22
+ const isPrint = file.includes('print_template');
23
+
24
+ // Load fields def
25
+ let fields = fieldsCache[flowId];
26
+ if (!fields) {
27
+ const fieldsPath = path.join(convertDir, `${flowId}.fields.json`);
28
+ if (fs.existsSync(fieldsPath)) {
29
+ try {
30
+ // Read fields as array
31
+ fields = JSON.parse(fs.readFileSync(fieldsPath, 'utf8'));
32
+ fieldsCache[flowId] = fields;
33
+ } catch (e) {
34
+ console.error(`Error parsing fields for ${flowId}:`, e.message);
35
+ }
36
+ }
37
+ }
38
+
39
+ try {
40
+ const newContent = converter.convertTemplate(content, fields, isPrint);
41
+ const newFile = file.replace('.blaze', '.liquid');
42
+ fs.writeFileSync(path.join(convertDir, newFile), newContent);
43
+ console.log(`✓ ${file} -> ${newFile}`);
44
+ } catch (error) {
45
+ console.error(`✗ Failed to convert ${file}:`, error.message);
46
+ }
47
+ });
48
+
49
+ console.log('==========================================');
50
+ console.log('转换完成');
51
+ console.log('==========================================');
@@ -456,12 +456,28 @@ fields:
456
456
  language: html
457
457
  is_wide: true
458
458
  group: template
459
+ instance_template_backup:
460
+ label: Form Template Backup
461
+ type: code
462
+ language: html
463
+ is_wide: true
464
+ group: template
465
+ hidden: true
466
+ omit: true
459
467
  print_template:
460
468
  label: Print Template
461
469
  type: code
462
470
  language: html
463
471
  is_wide: true
464
472
  group: template
473
+ print_template_backup:
474
+ label: Print Template Backup
475
+ type: code
476
+ language: html
477
+ is_wide: true
478
+ group: template
479
+ hidden: true
480
+ omit: true
465
481
  name_formula:
466
482
  label: Formula of Title
467
483
  type: text
@@ -352,6 +352,39 @@
352
352
  },
353
353
  ".antd-Table-content-colDragLine": {
354
354
  "display": "none !important"
355
+ },
356
+ "@media print": {
357
+ "html, body, #root, #main, .creator-content-wrapper, .builder-component, .builder-content, .builder-blocks, .builder-block, .amis-scope, .amis-routes-wrapper": {
358
+ "width": "100% !important",
359
+ "height": "auto !important",
360
+ "overflow": "visible !important",
361
+ "display": "block !important",
362
+ "position": "static !important"
363
+ },
364
+ ".resize-sensor, .no-print, .antd-Page-toolbar, .steedos-global-header-root": {
365
+ "display": "none !important"
366
+ },
367
+ ".steedos-instance-related-view-wrapper, .steedos-instance-related-view-wrapper .antd-Page-content, .steedos-instance-related-view-wrapper .antd-Page-main, .steedos-instance-related-view-wrapper .antd-Page-body, .steedos-instance-related-view-wrapper .steedos-instance-detail-wrapper, .steedos-instance-related-view-wrapper .antd-Service, .steedos-instance-related-view-wrapper .antd-Wrapper, .steedos-instance-related-view-wrapper .steedos-amis-instance-view, .steedos-instance-related-view-wrapper .steedos-amis-instance-view-body, .steedos-instance-related-view-wrapper .steedos-amis-instance-view-content, .steedos-instance-related-view-wrapper .liquid-amis-container": {
368
+ "display": "block !important",
369
+ "height": "auto !important",
370
+ "width": "100% !important",
371
+ "overflow": "visible !important",
372
+ "flex": "none !important",
373
+ "position": "static !important"
374
+ },
375
+ ".steedos-instance-related-view-wrapper .instance-form": {
376
+ "margin": "0 auto"
377
+ },
378
+ ".steedos-instance-related-view-wrapper .antd-Table-contentWrap": {
379
+ "overflow": "visible !important",
380
+ "height": "auto !important"
381
+ },
382
+ ".steedos-instance-related-view-wrapper .instance-form-view td": {
383
+ "border-width": "1px !important"
384
+ },
385
+ ".steedos-instance-related-view-wrapper .instance-approve-history .antd-Table-table tr td": {
386
+ "border-bottom": "1px solid #e8e8e8 !important"
387
+ }
355
388
  }
356
389
  },
357
390
  "wrapperCustomStyle": {}
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "@steedos-labs/plugin-workflow",
3
- "version": "3.0.15",
3
+ "version": "3.0.16",
4
4
  "main": "package.service.js",
5
5
  "license": "MIT",
6
6
  "scripts": {
7
7
  "build:watch": "tsc --watch",
8
- "release": "npm publish --registry https://registry.npmjs.org && npx cnpm sync @steedos-labs/plugin-workflow"
8
+ "release": "npm publish --registry https://registry.npmjs.org && npx cnpm sync @steedos-labs/plugin-workflow",
9
+ "export-templates": "node run.js export",
10
+ "convert-templates": "node convert-templates.js"
9
11
  },
10
12
  "dependencies": {
11
13
  "graphql-parse-resolve-info": "^4.12.3",
@@ -597,6 +597,73 @@ tbody .color-priority-muted *{
597
597
  }
598
598
 
599
599
 
600
+ .instance-form,.instance-template .td-title{
601
+ text-align: center;
602
+ }
603
+
604
+ .instance-form .td-childfield{
605
+ text-align: left;
606
+ }
607
+
608
+ .instance-template .table-page-body .antd-Form-label{
609
+ display: none !important;
610
+ }
611
+
612
+ .instance-form .form-table {
613
+ border-collapse: collapse;
614
+ border: 2px solid black;
615
+ }
616
+ .instance-form .form-table td {
617
+ border: 1px solid black;
618
+ padding: 4px 6px;
619
+ border-collapse: collapse;
620
+ }
621
+
622
+ .instance-form .td-childfield{
623
+ padding: 0 !important;
624
+ border: none !important;
625
+ }
626
+
627
+
628
+ .instance-form .steedos-input-table .antd-Table-table colgroup col:nth-child(2) {
629
+ width: 0px !important;
630
+ min-width: 0px !important;
631
+ }
632
+
633
+ .instance-form .steedos-input-table .antd-Table-table thead tr th:first-child {
634
+ width: 60px;
635
+ }
636
+
637
+ .instance-form .steedos-input-table .antd-Table-table thead .antd-Table-operationCell {
638
+ width: 0px !important;
639
+ }
640
+
641
+ .instance-form .steedos-input-table .antd-Table-table .steedos-input-table-column-operation {
642
+ border: none !important;
643
+ }
644
+
645
+ .instance-form .steedos-input-table .antd-Table-table .steedos-input-table-column-operation .antd-OperationField {
646
+ margin-left: -55px;
647
+ }
648
+
649
+ .instance-form td .loading {
650
+ display: none !important;
651
+ }
652
+
653
+
654
+ .instance-form .antd-Table-headToolbar{
655
+ display: none !important;
656
+ }
657
+
658
+ .table-page-title{
659
+ border: none !important;
660
+ }
661
+
662
+ .instance-name .page-title{
663
+ text-align: center;
664
+ border: none !important;
665
+ }
666
+
600
667
  /* 公共打印隐藏样式 */
601
668
  @media print {
602
669
  .no-print {
package/run.js ADDED
@@ -0,0 +1,388 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Workflow Template Export/Import Script
5
+ *
6
+ * This script exports and imports workflow templates (instance_template and print_template)
7
+ * from MongoDB to files for Blaze-to-Liquid conversion.
8
+ *
9
+ * Usage:
10
+ * node run.js export [flowId] [outputDir]
11
+ * node run.js import <flowId> [templateType] [inputDir]
12
+ *
13
+ * Examples:
14
+ * node run.js export # Export all flows
15
+ * node run.js export abc123 # Export specific flow
16
+ * node run.js export abc123 /custom/path # Export to custom directory
17
+ * node run.js import abc123 # Import both templates
18
+ * node run.js import abc123 instance_template # Import only instance_template
19
+ */
20
+ require('dotenv-flow').config({});
21
+ const { MongoClient } = require('mongodb');
22
+ const path = require('path');
23
+ const fs = require('fs').promises;
24
+
25
+ // MongoDB connection
26
+ let client = null;
27
+ let db = null;
28
+
29
+ // Connect to MongoDB using native driver
30
+ async function connectToMongoDB() {
31
+ if (db) {
32
+ return db;
33
+ }
34
+
35
+ const mongoUrl = process.env.MONGO_URL || 'mongodb://127.0.0.1:27017/steedos-plugins';
36
+ console.log('mongoUrl', mongoUrl)
37
+ try {
38
+ client = new MongoClient(mongoUrl, {
39
+ useNewUrlParser: true,
40
+ useUnifiedTopology: true,
41
+ directConnection: true
42
+ });
43
+ await client.connect();
44
+
45
+ // Extract database name from URL
46
+ const dbName = mongoUrl.split('/').pop().split('?')[0];
47
+ db = client.db(dbName);
48
+
49
+ console.log(`Connected to MongoDB: ${dbName}`);
50
+ return db;
51
+ } catch (error) {
52
+ console.error('Failed to connect to MongoDB:', error.message);
53
+ throw error;
54
+ }
55
+ }
56
+
57
+ // Close MongoDB connection
58
+ async function closeMongoDB() {
59
+ if (client) {
60
+ await client.close();
61
+ client = null;
62
+ db = null;
63
+ }
64
+ }
65
+
66
+ // Get collection from MongoDB
67
+ async function getCollection(name) {
68
+ const database = await connectToMongoDB();
69
+ return database.collection(name);
70
+ }
71
+
72
+ // Export templates to .blaze files
73
+ async function exportTemplates(flowId, outputDir) {
74
+ try {
75
+ const baseDir = outputDir || path.join(process.cwd(), 'flows-template');
76
+
77
+ // Create directory
78
+ try {
79
+ await fs.mkdir(baseDir, { recursive: true });
80
+ } catch (mkdirError) {
81
+ if (mkdirError.code !== 'EEXIST') {
82
+ throw new Error(`Failed to create directory ${baseDir}: ${mkdirError.message}`);
83
+ }
84
+ }
85
+
86
+ const flowsCollection = await getCollection('flows');
87
+ const formsCollection = await getCollection('forms');
88
+
89
+ // Build query
90
+ const query = {};
91
+ if (flowId) {
92
+ // Export specific flow by ID
93
+ query._id = flowId;
94
+ } else {
95
+ // Export all flows with templates (when flowId is not specified)
96
+ query.$or = [
97
+ { instance_template: { $exists: true, $ne: null, $ne: '' } },
98
+ { print_template: { $exists: true, $ne: null, $ne: '' } }
99
+ ];
100
+ }
101
+
102
+ const flows = await flowsCollection.find(query, {
103
+ projection: {
104
+ _id: 1,
105
+ name: 1,
106
+ instance_template: 1,
107
+ print_template: 1,
108
+ form: 1
109
+ }
110
+ }).toArray();
111
+
112
+ if (flows.length === 0) {
113
+ console.log('No flows found with templates.');
114
+ return;
115
+ }
116
+
117
+ if (flowId) {
118
+ console.log(`Found flow: ${flows[0]?.name || flowId}`);
119
+ } else {
120
+ console.log(`Found ${flows.length} flow(s) with templates to export.`);
121
+ }
122
+
123
+ const exportSummary = [];
124
+
125
+ for (const flow of flows) {
126
+ console.log(`\nExporting flow: ${flow.name} (${flow._id})`);
127
+
128
+ let fileCount = 0;
129
+ const flowSummary = {
130
+ id: flow._id,
131
+ name: flow.name,
132
+ templates: []
133
+ };
134
+
135
+ // Export instance_template
136
+ if (flow.instance_template) {
137
+ try {
138
+ const fileName = `${flow._id}.instance_template.blaze`;
139
+ const filePath = path.join(baseDir, fileName);
140
+ await fs.writeFile(filePath, flow.instance_template, 'utf8');
141
+ console.log(` ✓ ${fileName}`);
142
+ fileCount++;
143
+ flowSummary.templates.push('instance_template');
144
+ } catch (error) {
145
+ console.error(` ✗ Failed to write instance_template: ${error.message}`);
146
+ }
147
+ }
148
+
149
+ // Export print_template
150
+ if (flow.print_template) {
151
+ try {
152
+ const fileName = `${flow._id}.print_template.blaze`;
153
+ const filePath = path.join(baseDir, fileName);
154
+ await fs.writeFile(filePath, flow.print_template, 'utf8');
155
+ console.log(` ✓ ${fileName}`);
156
+ fileCount++;
157
+ flowSummary.templates.push('print_template');
158
+ } catch (error) {
159
+ console.error(` ✗ Failed to write print_template: ${error.message}`);
160
+ }
161
+ }
162
+
163
+ // Export field definitions
164
+ if (flow.form) {
165
+ const form = await formsCollection.findOne(
166
+ { _id: flow.form },
167
+ { projection: { 'current.fields': 1 } }
168
+ );
169
+
170
+ if (form && form.current && form.current.fields) {
171
+ try {
172
+ const fileName = `${flow._id}.fields.json`;
173
+ const filePath = path.join(baseDir, fileName);
174
+ await fs.writeFile(
175
+ filePath,
176
+ JSON.stringify(form.current.fields, null, 2),
177
+ 'utf8'
178
+ );
179
+ console.log(` ✓ ${fileName}`);
180
+ fileCount++;
181
+ flowSummary.templates.push('fields');
182
+ } catch (error) {
183
+ console.error(` ✗ Failed to write fields.json: ${error.message}`);
184
+ }
185
+ }
186
+ }
187
+
188
+ if (fileCount > 0) {
189
+ console.log(` Exported ${fileCount} file(s)`);
190
+ exportSummary.push(flowSummary);
191
+ }
192
+ }
193
+
194
+ console.log(`\n✅ Export completed. Files saved to: ${baseDir}`);
195
+
196
+ // Print summary
197
+ console.log('\n📋 导出清单:');
198
+ console.log('='.repeat(80));
199
+ exportSummary.forEach((item, index) => {
200
+ console.log(`${index + 1}. ${item.name} (${item.id})`);
201
+ console.log(` 模板: ${item.templates.join(', ')}`);
202
+ });
203
+ console.log('='.repeat(80));
204
+ console.log(`共导出 ${exportSummary.length} 个流程`);
205
+
206
+ // Save summary to file
207
+ const summaryFile = path.join(baseDir, 'export-summary.json');
208
+ await fs.writeFile(
209
+ summaryFile,
210
+ JSON.stringify(exportSummary, null, 2),
211
+ 'utf8'
212
+ );
213
+ console.log(`\n清单已保存至: ${summaryFile}`);
214
+ } catch (error) {
215
+ console.error(`\n❌ Export failed: ${error.message}`);
216
+ throw error;
217
+ }
218
+ }
219
+
220
+ // Import templates from .liquid files
221
+ async function importTemplates(flowId, templateType, inputDir) {
222
+ try {
223
+ if (!flowId) {
224
+ throw new Error('flowId is required for import');
225
+ }
226
+
227
+ const baseDir = inputDir || path.join(process.cwd(), 'flows-template');
228
+ const flowsCollection = await getCollection('flows');
229
+
230
+ // Verify flow exists
231
+ const flow = await flowsCollection.findOne(
232
+ { _id: flowId },
233
+ { projection: { _id: 1, name: 1, instance_template: 1, print_template: 1 } }
234
+ );
235
+
236
+ if (!flow) {
237
+ throw new Error(`Flow not found: ${flowId}`);
238
+ }
239
+
240
+ console.log(`\nImporting templates for flow: ${flow.name} (${flow._id})`);
241
+
242
+ const updateObj = {
243
+ modified: new Date()
244
+ };
245
+ const imported = [];
246
+
247
+ // Determine which templates to import
248
+ const templatesToImport = templateType
249
+ ? [templateType]
250
+ : ['instance_template', 'print_template'];
251
+
252
+ for (const type of templatesToImport) {
253
+ const liquidFile = path.join(baseDir, `${flowId}.${type}.liquid`);
254
+
255
+ try {
256
+ // Check if file exists and is readable
257
+ await fs.access(liquidFile, fs.constants.R_OK);
258
+
259
+ // Read the liquid file
260
+ const liquidContent = await fs.readFile(liquidFile, 'utf8');
261
+
262
+ // Backup old value if it exists
263
+ const backupField = `${type}_backup`;
264
+ if (flow[type]) {
265
+ updateObj[backupField] = flow[type];
266
+ console.log(` ℹ Backing up old ${type} to ${backupField}`);
267
+ }
268
+
269
+ // Set new value
270
+ updateObj[type] = liquidContent;
271
+
272
+ imported.push({
273
+ type,
274
+ file: `${flowId}.${type}.liquid`,
275
+ backed_up: !!flow[type]
276
+ });
277
+
278
+ console.log(` ✓ Imported ${type} from ${flowId}.${type}.liquid`);
279
+ } catch (fileError) {
280
+ if (fileError.code === 'ENOENT') {
281
+ // File doesn't exist, skip
282
+ console.log(` ⊗ ${type} file not found, skipping`);
283
+ continue;
284
+ }
285
+ if (fileError.code === 'EACCES') {
286
+ throw new Error(`Permission denied reading file ${liquidFile}`);
287
+ }
288
+ throw new Error(`Failed to read file ${liquidFile}: ${fileError.message}`);
289
+ }
290
+ }
291
+
292
+ if (imported.length === 0) {
293
+ throw new Error(`No template files found for flow ${flowId}`);
294
+ }
295
+
296
+ // Update the flow
297
+ await flowsCollection.updateOne(
298
+ { _id: flowId },
299
+ { $set: updateObj }
300
+ );
301
+
302
+ console.log(`\n✅ Import completed. Imported ${imported.length} template(s).`);
303
+ imported.forEach(item => {
304
+ console.log(` - ${item.type}: ${item.backed_up ? 'updated (old value backed up)' : 'created'}`);
305
+ });
306
+ } catch (error) {
307
+ console.error(`\n❌ Import failed: ${error.message}`);
308
+ throw error;
309
+ }
310
+ }
311
+
312
+ // Main function
313
+ async function main() {
314
+ const args = process.argv.slice(2);
315
+ const command = args[0];
316
+
317
+ if (!command || !['export', 'import'].includes(command)) {
318
+ console.log(`
319
+ Workflow Template Export/Import Script
320
+
321
+ Usage:
322
+ node run.js export [flowId] [outputDir]
323
+ node run.js import <flowId> [templateType] [inputDir]
324
+
325
+ Commands:
326
+ export Export templates to .blaze files
327
+ import Import templates from .liquid files
328
+
329
+ Export Examples:
330
+ node run.js export # Export all flows
331
+ node run.js export abc123 # Export specific flow
332
+ node run.js export abc123 /custom/path # Export to custom directory
333
+
334
+ Import Examples:
335
+ node run.js import abc123 # Import both templates
336
+ node run.js import abc123 instance_template # Import only instance_template
337
+ node run.js import abc123 print_template # Import only print_template
338
+
339
+ Directory Structure:
340
+ flows-template/
341
+ ├── {flowId}.instance_template.blaze # Exported Blaze template
342
+ ├── {flowId}.print_template.blaze # Exported Blaze template
343
+ ├── {flowId}.fields.json # Field definitions
344
+ ├── {flowId}.instance_template.liquid # Converted Liquid template (for import)
345
+ └── {flowId}.print_template.liquid # Converted Liquid template (for import)
346
+
347
+ Workflow:
348
+ 1. Export: node run.js export [flowId]
349
+ 2. Convert: Convert .blaze files to .liquid format (manually or with Copilot)
350
+ 3. Import: node run.js import <flowId>
351
+ `);
352
+ process.exit(command ? 1 : 0);
353
+ }
354
+
355
+ try {
356
+ if (command === 'export') {
357
+ const flowId = args[1];
358
+ const outputDir = args[2];
359
+ await exportTemplates(flowId, outputDir);
360
+ } else if (command === 'import') {
361
+ const flowId = args[1];
362
+ const templateType = args[2];
363
+ const inputDir = args[3];
364
+
365
+ // Validate templateType if provided
366
+ if (templateType && !['instance_template', 'print_template'].includes(templateType)) {
367
+ // Maybe it's the inputDir
368
+ await importTemplates(flowId, null, templateType);
369
+ } else {
370
+ await importTemplates(flowId, templateType, inputDir);
371
+ }
372
+ }
373
+
374
+ await closeMongoDB();
375
+ process.exit(0);
376
+ } catch (error) {
377
+ console.error('\n❌ Error:', error.message);
378
+ await closeMongoDB();
379
+ process.exit(1);
380
+ }
381
+ }
382
+
383
+ // Run if executed directly
384
+ if (require.main === module) {
385
+ main();
386
+ }
387
+
388
+ module.exports = { exportTemplates, importTemplates };
@@ -11,4 +11,5 @@ module.exports = {
11
11
  updateFormFields: require('./updateFormFields'),
12
12
  ...require('./getInstanceServiceSchema'),
13
13
  flow_copy: require('./flow_copy'),
14
+ migrateTemplates: require('./migrateTemplates'),
14
15
  }
@@ -0,0 +1,154 @@
1
+ const converter = require('../util/templateConverter');
2
+ const _ = require('lodash');
3
+ const { MongoClient } = require('mongodb');
4
+
5
+ // MongoDB connection
6
+ let client = null;
7
+ let db = null;
8
+
9
+ async function connectToMongoDB() {
10
+ if (db) {
11
+ return db;
12
+ }
13
+
14
+ const mongoUrl = process.env.MONGO_URL || 'mongodb://127.0.0.1:27017/steedos';
15
+ try {
16
+ client = new MongoClient(mongoUrl);
17
+ await client.connect();
18
+
19
+ const dbName = mongoUrl.split('/').pop().split('?')[0];
20
+ db = client.db(dbName);
21
+ return db;
22
+ } catch (error) {
23
+ console.error('Failed to connect to MongoDB:', error.message);
24
+ throw error;
25
+ }
26
+ }
27
+
28
+ async function getCollection(name) {
29
+ const database = await connectToMongoDB();
30
+ return database.collection(name);
31
+ }
32
+
33
+ module.exports = {
34
+ rest: {
35
+ method: 'GET',
36
+ fullPath: '/api/workflow/migrateTemplates'
37
+ },
38
+ params: {
39
+ batchSize: { type: 'number', optional: true, convert: true },
40
+ force: { type: 'boolean', optional: true, convert: true },
41
+ flowId: { type: 'string', optional: true }
42
+ },
43
+ async handler(ctx) {
44
+ const { user } = ctx.meta;
45
+
46
+ if (user.is_space_admin !== true) {
47
+ throw new Error('只有管理员才能执行此操作');
48
+ }
49
+
50
+ const { batchSize = 5000, force = false, flowId } = ctx.params;
51
+
52
+ const flowsCollection = await getCollection('flows');
53
+ const formsCollection = await getCollection('forms');
54
+
55
+ let flows = [];
56
+ if (flowId) {
57
+ flows = await flowsCollection.find({ _id: flowId }).toArray();
58
+ } else {
59
+ let query = {
60
+ $and: [
61
+ {
62
+ $or: [
63
+ { instance_template: { $exists: true, $ne: '' } },
64
+ { print_template: { $exists: true, $ne: '' } }
65
+ ]
66
+ }
67
+ ]
68
+ };
69
+
70
+ if (!force) {
71
+ query.$or = [
72
+ { migrate_status: { $ne: 'completed' } },
73
+ { migrate_status: { $exists: false } }
74
+ ];
75
+ }
76
+ flows = await flowsCollection.find(query).limit(batchSize).toArray();
77
+ }
78
+
79
+ const results = {
80
+ total: flows.length,
81
+ success: 0,
82
+ failed: 0,
83
+ skipped: 0,
84
+ details: []
85
+ };
86
+
87
+ for (const flow of flows) {
88
+ const flowId = flow._id;
89
+ const updateDoc = {};
90
+ let isChanged = false;
91
+
92
+ try {
93
+ let fields = {};
94
+ if (flow.form) {
95
+ const form = await formsCollection.findOne(
96
+ { _id: flow.form },
97
+ { projection: { 'current.fields': 1 } }
98
+ );
99
+ if (form && form.current && form.current.fields) {
100
+ fields = _.keyBy(form.current.fields, (f) => f.name || f.code);
101
+ fields._raw = form.current.fields;
102
+ }
103
+ }
104
+
105
+ if (!flow.backup_status) {
106
+ if (flow.instance_template) {
107
+ updateDoc.instance_template_backup = flow.instance_template;
108
+ }
109
+ if (flow.print_template) {
110
+ updateDoc.print_template_backup = flow.print_template;
111
+ }
112
+ updateDoc.backup_status = true;
113
+ isChanged = true;
114
+ }
115
+
116
+ const sourceInstance = updateDoc.instance_template_backup || flow.instance_template_backup || flow.instance_template;
117
+ const sourcePrint = updateDoc.print_template_backup || flow.print_template_backup || flow.print_template;
118
+
119
+ if (sourceInstance) {
120
+ const converted = converter.convertTemplate(sourceInstance, fields, false);
121
+ updateDoc.instance_template = converted;
122
+ isChanged = true;
123
+ }
124
+
125
+ if (sourcePrint) {
126
+ const converted = converter.convertTemplate(sourcePrint, fields, true);
127
+ updateDoc.print_template = converted;
128
+ isChanged = true;
129
+ }
130
+
131
+ updateDoc.migrate_status = 'completed';
132
+ updateDoc.migrated_at = new Date();
133
+
134
+ await flowsCollection.updateOne({ _id: flow._id }, { $set: updateDoc });
135
+
136
+ results.success++;
137
+ results.details.push({ id: flow._id, name: flow.name, status: 'success' });
138
+ } catch (error) {
139
+ console.error(`Error migrating flow ${flowId}:`, error);
140
+ results.failed++;
141
+ results.details.push({ id: flow._id, name: flow.name, status: 'error', error: error.message });
142
+
143
+ try {
144
+ await flowsCollection.updateOne(
145
+ { _id: flow._id },
146
+ { $set: { migrate_status: 'error', migrate_error: error.message } }
147
+ );
148
+ } catch (e) {}
149
+ }
150
+ }
151
+
152
+ return results;
153
+ }
154
+ }
@@ -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
+ };