@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,77 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Normalize example schema titles to ASCII (remove “UTF-8 fancy” punctuation).
4
+
5
+ Currently replaces common unicode punctuation (e.g., em-dash "—") with ASCII.
6
+ It then strips any remaining non-ASCII characters from the title.
7
+
8
+ Usage:
9
+ python3 scripts/normalize_schema_titles.py
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import os
16
+ from pathlib import Path
17
+
18
+
19
+ REPLACEMENTS = {
20
+ "\u2014": "-", # em dash —
21
+ "\u2013": "-", # en dash –
22
+ "\u2212": "-", # minus sign −
23
+ "\u00a0": " ", # non-breaking space
24
+ "\u2018": "'", # left single quote
25
+ "\u2019": "'", # right single quote
26
+ "\u201c": '"', # left double quote
27
+ "\u201d": '"', # right double quote
28
+ "\u2026": "...", # ellipsis
29
+ }
30
+
31
+
32
+ def normalize_title(title: str) -> str:
33
+ out = title
34
+ for k, v in REPLACEMENTS.items():
35
+ out = out.replace(k, v)
36
+ # Strip any remaining non-ascii characters
37
+ out = out.encode("ascii", "ignore").decode("ascii")
38
+ # Collapse double spaces created by removals
39
+ out = " ".join(out.split())
40
+ return out
41
+
42
+
43
+ def main() -> int:
44
+ repo_root = Path(__file__).resolve().parents[1]
45
+ schemas_root = repo_root / "examples" / "schemas"
46
+ if not schemas_root.exists():
47
+ print(f"ERROR: schemas folder not found: {schemas_root}")
48
+ return 2
49
+
50
+ changed = 0
51
+ scanned = 0
52
+ for path in schemas_root.rglob("*.json"):
53
+ scanned += 1
54
+ try:
55
+ data = json.loads(path.read_text(encoding="utf-8"))
56
+ except Exception as e:
57
+ print(f"SKIP (invalid json): {path}: {e}")
58
+ continue
59
+
60
+ title = data.get("title")
61
+ if not isinstance(title, str):
62
+ continue
63
+
64
+ new_title = normalize_title(title)
65
+ if new_title != title:
66
+ data["title"] = new_title
67
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
68
+ changed += 1
69
+
70
+ print(f"Scanned {scanned} schema files. Updated titles in {changed} files.")
71
+ return 0
72
+
73
+
74
+ if __name__ == "__main__":
75
+ raise SystemExit(main())
76
+
77
+
@@ -0,0 +1,541 @@
1
+ <!doctype html>
2
+ <html lang="en" data-theme="[[themeMode]]">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <!-- SPDX-License-Identifier: MIT -->
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <title>[[title]]</title>
8
+ <meta name="color-scheme" content="light dark">
9
+
10
+ <!-- Bootstrap 4 CSS -->
11
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" integrity="sha384-xOolHFLEh07PJGoPkLv1IbcEPTNtaed2xpHsD9ESMhqIYd0nLMwNLD69Npy4HI+N" crossorigin="anonymous">
12
+
13
+ <!-- Bootstrap-Vue CSS -->
14
+ <link rel="stylesheet" href="https://unpkg.com/bootstrap-vue@2.23.1/dist/bootstrap-vue.min.css">
15
+
16
+ <style>
17
+ /* PortalSmith theming
18
+ themeMode: [[themeMode]] ('auto' | 'light' | 'dark')
19
+ Uses CSS variables so we can skin Bootstrap 4 components without replacing Bootstrap.
20
+ */
21
+ :root {
22
+ --ps-bg: #f5f5f5;
23
+ --ps-surface: #ffffff;
24
+ --ps-text: #212529;
25
+ --ps-muted: #6c757d;
26
+ --ps-border: #dee2e6;
27
+ --ps-input-bg: #ffffff;
28
+ --ps-input-text: #212529;
29
+ --ps-input-border: #ced4da;
30
+ --ps-code-bg: rgba(0,0,0,0.04);
31
+ --ps-link: #0d6efd;
32
+ --ps-focus: rgba(13,110,253,0.25);
33
+ }
34
+
35
+ html[data-theme="dark"] {
36
+ --ps-bg: #0f1115;
37
+ --ps-surface: #171a21;
38
+ --ps-text: #e6eaf2;
39
+ --ps-muted: #aab3c2;
40
+ --ps-border: #2a2f3a;
41
+ --ps-input-bg: #11141a;
42
+ --ps-input-text: #e6eaf2;
43
+ --ps-input-border: #3a4150;
44
+ --ps-code-bg: rgba(255,255,255,0.08);
45
+ --ps-link: #8ab4ff;
46
+ --ps-focus: rgba(138,180,255,0.25);
47
+ }
48
+
49
+ /* Auto mode: follow OS preference when theme isn't forced */
50
+ @media (prefers-color-scheme: dark) {
51
+ html:not([data-theme]),
52
+ html[data-theme="auto"] {
53
+ --ps-bg: #0f1115;
54
+ --ps-surface: #171a21;
55
+ --ps-text: #e6eaf2;
56
+ --ps-muted: #aab3c2;
57
+ --ps-border: #2a2f3a;
58
+ --ps-input-bg: #11141a;
59
+ --ps-input-text: #e6eaf2;
60
+ --ps-input-border: #3a4150;
61
+ --ps-code-bg: rgba(255,255,255,0.08);
62
+ --ps-link: #8ab4ff;
63
+ --ps-focus: rgba(138,180,255,0.25);
64
+ }
65
+ }
66
+
67
+ body {
68
+ background-color: var(--ps-bg);
69
+ color: var(--ps-text);
70
+ padding: 20px 0;
71
+ }
72
+ a { color: var(--ps-link); }
73
+ .text-muted { color: var(--ps-muted) !important; }
74
+ .border { border-color: var(--ps-border) !important; }
75
+
76
+ /* Bootstrap components */
77
+ .card,
78
+ .modal-content,
79
+ .dropdown-menu,
80
+ .list-group-item {
81
+ background-color: var(--ps-surface);
82
+ color: var(--ps-text);
83
+ border-color: var(--ps-border);
84
+ }
85
+
86
+ /* Tables */
87
+ .table {
88
+ color: var(--ps-text);
89
+ }
90
+ .table th,
91
+ .table td {
92
+ color: var(--ps-text);
93
+ border-color: var(--ps-border) !important;
94
+ }
95
+ .table thead th {
96
+ background-color: var(--ps-surface);
97
+ color: var(--ps-text);
98
+ border-color: var(--ps-border) !important;
99
+ }
100
+ .table-bordered {
101
+ border-color: var(--ps-border) !important;
102
+ }
103
+
104
+ .form-control,
105
+ .custom-select {
106
+ background-color: var(--ps-input-bg);
107
+ color: var(--ps-input-text);
108
+ border-color: var(--ps-input-border);
109
+ }
110
+ .form-control:focus,
111
+ .custom-select:focus {
112
+ border-color: var(--ps-link);
113
+ box-shadow: 0 0 0 .2rem var(--ps-focus);
114
+ }
115
+
116
+ pre, code {
117
+ color: var(--ps-text);
118
+ }
119
+ code {
120
+ background: var(--ps-code-bg);
121
+ padding: 0.1rem 0.25rem;
122
+ border-radius: 0.2rem;
123
+ }
124
+
125
+ /* Header layout */
126
+ .ps-header {
127
+ display: flex;
128
+ justify-content: space-between;
129
+ align-items: center;
130
+ gap: 1rem;
131
+ margin-bottom: 1rem;
132
+ }
133
+ .ps-logo {
134
+ max-height: 48px;
135
+ max-width: 220px;
136
+ width: auto;
137
+ height: auto;
138
+ object-fit: contain;
139
+ }
140
+ .ps-watermark {
141
+ position: fixed;
142
+ right: 14px;
143
+ bottom: 14px;
144
+ z-index: 9999;
145
+ background: rgba(0,0,0,0.08);
146
+ border: 1px solid rgba(0,0,0,0.10);
147
+ color: var(--ps-text);
148
+ padding: 6px 10px;
149
+ border-radius: 8px;
150
+ font-size: 12px;
151
+ backdrop-filter: blur(2px);
152
+ }
153
+ html[data-theme="dark"] .ps-watermark {
154
+ background: rgba(255,255,255,0.10);
155
+ border-color: rgba(255,255,255,0.14);
156
+ }
157
+ .form-container {
158
+ max-width: 800px;
159
+ margin: 0 auto;
160
+ }
161
+ .field-error {
162
+ color: #dc3545;
163
+ font-size: 0.875rem;
164
+ margin-top: 0.25rem;
165
+ }
166
+ .section-header {
167
+ border-bottom: 2px solid #007bff;
168
+ padding-bottom: 0.5rem;
169
+ margin-bottom: 1.5rem;
170
+ margin-top: 2rem;
171
+ }
172
+ .section-header:first-child {
173
+ margin-top: 0;
174
+ }
175
+ .action-buttons {
176
+ margin-top: 2rem;
177
+ padding-top: 1rem;
178
+ border-top: 1px solid #dee2e6;
179
+ display: flex;
180
+ flex-wrap: wrap;
181
+ align-items: center;
182
+ gap: 0.5rem;
183
+ }
184
+ </style>
185
+ </head>
186
+ <body>
187
+ <div id="app">
188
+ <div class="container form-container">
189
+ <div class="ps-header">
190
+ <h1 class="mb-0">[[title]]</h1>
191
+ [[#logoUrl]]
192
+ <img class="ps-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
193
+ [[/logoUrl]]
194
+ [[^logoUrl]]
195
+ [[^licensed]]
196
+ <svg class="ps-logo" viewBox="0 0 420 96" role="img" aria-label="PortalSmith">
197
+ <rect x="0" y="0" width="420" height="96" rx="14" fill="var(--ps-surface)" stroke="var(--ps-border)"/>
198
+ <text x="24" y="58" font-size="34" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-text)">PortalSmith</text>
199
+ <text x="24" y="78" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="var(--ps-muted)">FormGen</text>
200
+ </svg>
201
+ [[/licensed]]
202
+ [[/logoUrl]]
203
+ </div>
204
+ <p v-if="description" class="text-muted mb-4">[[description]]</p>
205
+
206
+ <!-- Status Alert Area -->
207
+ <div
208
+ v-show="showAlert"
209
+ class="alert alert-dismissible fade show mb-3"
210
+ :class="'alert-' + (alertVariant || 'info')"
211
+ role="alert"
212
+ >
213
+ <span v-html="alertMessage"></span>
214
+ <button type="button" class="close" @click="showAlert = false" aria-label="Close">
215
+ <span aria-hidden="true">&times;</span>
216
+ </button>
217
+ </div>
218
+
219
+ <!-- Result View -->
220
+ <div v-if="showResult" class="card mb-3">
221
+ <div class="card-body">
222
+ <div class="d-flex justify-content-between align-items-center mb-2">
223
+ <h3 class="mb-0">Result</h3>
224
+ <div class="btn-group" role="group" aria-label="Result actions">
225
+ <button type="button" class="btn btn-outline-secondary" @click="showResult = false">
226
+ <i class="fa fa-arrow-left"></i> Back to form
227
+ </button>
228
+ <button type="button" class="btn btn-outline-primary" @click="downloadResultJson" :disabled="!lastResult">
229
+ <i class="fa fa-download"></i> Download JSON
230
+ </button>
231
+ </div>
232
+ </div>
233
+ <div v-if="lastResultStatus" class="text-muted mb-2">
234
+ HTTP Status: {{lastResultStatus}}
235
+ </div>
236
+
237
+ <div class="btn-group mb-2" role="group" aria-label="Result view">
238
+ <button type="button" class="btn btn-sm" :class="resultView === 'table' ? 'btn-primary' : 'btn-outline-primary'" @click="resultView = 'table'">
239
+ Table
240
+ </button>
241
+ <button type="button" class="btn btn-sm" :class="resultView === 'json' ? 'btn-primary' : 'btn-outline-primary'" @click="resultView = 'json'">
242
+ JSON
243
+ </button>
244
+ </div>
245
+
246
+ <div v-if="resultView === 'table'">
247
+ <div v-if="resultRows.length === 0" class="text-muted">
248
+ No structured fields to display.
249
+ </div>
250
+ <div v-else class="table-responsive">
251
+ <table class="table table-sm table-bordered mb-0">
252
+ <thead>
253
+ <tr>
254
+ <th style="width:30%;">Field</th>
255
+ <th>Value</th>
256
+ </tr>
257
+ </thead>
258
+ <tbody>
259
+ <tr v-for="row in resultRows" :key="row.key">
260
+ <th class="font-weight-normal">{{row.label}}</th>
261
+ <td style="white-space:pre-wrap;">{{row.value}}</td>
262
+ </tr>
263
+ </tbody>
264
+ </table>
265
+ </div>
266
+ </div>
267
+
268
+ <pre v-else class="border rounded p-2 mb-0" style="white-space:pre-wrap;max-height:520px;overflow:auto;">{{formattedResult}}</pre>
269
+ </div>
270
+ </div>
271
+
272
+ <!-- Form Card -->
273
+ <div v-else class="card">
274
+ <div class="card-body">
275
+ <form @submit.prevent="handleSubmit">
276
+ <!-- Dynamic Sections -->
277
+ <div v-for="section in schema.sections" :key="section.id" class="form-section">
278
+ <h3 v-if="section.title" class="section-header">{{section.title}}</h3>
279
+ <p v-if="section.description" class="text-muted mb-3">{{section.description}}</p>
280
+
281
+ <!-- Dynamic Fields -->
282
+ <div v-for="field in visibleFieldsBySection[section.id]" :key="field.id" class="form-group">
283
+ <label :for="field.id" class="font-weight-bold">
284
+ {{field.label || field.id}}
285
+ <span v-if="field.required" class="text-danger">*</span>
286
+ </label>
287
+
288
+ <!-- Text -->
289
+ <input
290
+ v-if="field.type === 'text'"
291
+ :id="field.id"
292
+ v-model="formData[field.id]"
293
+ :type="field.inputType || 'text'"
294
+ :placeholder="field.placeholder"
295
+ class="form-control"
296
+ @blur="validateField(field)"
297
+ >
298
+
299
+ <!-- Textarea -->
300
+ <textarea
301
+ v-else-if="field.type === 'textarea'"
302
+ :id="field.id"
303
+ v-model="formData[field.id]"
304
+ :placeholder="field.placeholder"
305
+ class="form-control"
306
+ :rows="field.rows || 3"
307
+ @blur="validateField(field)"
308
+ ></textarea>
309
+
310
+ <!-- Number -->
311
+ <input
312
+ v-else-if="field.type === 'number'"
313
+ :id="field.id"
314
+ v-model.number="formData[field.id]"
315
+ type="number"
316
+ :placeholder="field.placeholder"
317
+ :min="field.min"
318
+ :max="field.max"
319
+ :step="field.step"
320
+ class="form-control"
321
+ @blur="validateField(field)"
322
+ >
323
+
324
+ <!-- Select -->
325
+ <select
326
+ v-else-if="field.type === 'select'"
327
+ :id="field.id"
328
+ v-model="formData[field.id]"
329
+ class="form-control"
330
+ @change="validateField(field)"
331
+ >
332
+ <option value="">-- select --</option>
333
+ <option
334
+ v-for="(opt, optIdx) in (field.options || [])"
335
+ :key="field.id + '-opt-' + optIdx"
336
+ :value="opt.value"
337
+ >{{opt.text}}</option>
338
+ </select>
339
+
340
+ <!-- Checkbox -->
341
+ <div v-else-if="field.type === 'checkbox'" class="custom-control custom-checkbox">
342
+ <input
343
+ type="checkbox"
344
+ class="custom-control-input"
345
+ :id="field.id"
346
+ v-model="formData[field.id]"
347
+ @change="validateField(field)"
348
+ >
349
+ <label class="custom-control-label" :for="field.id">{{field.checkboxLabel || field.label}}</label>
350
+ </div>
351
+
352
+ <!-- Radio -->
353
+ <div v-else-if="field.type === 'radio'">
354
+ <div
355
+ v-for="(opt, optIdx) in (field.options || [])"
356
+ :key="field.id + '-radio-' + optIdx"
357
+ class="custom-control custom-radio"
358
+ >
359
+ <input
360
+ type="radio"
361
+ class="custom-control-input"
362
+ :id="field.id + '-radio-' + optIdx"
363
+ :name="field.id"
364
+ :value="opt.value"
365
+ v-model="formData[field.id]"
366
+ @change="validateField(field)"
367
+ >
368
+ <label class="custom-control-label" :for="field.id + '-radio-' + optIdx">{{opt.text}}</label>
369
+ </div>
370
+ </div>
371
+
372
+ <!-- Date -->
373
+ <input
374
+ v-else-if="field.type === 'date'"
375
+ :id="field.id"
376
+ v-model="formData[field.id]"
377
+ type="date"
378
+ class="form-control"
379
+ @blur="validateField(field)"
380
+ >
381
+
382
+ <!-- Key-Value Pairs -->
383
+ <div v-else-if="field.type === 'keyvalue'">
384
+ <!-- Individual Pairs Mode -->
385
+ <div v-if="!field.keyvalueMode || field.keyvalueMode === 'pairs'" class="keyvalue-pairs">
386
+ <div
387
+ v-for="(pair, pairIdx) in formData[field.id]"
388
+ :key="field.id + '-pair-' + pairIdx"
389
+ class="mb-2 border rounded p-2 bg-light"
390
+ >
391
+ <div class="form-row">
392
+ <div class="col-md-5 mb-2">
393
+ <label class="small mb-1">Key</label>
394
+ <input type="text" class="form-control form-control-sm" v-model="pair.key" placeholder="key">
395
+ </div>
396
+ <div class="col-md-7 mb-2">
397
+ <label class="small mb-1">Value</label>
398
+ <input type="text" class="form-control form-control-sm" v-model="pair.value" placeholder="value">
399
+ </div>
400
+ </div>
401
+ <button type="button" class="btn btn-outline-danger btn-sm" @click="removeKeyValuePair(field.id, pairIdx)">
402
+ Remove
403
+ </button>
404
+ </div>
405
+ <button type="button" class="btn btn-outline-primary btn-sm" @click="addKeyValuePair(field.id)">
406
+ Add Pair
407
+ </button>
408
+ </div>
409
+
410
+ <!-- Delimiter Text Mode -->
411
+ <div v-else-if="field.keyvalueMode === 'delimiter'">
412
+ <textarea
413
+ :id="field.id"
414
+ v-model="formData[field.id]"
415
+ class="form-control"
416
+ :placeholder="'key' + (field.keyvalueDelimiter || '=') + 'value (one per line)'"
417
+ :rows="field.rows || 5"
418
+ @blur="validateField(field)"
419
+ ></textarea>
420
+ <small class="form-text text-muted mt-2">
421
+ Enter key-value pairs, one per line, using format: <code>key{{field.keyvalueDelimiter || '='}}value</code>
422
+ </small>
423
+ </div>
424
+ </div>
425
+
426
+ <!-- Unknown -->
427
+ <div v-else class="alert alert-warning py-2">
428
+ Unknown field type: {{field.type}} for field {{field.id}}
429
+ </div>
430
+
431
+ <!-- Field Error Message -->
432
+ <div v-if="fieldErrors[field.id]" class="field-error">
433
+ {{fieldErrors[field.id]}}
434
+ </div>
435
+
436
+ <!-- Field Help Text -->
437
+ <small v-if="field.help" class="form-text text-muted">
438
+ {{field.help}}
439
+ </small>
440
+ </div>
441
+ </div>
442
+
443
+ <!-- Action Buttons -->
444
+ <div class="action-buttons">
445
+ <div class="btn-group mr-2 mb-2" role="group" aria-label="Draft actions">
446
+ <button type="button" class="btn btn-primary" @click="handleSaveDraft" :disabled="saving">
447
+ <i class="fa fa-save"></i> Save Draft
448
+ </button>
449
+ <!-- Use a label+input to open file picker reliably (some browsers block programmatic input.click()) -->
450
+ <label
451
+ for="ps-draft-file-input"
452
+ class="btn btn-secondary mb-0"
453
+ :class="loading ? 'disabled' : ''"
454
+ :style="loading ? { pointerEvents: 'none', opacity: 0.65 } : null"
455
+ >
456
+ <i class="fa fa-folder-open"></i> Load Draft
457
+ </label>
458
+ </div>
459
+
460
+ <input
461
+ id="ps-draft-file-input"
462
+ type="file"
463
+ accept="application/json,.json"
464
+ style="display:none"
465
+ @change="handleDraftFileSelected"
466
+ >
467
+
468
+ <button type="submit" class="btn btn-success mr-2 mb-2" :disabled="submitting || hasErrors">
469
+ <i class="fa fa-paper-plane"></i> Submit
470
+ </button>
471
+
472
+ <button type="button" class="btn btn-outline-warning mr-2 mb-2" @click="handleClearForm" :disabled="submitting">
473
+ <i class="fa fa-eraser"></i> Clear Form
474
+ </button>
475
+
476
+ <!-- CopyBlock Actions -->
477
+ <template v-for="action in copyBlockActions">
478
+ <button
479
+ type="button"
480
+ :key="action.id"
481
+ class="btn btn-info mr-2 mb-2"
482
+ @click="handleCopyBlock(action)"
483
+ >
484
+ <i class="fa fa-copy"></i> {{action.label || 'Copy Block'}}
485
+ </button>
486
+ </template>
487
+
488
+ <!-- Export Buttons -->
489
+ <div class="dropdown mb-2 d-inline-block" v-if="exportFormats.length > 0">
490
+ <button
491
+ type="button"
492
+ class="btn btn-outline-secondary dropdown-toggle"
493
+ data-toggle="dropdown"
494
+ aria-haspopup="true"
495
+ aria-expanded="false"
496
+ >
497
+ <i class="fa fa-download"></i> Export
498
+ </button>
499
+ <div class="dropdown-menu">
500
+ <button
501
+ v-for="format in exportFormats"
502
+ :key="format"
503
+ type="button"
504
+ class="dropdown-item"
505
+ @click="handleExport(format)"
506
+ >
507
+ {{format.toUpperCase()}}
508
+ </button>
509
+ </div>
510
+ </div>
511
+ </div>
512
+ </form>
513
+ </div>
514
+ </div>
515
+ <!-- License watermark (FREE mode) -->
516
+ <div v-show="license && license.watermark" class="ps-watermark">
517
+ Powered by <strong>PortalSmith FormGen</strong>
518
+ </div>
519
+ </div>
520
+ </div>
521
+
522
+ <!-- jQuery (required by Bootstrap-Vue) -->
523
+ <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
524
+
525
+ <!-- Vue 2 -->
526
+ <script src="https://cdn.jsdelivr.net/npm/vue@2.7.14/dist/vue.min.js"></script>
527
+
528
+ <!-- Bootstrap 4 JS (required for Bootstrap-Vue) -->
529
+ <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>
530
+
531
+ <!-- Bootstrap-Vue JS -->
532
+ <script src="https://unpkg.com/bootstrap-vue@2.23.1/dist/bootstrap-vue.min.js"></script>
533
+
534
+ <!-- uibuilder client script (uibuilder v7 IIFE) -->
535
+ <script defer src="../uibuilder/uibuilder.iife.min.js"></script>
536
+
537
+ <!-- Portal Logic -->
538
+ <script defer src="./index.js"></script>
539
+ </body>
540
+ </html>
541
+