@cyprnet/node-red-contrib-uibuilder-formgen 0.5.1 → 0.5.3
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 +8 -0
- package/examples/formgen-builder/src/index.html +63 -0
- package/examples/formgen-builder/src/index.js +310 -4
- package/nodes/uibuilder-formgen-v3.html +12 -0
- package/nodes/uibuilder-formgen-v3.js +24 -1
- package/nodes/uibuilder-formgen.html +12 -0
- package/nodes/uibuilder-formgen.js +24 -1
- package/package.json +1 -1
- package/templates/index.html.mustache +9 -2
- package/templates/index.v3.html.mustache +9 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.5.3
|
|
6
|
+
|
|
7
|
+
- FormGen node: added **Logo height/width** options (defaults to **252h x 1100w**) with per-message override support.
|
|
8
|
+
|
|
9
|
+
## 0.5.2
|
|
10
|
+
|
|
11
|
+
- Schema Builder: added **Generate Schema (Plain JSON)** to auto-create a PortalSmith schema from a plain JSON config object and load it into the builder for editing.
|
|
12
|
+
|
|
5
13
|
## 0.4.0
|
|
6
14
|
|
|
7
15
|
- Added **offline-first licensing framework** (Free vs Licensed).
|
|
@@ -239,6 +239,9 @@
|
|
|
239
239
|
<b-button variant="info" @click="loadSchema" class="mr-2">
|
|
240
240
|
<i class="fa fa-folder-open"></i> Load Schema
|
|
241
241
|
</b-button>
|
|
242
|
+
<b-button variant="warning" @click="openPlainJsonGenerator" class="mr-2">
|
|
243
|
+
<i class="fa fa-bolt"></i> Generate Schema (Plain JSON)
|
|
244
|
+
</b-button>
|
|
242
245
|
<b-button variant="success" @click="importJSON" class="mr-2">
|
|
243
246
|
<i class="fa fa-file-import"></i> Import JSON
|
|
244
247
|
</b-button>
|
|
@@ -709,6 +712,66 @@ email{{currentField.keyvalueDelimiter || '='}}john@example.com</code></pre>
|
|
|
709
712
|
</small>
|
|
710
713
|
</b-form-group>
|
|
711
714
|
</b-modal>
|
|
715
|
+
|
|
716
|
+
<!-- Plain JSON -> Schema Generator Modal -->
|
|
717
|
+
<b-modal
|
|
718
|
+
id="plain-json-modal"
|
|
719
|
+
title="Generate Schema from Plain JSON"
|
|
720
|
+
size="lg"
|
|
721
|
+
@ok="processPlainJsonGenerate"
|
|
722
|
+
@cancel="cancelPlainJsonGenerate"
|
|
723
|
+
ok-title="Generate"
|
|
724
|
+
cancel-title="Cancel"
|
|
725
|
+
>
|
|
726
|
+
<b-alert variant="info" show class="mb-3">
|
|
727
|
+
Paste a <strong>plain JSON object</strong> (like a config file). The builder will infer field types and create sections automatically.
|
|
728
|
+
You can edit everything after generation.
|
|
729
|
+
</b-alert>
|
|
730
|
+
|
|
731
|
+
<b-form-row>
|
|
732
|
+
<b-col md="6">
|
|
733
|
+
<b-form-group label="Form ID" label-for="plain-form-id">
|
|
734
|
+
<b-form-input
|
|
735
|
+
id="plain-form-id"
|
|
736
|
+
v-model="plainGenFormId"
|
|
737
|
+
placeholder="my_form_id"
|
|
738
|
+
/>
|
|
739
|
+
<small class="form-text text-muted">Alphanumeric, underscores, hyphens only. If blank, we will derive one.</small>
|
|
740
|
+
</b-form-group>
|
|
741
|
+
</b-col>
|
|
742
|
+
<b-col md="6">
|
|
743
|
+
<b-form-group label="Form Title" label-for="plain-title">
|
|
744
|
+
<b-form-input
|
|
745
|
+
id="plain-title"
|
|
746
|
+
v-model="plainGenTitle"
|
|
747
|
+
placeholder="My Form Title"
|
|
748
|
+
/>
|
|
749
|
+
<small class="form-text text-muted">If blank, we will derive a title.</small>
|
|
750
|
+
</b-form-group>
|
|
751
|
+
</b-col>
|
|
752
|
+
</b-form-row>
|
|
753
|
+
|
|
754
|
+
<b-form-group label="Description (optional)" label-for="plain-desc">
|
|
755
|
+
<b-form-textarea
|
|
756
|
+
id="plain-desc"
|
|
757
|
+
v-model="plainGenDescription"
|
|
758
|
+
rows="2"
|
|
759
|
+
placeholder="Optional description"
|
|
760
|
+
/>
|
|
761
|
+
</b-form-group>
|
|
762
|
+
|
|
763
|
+
<b-form-group label="Plain JSON" label-for="plain-json">
|
|
764
|
+
<b-form-textarea
|
|
765
|
+
id="plain-json"
|
|
766
|
+
v-model="plainJsonData"
|
|
767
|
+
rows="14"
|
|
768
|
+
placeholder='{\n "username": "incadmin",\n "password": "incadmin",\n "per_page": 1000\n}'
|
|
769
|
+
/>
|
|
770
|
+
<small class="form-text text-muted">
|
|
771
|
+
Must be valid JSON. Top-level must be an object (not an array).
|
|
772
|
+
</small>
|
|
773
|
+
</b-form-group>
|
|
774
|
+
</b-modal>
|
|
712
775
|
|
|
713
776
|
<!-- Save Schema Modal -->
|
|
714
777
|
<b-modal
|
|
@@ -104,6 +104,12 @@
|
|
|
104
104
|
// CSV/JSON import
|
|
105
105
|
csvData: '',
|
|
106
106
|
jsonData: '',
|
|
107
|
+
|
|
108
|
+
// Plain JSON -> Schema generator
|
|
109
|
+
plainJsonData: '',
|
|
110
|
+
plainGenFormId: '',
|
|
111
|
+
plainGenTitle: '',
|
|
112
|
+
plainGenDescription: '',
|
|
107
113
|
|
|
108
114
|
// Save/Load
|
|
109
115
|
saveName: '',
|
|
@@ -178,6 +184,308 @@
|
|
|
178
184
|
},
|
|
179
185
|
|
|
180
186
|
methods: {
|
|
187
|
+
// ---------- Plain JSON -> Schema generator ----------
|
|
188
|
+
openPlainJsonGenerator() {
|
|
189
|
+
// Pre-fill from current schema (if any)
|
|
190
|
+
this.plainGenFormId = this.schema.formId || '';
|
|
191
|
+
this.plainGenTitle = this.schema.title || '';
|
|
192
|
+
this.plainGenDescription = this.schema.description || '';
|
|
193
|
+
this.plainJsonData = '';
|
|
194
|
+
this.$bvModal.show('plain-json-modal');
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
cancelPlainJsonGenerate() {
|
|
198
|
+
this.plainJsonData = '';
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
processPlainJsonGenerate() {
|
|
202
|
+
try {
|
|
203
|
+
const raw = (this.plainJsonData || '').trim();
|
|
204
|
+
if (!raw) {
|
|
205
|
+
this.showAlertMessage('Please paste a plain JSON object to generate a schema', 'warning');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const obj = JSON.parse(raw);
|
|
210
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
211
|
+
this.showAlertMessage('Plain JSON must be a top-level object (e.g. { "key": "value" })', 'warning');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const generated = this.generateSchemaFromPlainJson(obj, {
|
|
216
|
+
formId: this.plainGenFormId,
|
|
217
|
+
title: this.plainGenTitle,
|
|
218
|
+
description: this.plainGenDescription
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
this.applySchemaObject(generated);
|
|
222
|
+
this.$bvModal.hide('plain-json-modal');
|
|
223
|
+
|
|
224
|
+
const sectionCount = (generated.sections || []).length;
|
|
225
|
+
const fieldCount = (generated.sections || []).reduce((sum, s) => sum + ((s.fields || []).length), 0);
|
|
226
|
+
this.showAlertMessage(`Generated schema: ${fieldCount} field(s) in ${sectionCount} section(s)`, 'success');
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error('Plain JSON generate error:', error);
|
|
229
|
+
this.showAlertMessage('Generation failed: ' + (error && error.message ? error.message : String(error)), 'danger');
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
applySchemaObject(loadedSchema) {
|
|
234
|
+
// Deep copy and ensure required properties exist
|
|
235
|
+
const s = JSON.parse(JSON.stringify(loadedSchema || {}));
|
|
236
|
+
if (!s.schemaVersion) s.schemaVersion = '1.0';
|
|
237
|
+
if (!s.sections) s.sections = [];
|
|
238
|
+
if (!s.actions) s.actions = [];
|
|
239
|
+
|
|
240
|
+
this.$set(this, 'schema', {
|
|
241
|
+
schemaVersion: s.schemaVersion,
|
|
242
|
+
formId: s.formId || '',
|
|
243
|
+
title: s.title || '',
|
|
244
|
+
description: s.description || '',
|
|
245
|
+
sections: s.sections || [],
|
|
246
|
+
actions: s.actions || []
|
|
247
|
+
});
|
|
248
|
+
this.$forceUpdate();
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
generateSchemaFromPlainJson(obj, meta) {
|
|
252
|
+
const safeId = (s) => {
|
|
253
|
+
const base = String(s || '')
|
|
254
|
+
.trim()
|
|
255
|
+
.replace(/\s+/g, '_')
|
|
256
|
+
.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
257
|
+
const cleaned = base.replace(/_+/g, '_').replace(/^_+|_+$/g, '');
|
|
258
|
+
if (!cleaned) return 'field';
|
|
259
|
+
if (/^[0-9]/.test(cleaned)) return 'f_' + cleaned;
|
|
260
|
+
return cleaned;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const titleFromKey = (key) => {
|
|
264
|
+
const s = String(key || '')
|
|
265
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
266
|
+
.replace(/[_-]+/g, ' ')
|
|
267
|
+
.trim();
|
|
268
|
+
if (!s) return 'Field';
|
|
269
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const isPlainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
273
|
+
|
|
274
|
+
const isString = (v) => typeof v === 'string' || v instanceof String;
|
|
275
|
+
|
|
276
|
+
const classifySection = (key, value) => {
|
|
277
|
+
const k = String(key || '').toLowerCase();
|
|
278
|
+
|
|
279
|
+
// Specific patterns (matches the style we used manually for azSync-like payloads)
|
|
280
|
+
if (k === 'username' || k === 'password' || k.includes('apikey') || k.includes('api_key') || k.includes('token') || k.includes('secret')) {
|
|
281
|
+
return { id: 'auth', title: 'Authentication' };
|
|
282
|
+
}
|
|
283
|
+
if (k === 'flowname' || k.startsWith('job_') || k.includes('purge') || k.includes('log')) {
|
|
284
|
+
return { id: 'job', title: 'Job Settings' };
|
|
285
|
+
}
|
|
286
|
+
if (k.startsWith('az_') || k === 'type' || k === 'per_page' || k.includes('subscription') || k.startsWith('device_') || k === 'dnsdomain') {
|
|
287
|
+
return { id: 'azure', title: 'Azure Settings' };
|
|
288
|
+
}
|
|
289
|
+
if (k.startsWith('sync') || k.startsWith('process_') || k.startsWith('delete_') || k === 'async_op' || k === 'create_rr') {
|
|
290
|
+
return { id: 'sync', title: 'Sync Behavior' };
|
|
291
|
+
}
|
|
292
|
+
if (k.startsWith('throttle') || k.endsWith('_delay') || k.includes('backoff')) {
|
|
293
|
+
return { id: 'throttle', title: 'Throttle & Timing' };
|
|
294
|
+
}
|
|
295
|
+
if (k.startsWith('udf') || k.startsWith('tags_') || k.includes('template')) {
|
|
296
|
+
return { id: 'udf', title: 'UDF / Templates' };
|
|
297
|
+
}
|
|
298
|
+
if (k === 'op_filters' || k.startsWith('op_filters')) {
|
|
299
|
+
return { id: 'filters', title: 'Operation Filters' };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Fallback
|
|
303
|
+
return { id: 'general', title: 'General' };
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const schema = {
|
|
307
|
+
schemaVersion: '1.0',
|
|
308
|
+
formId: safeId(meta && meta.formId ? meta.formId : (obj.flowName || obj.formId || 'generated_form')),
|
|
309
|
+
title: (meta && meta.title ? meta.title : (obj.flowName ? `${titleFromKey(obj.flowName)} Configuration` : 'Generated Configuration')),
|
|
310
|
+
description: (meta && meta.description) || '',
|
|
311
|
+
sections: [],
|
|
312
|
+
actions: []
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const sectionsById = {};
|
|
316
|
+
const ensureSection = (sec) => {
|
|
317
|
+
if (!sectionsById[sec.id]) {
|
|
318
|
+
sectionsById[sec.id] = { id: sec.id, title: sec.title, description: '', fields: [] };
|
|
319
|
+
}
|
|
320
|
+
return sectionsById[sec.id];
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const pushField = (section, field) => {
|
|
324
|
+
if (!field || !field.id || !field.type) return;
|
|
325
|
+
// Avoid duplicate ids in a section by suffixing
|
|
326
|
+
const existing = new Set((section.fields || []).map(f => f.id));
|
|
327
|
+
let fid = field.id;
|
|
328
|
+
let n = 2;
|
|
329
|
+
while (existing.has(fid)) {
|
|
330
|
+
fid = `${field.id}_${n++}`;
|
|
331
|
+
}
|
|
332
|
+
field.id = fid;
|
|
333
|
+
section.fields.push(field);
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const inferPrimitiveField = (key, value, labelOverride) => {
|
|
337
|
+
const k = String(key || '');
|
|
338
|
+
const lower = k.toLowerCase();
|
|
339
|
+
const label = labelOverride || titleFromKey(k);
|
|
340
|
+
|
|
341
|
+
if (typeof value === 'boolean') {
|
|
342
|
+
return {
|
|
343
|
+
id: safeId(k),
|
|
344
|
+
label,
|
|
345
|
+
type: 'checkbox',
|
|
346
|
+
required: false,
|
|
347
|
+
defaultValue: value
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
352
|
+
const isInt = Number.isInteger(value);
|
|
353
|
+
const field = {
|
|
354
|
+
id: safeId(k),
|
|
355
|
+
label,
|
|
356
|
+
type: 'number',
|
|
357
|
+
required: false,
|
|
358
|
+
defaultValue: value,
|
|
359
|
+
step: isInt ? 1 : 0.01
|
|
360
|
+
};
|
|
361
|
+
if (value >= 0) field.min = 0;
|
|
362
|
+
return field;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Strings and everything else -> text
|
|
366
|
+
const field = {
|
|
367
|
+
id: safeId(k),
|
|
368
|
+
label,
|
|
369
|
+
type: 'text',
|
|
370
|
+
required: false,
|
|
371
|
+
defaultValue: value === null || value === undefined ? '' : String(value)
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
if (lower.includes('password')) field.inputType = 'password';
|
|
375
|
+
else if (lower.includes('email')) { field.inputType = 'email'; field.validate = 'email'; }
|
|
376
|
+
else if (lower.includes('phone') || lower.includes('tel')) { field.inputType = 'tel'; field.validate = 'phone'; }
|
|
377
|
+
else if (lower.includes('url')) field.inputType = 'url';
|
|
378
|
+
|
|
379
|
+
// Note: keep "true"/"false" strings as strings (we only convert actual booleans)
|
|
380
|
+
if (isString(value) && (String(value) === 'true' || String(value) === 'false')) {
|
|
381
|
+
field.help = 'This value was a string in the source JSON.';
|
|
382
|
+
}
|
|
383
|
+
return field;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const inferArrayField = (key, arr, labelOverride, helpPrefix) => {
|
|
387
|
+
const label = labelOverride || titleFromKey(key);
|
|
388
|
+
const isPrim = Array.isArray(arr) && arr.every(v => v === null || ['string', 'number', 'boolean'].includes(typeof v));
|
|
389
|
+
const field = {
|
|
390
|
+
id: safeId(key),
|
|
391
|
+
label,
|
|
392
|
+
type: 'textarea',
|
|
393
|
+
required: false,
|
|
394
|
+
placeholder: 'One per line',
|
|
395
|
+
rows: 4
|
|
396
|
+
};
|
|
397
|
+
if (helpPrefix) field.help = helpPrefix;
|
|
398
|
+
if (isPrim) {
|
|
399
|
+
field.defaultValue = arr.map(v => (v === null || v === undefined) ? '' : String(v)).filter(s => s !== '').join('\n');
|
|
400
|
+
} else {
|
|
401
|
+
field.defaultValue = JSON.stringify(arr, null, 2);
|
|
402
|
+
field.help = (field.help ? field.help + ' ' : '') + 'Array contains objects; generated as JSON text.';
|
|
403
|
+
}
|
|
404
|
+
return field;
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
const inferObjectFieldOrFields = (key, valueObj) => {
|
|
408
|
+
// Special-case op_filters style: object of arrays -> separate list fields
|
|
409
|
+
if (String(key).toLowerCase() === 'op_filters' && isPlainObject(valueObj)) {
|
|
410
|
+
const fields = [];
|
|
411
|
+
Object.keys(valueObj).forEach((subKey) => {
|
|
412
|
+
const v = valueObj[subKey];
|
|
413
|
+
const fullId = `${key}_${subKey}`;
|
|
414
|
+
if (Array.isArray(v)) {
|
|
415
|
+
fields.push(inferArrayField(fullId, v, `${titleFromKey(subKey)} Filter (list)`, undefined));
|
|
416
|
+
} else {
|
|
417
|
+
fields.push(inferPrimitiveField(fullId, v, `${titleFromKey(subKey)} Filter`));
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
return fields;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Object of string values -> keyvalue pairs
|
|
424
|
+
const keys = Object.keys(valueObj || {});
|
|
425
|
+
const allStringValues = keys.length > 0 && keys.every(k => typeof valueObj[k] === 'string' || valueObj[k] instanceof String);
|
|
426
|
+
if (allStringValues) {
|
|
427
|
+
return [{
|
|
428
|
+
id: safeId(key),
|
|
429
|
+
label: titleFromKey(key),
|
|
430
|
+
type: 'keyvalue',
|
|
431
|
+
required: false,
|
|
432
|
+
keyvalueMode: 'pairs',
|
|
433
|
+
pairs: keys.map(k => ({ key: String(k), value: String(valueObj[k]) }))
|
|
434
|
+
}];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Fallback: flatten one level into individual fields
|
|
438
|
+
const fields = [];
|
|
439
|
+
keys.forEach((subKey) => {
|
|
440
|
+
const v = valueObj[subKey];
|
|
441
|
+
const fullKey = `${key}_${subKey}`;
|
|
442
|
+
if (Array.isArray(v)) fields.push(inferArrayField(fullKey, v, titleFromKey(subKey), `From ${key}.${subKey}.`));
|
|
443
|
+
else if (isPlainObject(v)) fields.push(inferPrimitiveField(fullKey, JSON.stringify(v), titleFromKey(subKey)));
|
|
444
|
+
else fields.push(inferPrimitiveField(fullKey, v, titleFromKey(subKey)));
|
|
445
|
+
});
|
|
446
|
+
if (fields.length === 0) {
|
|
447
|
+
// Empty object -> represent as textarea JSON
|
|
448
|
+
return [inferPrimitiveField(key, JSON.stringify(valueObj || {}), titleFromKey(key))];
|
|
449
|
+
}
|
|
450
|
+
return fields;
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
// Process top-level keys in stable order
|
|
454
|
+
Object.keys(obj).sort().forEach((key) => {
|
|
455
|
+
const value = obj[key];
|
|
456
|
+
const secInfo = classifySection(key, value);
|
|
457
|
+
const section = ensureSection(secInfo);
|
|
458
|
+
|
|
459
|
+
if (Array.isArray(value)) {
|
|
460
|
+
pushField(section, inferArrayField(key, value));
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (isPlainObject(value)) {
|
|
465
|
+
const fields = inferObjectFieldOrFields(key, value);
|
|
466
|
+
fields.forEach(f => pushField(section, f));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
pushField(section, inferPrimitiveField(key, value));
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
// Finalize sections: keep a preferred order when present
|
|
474
|
+
const preferredOrder = ['auth', 'job', 'azure', 'sync', 'throttle', 'udf', 'filters', 'general'];
|
|
475
|
+
const allSections = Object.values(sectionsById).filter(s => s.fields && s.fields.length > 0);
|
|
476
|
+
allSections.sort((a, b) => {
|
|
477
|
+
const ia = preferredOrder.indexOf(a.id);
|
|
478
|
+
const ib = preferredOrder.indexOf(b.id);
|
|
479
|
+
if (ia === -1 && ib === -1) return a.id.localeCompare(b.id);
|
|
480
|
+
if (ia === -1) return 1;
|
|
481
|
+
if (ib === -1) return -1;
|
|
482
|
+
return ia - ib;
|
|
483
|
+
});
|
|
484
|
+
schema.sections = allSections;
|
|
485
|
+
|
|
486
|
+
return schema;
|
|
487
|
+
},
|
|
488
|
+
|
|
181
489
|
isSchemaEmpty() {
|
|
182
490
|
const s = this.schema || {};
|
|
183
491
|
return (
|
|
@@ -1190,12 +1498,10 @@
|
|
|
1190
1498
|
}
|
|
1191
1499
|
|
|
1192
1500
|
// Replace current schema
|
|
1193
|
-
this.
|
|
1501
|
+
this.applySchemaObject(imported);
|
|
1194
1502
|
|
|
1195
1503
|
// Ensure actions array exists
|
|
1196
|
-
|
|
1197
|
-
this.schema.actions = [];
|
|
1198
|
-
}
|
|
1504
|
+
// (applySchemaObject already ensures actions exists)
|
|
1199
1505
|
|
|
1200
1506
|
this.$bvModal.hide('json-modal');
|
|
1201
1507
|
this.showAlertMessage('Schema imported successfully', 'success');
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
themeMode: { value: "auto" },
|
|
13
13
|
logoPath: { value: "" },
|
|
14
14
|
logoAlt: { value: "Logo" },
|
|
15
|
+
logoHeight: { value: 252 },
|
|
16
|
+
logoWidth: { value: 1100 },
|
|
15
17
|
licenseConfig: { value: "", type: "portalsmith-license", required: false },
|
|
16
18
|
submitMode: { value: "uibuilder" },
|
|
17
19
|
submitUrl: { value: "" },
|
|
@@ -127,6 +129,16 @@
|
|
|
127
129
|
<input type="text" id="node-input-logoAlt" placeholder="Logo" />
|
|
128
130
|
</div>
|
|
129
131
|
|
|
132
|
+
<div class="form-row">
|
|
133
|
+
<label for="node-input-logoHeight"><i class="fa fa-arrows-v"></i> Logo height (px)</label>
|
|
134
|
+
<input type="number" id="node-input-logoHeight" min="1" placeholder="252" />
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div class="form-row">
|
|
138
|
+
<label for="node-input-logoWidth"><i class="fa fa-arrows-h"></i> Logo width (px)</label>
|
|
139
|
+
<input type="number" id="node-input-logoWidth" min="1" placeholder="1100" />
|
|
140
|
+
</div>
|
|
141
|
+
|
|
130
142
|
<hr/>
|
|
131
143
|
<div class="form-row">
|
|
132
144
|
<strong>Licensing (offline-first)</strong>
|
|
@@ -14,6 +14,17 @@ module.exports = function(RED) {
|
|
|
14
14
|
const https = require("https");
|
|
15
15
|
const licensing = require("../lib/licensing");
|
|
16
16
|
|
|
17
|
+
const DEFAULT_LOGO_MAX_HEIGHT_PX = 252;
|
|
18
|
+
const DEFAULT_LOGO_MAX_WIDTH_PX = 1100;
|
|
19
|
+
|
|
20
|
+
function toPositiveIntOrDefault(value, defaultValue) {
|
|
21
|
+
const n = Number(value);
|
|
22
|
+
if (!Number.isFinite(n)) return defaultValue;
|
|
23
|
+
const i = Math.round(n);
|
|
24
|
+
if (i <= 0) return defaultValue;
|
|
25
|
+
return i;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
// Register admin endpoints once (used by the editor UI to show license status)
|
|
18
29
|
let _adminRegistered = false;
|
|
19
30
|
function registerAdminEndpoints() {
|
|
@@ -358,6 +369,14 @@ module.exports = function(RED) {
|
|
|
358
369
|
const logoRequested = isNonEmptyString(msg.options?.logoPath ?? config.logoPath ?? "");
|
|
359
370
|
let logoPathRaw = msg.options?.logoPath ?? config.logoPath ?? "";
|
|
360
371
|
const logoAltRaw = msg.options?.logoAlt ?? config.logoAlt ?? "Logo";
|
|
372
|
+
const logoMaxHeightPx = toPositiveIntOrDefault(
|
|
373
|
+
msg.options?.logoHeight ?? config.logoHeight ?? DEFAULT_LOGO_MAX_HEIGHT_PX,
|
|
374
|
+
DEFAULT_LOGO_MAX_HEIGHT_PX
|
|
375
|
+
);
|
|
376
|
+
const logoMaxWidthPx = toPositiveIntOrDefault(
|
|
377
|
+
msg.options?.logoWidth ?? config.logoWidth ?? DEFAULT_LOGO_MAX_WIDTH_PX,
|
|
378
|
+
DEFAULT_LOGO_MAX_WIDTH_PX
|
|
379
|
+
);
|
|
361
380
|
if (isNonEmptyString(logoPathRaw) && licensePublic.brandingLocked) {
|
|
362
381
|
node.warn("PortalSmith FormGen: custom logo is disabled in Free mode (watermarked). Using default branding.");
|
|
363
382
|
licensePublic.brandingAttempted = Boolean(logoRequested);
|
|
@@ -403,6 +422,8 @@ module.exports = function(RED) {
|
|
|
403
422
|
themeMode: themeMode,
|
|
404
423
|
logoUrl: logoUrl,
|
|
405
424
|
logoAlt: logoAlt,
|
|
425
|
+
logoMaxHeightPx: logoMaxHeightPx,
|
|
426
|
+
logoMaxWidthPx: logoMaxWidthPx,
|
|
406
427
|
licensed: licensePublic.licensed
|
|
407
428
|
};
|
|
408
429
|
|
|
@@ -434,7 +455,7 @@ module.exports = function(RED) {
|
|
|
434
455
|
await fs.writeJson(targetSchema, schema, { spaces: 2 });
|
|
435
456
|
|
|
436
457
|
const runtimeData = {
|
|
437
|
-
generatorVersion: "0.5.
|
|
458
|
+
generatorVersion: "0.5.3",
|
|
438
459
|
generatorNode: "uibuilder-formgen-v3",
|
|
439
460
|
timestamp: new Date().toISOString(),
|
|
440
461
|
instanceName: instanceName,
|
|
@@ -443,6 +464,8 @@ module.exports = function(RED) {
|
|
|
443
464
|
themeMode: themeMode,
|
|
444
465
|
logoUrl: logoUrl || "",
|
|
445
466
|
logoAlt: logoAlt || "",
|
|
467
|
+
logoMaxHeightPx: logoMaxHeightPx,
|
|
468
|
+
logoMaxWidthPx: logoMaxWidthPx,
|
|
446
469
|
submitMode: submitMode,
|
|
447
470
|
submitUrl: submitUrl,
|
|
448
471
|
license: licensePublic,
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
themeMode: { value: "auto" },
|
|
13
13
|
logoPath: { value: "" },
|
|
14
14
|
logoAlt: { value: "Logo" },
|
|
15
|
+
logoHeight: { value: 252 },
|
|
16
|
+
logoWidth: { value: 1100 },
|
|
15
17
|
licenseConfig: { value: "", type: "portalsmith-license", required: false },
|
|
16
18
|
submitMode: { value: "uibuilder" },
|
|
17
19
|
submitUrl: { value: "" },
|
|
@@ -139,6 +141,16 @@
|
|
|
139
141
|
<input type="text" id="node-input-logoAlt" placeholder="Logo" />
|
|
140
142
|
</div>
|
|
141
143
|
|
|
144
|
+
<div class="form-row">
|
|
145
|
+
<label for="node-input-logoHeight"><i class="fa fa-arrows-v"></i> Logo height (px)</label>
|
|
146
|
+
<input type="number" id="node-input-logoHeight" min="1" placeholder="252" />
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<div class="form-row">
|
|
150
|
+
<label for="node-input-logoWidth"><i class="fa fa-arrows-h"></i> Logo width (px)</label>
|
|
151
|
+
<input type="number" id="node-input-logoWidth" min="1" placeholder="1100" />
|
|
152
|
+
</div>
|
|
153
|
+
|
|
142
154
|
<hr/>
|
|
143
155
|
<div class="form-row">
|
|
144
156
|
<strong>Licensing (offline-first)</strong>
|
|
@@ -14,6 +14,17 @@ module.exports = function(RED) {
|
|
|
14
14
|
const https = require("https");
|
|
15
15
|
const licensing = require("../lib/licensing");
|
|
16
16
|
|
|
17
|
+
const DEFAULT_LOGO_MAX_HEIGHT_PX = 252;
|
|
18
|
+
const DEFAULT_LOGO_MAX_WIDTH_PX = 1100;
|
|
19
|
+
|
|
20
|
+
function toPositiveIntOrDefault(value, defaultValue) {
|
|
21
|
+
const n = Number(value);
|
|
22
|
+
if (!Number.isFinite(n)) return defaultValue;
|
|
23
|
+
const i = Math.round(n);
|
|
24
|
+
if (i <= 0) return defaultValue;
|
|
25
|
+
return i;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
// Register admin endpoints once (used by the editor UI to show license status)
|
|
18
29
|
let _adminRegistered = false;
|
|
19
30
|
function registerAdminEndpoints() {
|
|
@@ -362,6 +373,14 @@ module.exports = function(RED) {
|
|
|
362
373
|
const logoRequested = isNonEmptyString(msg.options?.logoPath ?? config.logoPath ?? "");
|
|
363
374
|
let logoPathRaw = msg.options?.logoPath ?? config.logoPath ?? "";
|
|
364
375
|
const logoAltRaw = msg.options?.logoAlt ?? config.logoAlt ?? "Logo";
|
|
376
|
+
const logoMaxHeightPx = toPositiveIntOrDefault(
|
|
377
|
+
msg.options?.logoHeight ?? config.logoHeight ?? DEFAULT_LOGO_MAX_HEIGHT_PX,
|
|
378
|
+
DEFAULT_LOGO_MAX_HEIGHT_PX
|
|
379
|
+
);
|
|
380
|
+
const logoMaxWidthPx = toPositiveIntOrDefault(
|
|
381
|
+
msg.options?.logoWidth ?? config.logoWidth ?? DEFAULT_LOGO_MAX_WIDTH_PX,
|
|
382
|
+
DEFAULT_LOGO_MAX_WIDTH_PX
|
|
383
|
+
);
|
|
365
384
|
if (isNonEmptyString(logoPathRaw) && licensePublic.brandingLocked) {
|
|
366
385
|
node.warn("PortalSmith FormGen: custom logo is disabled in Free mode (watermarked). Using default branding.");
|
|
367
386
|
licensePublic.brandingAttempted = Boolean(logoRequested);
|
|
@@ -425,6 +444,8 @@ module.exports = function(RED) {
|
|
|
425
444
|
themeMode: themeMode,
|
|
426
445
|
logoUrl: logoUrl,
|
|
427
446
|
logoAlt: logoAlt,
|
|
447
|
+
logoMaxHeightPx: logoMaxHeightPx,
|
|
448
|
+
logoMaxWidthPx: logoMaxWidthPx,
|
|
428
449
|
licensed: licensePublic.licensed
|
|
429
450
|
};
|
|
430
451
|
|
|
@@ -463,7 +484,7 @@ module.exports = function(RED) {
|
|
|
463
484
|
|
|
464
485
|
// Write runtime metadata
|
|
465
486
|
const runtimeData = {
|
|
466
|
-
generatorVersion: "0.5.
|
|
487
|
+
generatorVersion: "0.5.3",
|
|
467
488
|
timestamp: new Date().toISOString(),
|
|
468
489
|
instanceName: instanceName,
|
|
469
490
|
storageMode: storageMode,
|
|
@@ -471,6 +492,8 @@ module.exports = function(RED) {
|
|
|
471
492
|
themeMode: themeMode,
|
|
472
493
|
logoUrl: logoUrl || "",
|
|
473
494
|
logoAlt: logoAlt || "",
|
|
495
|
+
logoMaxHeightPx: logoMaxHeightPx,
|
|
496
|
+
logoMaxWidthPx: logoMaxWidthPx,
|
|
474
497
|
submitMode: submitMode,
|
|
475
498
|
submitUrl: submitUrl,
|
|
476
499
|
license: licensePublic,
|
package/package.json
CHANGED
|
@@ -137,6 +137,13 @@
|
|
|
137
137
|
height: auto;
|
|
138
138
|
object-fit: contain;
|
|
139
139
|
}
|
|
140
|
+
.ps-custom-logo {
|
|
141
|
+
max-height: var(--ps-logo-max-height, 252px);
|
|
142
|
+
max-width: var(--ps-logo-max-width, 1100px);
|
|
143
|
+
width: auto;
|
|
144
|
+
height: auto;
|
|
145
|
+
object-fit: contain;
|
|
146
|
+
}
|
|
140
147
|
.ps-watermark {
|
|
141
148
|
position: fixed;
|
|
142
149
|
right: 14px;
|
|
@@ -186,10 +193,10 @@
|
|
|
186
193
|
<body>
|
|
187
194
|
<div id="app">
|
|
188
195
|
<div class="container form-container">
|
|
189
|
-
<div class="ps-header">
|
|
196
|
+
<div class="ps-header" style="--ps-logo-max-height: [[logoMaxHeightPx]]px; --ps-logo-max-width: [[logoMaxWidthPx]]px;">
|
|
190
197
|
<h1 class="mb-0">[[title]]</h1>
|
|
191
198
|
[[#logoUrl]]
|
|
192
|
-
<img class="ps-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
|
|
199
|
+
<img class="ps-custom-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
|
|
193
200
|
[[/logoUrl]]
|
|
194
201
|
[[^logoUrl]]
|
|
195
202
|
[[^licensed]]
|
|
@@ -120,6 +120,13 @@
|
|
|
120
120
|
height: auto;
|
|
121
121
|
object-fit: contain;
|
|
122
122
|
}
|
|
123
|
+
.ps-custom-logo {
|
|
124
|
+
max-height: var(--ps-logo-max-height, 252px);
|
|
125
|
+
max-width: var(--ps-logo-max-width, 1100px);
|
|
126
|
+
width: auto;
|
|
127
|
+
height: auto;
|
|
128
|
+
object-fit: contain;
|
|
129
|
+
}
|
|
123
130
|
.ps-watermark {
|
|
124
131
|
position: fixed;
|
|
125
132
|
right: 14px;
|
|
@@ -169,10 +176,10 @@
|
|
|
169
176
|
<body>
|
|
170
177
|
<div id="app">
|
|
171
178
|
<div class="container form-container">
|
|
172
|
-
<div class="ps-header">
|
|
179
|
+
<div class="ps-header" style="--ps-logo-max-height: [[logoMaxHeightPx]]px; --ps-logo-max-width: [[logoMaxWidthPx]]px;">
|
|
173
180
|
<h1 class="mb-0">[[title]]</h1>
|
|
174
181
|
[[#logoUrl]]
|
|
175
|
-
<img class="ps-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
|
|
182
|
+
<img class="ps-custom-logo" src="[[logoUrl]]" alt="[[logoAlt]]">
|
|
176
183
|
[[/logoUrl]]
|
|
177
184
|
[[^logoUrl]]
|
|
178
185
|
[[^licensed]]
|