@cyprnet/node-red-contrib-uibuilder-formgen 0.4.11

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE +22 -0
  3. package/README.md +58 -0
  4. package/docs/user-guide.html +565 -0
  5. package/examples/formgen-builder/src/index.html +921 -0
  6. package/examples/formgen-builder/src/index.js +1338 -0
  7. package/examples/portalsmith-formgen-example.json +531 -0
  8. package/examples/schema-builder-integration.json +109 -0
  9. package/examples/schemas/Banking/banking_fraud_report.json +102 -0
  10. package/examples/schemas/Banking/banking_kyc_update.json +59 -0
  11. package/examples/schemas/Banking/banking_loan_application.json +113 -0
  12. package/examples/schemas/Banking/banking_new_account.json +98 -0
  13. package/examples/schemas/Banking/banking_wire_transfer_request.json +94 -0
  14. package/examples/schemas/HR/hr_employee_change_form.json +65 -0
  15. package/examples/schemas/HR/hr_exit_interview.json +105 -0
  16. package/examples/schemas/HR/hr_job_application.json +166 -0
  17. package/examples/schemas/HR/hr_onboarding_request.json +140 -0
  18. package/examples/schemas/HR/hr_time_off_request.json +95 -0
  19. package/examples/schemas/HR/hr_training_request.json +70 -0
  20. package/examples/schemas/Healthcare/health_appointment_request.json +103 -0
  21. package/examples/schemas/Healthcare/health_incident_report.json +82 -0
  22. package/examples/schemas/Healthcare/health_lab_order_request.json +72 -0
  23. package/examples/schemas/Healthcare/health_medication_refill.json +72 -0
  24. package/examples/schemas/Healthcare/health_patient_intake.json +113 -0
  25. package/examples/schemas/IT/it_access_request.json +145 -0
  26. package/examples/schemas/IT/it_dhcp_reservation.json +175 -0
  27. package/examples/schemas/IT/it_dns_domain_external.json +192 -0
  28. package/examples/schemas/IT/it_dns_domain_internal.json +171 -0
  29. package/examples/schemas/IT/it_network_change_request.json +126 -0
  30. package/examples/schemas/IT/it_network_request-form.json +299 -0
  31. package/examples/schemas/IT/it_new_hardware_request.json +155 -0
  32. package/examples/schemas/IT/it_password_reset.json +133 -0
  33. package/examples/schemas/IT/it_software_license_request.json +93 -0
  34. package/examples/schemas/IT/it_static_ip_request.json +199 -0
  35. package/examples/schemas/IT/it_subnet_request_form.json +216 -0
  36. package/examples/schemas/Maintenance/maint_checklist.json +176 -0
  37. package/examples/schemas/Maintenance/maint_facility_issue_report.json +127 -0
  38. package/examples/schemas/Maintenance/maint_incident_intake.json +174 -0
  39. package/examples/schemas/Maintenance/maint_inventory_restock.json +79 -0
  40. package/examples/schemas/Maintenance/maint_safety_audit.json +92 -0
  41. package/examples/schemas/Maintenance/maint_vehicle_inspection.json +112 -0
  42. package/examples/schemas/Maintenance/maint_work_order.json +134 -0
  43. package/index.js +12 -0
  44. package/lib/licensing.js +254 -0
  45. package/nodes/portalsmith-license.html +40 -0
  46. package/nodes/portalsmith-license.js +23 -0
  47. package/nodes/uibuilder-formgen.html +261 -0
  48. package/nodes/uibuilder-formgen.js +598 -0
  49. package/package.json +47 -0
  50. package/scripts/normalize_schema_titles.py +77 -0
  51. package/templates/index.html.mustache +541 -0
  52. package/templates/index.js.mustache +1135 -0
