@cyprnet/node-red-contrib-uibuilder-formgen 0.5.29 → 0.5.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +10 -0
- package/docs/user-guide.html +513 -52
- package/examples/formgen-builder/src/index.html +15 -6
- package/examples/formgen-builder/src/index.js +19 -1
- package/examples/formgen-builder-uib2/src/index.html +15 -6
- package/examples/formgen-builder-uib2/src/index.js +19 -1
- package/package.json +1 -1
- package/templates/index.js.mustache +24 -4
- package/templates/index.v3.js.mustache +48 -4
package/CHANGELOG.md
CHANGED
|
@@ -51,6 +51,31 @@ All notable changes to this package will be documented in this file.
|
|
|
51
51
|
|
|
52
52
|
- Schema Builder: make field move controls always visible by using text arrows (↑/↓) instead of relying on Font Awesome icon rendering.
|
|
53
53
|
|
|
54
|
+
## 0.5.30
|
|
55
|
+
|
|
56
|
+
- Schema Builder: Regex validation now always shows a dedicated pattern input, and existing <code>validate:"regex:<pattern>"</code> fields correctly load the pattern for editing.
|
|
57
|
+
|
|
58
|
+
## 0.5.31
|
|
59
|
+
|
|
60
|
+
- Schema Builder: fixed Vue 2 reactivity when editing older fields that did not include <code>validate</code>/<code>validateMessage</code> properties. Selecting “Regex Pattern” now reliably reveals the pattern input.
|
|
61
|
+
|
|
62
|
+
## 0.5.32
|
|
63
|
+
|
|
64
|
+
- Schema Builder: Regex Pattern input is now rendered as its own form group (not an inline block), avoiding cases where the input existed in the DOM but was not visible due to modal layout/CSS interactions.
|
|
65
|
+
|
|
66
|
+
## 0.5.33
|
|
67
|
+
|
|
68
|
+
- Validation: Vue 3 portals now support <code>validate</code> (email/phone/regex) the same way Vue 2 portals do.
|
|
69
|
+
- Validation: regex validators now accept both <code>regex:<pattern></code> and <code>regex:/<pattern>/<flags></code>. Invalid regex patterns now surface a visible validation error instead of silently passing.
|
|
70
|
+
|
|
71
|
+
## 0.5.34
|
|
72
|
+
|
|
73
|
+
- Schema Builder: Regex Pattern placeholder text now shows the user-typed form (<code>\.</code>) instead of the JSON-escaped form (<code>\\.</code>).
|
|
74
|
+
|
|
75
|
+
## 0.5.35
|
|
76
|
+
|
|
77
|
+
- Docs: README + offline user guide now explicitly call out that generated portals require copying <code>form.schema.json</code> alongside <code>index.html</code>/<code>index.js</code>.
|
|
78
|
+
|
|
54
79
|
## 0.5.21
|
|
55
80
|
|
|
56
81
|
- Legacy uibuilder 2.x: fixed vendor library paths (<code>../vendor/...</code>) in the uib2 portal template and uib2 Schema Builder so uibuilder comms work and <code>lookup:get</code> messages emit correctly.
|
package/README.md
CHANGED
|
@@ -38,6 +38,16 @@ Then restart Node-RED.
|
|
|
38
38
|
2. Send a schema into `uibuilder-formgen` (legacy Vue 2) or `uibuilder-formgen-v3` (Vue 3) as `msg.schema` (or use the example flow).
|
|
39
39
|
3. Open `/uibuilder/<instance>/` in your browser.
|
|
40
40
|
|
|
41
|
+
### Important: the portal needs `form.schema.json`
|
|
42
|
+
|
|
43
|
+
Generated portals are a 3-file set that must stay together in the uibuilder instance’s served folder:
|
|
44
|
+
|
|
45
|
+
- `src/index.html`
|
|
46
|
+
- `src/index.js`
|
|
47
|
+
- `src/form.schema.json`
|
|
48
|
+
|
|
49
|
+
If you copy a portal to another uibuilder instance (or commit/restore from git), make sure you copy **all three**. The portal loads the schema at runtime and will not work correctly if `form.schema.json` is missing.
|
|
50
|
+
|
|
41
51
|
### Node differences (legacy vs v3)
|
|
42
52
|
|
|
43
53
|
- **`uibuilder-formgen` (legacy)**: generates portals using **Vue 2 + Bootstrap 4**
|
package/docs/user-guide.html
CHANGED
|
@@ -162,9 +162,11 @@
|
|
|
162
162
|
<li><a href="#install">Install & upgrade</a></li>
|
|
163
163
|
<li><a href="#licensing">Licensing (Free vs Licensed)</a></li>
|
|
164
164
|
<li><a href="#folders">Folder locations (standard + Projects)</a></li>
|
|
165
|
+
<li><a href="#portalsmith-setup">PortalSmith setup (end-to-end)</a></li>
|
|
165
166
|
<li><a href="#schema-builder">Schema Builder (form designer)</a></li>
|
|
166
167
|
<li><a href="#schemas">Schema examples library (industry templates)</a></li>
|
|
167
168
|
<li><a href="#schema-reference">Schema reference (field types)</a></li>
|
|
169
|
+
<li><a href="#lookup-contracts">Lookup / Auto-fill (message contracts)</a></li>
|
|
168
170
|
<li><a href="#generate">Generating a portal (uibuilder-formgen)</a></li>
|
|
169
171
|
<li><a href="#customize">Customization options (theme, logo, submit, API proxy)</a></li>
|
|
170
172
|
<li><a href="#submit-results">Submitting & results page</a></li>
|
|
@@ -272,19 +274,49 @@
|
|
|
272
274
|
|
|
273
275
|
<section id="install">
|
|
274
276
|
<h2>Install & upgrade</h2>
|
|
275
|
-
<h3>
|
|
276
|
-
<
|
|
277
|
-
<li>
|
|
278
|
-
<li>
|
|
279
|
-
<li><strong>
|
|
280
|
-
</
|
|
277
|
+
<h3>Prerequisites</h3>
|
|
278
|
+
<ul>
|
|
279
|
+
<li><strong>Node-RED is installed</strong> and you know your <code>userDir</code> (commonly <code>~/.node-red</code>).</li>
|
|
280
|
+
<li><strong>Node.js + npm</strong> are installed on the host that runs Node-RED.</li>
|
|
281
|
+
<li><strong>uibuilder</strong> is installed (v7+ recommended; uibuilder 2.x supported via the legacy generator node).</li>
|
|
282
|
+
</ul>
|
|
281
283
|
|
|
282
|
-
<h3>Install
|
|
284
|
+
<h3>Install via the Node-RED palette (UI)</h3>
|
|
283
285
|
<ol>
|
|
284
|
-
<li>
|
|
285
|
-
<li>
|
|
286
|
-
<li>
|
|
286
|
+
<li>Node-RED menu → <strong>Manage palette</strong> → <strong>Install</strong></li>
|
|
287
|
+
<li>Search and install: <code>@cyprnet/node-red-contrib-uibuilder-formgen</code></li>
|
|
288
|
+
<li>Install/upgrade: <code>node-red-contrib-uibuilder</code></li>
|
|
289
|
+
<li><strong>Restart Node-RED</strong> after installs/upgrades</li>
|
|
287
290
|
</ol>
|
|
291
|
+
|
|
292
|
+
<h3>Install via npm (recommended for servers)</h3>
|
|
293
|
+
<div class="note">
|
|
294
|
+
Stop Node-RED before running npm installs. After installation, start/restart Node-RED so it loads the new nodes.
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<h4>Install from npm registry</h4>
|
|
298
|
+
<pre># Standard userDir
|
|
299
|
+
cd ~/.node-red
|
|
300
|
+
npm install @cyprnet/node-red-contrib-uibuilder-formgen
|
|
301
|
+
|
|
302
|
+
# Install/upgrade uibuilder (v7+ recommended)
|
|
303
|
+
npm install node-red-contrib-uibuilder</pre>
|
|
304
|
+
|
|
305
|
+
<h4>Install from a local build (.tgz)</h4>
|
|
306
|
+
<pre># Example: install a specific build artifact
|
|
307
|
+
cd ~/.node-red
|
|
308
|
+
npm install /absolute/path/to/cyprnet-node-red-contrib-uibuilder-formgen-0.5.29.tgz</pre>
|
|
309
|
+
|
|
310
|
+
<h4>Upgrade (npm)</h4>
|
|
311
|
+
<pre>cd ~/.node-red
|
|
312
|
+
npm update @cyprnet/node-red-contrib-uibuilder-formgen
|
|
313
|
+
npm update node-red-contrib-uibuilder</pre>
|
|
314
|
+
|
|
315
|
+
<div class="note ok">
|
|
316
|
+
<strong>Verification:</strong> After restart, you should see the generator nodes in the palette:
|
|
317
|
+
<code>uibuilder-formgen</code>, <code>uibuilder-formgen-v3</code>, and <code>uibuilder-formgen-uib2</code>,
|
|
318
|
+
plus the <code>portalsmith-license</code> config node.
|
|
319
|
+
</div>
|
|
288
320
|
</section>
|
|
289
321
|
|
|
290
322
|
<section id="licensing">
|
|
@@ -348,6 +380,102 @@
|
|
|
348
380
|
</div>
|
|
349
381
|
</section>
|
|
350
382
|
|
|
383
|
+
<section id="portalsmith-setup">
|
|
384
|
+
<h2>PortalSmith setup (end-to-end)</h2>
|
|
385
|
+
|
|
386
|
+
<p>
|
|
387
|
+
This is a detailed, step-by-step “cookbook” for a complete PortalSmith setup:
|
|
388
|
+
design a schema, generate a portal into a uibuilder instance, and wire up submit and lookup/autofill behavior.
|
|
389
|
+
</p>
|
|
390
|
+
|
|
391
|
+
<div class="note ok">
|
|
392
|
+
<strong>Terminology</strong>
|
|
393
|
+
<ul>
|
|
394
|
+
<li><strong>Builder instance</strong>: a uibuilder page running the Schema Builder UI (example URL: <code>formgen-builder</code>).</li>
|
|
395
|
+
<li><strong>Portal instance</strong>: a uibuilder page generated by FormGen (example URL: <code>my-portal</code>).</li>
|
|
396
|
+
<li><strong>Generator node</strong>: one of <code>uibuilder-formgen</code> / <code>uibuilder-formgen-v3</code> / <code>uibuilder-formgen-uib2</code>.</li>
|
|
397
|
+
</ul>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<h3>1) Create the uibuilder instance that will host the generated portal</h3>
|
|
401
|
+
<ol>
|
|
402
|
+
<li>Drag a <code>uibuilder</code> node into your flow.</li>
|
|
403
|
+
<li>Set <strong>URL</strong> (instance name) to something simple, e.g. <code>my-portal</code>.</li>
|
|
404
|
+
<li>Deploy once. This creates the instance folder on disk.</li>
|
|
405
|
+
</ol>
|
|
406
|
+
|
|
407
|
+
<div class="note">
|
|
408
|
+
<strong>Where FormGen writes files:</strong>
|
|
409
|
+
FormGen writes into the portal instance’s <code>src/</code> folder:
|
|
410
|
+
<code>src/index.html</code>, <code>src/index.js</code>, <code>src/form.schema.json</code>, plus <code>portalsmith.runtime.json</code>.
|
|
411
|
+
</div>
|
|
412
|
+
|
|
413
|
+
<div class="note ok">
|
|
414
|
+
<strong>Important (manual copying / git restores):</strong>
|
|
415
|
+
A generated portal is a 3-file set. If you copy a portal to another uibuilder instance (or restore it from git/backup),
|
|
416
|
+
you must copy <strong>all three</strong> runtime files together:
|
|
417
|
+
<code>src/index.html</code>, <code>src/index.js</code>, and <code>src/form.schema.json</code>.
|
|
418
|
+
The portal loads the schema at runtime; if <code>form.schema.json</code> is missing, the portal will not render correctly.
|
|
419
|
+
</div>
|
|
420
|
+
|
|
421
|
+
<h3>2) Generate a portal from a schema</h3>
|
|
422
|
+
|
|
423
|
+
<h4>Method A: Generate from an Inject node (recommended first test)</h4>
|
|
424
|
+
<ol>
|
|
425
|
+
<li>Add an <strong>Inject</strong> node and set it to send a JSON object.</li>
|
|
426
|
+
<li>Put your schema object in <code>msg.schema</code> (preferred). (Fallback: put the schema object in <code>msg.payload</code>.)</li>
|
|
427
|
+
<li>Wire: <strong>Inject → uibuilder-formgen (or v3/uib2) → Debug</strong>.</li>
|
|
428
|
+
<li>In the FormGen node, set <strong>Instance</strong> to your portal instance name (e.g. <code>my-portal</code>).</li>
|
|
429
|
+
<li>Deploy, then click Inject.</li>
|
|
430
|
+
<li>Open the portal URL: <code>/uibuilder/my-portal/</code> (or your configured uibuilder base path).</li>
|
|
431
|
+
</ol>
|
|
432
|
+
|
|
433
|
+
<h4>Method B: Generate from the Schema Builder UI (recommended day-to-day)</h4>
|
|
434
|
+
<ol>
|
|
435
|
+
<li>Host the Schema Builder UI in a uibuilder instance (see <a href="#schema-builder">Schema Builder</a>).</li>
|
|
436
|
+
<li>Design your schema in the UI.</li>
|
|
437
|
+
<li>Click <strong>Generate Form</strong>.</li>
|
|
438
|
+
<li>Your flow receives <code>{type:'generate', schema:{...}}</code> from the builder instance and must route it into a FormGen node.</li>
|
|
439
|
+
</ol>
|
|
440
|
+
|
|
441
|
+
<h3>Recommended Node-RED flow wiring for a portal instance (submit + lookup)</h3>
|
|
442
|
+
<p>
|
|
443
|
+
In most PortalSmith deployments, a single uibuilder instance handles both submits and lookup/autofill requests.
|
|
444
|
+
The cleanest pattern is to route by <code>msg.payload.type</code> from the uibuilder node’s output #1.
|
|
445
|
+
</p>
|
|
446
|
+
<pre>my-portal (uibuilder node) output #1
|
|
447
|
+
↓
|
|
448
|
+
Switch on msg.payload.type
|
|
449
|
+
├─ "lookup:get" → lookup handler → (respond) my-portal (uibuilder input)
|
|
450
|
+
├─ "submit" → submit handler → (respond) my-portal (uibuilder input)
|
|
451
|
+
└─ (other) → Debug / ignore</pre>
|
|
452
|
+
<div class="note">
|
|
453
|
+
<strong>Important:</strong> Always send lookup/submit responses back into the <em>same</em> uibuilder instance that originated the request.
|
|
454
|
+
If you have separate builder + portal instances, make sure each response goes back to the correct uibuilder node.
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
<h3>3) Wire Submit (portal → Node-RED → portal)</h3>
|
|
458
|
+
<p>
|
|
459
|
+
By default, portals submit using <strong>uibuilder messages</strong>. The portal sends a submit request out of the uibuilder node (output #1).
|
|
460
|
+
Your flow should respond back into the same uibuilder node’s input so the portal can show results and clear spinners.
|
|
461
|
+
</p>
|
|
462
|
+
<pre>Portal (browser)
|
|
463
|
+
↓ (uibuilder send)
|
|
464
|
+
Node-RED: uibuilder node (output #1)
|
|
465
|
+
↓
|
|
466
|
+
Your flow logic (Function / HTTP / DB / API proxy)
|
|
467
|
+
↓
|
|
468
|
+
Node-RED: uibuilder node (input)
|
|
469
|
+
↓
|
|
470
|
+
Portal (browser) shows Table/JSON/Form results</pre>
|
|
471
|
+
|
|
472
|
+
<h3>4) Wire Lookup/Auto-fill (portal → Node-RED → portal)</h3>
|
|
473
|
+
<p>
|
|
474
|
+
Lookup/Auto-fill uses the same uibuilder channel. The portal emits <code>lookup:get</code> requests and expects <code>lookup:data</code> (or <code>lookup:error</code>) replies.
|
|
475
|
+
See <a href="#lookup-contracts">Lookup/Auto-fill: message contracts</a>.
|
|
476
|
+
</p>
|
|
477
|
+
</section>
|
|
478
|
+
|
|
351
479
|
<section id="schema-builder">
|
|
352
480
|
<h2>Schema Builder (form designer)</h2>
|
|
353
481
|
<p>The Schema Builder is a uibuilder instance that you host like any other uibuilder page.</p>
|
|
@@ -362,45 +490,80 @@
|
|
|
362
490
|
<li><strong>New Schema</strong> button resets your current schema</li>
|
|
363
491
|
</ul>
|
|
364
492
|
|
|
365
|
-
<h3>
|
|
493
|
+
<h3>Hosting the Schema Builder (uibuilder v7+)</h3>
|
|
494
|
+
<p>
|
|
495
|
+
Use this when you are on modern uibuilder (v7+). You host the builder like any other uibuilder page.
|
|
496
|
+
</p>
|
|
366
497
|
<ol>
|
|
367
|
-
<li>Create a uibuilder instance (example
|
|
368
|
-
<li>
|
|
369
|
-
<li>
|
|
498
|
+
<li>Create a uibuilder instance (example URL: <code>formgen-builder</code>).</li>
|
|
499
|
+
<li>Deploy once (creates the instance folder).</li>
|
|
500
|
+
<li>Copy the builder files into the instance folder’s <code>src/</code>:</li>
|
|
501
|
+
</ol>
|
|
502
|
+
<pre># Standard userDir example
|
|
503
|
+
cd ~/.node-red
|
|
504
|
+
cp node_modules/@cyprnet/node-red-contrib-uibuilder-formgen/examples/formgen-builder/src/index.html uibuilder/formgen-builder/src/index.html
|
|
505
|
+
cp node_modules/@cyprnet/node-red-contrib-uibuilder-formgen/examples/formgen-builder/src/index.js uibuilder/formgen-builder/src/index.js</pre>
|
|
506
|
+
<ol start="4">
|
|
507
|
+
<li>Hard refresh your browser and open: <code>/uibuilder/formgen-builder/</code>.</li>
|
|
370
508
|
</ol>
|
|
371
509
|
|
|
372
510
|
<div class="note">
|
|
373
|
-
<strong>Builder dependencies:</strong> the builder’s HTML includes
|
|
511
|
+
<strong>Builder dependencies:</strong> the builder’s HTML includes Vue/Bootstrap/Bootstrap‑Vue/jQuery itself. Your uibuilder instance just needs to serve the files.
|
|
374
512
|
</div>
|
|
375
513
|
|
|
514
|
+
<h3>Hosting the Schema Builder (legacy uibuilder 2.8)</h3>
|
|
515
|
+
<p>
|
|
516
|
+
Use this when you are on <strong>uibuilder 2.x</strong> (for example 2.8).
|
|
517
|
+
The legacy builder uses uibuilder 2.x’s frontend bundle (<code>uibuilderfe.min.js</code>) and loads Vue/Bootstrap/Bootstrap‑Vue from uibuilder’s <code>vendor/</code> folder.
|
|
518
|
+
</p>
|
|
519
|
+
<ol>
|
|
520
|
+
<li>Create a uibuilder instance (example URL: <code>formgen-builder</code> or <code>formgen-builder-uib2</code> — the name is not hard-coded).</li>
|
|
521
|
+
<li>Deploy once.</li>
|
|
522
|
+
<li>Copy the legacy builder files into that instance folder’s <code>src/</code>:</li>
|
|
523
|
+
</ol>
|
|
524
|
+
<pre># Example (adjust your userDir/projects path as needed)
|
|
525
|
+
cd ~/.node-red
|
|
526
|
+
cp node_modules/@cyprnet/node-red-contrib-uibuilder-formgen/examples/formgen-builder-uib2/src/index.html uibuilder/formgen-builder/src/index.html
|
|
527
|
+
cp node_modules/@cyprnet/node-red-contrib-uibuilder-formgen/examples/formgen-builder-uib2/src/index.js uibuilder/formgen-builder/src/index.js</pre>
|
|
376
528
|
<div class="note ok">
|
|
377
|
-
<strong>
|
|
378
|
-
After installing <code>@cyprnet/node-red-contrib-uibuilder-formgen</code>, copy these files from the package:
|
|
379
|
-
<ul>
|
|
380
|
-
<li><code>examples/formgen-builder/src/index.html</code></li>
|
|
381
|
-
<li><code>examples/formgen-builder/src/index.js</code></li>
|
|
382
|
-
</ul>
|
|
383
|
-
Into your uibuilder instance folder:
|
|
384
|
-
<ul>
|
|
385
|
-
<li><code><userDir>/uibuilder/formgen-builder/src/</code></li>
|
|
386
|
-
<li>or Projects: <code><userDir>/projects/<projectName>/uibuilder/formgen-builder/src/</code></li>
|
|
387
|
-
</ul>
|
|
529
|
+
<strong>uibuilder 2.8 serving behavior:</strong> uibuilder 2.8 serves directly from <code>src/</code>, so copying into <code>src/</code> is sufficient.
|
|
388
530
|
</div>
|
|
389
531
|
|
|
390
|
-
<
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
532
|
+
<h3>Wiring the Builder to the Generator node (required)</h3>
|
|
533
|
+
<p>
|
|
534
|
+
Clicking <strong>Generate Form</strong> in the builder sends a message to Node-RED via uibuilder:
|
|
535
|
+
<code>{type:'generate', schema:{...}}</code>.
|
|
536
|
+
Your flow must transform that into <code>msg.schema</code> and pass it into a FormGen generator node.
|
|
537
|
+
</p>
|
|
538
|
+
<pre>Builder uibuilder node (output #1)
|
|
539
|
+
↓
|
|
540
|
+
Function/Switch (route builder messages)
|
|
541
|
+
↓
|
|
542
|
+
uibuilder-formgen (or -v3 / -uib2)
|
|
543
|
+
↓
|
|
544
|
+
Debug + (optional) send status back to builder uibuilder (input)</pre>
|
|
545
|
+
|
|
546
|
+
<h4>Minimum Function node to route “Generate Form”</h4>
|
|
547
|
+
<pre>// Place this between the builder uibuilder node (output #1) and a uibuilder-formgen node
|
|
548
|
+
const p = msg.payload || {};
|
|
549
|
+
if (p.type !== 'generate') return null;
|
|
550
|
+
|
|
551
|
+
msg.schema = p.schema; // what formgen expects
|
|
552
|
+
// Optional: pick which uibuilder instance to generate into (otherwise configure the formgen node Instance field)
|
|
553
|
+
// msg.uibuilder = 'my-portal';
|
|
554
|
+
|
|
555
|
+
// Optional: generation options
|
|
556
|
+
msg.options = Object.assign({}, msg.options, {
|
|
557
|
+
overwrite: true,
|
|
558
|
+
protectEdits: true
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
return msg;</pre>
|
|
562
|
+
|
|
563
|
+
<div class="note">
|
|
564
|
+
<strong>Returning status to the Builder UI:</strong>
|
|
565
|
+
If you wire the generator output back into the builder uibuilder node’s input, the builder will show a success/error alert.
|
|
566
|
+
It expects <code>msg.payload.type</code> to be <code>generated</code> or <code>error</code> and will display <code>msg.payload.url</code> if present.
|
|
404
567
|
</div>
|
|
405
568
|
|
|
406
569
|
<div class="note">
|
|
@@ -460,6 +623,118 @@
|
|
|
460
623
|
</tbody>
|
|
461
624
|
</table>
|
|
462
625
|
|
|
626
|
+
<h3>Common field properties (all types)</h3>
|
|
627
|
+
<p>These properties are available on most field types and are surfaced by the Schema Builder UI:</p>
|
|
628
|
+
<ul>
|
|
629
|
+
<li><code>id</code> (required): unique field identifier. This becomes the key in submitted JSON.</li>
|
|
630
|
+
<li><code>label</code>: display label.</li>
|
|
631
|
+
<li><code>type</code> (required): one of the supported field types.</li>
|
|
632
|
+
<li><code>required</code>: if true, the portal will block submit if empty.</li>
|
|
633
|
+
<li><code>placeholder</code>: placeholder text for inputs.</li>
|
|
634
|
+
<li><code>defaultValue</code>: initial value.</li>
|
|
635
|
+
<li><code>help</code> (optional): help text shown under the field (if enabled by the template).</li>
|
|
636
|
+
<li><code>validate</code> (optional): additional validators (see below).</li>
|
|
637
|
+
<li><code>validateMessage</code> (optional): error message override when validation fails.</li>
|
|
638
|
+
</ul>
|
|
639
|
+
|
|
640
|
+
<h3>Validation</h3>
|
|
641
|
+
<p>In addition to <code>required</code>, the generated portals support:</p>
|
|
642
|
+
<ul>
|
|
643
|
+
<li><code>validate: "email"</code></li>
|
|
644
|
+
<li><code>validate: "phone"</code></li>
|
|
645
|
+
<li><code>validate: "regex:<pattern>"</code> (JavaScript regex pattern source)</li>
|
|
646
|
+
<li><code>validate: "regex:/<pattern>/<flags>"</code> (optional slashes/flags for convenience, e.g. <code>regex:/^abc$/i</code>)</li>
|
|
647
|
+
</ul>
|
|
648
|
+
<pre>{
|
|
649
|
+
"id": "employeeId",
|
|
650
|
+
"type": "text",
|
|
651
|
+
"label": "Employee ID",
|
|
652
|
+
"required": true,
|
|
653
|
+
"validate": "regex:^[A-Z]{2}-\\d{6}$",
|
|
654
|
+
"validateMessage": "Expected format: AA-123456"
|
|
655
|
+
}</pre>
|
|
656
|
+
|
|
657
|
+
<h3>Field type details & examples</h3>
|
|
658
|
+
|
|
659
|
+
<h4>text</h4>
|
|
660
|
+
<p>Common add-ons:</p>
|
|
661
|
+
<ul>
|
|
662
|
+
<li><code>inputType</code>: <code>text</code> (default), <code>password</code>, <code>email</code>, <code>tel</code>, <code>url</code></li>
|
|
663
|
+
</ul>
|
|
664
|
+
<pre>{
|
|
665
|
+
"id": "password",
|
|
666
|
+
"type": "text",
|
|
667
|
+
"label": "Password",
|
|
668
|
+
"required": true,
|
|
669
|
+
"inputType": "password",
|
|
670
|
+
"placeholder": "Enter password"
|
|
671
|
+
}</pre>
|
|
672
|
+
|
|
673
|
+
<h4>textarea</h4>
|
|
674
|
+
<p>Common add-ons:</p>
|
|
675
|
+
<ul>
|
|
676
|
+
<li><code>rows</code>: textarea height (rows)</li>
|
|
677
|
+
</ul>
|
|
678
|
+
<pre>{
|
|
679
|
+
"id": "notes",
|
|
680
|
+
"type": "textarea",
|
|
681
|
+
"label": "Notes",
|
|
682
|
+
"rows": 6
|
|
683
|
+
}</pre>
|
|
684
|
+
|
|
685
|
+
<h4>number</h4>
|
|
686
|
+
<p>Common add-ons:</p>
|
|
687
|
+
<ul>
|
|
688
|
+
<li><code>min</code>, <code>max</code>, <code>step</code></li>
|
|
689
|
+
</ul>
|
|
690
|
+
<pre>{
|
|
691
|
+
"id": "retryCount",
|
|
692
|
+
"type": "number",
|
|
693
|
+
"label": "Retry Count",
|
|
694
|
+
"min": 0,
|
|
695
|
+
"max": 10,
|
|
696
|
+
"step": 1,
|
|
697
|
+
"defaultValue": 3
|
|
698
|
+
}</pre>
|
|
699
|
+
|
|
700
|
+
<h4>select / radio</h4>
|
|
701
|
+
<p>
|
|
702
|
+
Options are typically an array of objects. Use <code>value</code> for the submitted value and <code>label</code> for display text.
|
|
703
|
+
The user selection is stored as the option <code>value</code>.
|
|
704
|
+
</p>
|
|
705
|
+
<pre>{
|
|
706
|
+
"id": "os",
|
|
707
|
+
"type": "select",
|
|
708
|
+
"label": "Operating System",
|
|
709
|
+
"required": true,
|
|
710
|
+
"options": [
|
|
711
|
+
{ "value": "win11", "label": "Windows 11" },
|
|
712
|
+
{ "value": "ubuntu", "label": "Ubuntu" }
|
|
713
|
+
]
|
|
714
|
+
}</pre>
|
|
715
|
+
<div class="note">
|
|
716
|
+
<strong>Lookup/autofill tip:</strong> If you want a lookup rule to match what a select stores, set <code>matchKey</code> to the property that equals the selected <code>value</code>
|
|
717
|
+
(commonly <code>value</code>, not <code>label</code>).
|
|
718
|
+
</div>
|
|
719
|
+
|
|
720
|
+
<h4>checkbox</h4>
|
|
721
|
+
<p>Checkbox fields submit a boolean:</p>
|
|
722
|
+
<pre>{
|
|
723
|
+
"id": "acknowledged",
|
|
724
|
+
"type": "checkbox",
|
|
725
|
+
"label": "I understand",
|
|
726
|
+
"defaultValue": false
|
|
727
|
+
}</pre>
|
|
728
|
+
|
|
729
|
+
<h4>date</h4>
|
|
730
|
+
<p>Date values are submitted as strings (implementation depends on template/browser):</p>
|
|
731
|
+
<pre>{
|
|
732
|
+
"id": "startDate",
|
|
733
|
+
"type": "date",
|
|
734
|
+
"label": "Start Date",
|
|
735
|
+
"required": true
|
|
736
|
+
}</pre>
|
|
737
|
+
|
|
463
738
|
<h3>Key/value fields (keyvalue)</h3>
|
|
464
739
|
<p><strong>Pairs mode</strong> stores an array of objects:</p>
|
|
465
740
|
<pre>{
|
|
@@ -804,6 +1079,144 @@ return msg;</pre>
|
|
|
804
1079
|
</div>
|
|
805
1080
|
</section>
|
|
806
1081
|
|
|
1082
|
+
<section id="lookup-contracts">
|
|
1083
|
+
<h2>Lookup / Auto-fill (message contracts)</h2>
|
|
1084
|
+
|
|
1085
|
+
<p>
|
|
1086
|
+
When you set <code>autoFill.source.type = "uibuilder"</code>, the portal uses uibuilder messages to ask Node-RED for lookup data.
|
|
1087
|
+
The message namespace is carried in <code>msg.payload.type</code> using <code>lookup:*</code> strings.
|
|
1088
|
+
</p>
|
|
1089
|
+
|
|
1090
|
+
<h3>How the portal routes messages (why <code>lookup:*</code> matters)</h3>
|
|
1091
|
+
<p>
|
|
1092
|
+
The generated portal watches incoming uibuilder messages and routes behavior based on <code>msg.payload.type</code>:
|
|
1093
|
+
</p>
|
|
1094
|
+
<ul>
|
|
1095
|
+
<li><strong><code>lookup:data</code></strong>: cache lookup items and immediately re-apply any autofill rules that use that <code>lookupId</code>.</li>
|
|
1096
|
+
<li><strong><code>lookup:error</code></strong>: show a warning and stop waiting for that lookup.</li>
|
|
1097
|
+
<li><strong><code>submit:ok</code> / <code>submit:error</code></strong>: clear submit spinners and show results.</li>
|
|
1098
|
+
</ul>
|
|
1099
|
+
<div class="note">
|
|
1100
|
+
<strong>Important:</strong> The portal does not “guess” what a lookup response is. It specifically checks for <code>payload.type === 'lookup:data'</code> (or <code>lookup:error</code>).
|
|
1101
|
+
If your response uses a different <code>type</code> string, the UI will not update.
|
|
1102
|
+
</div>
|
|
1103
|
+
|
|
1104
|
+
<h3>Request (portal → Node-RED)</h3>
|
|
1105
|
+
<p>
|
|
1106
|
+
When a user changes the source field for an autofill rule, the portal sends a message out of the uibuilder node (output #1).
|
|
1107
|
+
For text typing, it uses debouncing and will also flush on blur.
|
|
1108
|
+
</p>
|
|
1109
|
+
<pre>{
|
|
1110
|
+
"payload": {
|
|
1111
|
+
"type": "lookup:get",
|
|
1112
|
+
"lookupId": "ips",
|
|
1113
|
+
"matchValue": "192.168.1.0/24"
|
|
1114
|
+
}
|
|
1115
|
+
}</pre>
|
|
1116
|
+
<ul>
|
|
1117
|
+
<li><strong><code>lookupId</code></strong>: identifies the lookup service/list. You define this string in the schema and in your Node-RED flow.</li>
|
|
1118
|
+
<li><strong><code>matchValue</code></strong>: the current value of the source field (trimmed). It may be <code>null</code> if the portal has no value yet.</li>
|
|
1119
|
+
</ul>
|
|
1120
|
+
|
|
1121
|
+
<h3>Response (Node-RED → portal)</h3>
|
|
1122
|
+
<p>
|
|
1123
|
+
Your flow should respond back into the same uibuilder node’s input with one of these messages:
|
|
1124
|
+
</p>
|
|
1125
|
+
<h4>Success: lookup data</h4>
|
|
1126
|
+
<pre>{
|
|
1127
|
+
"payload": {
|
|
1128
|
+
"type": "lookup:data",
|
|
1129
|
+
"lookupId": "ips",
|
|
1130
|
+
"matchValue": "192.168.1.0/24",
|
|
1131
|
+
"items": [
|
|
1132
|
+
{
|
|
1133
|
+
"ip": "192.168.1.0/24",
|
|
1134
|
+
"ips": ["192.168.1.0", "192.168.1.1", "192.168.1.2"]
|
|
1135
|
+
}
|
|
1136
|
+
]
|
|
1137
|
+
}
|
|
1138
|
+
}</pre>
|
|
1139
|
+
<div class="note ok">
|
|
1140
|
+
<strong>Golden rules:</strong>
|
|
1141
|
+
<ul>
|
|
1142
|
+
<li><strong><code>items</code> must be an array</strong>, even if you only have one record to return.</li>
|
|
1143
|
+
<li>Each item must contain the properties referenced by your rule’s <code>matchKey</code> and each mapping’s <code>valueKey</code>.</li>
|
|
1144
|
+
<li>Return the same <code>lookupId</code> you were asked for.</li>
|
|
1145
|
+
</ul>
|
|
1146
|
+
</div>
|
|
1147
|
+
|
|
1148
|
+
<h4>Error: lookup failed</h4>
|
|
1149
|
+
<pre>{
|
|
1150
|
+
"payload": {
|
|
1151
|
+
"type": "lookup:error",
|
|
1152
|
+
"lookupId": "ips",
|
|
1153
|
+
"matchValue": "192.168.1.0/24",
|
|
1154
|
+
"error": "Timed out calling backend"
|
|
1155
|
+
}
|
|
1156
|
+
}</pre>
|
|
1157
|
+
|
|
1158
|
+
<h3>Match keys, value keys, and dot paths</h3>
|
|
1159
|
+
<ul>
|
|
1160
|
+
<li><strong><code>matchKey</code></strong> is the property path in each item used to compare with the user’s source field value.</li>
|
|
1161
|
+
<li><strong><code>valueKey</code></strong> is the property path copied into the target field.</li>
|
|
1162
|
+
<li>Both support dot notation (example: <code>meta.id</code> or <code>details.network.ips</code>).</li>
|
|
1163
|
+
</ul>
|
|
1164
|
+
|
|
1165
|
+
<h3>Textarea targets and “array” values</h3>
|
|
1166
|
+
<p>
|
|
1167
|
+
If your mapping writes into a <code>textarea</code> (or a <code>keyvalue</code> field in delimiter mode), the portal can render:
|
|
1168
|
+
</p>
|
|
1169
|
+
<ul>
|
|
1170
|
+
<li><strong>Array of strings</strong>: joined into newline-separated text.</li>
|
|
1171
|
+
<li><strong>Array of objects</strong> like <code>{value,label}</code>: joined into newline-separated lines using the best available string per item.</li>
|
|
1172
|
+
</ul>
|
|
1173
|
+
|
|
1174
|
+
<h3>Debounce / blur behavior (text field lookups)</h3>
|
|
1175
|
+
<p>
|
|
1176
|
+
For <code>text</code> and <code>textarea</code> source fields using dynamic lookup (<code>source.type="uibuilder"</code>),
|
|
1177
|
+
the portal requests lookups in a user-friendly way:
|
|
1178
|
+
</p>
|
|
1179
|
+
<ul>
|
|
1180
|
+
<li><strong>Debounced typing:</strong> it waits briefly after the user stops typing before requesting <code>lookup:get</code>.</li>
|
|
1181
|
+
<li><strong>Blur flush:</strong> when the input loses focus, it triggers an immediate lookup for the final value.</li>
|
|
1182
|
+
</ul>
|
|
1183
|
+
|
|
1184
|
+
<h3>Lookup caching (important for performance)</h3>
|
|
1185
|
+
<p>
|
|
1186
|
+
The portal caches dynamic lookup results by key:
|
|
1187
|
+
<code><lookupId>::<matchValue></code> (and optionally by <code><lookupId></code> for “full list” responses).
|
|
1188
|
+
This avoids repeatedly calling Node-RED for the same selection/value.
|
|
1189
|
+
</p>
|
|
1190
|
+
|
|
1191
|
+
<h3>Example Node-RED handler (single Function node)</h3>
|
|
1192
|
+
<p>
|
|
1193
|
+
Wire: <strong>uibuilder (portal) output #1 → Function → uibuilder input</strong>.
|
|
1194
|
+
This pattern supports multiple <code>lookupId</code> values in one place.
|
|
1195
|
+
</p>
|
|
1196
|
+
<pre>// Function node: handle lookup requests
|
|
1197
|
+
const p = msg.payload || {};
|
|
1198
|
+
if (p.type !== 'lookup:get') return null;
|
|
1199
|
+
|
|
1200
|
+
if (p.lookupId === 'ips') {
|
|
1201
|
+
// Example: p.matchValue holds the typed subnet/CIDR
|
|
1202
|
+
msg.payload = {
|
|
1203
|
+
type: 'lookup:data',
|
|
1204
|
+
lookupId: 'ips',
|
|
1205
|
+
matchValue: p.matchValue ?? null,
|
|
1206
|
+
items: [
|
|
1207
|
+
{
|
|
1208
|
+
ip: String(p.matchValue || '').trim(),
|
|
1209
|
+
ips: ['192.168.1.0', '192.168.1.1', '192.168.1.2']
|
|
1210
|
+
}
|
|
1211
|
+
]
|
|
1212
|
+
};
|
|
1213
|
+
return msg;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
msg.payload = { type: 'lookup:error', lookupId: p.lookupId, matchValue: p.matchValue ?? null, error: 'Unknown lookupId' };
|
|
1217
|
+
return msg;</pre>
|
|
1218
|
+
</section>
|
|
1219
|
+
|
|
807
1220
|
<section id="generate">
|
|
808
1221
|
<h2>Generating a portal (uibuilder-formgen / uibuilder-formgen-v3 / uibuilder-formgen-uib2)</h2>
|
|
809
1222
|
<p>
|
|
@@ -822,19 +1235,67 @@ return msg;</pre>
|
|
|
822
1235
|
<strong>How to confirm which runtime generated a portal:</strong> open the generated <code>portalsmith.runtime.json</code> file and check <code>generatorNode</code>.
|
|
823
1236
|
</div>
|
|
824
1237
|
|
|
825
|
-
<h3>Minimum input</h3>
|
|
826
|
-
<
|
|
827
|
-
|
|
828
|
-
}
|
|
1238
|
+
<h3>Minimum input (Node-RED message)</h3>
|
|
1239
|
+
<p>The generator nodes read the schema from <code>msg.schema</code> (preferred) or <code>msg.payload</code> (fallback).</p>
|
|
1240
|
+
<pre>// Minimum: send a schema object
|
|
1241
|
+
msg.schema = { ...schema json... };</pre>
|
|
829
1242
|
|
|
830
|
-
<h3>
|
|
831
|
-
<
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
1243
|
+
<h3>Pick the uibuilder instance to generate into</h3>
|
|
1244
|
+
<p>You can select the target instance in three ways (highest precedence first):</p>
|
|
1245
|
+
<ol>
|
|
1246
|
+
<li><code>msg.uibuilder</code></li>
|
|
1247
|
+
<li><code>msg.options.instance</code></li>
|
|
1248
|
+
<li>Node config field: <strong>Instance</strong> (<code>instanceName</code>)</li>
|
|
1249
|
+
</ol>
|
|
1250
|
+
<pre>// Example: generate into URL "/uibuilder/my-portal/"
|
|
1251
|
+
msg.uibuilder = "my-portal";</pre>
|
|
1252
|
+
|
|
1253
|
+
<h3>Options (<code>msg.options</code> overrides node config)</h3>
|
|
1254
|
+
<p>
|
|
1255
|
+
Generator nodes accept <code>msg.options</code> for path overrides, overwrite protection, theming, and submit behavior.
|
|
1256
|
+
Common options include:
|
|
1257
|
+
</p>
|
|
1258
|
+
<ul>
|
|
1259
|
+
<li><strong>Overwrite & protection</strong>:
|
|
1260
|
+
<ul>
|
|
1261
|
+
<li><code>overwrite</code>: boolean (default true)</li>
|
|
1262
|
+
<li><code>protectEdits</code>: boolean (default true)</li>
|
|
1263
|
+
<li><code>forceOverwrite</code>: boolean (default false) — override protection once (use with backups)</li>
|
|
1264
|
+
</ul>
|
|
1265
|
+
</li>
|
|
1266
|
+
<li><strong>Paths</strong> (optional; useful for Projects or non-standard installs):
|
|
1267
|
+
<ul>
|
|
1268
|
+
<li><code>projectName</code>: use <code><userDir>/projects/<projectName>/uibuilder</code></li>
|
|
1269
|
+
<li><code>uibRootDir</code>: absolute path to the uibuilder root folder</li>
|
|
1270
|
+
<li><code>instanceRootDir</code>: absolute path to the specific uibuilder instance folder</li>
|
|
1271
|
+
</ul>
|
|
1272
|
+
</li>
|
|
1273
|
+
<li><strong>Portal UX</strong>:
|
|
1274
|
+
<ul>
|
|
1275
|
+
<li><code>themeMode</code>: <code>auto</code> | <code>light</code> | <code>dark</code></li>
|
|
1276
|
+
<li><code>storageMode</code>: <code>file</code> | <code>localstorage</code></li>
|
|
1277
|
+
<li><code>exportFormats</code>: array like <code>["json","csv","html"]</code></li>
|
|
1278
|
+
</ul>
|
|
1279
|
+
</li>
|
|
1280
|
+
</ul>
|
|
1281
|
+
|
|
1282
|
+
<pre>// Example: full generation message
|
|
1283
|
+
msg.schema = { ...schema json... };
|
|
1284
|
+
msg.uibuilder = "my-portal";
|
|
1285
|
+
msg.options = {
|
|
1286
|
+
overwrite: true,
|
|
1287
|
+
protectEdits: true,
|
|
1288
|
+
// forceOverwrite: true, // only if you intend to replace edited files
|
|
1289
|
+
themeMode: "auto",
|
|
1290
|
+
exportFormats: ["json","csv","html"]
|
|
1291
|
+
};
|
|
1292
|
+
return msg;</pre>
|
|
1293
|
+
|
|
1294
|
+
<div class="note">
|
|
1295
|
+
<strong>uibuilder v7+ note (src vs dist):</strong>
|
|
1296
|
+
FormGen writes to <code>src/</code>. If your uibuilder instance is configured to serve <code>dist/</code>,
|
|
1297
|
+
either switch it to serve <code>src/</code> while developing, or copy/build into <code>dist/</code> as part of your deployment process.
|
|
1298
|
+
</div>
|
|
838
1299
|
|
|
839
1300
|
<h3>Protecting user customizations (important)</h3>
|
|
840
1301
|
<p>
|
|
@@ -786,18 +786,27 @@ email{{currentField.keyvalueDelimiter || '='}}john@example.com</code></pre>
|
|
|
786
786
|
v-model="currentField.validate"
|
|
787
787
|
:options="validationTypes"
|
|
788
788
|
/>
|
|
789
|
-
<b-form-input
|
|
790
|
-
v-if="currentField.validate === 'regex'"
|
|
791
|
-
v-model="currentField.validatePattern"
|
|
792
|
-
placeholder="Regex pattern (e.g., ^[A-Z]{2}\\d{4}$)"
|
|
793
|
-
class="mt-2"
|
|
794
|
-
/>
|
|
795
789
|
<small class="form-text text-muted">
|
|
796
790
|
<strong>Email:</strong> Validates email format (user@domain.com)<br>
|
|
797
791
|
<strong>Phone:</strong> Validates phone number format (10+ digits)<br>
|
|
798
792
|
<strong>Regex:</strong> Custom pattern validation (e.g., ^[A-Z]{2}\\d{4}$ for codes like AB1234)
|
|
799
793
|
</small>
|
|
800
794
|
</b-form-group>
|
|
795
|
+
|
|
796
|
+
<b-form-group
|
|
797
|
+
v-if="currentField.validate === 'regex'"
|
|
798
|
+
label="Regex pattern"
|
|
799
|
+
label-for="validate-pattern"
|
|
800
|
+
>
|
|
801
|
+
<b-form-input
|
|
802
|
+
id="validate-pattern"
|
|
803
|
+
v-model="currentField.validatePattern"
|
|
804
|
+
placeholder="e.g., ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}\.$"
|
|
805
|
+
/>
|
|
806
|
+
<small class="form-text text-muted">
|
|
807
|
+
JavaScript RegExp source only (no <code>/.../</code> slashes). In this box you type a single backslash (e.g. <code>\.</code>). When exported to JSON, it will appear escaped as <code>\\.</code>.
|
|
808
|
+
</small>
|
|
809
|
+
</b-form-group>
|
|
801
810
|
|
|
802
811
|
<b-form-group label="Validation Message" label-for="validate-msg" v-if="currentField.validate">
|
|
803
812
|
<b-form-input
|
|
@@ -901,6 +901,17 @@
|
|
|
901
901
|
this.editingFieldLocation = { sectionIdx, fieldIdx };
|
|
902
902
|
const field = this.schema.sections[sectionIdx].fields[fieldIdx];
|
|
903
903
|
this.currentField = JSON.parse(JSON.stringify(field));
|
|
904
|
+
|
|
905
|
+
// Ensure validation fields are reactive even if the saved field object did not include them
|
|
906
|
+
if (!Object.prototype.hasOwnProperty.call(this.currentField, 'validate')) {
|
|
907
|
+
this.$set(this.currentField, 'validate', null);
|
|
908
|
+
}
|
|
909
|
+
if (!Object.prototype.hasOwnProperty.call(this.currentField, 'validatePattern')) {
|
|
910
|
+
this.$set(this.currentField, 'validatePattern', '');
|
|
911
|
+
}
|
|
912
|
+
if (!Object.prototype.hasOwnProperty.call(this.currentField, 'validateMessage')) {
|
|
913
|
+
this.$set(this.currentField, 'validateMessage', '');
|
|
914
|
+
}
|
|
904
915
|
|
|
905
916
|
// Normalize options
|
|
906
917
|
if (this.currentField.options && Array.isArray(this.currentField.options)) {
|
|
@@ -931,8 +942,9 @@
|
|
|
931
942
|
|
|
932
943
|
// Handle validation
|
|
933
944
|
if (this.currentField.validate && this.currentField.validate.startsWith('regex:')) {
|
|
945
|
+
const raw = String(this.currentField.validate);
|
|
934
946
|
this.currentField.validate = 'regex';
|
|
935
|
-
this.currentField.validatePattern =
|
|
947
|
+
this.currentField.validatePattern = raw.substring(6);
|
|
936
948
|
}
|
|
937
949
|
|
|
938
950
|
// Normalize autoFill
|
|
@@ -985,6 +997,12 @@
|
|
|
985
997
|
if (!this.currentField.id || !this.currentField.label) {
|
|
986
998
|
return fail('Field ID and Label are required');
|
|
987
999
|
}
|
|
1000
|
+
|
|
1001
|
+
// Validate regex pattern if selected
|
|
1002
|
+
if (this.currentField.validate === 'regex') {
|
|
1003
|
+
const pat = String(this.currentField.validatePattern || '').trim();
|
|
1004
|
+
if (!pat) return fail('Regex validation requires a pattern');
|
|
1005
|
+
}
|
|
988
1006
|
|
|
989
1007
|
// Validate field ID
|
|
990
1008
|
if (!/^[a-zA-Z0-9._-]+$/.test(this.currentField.id)) {
|
|
@@ -785,18 +785,27 @@ email{{currentField.keyvalueDelimiter || '='}}john@example.com</code></pre>
|
|
|
785
785
|
v-model="currentField.validate"
|
|
786
786
|
:options="validationTypes"
|
|
787
787
|
/>
|
|
788
|
-
<b-form-input
|
|
789
|
-
v-if="currentField.validate === 'regex'"
|
|
790
|
-
v-model="currentField.validatePattern"
|
|
791
|
-
placeholder="Regex pattern (e.g., ^[A-Z]{2}\\d{4}$)"
|
|
792
|
-
class="mt-2"
|
|
793
|
-
/>
|
|
794
788
|
<small class="form-text text-muted">
|
|
795
789
|
<strong>Email:</strong> Validates email format (user@domain.com)<br>
|
|
796
790
|
<strong>Phone:</strong> Validates phone number format (10+ digits)<br>
|
|
797
791
|
<strong>Regex:</strong> Custom pattern validation (e.g., ^[A-Z]{2}\\d{4}$ for codes like AB1234)
|
|
798
792
|
</small>
|
|
799
793
|
</b-form-group>
|
|
794
|
+
|
|
795
|
+
<b-form-group
|
|
796
|
+
v-if="currentField.validate === 'regex'"
|
|
797
|
+
label="Regex pattern"
|
|
798
|
+
label-for="validate-pattern"
|
|
799
|
+
>
|
|
800
|
+
<b-form-input
|
|
801
|
+
id="validate-pattern"
|
|
802
|
+
v-model="currentField.validatePattern"
|
|
803
|
+
placeholder="e.g., ^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}\.$"
|
|
804
|
+
/>
|
|
805
|
+
<small class="form-text text-muted">
|
|
806
|
+
JavaScript RegExp source only (no <code>/.../</code> slashes). If hand-editing JSON, escape backslashes (e.g. write <code>\\.</code> not <code>\.</code>).
|
|
807
|
+
</small>
|
|
808
|
+
</b-form-group>
|
|
800
809
|
|
|
801
810
|
<b-form-group label="Validation Message" label-for="validate-msg" v-if="currentField.validate">
|
|
802
811
|
<b-form-input
|
|
@@ -901,6 +901,17 @@
|
|
|
901
901
|
this.editingFieldLocation = { sectionIdx, fieldIdx };
|
|
902
902
|
const field = this.schema.sections[sectionIdx].fields[fieldIdx];
|
|
903
903
|
this.currentField = JSON.parse(JSON.stringify(field));
|
|
904
|
+
|
|
905
|
+
// Ensure validation fields are reactive even if the saved field object did not include them
|
|
906
|
+
if (!Object.prototype.hasOwnProperty.call(this.currentField, 'validate')) {
|
|
907
|
+
this.$set(this.currentField, 'validate', null);
|
|
908
|
+
}
|
|
909
|
+
if (!Object.prototype.hasOwnProperty.call(this.currentField, 'validatePattern')) {
|
|
910
|
+
this.$set(this.currentField, 'validatePattern', '');
|
|
911
|
+
}
|
|
912
|
+
if (!Object.prototype.hasOwnProperty.call(this.currentField, 'validateMessage')) {
|
|
913
|
+
this.$set(this.currentField, 'validateMessage', '');
|
|
914
|
+
}
|
|
904
915
|
|
|
905
916
|
// Normalize options
|
|
906
917
|
if (this.currentField.options && Array.isArray(this.currentField.options)) {
|
|
@@ -931,8 +942,9 @@
|
|
|
931
942
|
|
|
932
943
|
// Handle validation
|
|
933
944
|
if (this.currentField.validate && this.currentField.validate.startsWith('regex:')) {
|
|
945
|
+
const raw = String(this.currentField.validate);
|
|
934
946
|
this.currentField.validate = 'regex';
|
|
935
|
-
this.currentField.validatePattern =
|
|
947
|
+
this.currentField.validatePattern = raw.substring(6);
|
|
936
948
|
}
|
|
937
949
|
|
|
938
950
|
// Normalize autoFill
|
|
@@ -985,6 +997,12 @@
|
|
|
985
997
|
if (!this.currentField.id || !this.currentField.label) {
|
|
986
998
|
return fail('Field ID and Label are required');
|
|
987
999
|
}
|
|
1000
|
+
|
|
1001
|
+
// Validate regex pattern if selected
|
|
1002
|
+
if (this.currentField.validate === 'regex') {
|
|
1003
|
+
const pat = String(this.currentField.validatePattern || '').trim();
|
|
1004
|
+
if (!pat) return fail('Regex validation requires a pattern');
|
|
1005
|
+
}
|
|
988
1006
|
|
|
989
1007
|
// Validate field ID
|
|
990
1008
|
if (!/^[a-zA-Z0-9._-]+$/.test(this.currentField.id)) {
|
package/package.json
CHANGED
|
@@ -1213,14 +1213,34 @@
|
|
|
1213
1213
|
error = 'Please enter a valid phone number';
|
|
1214
1214
|
}
|
|
1215
1215
|
} else if (field.validate.startsWith('regex:')) {
|
|
1216
|
-
const
|
|
1216
|
+
const raw = String(field.validate).substring(6);
|
|
1217
|
+
const s = String(raw || '').trim();
|
|
1217
1218
|
try {
|
|
1218
|
-
|
|
1219
|
-
|
|
1219
|
+
// Support both:
|
|
1220
|
+
// - regex:<pattern>
|
|
1221
|
+
// - regex:/<pattern>/<flags>
|
|
1222
|
+
// (Note: slashes are NOT required; they are optional for user convenience.)
|
|
1223
|
+
let regex;
|
|
1224
|
+
if (s.startsWith('/') && s.lastIndexOf('/') > 0) {
|
|
1225
|
+
const lastSlash = s.lastIndexOf('/');
|
|
1226
|
+
const body = s.slice(1, lastSlash);
|
|
1227
|
+
const flags = s.slice(lastSlash + 1);
|
|
1228
|
+
if (/^[gimsuy]*$/.test(flags)) {
|
|
1229
|
+
regex = new RegExp(body, flags);
|
|
1230
|
+
} else {
|
|
1231
|
+
// Invalid flags - fall back to raw pattern (so we still fail visibly if invalid)
|
|
1232
|
+
regex = new RegExp(s);
|
|
1233
|
+
}
|
|
1234
|
+
} else {
|
|
1235
|
+
regex = new RegExp(s);
|
|
1236
|
+
}
|
|
1237
|
+
if (!regex.test(String(value))) {
|
|
1220
1238
|
error = field.validateMessage || 'Invalid format';
|
|
1221
1239
|
}
|
|
1222
1240
|
} catch (e) {
|
|
1223
|
-
console.warn('Invalid regex pattern:',
|
|
1241
|
+
console.warn('Invalid regex pattern:', s, e);
|
|
1242
|
+
// Treat misconfigured regex as a validation error so it is visible
|
|
1243
|
+
error = field.validateMessage || 'Invalid validation pattern (regex)';
|
|
1224
1244
|
}
|
|
1225
1245
|
}
|
|
1226
1246
|
}
|
|
@@ -747,18 +747,62 @@
|
|
|
747
747
|
if (!field || !field.id) return true;
|
|
748
748
|
const v = this.formData[field.id];
|
|
749
749
|
let err = '';
|
|
750
|
+
|
|
751
|
+
// Required validation
|
|
750
752
|
if (field.required) {
|
|
751
753
|
if (field.type === 'checkbox') {
|
|
752
|
-
if (!v) err = 'Required';
|
|
754
|
+
if (!v) err = field.validateMessage || 'Required';
|
|
753
755
|
} else if (field.type === 'keyvalue' && field.keyvalueMode === 'delimiter') {
|
|
754
756
|
const lines = keyvalueDelimiterLines(v);
|
|
755
|
-
if (!lines.length) err = 'Required';
|
|
757
|
+
if (!lines.length) err = field.validateMessage || 'Required';
|
|
756
758
|
} else if (Array.isArray(v)) {
|
|
757
|
-
if (!v.length) err = 'Required';
|
|
759
|
+
if (!v.length) err = field.validateMessage || 'Required';
|
|
758
760
|
} else {
|
|
759
|
-
if (v === null || v === undefined || String(v).trim() === '') err = 'Required';
|
|
761
|
+
if (v === null || v === undefined || String(v).trim() === '') err = field.validateMessage || 'Required';
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Skip other validations if field is empty and not required
|
|
766
|
+
if (!err && (!v || String(v).trim() === '') && !field.required) {
|
|
767
|
+
this.fieldErrors[field.id] = '';
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Custom validators (email/phone/regex:<pattern> or regex:/pattern/flags)
|
|
772
|
+
if (!err && field.validate) {
|
|
773
|
+
const validate = String(field.validate || '').trim();
|
|
774
|
+
if (validate === 'email') {
|
|
775
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
776
|
+
if (!emailRegex.test(String(v))) err = field.validateMessage || 'Please enter a valid email address';
|
|
777
|
+
} else if (validate === 'phone') {
|
|
778
|
+
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
|
|
779
|
+
const s = String(v);
|
|
780
|
+
if (!phoneRegex.test(s) || s.replace(/\D/g, '').length < 10) err = field.validateMessage || 'Please enter a valid phone number';
|
|
781
|
+
} else if (validate.startsWith('regex:')) {
|
|
782
|
+
const raw = validate.substring(6);
|
|
783
|
+
const s = String(raw || '').trim();
|
|
784
|
+
try {
|
|
785
|
+
let regex;
|
|
786
|
+
if (s.startsWith('/') && s.lastIndexOf('/') > 0) {
|
|
787
|
+
const lastSlash = s.lastIndexOf('/');
|
|
788
|
+
const body = s.slice(1, lastSlash);
|
|
789
|
+
const flags = s.slice(lastSlash + 1);
|
|
790
|
+
if (/^[gimsuy]*$/.test(flags)) {
|
|
791
|
+
regex = new RegExp(body, flags);
|
|
792
|
+
} else {
|
|
793
|
+
regex = new RegExp(s);
|
|
794
|
+
}
|
|
795
|
+
} else {
|
|
796
|
+
regex = new RegExp(s);
|
|
797
|
+
}
|
|
798
|
+
if (!regex.test(String(v))) err = field.validateMessage || 'Invalid format';
|
|
799
|
+
} catch (e) {
|
|
800
|
+
console.warn('Invalid regex pattern:', s, e);
|
|
801
|
+
err = field.validateMessage || 'Invalid validation pattern (regex)';
|
|
802
|
+
}
|
|
760
803
|
}
|
|
761
804
|
}
|
|
805
|
+
|
|
762
806
|
this.fieldErrors[field.id] = err;
|
|
763
807
|
return !err;
|
|
764
808
|
},
|