@@ -0,0 +1,921 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>PortalSmith Schema Builder</title>
7
+
8
+ <!-- Bootstrap 4 CSS -->
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
10
+
11
+ <!-- Bootstrap-Vue CSS -->
12
+ <link rel="stylesheet" href="https://unpkg.com/bootstrap-vue@2.23.1/dist/bootstrap-vue.min.css">
13
+
14
+ <!-- Font Awesome -->
15
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
16
+
17
+ <style>
18
+ body {
19
+ background-color: #f5f5f5;
20
+ padding: 20px 0;
21
+ }
22
+ .builder-container {
23
+ max-width: 1400px;
24
+ margin: 0 auto;
25
+ }
26
+ .schema-meta {
27
+ background: white;
28
+ padding: 1.5rem;
29
+ border-radius: 0.25rem;
30
+ margin-bottom: 1.5rem;
31
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
32
+ }
33
+ .section-card {
34
+ background: white;
35
+ border: 1px solid #dee2e6;
36
+ border-radius: 0.25rem;
37
+ margin-bottom: 1rem;
38
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
39
+ }
40
+ .section-header {
41
+ background: #f8f9fa;
42
+ padding: 1rem;
43
+ border-bottom: 1px solid #dee2e6;
44
+ display: flex;
45
+ justify-content: space-between;
46
+ align-items: center;
47
+ }
48
+ .section-body {
49
+ padding: 1rem;
50
+ }
51
+ .field-item {
52
+ background: #f8f9fa;
53
+ padding: 0.75rem;
54
+ margin-bottom: 0.5rem;
55
+ border-radius: 0.25rem;
56
+ border-left: 3px solid #007bff;
57
+ display: flex;
58
+ justify-content: space-between;
59
+ align-items: center;
60
+ }
61
+ .field-info {
62
+ flex: 1;
63
+ }
64
+ .field-badges {
65
+ display: flex;
66
+ gap: 0.5rem;
67
+ align-items: center;
68
+ }
69
+ .badge {
70
+ font-size: 0.75rem;
71
+ }
72
+ .empty-state {
73
+ text-align: center;
74
+ padding: 2rem;
75
+ color: #6c757d;
76
+ }
77
+ .action-bar {
78
+ background: white;
79
+ padding: 1rem;
80
+ border-radius: 0.25rem;
81
+ margin-top: 1.5rem;
82
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
83
+ display: flex;
84
+ gap: 0.5rem;
85
+ flex-wrap: wrap;
86
+ }
87
+ .preview-panel {
88
+ background: white;
89
+ border: 1px solid #dee2e6;
90
+ border-radius: 0.25rem;
91
+ padding: 1rem;
92
+ margin-top: 1.5rem;
93
+ max-height: 600px;
94
+ overflow-y: auto;
95
+ }
96
+ </style>
97
+ </head>
98
+ <body>
99
+ <div id="app">
100
+ <div class="container-fluid builder-container">
101
+ <div class="d-flex justify-content-between align-items-center mb-4">
102
+ <h1 class="mb-0">
103
+ <i class="fa fa-wrench"></i> PortalSmith Schema Builder
104
+ </h1>
105
+ <div class="d-flex align-items-center">
106
+ <b-button variant="outline-danger" class="mr-2" @click="newSchema">
107
+ <i class="fa fa-file"></i> New Schema
108
+ </b-button>
109
+ <b-button variant="info" @click="showHelp = true">
110
+ <i class="fa fa-question-circle"></i> Help
111
+ </b-button>
112
+ </div>
113
+ </div>
114
+
115
+ <!-- Alert Area -->
116
+ <b-alert
117
+ v-model="showAlert"
118
+ :variant="alertVariant"
119
+ dismissible
120
+ class="mb-3"
121
+ >
122
+ <div v-html="alertMessage"></div>
123
+ </b-alert>
124
+
125
+ <!-- Schema Metadata -->
126
+ <div class="schema-meta">
127
+ <div class="d-flex justify-content-between align-items-center mb-3">
128
+ <h3 class="mb-0">Form Metadata</h3>
129
+ <b-button variant="link" size="sm" @click="showFieldHelp('metadata')" class="p-0">
130
+ <i class="fa fa-info-circle text-info"></i>
131
+ </b-button>
132
+ </div>
133
+ <b-form-row>
134
+ <b-col md="6">
135
+ <b-form-group label="Form ID" label-for="formId">
136
+ <b-form-input
137
+ id="formId"
138
+ v-model="schema.formId"
139
+ placeholder="my_form_id"
140
+ :state="formIdState"
141
+ />
142
+ <small class="form-text text-muted">
143
+ Unique identifier (alphanumeric, underscores, hyphens only).
144
+ Used as the form identifier in Node-RED.
145
+ </small>
146
+ </b-form-group>
147
+ </b-col>
148
+ <b-col md="6">
149
+ <b-form-group label="Form Title" label-for="title">
150
+ <b-form-input
151
+ id="title"
152
+ v-model="schema.title"
153
+ placeholder="My Form Title"
154
+ />
155
+ <small class="form-text text-muted">Displayed at the top of the generated form</small>
156
+ </b-form-group>
157
+ </b-col>
158
+ </b-form-row>
159
+ <b-form-group label="Description" label-for="description">
160
+ <b-form-textarea
161
+ id="description"
162
+ v-model="schema.description"
163
+ placeholder="Optional form description"
164
+ rows="2"
165
+ />
166
+ <small class="form-text text-muted">Optional description shown below the form title</small>
167
+ </b-form-group>
168
+ </div>
169
+
170
+ <!-- Sections -->
171
+ <div class="mb-3">
172
+ <div class="d-flex justify-content-between align-items-center mb-3">
173
+ <h3>Sections</h3>
174
+ <b-button variant="primary" @click="addSection">
175
+ <i class="fa fa-plus"></i> Add Section
176
+ </b-button>
177
+ </div>
178
+
179
+ <div v-if="schema.sections.length === 0" class="empty-state">
180
+ <i class="fa fa-inbox fa-3x mb-3"></i>
181
+ <p>No sections yet. Add your first section to get started.</p>
182
+ </div>
183
+
184
+ <div v-for="(section, sectionIdx) in schema.sections" :key="section.id" class="section-card">
185
+ <div class="section-header">
186
+ <div>
187
+ <strong>{{section.title || 'Untitled Section'}}</strong>
188
+ <small class="text-muted ml-2">({{section.fields.length}} fields)</small>
189
+ </div>
190
+ <div>
191
+ <b-button variant="outline-primary" size="sm" @click="editSection(sectionIdx)" class="mr-2">
192
+ <i class="fa fa-edit"></i> Edit
193
+ </b-button>
194
+ <b-button variant="outline-success" size="sm" @click="addField(sectionIdx)" class="mr-2">
195
+ <i class="fa fa-plus"></i> Add Field
196
+ </b-button>
197
+ <b-button variant="outline-danger" size="sm" @click="deleteSection(sectionIdx)">
198
+ <i class="fa fa-trash"></i> Delete
199
+ </b-button>
200
+ </div>
201
+ </div>
202
+ <div class="section-body">
203
+ <p v-if="section.description" class="text-muted mb-3">{{section.description}}</p>
204
+
205
+ <div v-if="section.fields.length === 0" class="empty-state">
206
+ <p class="mb-0">No fields in this section. Add a field to get started.</p>
207
+ </div>
208
+
209
+ <div v-for="(field, fieldIdx) in section.fields" :key="field.id" class="field-item">
210
+ <div class="field-info">
211
+ <strong>{{field.label || field.id}}</strong>
212
+ <div class="field-badges mt-1">
213
+ <b-badge :variant="field.required ? 'danger' : 'secondary'">
214
+ {{field.type}}
215
+ </b-badge>
216
+ <b-badge v-if="field.required" variant="warning">Required</b-badge>
217
+ <b-badge v-if="field.validate" variant="info">{{field.validate}}</b-badge>
218
+ <small class="text-muted ml-2">{{field.id}}</small>
219
+ </div>
220
+ </div>
221
+ <div>
222
+ <b-button variant="outline-primary" size="sm" @click="editField(sectionIdx, fieldIdx)" class="mr-2">
223
+ <i class="fa fa-edit"></i>
224
+ </b-button>
225
+ <b-button variant="outline-danger" size="sm" @click="deleteField(sectionIdx, fieldIdx)">
226
+ <i class="fa fa-trash"></i>
227
+ </b-button>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <!-- Action Bar -->
235
+ <div class="action-bar">
236
+ <b-button variant="success" @click="saveSchema" class="mr-2" :disabled="!canSave">
237
+ <i class="fa fa-save"></i> Save Schema
238
+ </b-button>
239
+ <b-button variant="info" @click="loadSchema" class="mr-2">
240
+ <i class="fa fa-folder-open"></i> Load Schema
241
+ </b-button>
242
+ <b-button variant="success" @click="importJSON" class="mr-2">
243
+ <i class="fa fa-file-import"></i> Import JSON
244
+ </b-button>
245
+ <b-button variant="info" @click="importCSV" class="mr-2">
246
+ <i class="fa fa-file-csv"></i> Import CSV
247
+ </b-button>
248
+ <b-button variant="secondary" @click="exportJSON" class="mr-2">
249
+ <i class="fa fa-file-export"></i> Export JSON
250
+ </b-button>
251
+ <b-button variant="primary" @click="generateForm" :disabled="!canGenerate">
252
+ <i class="fa fa-magic"></i> Generate Form
253
+ </b-button>
254
+ <b-button variant="outline-secondary" @click="showHelp = true" class="ml-auto">
255
+ <i class="fa fa-question-circle"></i> Help
256
+ </b-button>
257
+ </div>
258
+
259
+ <!-- JSON Preview (Collapsible) -->
260
+ <b-card class="mt-3">
261
+ <div slot="header" class="d-flex justify-content-between align-items-center">
262
+ <span>JSON Preview</span>
263
+ <b-button variant="outline-secondary" size="sm" @click="showPreview = !showPreview">
264
+ {{showPreview ? 'Hide' : 'Show'}}
265
+ </b-button>
266
+ </div>
267
+ <div v-if="showPreview" class="preview-panel">
268
+ <pre>{{jsonPreview}}</pre>
269
+ </div>
270
+ </b-card>
271
+ </div>
272
+
273
+ <!-- Section Editor Modal -->
274
+ <b-modal
275
+ id="section-modal"
276
+ :title="editingSection !== null ? 'Edit Section' : 'Add Section'"
277
+ @ok="saveSection"
278
+ @cancel="cancelEditSection"
279
+ >
280
+ <b-form-group label="Section ID" label-for="section-id">
281
+ <b-form-input
282
+ id="section-id"
283
+ v-model="currentSection.id"
284
+ placeholder="section_id"
285
+ />
286
+ </b-form-group>
287
+ <b-form-group label="Section Title" label-for="section-title">
288
+ <b-form-input
289
+ id="section-title"
290
+ v-model="currentSection.title"
291
+ placeholder="Section Title"
292
+ />
293
+ </b-form-group>
294
+ <b-form-group label="Description" label-for="section-desc">
295
+ <b-form-textarea
296
+ id="section-desc"
297
+ v-model="currentSection.description"
298
+ placeholder="Optional description"
299
+ rows="2"
300
+ />
301
+ </b-form-group>
302
+ </b-modal>
303
+
304
+ <!-- Field Editor Modal -->
305
+ <b-modal
306
+ id="field-modal"
307
+ :title="editingField ? 'Edit Field' : 'Add Field'"
308
+ size="lg"
309
+ @ok="saveField"
310
+ @cancel="cancelEditField"
311
+ >
312
+ <b-form-row>
313
+ <b-col md="6">
314
+ <b-form-group label="Field ID" label-for="field-id">
315
+ <b-form-input
316
+ id="field-id"
317
+ v-model="currentField.id"
318
+ placeholder="field_id"
319
+ :state="fieldIdState"
320
+ />
321
+ <small class="form-text text-muted">Unique identifier</small>
322
+ </b-form-group>
323
+ </b-col>
324
+ <b-col md="6">
325
+ <b-form-group label="Field Label" label-for="field-label">
326
+ <b-form-input
327
+ id="field-label"
328
+ v-model="currentField.label"
329
+ placeholder="Field Label"
330
+ />
331
+ </b-form-group>
332
+ </b-col>
333
+ </b-form-row>
334
+
335
+ <b-form-row>
336
+ <b-col md="6">
337
+ <b-form-group label="Field Type" label-for="field-type">
338
+ <b-form-select
339
+ id="field-type"
340
+ v-model="currentField.type"
341
+ :options="fieldTypes"
342
+ @change="onFieldTypeChange"
343
+ />
344
+ <small class="form-text text-muted">
345
+ <strong>Text:</strong> Single-line input (can be email, tel, url, password)<br>
346
+ <strong>Textarea:</strong> Multi-line text input<br>
347
+ <strong>Number:</strong> Numeric input with min/max/step<br>
348
+ <strong>Select:</strong> Dropdown menu with options<br>
349
+ <strong>Radio:</strong> Radio button group with options<br>
350
+ <strong>Checkbox:</strong> Boolean checkbox<br>
351
+ <strong>Date:</strong> Date picker
352
+ </small>
353
+ </b-form-group>
354
+ </b-col>
355
+ <b-col md="6">
356
+ <b-form-group label="Input Type" label-for="input-type" v-if="currentField.type === 'text'">
357
+ <b-form-select
358
+ id="input-type"
359
+ v-model="currentField.inputType"
360
+ :options="inputTypes"
361
+ />
362
+ </b-form-group>
363
+ </b-col>
364
+ </b-form-row>
365
+
366
+ <b-form-row>
367
+ <b-col md="6">
368
+ <b-form-checkbox v-model="currentField.required">
369
+ Required Field
370
+ </b-form-checkbox>
371
+ </b-col>
372
+ <b-col md="6">
373
+ <b-form-group label="Default Value" label-for="default-value">
374
+ <b-form-input
375
+ id="default-value"
376
+ v-model="currentField.defaultValue"
377
+ :type="currentField.type === 'number' ? 'number' : 'text'"
378
+ placeholder="Default value"
379
+ />
380
+ </b-form-group>
381
+ </b-col>
382
+ </b-form-row>
383
+
384
+ <b-form-group label="Placeholder" label-for="placeholder">
385
+ <b-form-input
386
+ id="placeholder"
387
+ v-model="currentField.placeholder"
388
+ placeholder="Placeholder text"
389
+ />
390
+ </b-form-group>
391
+
392
+ <b-form-group label="Help Text" label-for="help">
393
+ <b-form-textarea
394
+ id="help"
395
+ v-model="currentField.help"
396
+ placeholder="Help text shown below field"
397
+ rows="2"
398
+ />
399
+ </b-form-group>
400
+
401
+ <b-form-row v-if="currentField.type === 'textarea'">
402
+ <b-col md="6">
403
+ <b-form-group label="Rows" label-for="rows">
404
+ <b-form-input
405
+ id="rows"
406
+ v-model.number="currentField.rows"
407
+ type="number"
408
+ min="1"
409
+ max="20"
410
+ />
411
+ </b-form-group>
412
+ </b-col>
413
+ </b-form-row>
414
+
415
+ <b-form-row v-if="currentField.type === 'number'">
416
+ <b-col md="4">
417
+ <b-form-group label="Min" label-for="min">
418
+ <b-form-input
419
+ id="min"
420
+ v-model.number="currentField.min"
421
+ type="number"
422
+ />
423
+ </b-form-group>
424
+ </b-col>
425
+ <b-col md="4">
426
+ <b-form-group label="Max" label-for="max">
427
+ <b-form-input
428
+ id="max"
429
+ v-model.number="currentField.max"
430
+ type="number"
431
+ />
432
+ </b-form-group>
433
+ </b-col>
434
+ <b-col md="4">
435
+ <b-form-group label="Step" label-for="step">
436
+ <b-form-input
437
+ id="step"
438
+ v-model.number="currentField.step"
439
+ type="number"
440
+ />
441
+ </b-form-group>
442
+ </b-col>
443
+ </b-form-row>
444
+
445
+ <b-form-group
446
+ v-if="currentField.type === 'select' || currentField.type === 'radio'"
447
+ label="Options"
448
+ label-for="options"
449
+ >
450
+ <small class="form-text text-muted mb-2">
451
+ <strong>Required for Select/Radio fields.</strong> Each option needs a <strong>value</strong> (stored data) and <strong>text</strong> (display label).
452
+ For a blank/default option, use empty value with text like "Select...".
453
+ </small>
454
+ <div v-if="currentField.options.length === 0" class="alert alert-warning mb-2">
455
+ <i class="fa fa-exclamation-triangle"></i> No options defined. Add at least one option for this field to work.
456
+ </div>
457
+ <div v-for="(option, optIdx) in currentField.options" :key="'option-' + optIdx" class="mb-3 border rounded p-3 bg-light">
458
+ <div class="d-flex justify-content-between align-items-center mb-2">
459
+ <small class="text-muted font-weight-bold">Option {{optIdx + 1}}</small>
460
+ <b-button variant="outline-danger" size="sm" @click="removeOption(optIdx)" title="Remove this option">
461
+ <i class="fa fa-trash"></i> Remove
462
+ </b-button>
463
+ </div>
464
+ <b-form-row>
465
+ <b-col md="5">
466
+ <b-form-group label="Value" :label-for="'opt-value-' + optIdx" label-size="sm" class="mb-2">
467
+ <b-form-input
468
+ :id="'opt-value-' + optIdx"
469
+ v-model="option.value"
470
+ placeholder="e.g., 'static' or 'option1'"
471
+ @paste="handleOptionPaste($event, optIdx, 'value')"
472
+ />
473
+ <small class="form-text text-muted">The stored value (what gets saved)</small>
474
+ </b-form-group>
475
+ </b-col>
476
+ <b-col md="7">
477
+ <b-form-group label="Display Text" :label-for="'opt-text-' + optIdx" label-size="sm" class="mb-2">
478
+ <b-form-input
479
+ :id="'opt-text-' + optIdx"
480
+ v-model="option.text"
481
+ placeholder="e.g., 'Static IP' or 'Option 1'"
482
+ @paste="handleOptionPaste($event, optIdx, 'text')"
483
+ />
484
+ <small class="form-text text-muted">What users see in the dropdown</small>
485
+ </b-form-group>
486
+ </b-col>
487
+ </b-form-row>
488
+ </div>
489
+ <div class="d-flex justify-content-between align-items-center mt-2">
490
+ <b-button variant="outline-primary" size="sm" @click="addOption">
491
+ <i class="fa fa-plus"></i> Add Option
492
+ </b-button>
493
+ <b-button variant="outline-info" size="sm" @click="showBulkOptions = !showBulkOptions">
494
+ <i class="fa fa-paste"></i> Bulk Add
495
+ </b-button>
496
+ </div>
497
+ <div v-if="showBulkOptions" class="mt-2 p-2 border rounded bg-light">
498
+ <b-form-group label="Paste Options (one per line)" label-for="bulk-options" class="mb-2">
499
+ <b-form-textarea
500
+ id="bulk-options"
501
+ v-model="bulkOptionsText"
502
+ placeholder='value="static" | text="Static IP"&#10;value="dhcp" | text="DHCP"&#10;value="reserved" | text="Reserved"'
503
+ rows="5"
504
+ />
505
+ <small class="form-text text-muted">
506
+ Format: <code>value="xxx" | text="YYY"</code> (one per line) or just <code>Option 1</code> (will use same for value and text)
507
+ </small>
508
+ </b-form-group>
509
+ <b-button variant="primary" size="sm" @click="parseBulkOptions" class="mr-2">
510
+ <i class="fa fa-check"></i> Parse & Add
511
+ </b-button>
512
+ <b-button variant="outline-secondary" size="sm" @click="showBulkOptions = false; bulkOptionsText = ''">
513
+ Cancel
514
+ </b-button>
515
+ </div>
516
+ <div class="mt-2">
517
+ <small class="text-muted">
518
+ <strong>Example:</strong> value="developer" | text="Software Developer"
519
+ (user sees "Software Developer", form stores "developer")
520
+ </small>
521
+ </div>
522
+ </b-form-group>
523
+
524
+ <!-- Key-Value Pairs Editor -->
525
+ <b-form-group
526
+ v-if="currentField.type === 'keyvalue'"
527
+ label="Key-Value Configuration"
528
+ label-for="keyvalue-config"
529
+ >
530
+ <b-form-row class="mb-3">
531
+ <b-col md="6">
532
+ <b-form-group label="Input Mode" label-for="keyvalue-mode">
533
+ <b-form-select
534
+ id="keyvalue-mode"
535
+ v-model="currentField.keyvalueMode"
536
+ :options="[
537
+ { value: 'pairs', text: 'Individual Pairs' },
538
+ { value: 'delimiter', text: 'Delimiter Text (field=value)' }
539
+ ]"
540
+ @change="onKeyValueModeChange"
541
+ />
542
+ <small class="form-text text-muted">
543
+ <strong>Individual Pairs:</strong> Users add/edit pairs one at a time<br>
544
+ <strong>Delimiter Text:</strong> Users enter "key=value" format in a text area
545
+ </small>
546
+ </b-form-group>
547
+ </b-col>
548
+ <b-col md="6" v-if="currentField.keyvalueMode === 'delimiter'">
549
+ <b-form-group label="Delimiter" label-for="keyvalue-delimiter">
550
+ <b-form-input
551
+ id="keyvalue-delimiter"
552
+ v-model="currentField.keyvalueDelimiter"
553
+ placeholder="="
554
+ maxlength="5"
555
+ />
556
+ <small class="form-text text-muted">
557
+ Character(s) that separate key from value (e.g., "=", ":", "|")
558
+ </small>
559
+ </b-form-group>
560
+ </b-col>
561
+ </b-form-row>
562
+
563
+ <!-- Individual Pairs Mode -->
564
+ <div v-if="!currentField.keyvalueMode || currentField.keyvalueMode === 'pairs'">
565
+ <small class="form-text text-muted mb-2">
566
+ <strong>Required for Key-Value fields.</strong> Each pair has a <strong>key</strong> (field name) and <strong>value</strong> (field value).
567
+ Users can add multiple pairs dynamically in the form.
568
+ </small>
569
+ <div v-if="currentField.pairs.length === 0" class="alert alert-warning mb-2">
570
+ <i class="fa fa-exclamation-triangle"></i> No pairs defined. Add at least one pair for this field to work.
571
+ </div>
572
+ <div v-for="(pair, pairIdx) in currentField.pairs" :key="'pair-' + pairIdx" class="mb-3 border rounded p-3 bg-light">
573
+ <div class="d-flex justify-content-between align-items-center mb-2">
574
+ <small class="text-muted font-weight-bold">Pair {{pairIdx + 1}}</small>
575
+ <b-button variant="outline-danger" size="sm" @click="removePair(pairIdx)" title="Remove this pair">
576
+ <i class="fa fa-trash"></i> Remove
577
+ </b-button>
578
+ </div>
579
+ <b-form-row>
580
+ <b-col md="5">
581
+ <b-form-group label="Key" :label-for="'pair-key-' + pairIdx" label-size="sm" class="mb-2">
582
+ <b-form-input
583
+ :id="'pair-key-' + pairIdx"
584
+ v-model="pair.key"
585
+ placeholder="e.g., 'name' or 'ip_address'"
586
+ />
587
+ <small class="form-text text-muted">The key/field name</small>
588
+ </b-form-group>
589
+ </b-col>
590
+ <b-col md="7">
591
+ <b-form-group label="Value" :label-for="'pair-value-' + pairIdx" label-size="sm" class="mb-2">
592
+ <b-form-input
593
+ :id="'pair-value-' + pairIdx"
594
+ v-model="pair.value"
595
+ placeholder="e.g., 'John Doe' or '192.168.1.1'"
596
+ />
597
+ <small class="form-text text-muted">The value for this key</small>
598
+ </b-form-group>
599
+ </b-col>
600
+ </b-form-row>
601
+ </div>
602
+ <b-button variant="outline-primary" size="sm" @click="addPair">
603
+ <i class="fa fa-plus"></i> Add Pair
604
+ </b-button>
605
+ <div class="mt-2">
606
+ <small class="text-muted">
607
+ <strong>Note:</strong> These are default/example pairs. Users can add, edit, and remove pairs in the generated form.
608
+ </small>
609
+ </div>
610
+ </div>
611
+
612
+ <!-- Delimiter Mode -->
613
+ <div v-if="currentField.keyvalueMode === 'delimiter'" class="mt-3">
614
+ <b-alert variant="info" show class="mb-3">
615
+ <strong>Delimiter Text Mode</strong><br>
616
+ Users will enter key-value pairs in a text area, one per line, using the format:
617
+ <code>key{{currentField.keyvalueDelimiter || '='}}value</code>
618
+ </b-alert>
619
+
620
+ <b-alert variant="success" show class="mb-3">
621
+ <i class="fa fa-info-circle"></i> <strong>Preview:</strong> In the generated form, users will see an editable textarea where they can enter key-value pairs, one per line, using the format: <code>key{{currentField.keyvalueDelimiter || '='}}value</code>
622
+ </b-alert>
623
+
624
+ <div class="border rounded p-3 bg-light mt-3">
625
+ <strong>Example Format:</strong>
626
+ <pre class="mb-0 mt-2 bg-white p-2 border rounded"><code>name{{currentField.keyvalueDelimiter || '='}}John Doe
627
+ ip_address{{currentField.keyvalueDelimiter || '='}}192.168.1.1
628
+ email{{currentField.keyvalueDelimiter || '='}}john@example.com</code></pre>
629
+ </div>
630
+ <small class="form-text text-muted mt-2">
631
+ <strong>Note:</strong> The delimiter field above controls what character separates the key from the value.
632
+ Users can add as many pairs as needed in the generated form.
633
+ </small>
634
+ </div>
635
+ </b-form-group>
636
+
637
+ <b-form-group label="Validation" label-for="validate">
638
+ <b-form-select
639
+ id="validate"
640
+ v-model="currentField.validate"
641
+ :options="validationTypes"
642
+ />
643
+ <b-form-input
644
+ v-if="currentField.validate === 'regex'"
645
+ v-model="currentField.validatePattern"
646
+ placeholder="Regex pattern (e.g., ^[A-Z]{2}\\d{4}$)"
647
+ class="mt-2"
648
+ />
649
+ <small class="form-text text-muted">
650
+ <strong>Email:</strong> Validates email format (user@domain.com)<br>
651
+ <strong>Phone:</strong> Validates phone number format (10+ digits)<br>
652
+ <strong>Regex:</strong> Custom pattern validation (e.g., ^[A-Z]{2}\\d{4}$ for codes like AB1234)
653
+ </small>
654
+ </b-form-group>
655
+
656
+ <b-form-group label="Validation Message" label-for="validate-msg" v-if="currentField.validate">
657
+ <b-form-input
658
+ id="validate-msg"
659
+ v-model="currentField.validateMessage"
660
+ placeholder="Custom error message"
661
+ />
662
+ </b-form-group>
663
+ </b-modal>
664
+
665
+ <!-- CSV Import Modal -->
666
+ <b-modal
667
+ id="csv-modal"
668
+ title="Import from CSV"
669
+ size="lg"
670
+ @ok="processCSV"
671
+ @cancel="cancelCSV"
672
+ >
673
+ <b-form-group label="CSV Data" label-for="csv-data">
674
+ <b-form-textarea
675
+ id="csv-data"
676
+ v-model="csvData"
677
+ rows="10"
678
+ placeholder="Paste CSV data here. First row should be headers: id,label,type,required,section"
679
+ />
680
+ <small class="form-text text-muted">
681
+ Expected columns: id, label, type, required (true/false), section (optional), placeholder, help, validate
682
+ </small>
683
+ </b-form-group>
684
+ <b-alert variant="info" show>
685
+ <strong>CSV Format:</strong><br>
686
+ id,label,type,required,section,placeholder<br>
687
+ fullName,Full Name,text,true,personal,John Doe<br>
688
+ email,Email Address,text,true,personal,john@example.com
689
+ </b-alert>
690
+ </b-modal>
691
+
692
+ <!-- JSON Import Modal -->
693
+ <b-modal
694
+ id="json-modal"
695
+ title="Import from JSON"
696
+ size="lg"
697
+ @ok="processJSON"
698
+ @cancel="cancelJSON"
699
+ >
700
+ <b-form-group label="JSON Schema" label-for="json-data">
701
+ <b-form-textarea
702
+ id="json-data"
703
+ v-model="jsonData"
704
+ rows="15"
705
+ placeholder="Paste JSON schema here"
706
+ />
707
+ <small class="form-text text-muted">
708
+ Paste a complete schema JSON. Must include schemaVersion, formId, title, and sections array.
709
+ </small>
710
+ </b-form-group>
711
+ </b-modal>
712
+
713
+ <!-- Save Schema Modal -->
714
+ <b-modal
715
+ id="save-modal"
716
+ title="Save Schema"
717
+ @ok="handleSaveOk"
718
+ @cancel="cancelSave"
719
+ @hide="cancelSave"
720
+ ok-title="Save"
721
+ cancel-title="Cancel"
722
+ >
723
+ <b-form-group label="Schema Name" label-for="save-name">
724
+ <b-form-input
725
+ id="save-name"
726
+ v-model="saveName"
727
+ placeholder="My Schema"
728
+ />
729
+ <small class="form-text text-muted">
730
+ Enter a name for this schema. It will be saved in your browser's local storage.
731
+ </small>
732
+ </b-form-group>
733
+ <b-alert variant="info" show>
734
+ <strong>Saved Schemas:</strong>
735
+ <ul class="mb-0 mt-2" v-if="savedSchemas.length > 0">
736
+ <li v-for="(saved, idx) in savedSchemas" :key="idx">
737
+ {{saved.name}} <small class="text-muted">({{saved.date}})</small>
738
+ <b-button variant="link" size="sm" @click="deleteSavedSchema(idx)" class="p-0 ml-2 text-danger">
739
+ <i class="fa fa-trash"></i>
740
+ </b-button>
741
+ </li>
742
+ </ul>
743
+ <p v-else class="mb-0">No saved schemas yet.</p>
744
+ </b-alert>
745
+ </b-modal>
746
+
747
+ <!-- Load Schema Modal -->
748
+ <b-modal
749
+ id="load-modal"
750
+ title="Load Saved Schema"
751
+ @ok="handleLoadOk"
752
+ @cancel="cancelLoad"
753
+ @hide="cancelLoad"
754
+ ok-title="Load"
755
+ cancel-title="Cancel"
756
+ >
757
+ <b-form-group label="Select Schema" label-for="load-select" v-if="savedSchemas.length > 0">
758
+ <b-form-select
759
+ id="load-select"
760
+ v-model="selectedLoadSchema"
761
+ :options="savedSchemaOptions"
762
+ />
763
+ <small class="form-text text-muted mt-2">
764
+ Selected schema will replace your current work. Make sure to save first if needed.
765
+ </small>
766
+ </b-form-group>
767
+ <b-alert variant="warning" show v-else>
768
+ No saved schemas found. Save a schema first to load it later.
769
+ </b-alert>
770
+ </b-modal>
771
+
772
+ <!-- Help Modal -->
773
+ <b-modal
774
+ id="help-modal"
775
+ title="Schema Builder Help"
776
+ size="lg"
777
+ v-model="showHelp"
778
+ @hide="showHelp = false"
779
+ ok-only
780
+ ok-title="Got it!"
781
+ >
782
+ <div>
783
+ <h4>Getting Started</h4>
784
+ <ol>
785
+ <li><strong>Set Form Metadata:</strong> Enter a unique Form ID (alphanumeric, underscores, hyphens), title, and optional description.</li>
786
+ <li><strong>Add Sections:</strong> Click "Add Section" to create form sections. Each section groups related fields.</li>
787
+ <li><strong>Add Fields:</strong> Click "Add Field" within a section to add form fields. Configure field properties in the editor.</li>
788
+ <li><strong>Generate Form:</strong> Click "Generate Form" to send the schema to Node-RED and create the form.</li>
789
+ </ol>
790
+
791
+ <h4 class="mt-4">Field Types</h4>
792
+ <ul>
793
+ <li><strong>Text:</strong> Single-line text input. Can be configured as email, tel, url, or password via Input Type.</li>
794
+ <li><strong>Textarea:</strong> Multi-line text input. Set number of rows (default: 3).</li>
795
+ <li><strong>Number:</strong> Numeric input. Set min, max, and step values.</li>
796
+ <li><strong>Select:</strong> Dropdown menu. Requires options (value/text pairs). See "Options" section below.</li>
797
+ <li><strong>Radio:</strong> Radio button group. Requires options (value/text pairs). See "Options" section below.</li>
798
+ <li><strong>Checkbox:</strong> Boolean checkbox. Default value: false.</li>
799
+ <li><strong>Date:</strong> HTML5 date picker.</li>
800
+ </ul>
801
+
802
+ <h4 class="mt-4">Field Properties</h4>
803
+ <ul>
804
+ <li><strong>Field ID:</strong> Unique identifier within the section (alphanumeric, underscores, hyphens). Used as the data key.</li>
805
+ <li><strong>Label:</strong> Display label shown to users.</li>
806
+ <li><strong>Required:</strong> Mark field as required for form validation.</li>
807
+ <li><strong>Placeholder:</strong> Hint text shown in empty fields.</li>
808
+ <li><strong>Help Text:</strong> Additional guidance shown below the field.</li>
809
+ <li><strong>Default Value:</strong> Pre-filled value when form loads.</li>
810
+ <li><strong>Validation:</strong> Email, phone, or custom regex pattern validation.</li>
811
+ </ul>
812
+
813
+ <h4 class="mt-4">Options for Select/Radio Fields</h4>
814
+ <p>Select and Radio fields require <strong>options</strong> to be defined. Each option has two parts:</p>
815
+ <ul>
816
+ <li><strong>Value:</strong> The actual data stored when this option is selected (e.g., "developer", "option1", "")</li>
817
+ <li><strong>Text:</strong> The label displayed to users (e.g., "Software Developer", "Option 1", "Select...")</li>
818
+ </ul>
819
+ <p><strong>How to add options:</strong></p>
820
+ <ol>
821
+ <li>Select field type "Select" or "Radio"</li>
822
+ <li>The Options section will appear in the field editor</li>
823
+ <li>Click "Add Option" to add a new option row</li>
824
+ <li>Fill in the <strong>value</strong> (what gets stored) and <strong>text</strong> (what users see)</li>
825
+ <li>Add as many options as needed</li>
826
+ <li>Remove options using the X button</li>
827
+ </ol>
828
+ <p><strong>Example:</strong></p>
829
+ <pre class="bg-light p-2 rounded">Value: "developer" | Text: "Software Developer"
830
+ Value: "designer" | Text: "UI/UX Designer"
831
+ Value: "" | Text: "Select a position..." (blank/default option)</pre>
832
+ <p><strong>Note:</strong> For a blank/default option (like "Select..."), use an empty value with descriptive text.</p>
833
+
834
+ <h4 class="mt-4">CSV Import Format</h4>
835
+ <p>CSV files should have the following columns:</p>
836
+ <ul>
837
+ <li><strong>Required:</strong> id, label, type</li>
838
+ <li><strong>Optional:</strong> required (true/false), section (groups fields), placeholder, help, validate</li>
839
+ </ul>
840
+ <p><strong>Example:</strong></p>
841
+ <pre class="bg-light p-2 rounded">id,label,type,required,section,placeholder,help,validate
842
+ fullName,Full Name,text,true,personal,John Doe,Enter legal name,
843
+ email,Email Address,text,true,personal,john@example.com,,email
844
+ phone,Phone Number,text,true,personal,+1 555-1234,Include country code,phone</pre>
845
+
846
+ <h4 class="mt-4">JSON Schema Structure</h4>
847
+ <p>Schemas must follow this structure:</p>
848
+ <pre class="bg-light p-2 rounded">{
849
+ "schemaVersion": "1.0",
850
+ "formId": "my_form",
851
+ "title": "My Form",
852
+ "description": "Optional description",
853
+ "sections": [
854
+ {
855
+ "id": "section1",
856
+ "title": "Section Title",
857
+ "description": "Optional",
858
+ "fields": [
859
+ {
860
+ "id": "field1",
861
+ "label": "Field Label",
862
+ "type": "text",
863
+ "required": true
864
+ }
865
+ ]
866
+ }
867
+ ],
868
+ "actions": []
869
+ }</pre>
870
+
871
+ <h4 class="mt-4">Validation Rules</h4>
872
+ <ul>
873
+ <li><strong>Form ID:</strong> Must be unique, alphanumeric with underscores/hyphens only</li>
874
+ <li><strong>Section ID:</strong> Must be unique within the form</li>
875
+ <li><strong>Field ID:</strong> Must be unique within its section</li>
876
+ <li><strong>Select/Radio:</strong> Must have at least one option defined</li>
877
+ <li><strong>Required Fields:</strong> Will be validated before form submission</li>
878
+ </ul>
879
+
880
+ <h4 class="mt-4">Tips</h4>
881
+ <ul>
882
+ <li>Use descriptive section titles to organize related fields</li>
883
+ <li>Add help text to guide users on expected input formats</li>
884
+ <li>Use validation (email, phone, regex) to ensure data quality</li>
885
+ <li>Export JSON to save your schema for reuse</li>
886
+ <li>Import existing schemas to modify or use as templates</li>
887
+ <li>Check the JSON Preview to see the final schema structure</li>
888
+ </ul>
889
+
890
+ <h4 class="mt-4">Integration</h4>
891
+ <p>After clicking "Generate Form", the schema is sent to Node-RED. Connect your flow:</p>
892
+ <ol>
893
+ <li>Builder uibuilder node → Switch node (route by type: 'generate')</li>
894
+ <li>Switch → Function node (prepare message for formgen node)</li>
895
+ <li>Function → uibuilder-formgen node</li>
896
+ <li>formgen node → Builder uibuilder node (for success response)</li>
897
+ </ol>
898
+ <p>See <code>examples/schema-builder-integration.json</code> for a complete example flow.</p>
899
+ </div>
900
+ </b-modal>
901
+ </div>
902
+
903
+ <!-- jQuery (required for Bootstrap) -->
904
+ <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script>
905
+
906
+ <!-- Vue 2 -->
907
+ <script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.min.js"></script>
908
+
909
+ <!-- Bootstrap 4 JS -->
910
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-Fy6S3B9q64WdZWQUiU+q4/2Lc9npb8tCaSX9FK7E8HnRr0Jz8D6OP9dO5Vg3Q9ct" crossorigin="anonymous"></script>
911
+
912
+ <!-- Bootstrap-Vue JS -->
913
+ <script src="https://unpkg.com/bootstrap-vue@2.23.1/dist/bootstrap-vue.min.js"></script>
914
+
915
+ <!-- uibuilder client script -->
916
+ <script defer src="../uibuilder/uibuilder.iife.min.js"></script>
917
+
918
+ <!-- Builder Logic -->
919
+ <script defer src="./index.js"></script>
920
+ </body>
921
+ </html>