webspresso 0.0.85 → 0.0.87
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/README.md +28 -1
- package/core/orm/model.js +3 -0
- package/core/orm/schema-helpers.js +1 -0
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +38 -2
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +2 -0
- package/plugins/admin-panel/client/parts/04-field-renderers.js +18 -2
- package/plugins/admin-panel/client/parts/07-model-list.js +24 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +48 -6
- package/plugins/admin-panel/core/admin-module.js +1 -0
- package/plugins/admin-panel/core/registry.js +1 -0
- package/plugins/admin-panel/field-renderers/basic.js +18 -2
- package/plugins/admin-panel/modules/custom-pages.js +41 -1
package/README.md
CHANGED
|
@@ -808,6 +808,33 @@ Options:
|
|
|
808
808
|
|
|
809
809
|
See **`doc/index.html#admin-user-management`** and **Session authentication** in **`.agents/skills/webspresso-usage/REFERENCE-framework.md`** for the split between **`adminUser`** and **`createApp({ auth })`**.
|
|
810
810
|
|
|
811
|
+
**3-State Table Column Sorting & Configuration:**
|
|
812
|
+
|
|
813
|
+
Admin Panel table headers support interactive 3-state column sorting:
|
|
814
|
+
- **State 1 (`Unsorted`)**: Default state (`↕` icon).
|
|
815
|
+
- **State 2 (`ASC`)**: 1st click sorts ascending (`▲` icon, `?sort=col&order=asc`).
|
|
816
|
+
- **State 3 (`DESC`)**: 2nd click sorts descending (`▼` icon, `?sort=col&order=desc`).
|
|
817
|
+
- **State 4 (`Unsorted`)**: 3rd click clears sorting and reverts to default primary key ordering.
|
|
818
|
+
|
|
819
|
+
Configuration options:
|
|
820
|
+
- **Column-level**: Standard scalar columns (`string`, `number`, `boolean`, `timestamp`) are sortable by default. Disable or enable per column:
|
|
821
|
+
```javascript
|
|
822
|
+
schema: zdb.schema({
|
|
823
|
+
name: zdb.string({ sortable: true }),
|
|
824
|
+
secret_notes: zdb.text().config({ sortable: false }), // or zdb.text({ sortable: false })
|
|
825
|
+
})
|
|
826
|
+
```
|
|
827
|
+
- **Model-level**: Restrict sortable columns or disable sorting completely on a model in `defineModel`:
|
|
828
|
+
```javascript
|
|
829
|
+
admin: {
|
|
830
|
+
enabled: true,
|
|
831
|
+
sortableColumns: ['name', 'price'], // Only name and price are sortable
|
|
832
|
+
// Or disable all sorting:
|
|
833
|
+
// sortable: false,
|
|
834
|
+
}
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
|
|
811
838
|
|
|
812
839
|
**Custom Admin Pages (registerModule):**
|
|
813
840
|
|
|
@@ -858,7 +885,7 @@ if (adminApi) {
|
|
|
858
885
|
| `scripts` | External script URLs, script objects, or inline scripts injected into SSR Admin Panel (`registerScript`) |
|
|
859
886
|
| `styles` | External stylesheet URLs or inline CSS injected into SSR Admin Panel (`registerStyle`) |
|
|
860
887
|
|
|
861
|
-
Pages support JavaScript client components (`componentFile`, `pagesDir`, `component`)
|
|
888
|
+
Pages support JavaScript client components (`componentFile`, `pagesDir`, `component`), external URLs (`url` or `iframeUrl` inside `<iframe>`), as well as **native custom HTML pages** (`html` or `htmlFile` for local HTML files like EUIX Engine apps, Vue, or Alpine). HTML pages render directly in the page DOM with full Tailwind CSS styling, dark mode, and script execution without iframe overhead. Set `iframe: true` to isolate in an iframe if needed. By default (`layout: true`), custom page views are automatically wrapped in the Admin Layout with Breadcrumbs. Set `layout: false` for full-screen / custom canvas pages.
|
|
862
889
|
|
|
863
890
|
**Example component file (`admin/reports.js`):**
|
|
864
891
|
|
package/core/orm/model.js
CHANGED
|
@@ -91,6 +91,9 @@ function defineModel(options) {
|
|
|
91
91
|
icon: admin.icon || null,
|
|
92
92
|
customFields: admin.customFields || {},
|
|
93
93
|
queries: admin.queries || {},
|
|
94
|
+
sortable: admin.sortable,
|
|
95
|
+
sortableColumns: admin.sortableColumns,
|
|
96
|
+
columns: admin.columns,
|
|
94
97
|
},
|
|
95
98
|
rest: {
|
|
96
99
|
enabled: rest.enabled === true,
|
|
@@ -231,6 +231,7 @@ class SchemaBuilder {
|
|
|
231
231
|
if (options.readonly !== undefined) this._ui.readonly = options.readonly;
|
|
232
232
|
if (options.width !== undefined) this._ui.width = options.width;
|
|
233
233
|
if (options.rows !== undefined) this._ui.rows = options.rows;
|
|
234
|
+
if (options.sortable !== undefined) this._ui.sortable = options.sortable;
|
|
234
235
|
return this;
|
|
235
236
|
}
|
|
236
237
|
|
package/package.json
CHANGED
|
@@ -268,6 +268,28 @@ function createApiHandlers(options) {
|
|
|
268
268
|
}
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Helper to determine if a column is sortable based on model and column configs
|
|
273
|
+
*/
|
|
274
|
+
function isColumnSortable(name, meta, model) {
|
|
275
|
+
if (!meta) return false;
|
|
276
|
+
if (model.admin && model.admin.sortable === false) return false;
|
|
277
|
+
if (model.admin && Array.isArray(model.admin.sortableColumns)) {
|
|
278
|
+
return model.admin.sortableColumns.includes(name);
|
|
279
|
+
}
|
|
280
|
+
if (model.admin?.columns?.[name]?.sortable !== undefined) {
|
|
281
|
+
return Boolean(model.admin.columns[name].sortable);
|
|
282
|
+
}
|
|
283
|
+
if (meta.sortable !== undefined) {
|
|
284
|
+
return Boolean(meta.sortable);
|
|
285
|
+
}
|
|
286
|
+
if (meta.ui?.sortable !== undefined) {
|
|
287
|
+
return Boolean(meta.ui.sortable);
|
|
288
|
+
}
|
|
289
|
+
const nonSortableTypes = ['json', 'array', 'file'];
|
|
290
|
+
return !nonSortableTypes.includes(meta.type);
|
|
291
|
+
}
|
|
292
|
+
|
|
271
293
|
/**
|
|
272
294
|
* Get model metadata
|
|
273
295
|
*/
|
|
@@ -304,6 +326,7 @@ function createApiHandlers(options) {
|
|
|
304
326
|
validations: meta.validations || null,
|
|
305
327
|
ui: meta.ui ? { ...meta.ui, hidden: isHidden || meta.ui.hidden } : (isHidden ? { hidden: true } : null),
|
|
306
328
|
hidden: isHidden, // Excluded from list display and API responses
|
|
329
|
+
sortable: isColumnSortable(name, meta, model),
|
|
307
330
|
});
|
|
308
331
|
}
|
|
309
332
|
|
|
@@ -486,9 +509,22 @@ function createApiHandlers(options) {
|
|
|
486
509
|
// Apply pagination
|
|
487
510
|
query = query.offset(offset).limit(perPage);
|
|
488
511
|
|
|
489
|
-
// Apply sorting
|
|
512
|
+
// Apply sorting
|
|
490
513
|
const primaryKey = model.primaryKey || 'id';
|
|
491
|
-
|
|
514
|
+
const sortParam = req.query.sort;
|
|
515
|
+
const orderParam = (req.query.order || 'desc').toLowerCase();
|
|
516
|
+
const sortOrder = orderParam === 'asc' ? 'asc' : 'desc';
|
|
517
|
+
|
|
518
|
+
if (sortParam && model.columns.has(sortParam)) {
|
|
519
|
+
const colMeta = model.columns.get(sortParam);
|
|
520
|
+
if (isColumnSortable(sortParam, colMeta, model)) {
|
|
521
|
+
query = query.orderBy(sortParam, sortOrder);
|
|
522
|
+
} else {
|
|
523
|
+
query = query.orderBy(primaryKey, 'desc');
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
query = query.orderBy(primaryKey, 'desc');
|
|
527
|
+
}
|
|
492
528
|
|
|
493
529
|
// Get records
|
|
494
530
|
const records = await query.list();
|
|
@@ -96,6 +96,8 @@ const state = {
|
|
|
96
96
|
formData: {}, // Form field values
|
|
97
97
|
editing: false,
|
|
98
98
|
filters: {}, // Active filters { column: { op, value, from, to } }
|
|
99
|
+
sortColumn: null, // Currently sorted column name
|
|
100
|
+
sortDirection: null, // Sort direction: 'asc' | 'desc' | null
|
|
99
101
|
filterPanelOpen: false, // Filter panel visibility (deprecated)
|
|
100
102
|
filterDrawerOpen: false, // Filter drawer visibility
|
|
101
103
|
bulkFields: [], // Bulk-updatable fields (enum/boolean/date/datetime/timestamp)
|
|
@@ -300,7 +300,15 @@ const FieldRenderers = {
|
|
|
300
300
|
const label = ui.label || formatColumnLabel(col.name);
|
|
301
301
|
const placeholder = ui.placeholder || '';
|
|
302
302
|
const hint = ui.hint || '';
|
|
303
|
-
|
|
303
|
+
let dateValue = '';
|
|
304
|
+
if (value) {
|
|
305
|
+
try {
|
|
306
|
+
const d = new Date(value);
|
|
307
|
+
if (!isNaN(d.getTime())) {
|
|
308
|
+
dateValue = d.toISOString().split('T')[0];
|
|
309
|
+
}
|
|
310
|
+
} catch {}
|
|
311
|
+
}
|
|
304
312
|
|
|
305
313
|
return m('.mb-4', [
|
|
306
314
|
m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
|
|
@@ -329,7 +337,15 @@ const FieldRenderers = {
|
|
|
329
337
|
const label = ui.label || formatColumnLabel(col.name);
|
|
330
338
|
const placeholder = ui.placeholder || '';
|
|
331
339
|
const hint = ui.hint || '';
|
|
332
|
-
|
|
340
|
+
let dateTimeValue = '';
|
|
341
|
+
if (value) {
|
|
342
|
+
try {
|
|
343
|
+
const d = new Date(value);
|
|
344
|
+
if (!isNaN(d.getTime())) {
|
|
345
|
+
dateTimeValue = d.toISOString().slice(0, 16);
|
|
346
|
+
}
|
|
347
|
+
} catch {}
|
|
348
|
+
}
|
|
333
349
|
|
|
334
350
|
return m('.mb-4', [
|
|
335
351
|
m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
|
|
@@ -90,13 +90,13 @@ function formatCellValue(value, col, record = null) {
|
|
|
90
90
|
case 'timestamp':
|
|
91
91
|
try {
|
|
92
92
|
const date = new Date(value);
|
|
93
|
-
return date.toLocaleString();
|
|
93
|
+
return isNaN(date.getTime()) ? String(value) : date.toLocaleString();
|
|
94
94
|
} catch { return String(value); }
|
|
95
95
|
|
|
96
96
|
case 'date':
|
|
97
97
|
try {
|
|
98
98
|
const date = new Date(value);
|
|
99
|
-
return date.toLocaleDateString();
|
|
99
|
+
return isNaN(date.getTime()) ? String(value) : date.toLocaleDateString();
|
|
100
100
|
} catch { return String(value); }
|
|
101
101
|
|
|
102
102
|
case 'json':
|
|
@@ -523,21 +523,31 @@ function parseFilterQuery(queryString) {
|
|
|
523
523
|
return filters;
|
|
524
524
|
}
|
|
525
525
|
|
|
526
|
-
// Load records with pagination and
|
|
527
|
-
function loadRecords(modelName, page = 1, filters = null) {
|
|
526
|
+
// Load records with pagination, filters, and sorting
|
|
527
|
+
function loadRecords(modelName, page = 1, filters = null, sortColumn = undefined, sortDirection = undefined) {
|
|
528
528
|
state.loading = true;
|
|
529
529
|
state.error = null;
|
|
530
530
|
|
|
531
531
|
const perPage = state.pagination.perPage || 20;
|
|
532
532
|
const activeFilters = filters !== null ? filters : state.filters;
|
|
533
|
+
const activeSortCol = sortColumn !== undefined ? sortColumn : state.sortColumn;
|
|
534
|
+
const activeSortDir = sortDirection !== undefined ? sortDirection : state.sortDirection;
|
|
535
|
+
|
|
536
|
+
state.sortColumn = activeSortCol;
|
|
537
|
+
state.sortDirection = activeSortDir;
|
|
538
|
+
|
|
533
539
|
const filterQuery = buildFilterQuery(activeFilters);
|
|
534
540
|
|
|
535
|
-
// Update URL with filters
|
|
541
|
+
// Update URL with filters and sort params
|
|
536
542
|
const queryParams = new URLSearchParams();
|
|
537
543
|
queryParams.set('page', page);
|
|
538
544
|
if (state.trashedView) {
|
|
539
545
|
queryParams.set('trashed', 'only');
|
|
540
546
|
}
|
|
547
|
+
if (activeSortCol && activeSortDir) {
|
|
548
|
+
queryParams.set('sort', activeSortCol);
|
|
549
|
+
queryParams.set('order', activeSortDir);
|
|
550
|
+
}
|
|
541
551
|
if (Object.keys(activeFilters).length > 0) {
|
|
542
552
|
for (const [col, filter] of Object.entries(activeFilters)) {
|
|
543
553
|
if (!filter || !filterPayloadIsActive(filter)) continue;
|
|
@@ -566,7 +576,8 @@ function loadRecords(modelName, page = 1, filters = null) {
|
|
|
566
576
|
window.history.replaceState({}, '', newUrl);
|
|
567
577
|
|
|
568
578
|
const trashedParam = state.trashedView ? '&trashed=only' : '';
|
|
569
|
-
|
|
579
|
+
const sortParam = activeSortCol && activeSortDir ? '&sort=' + encodeURIComponent(activeSortCol) + '&order=' + encodeURIComponent(activeSortDir) : '';
|
|
580
|
+
return api.get('/models/' + modelName + '/records?page=' + page + '&perPage=' + perPage + trashedParam + filterQuery + sortParam)
|
|
570
581
|
.then(result => {
|
|
571
582
|
state.records = result.data || [];
|
|
572
583
|
state.pagination = {
|
|
@@ -601,11 +612,16 @@ function initializeModelView(modelName) {
|
|
|
601
612
|
state.selectedBulkField = null;
|
|
602
613
|
state._currentModelName = modelName;
|
|
603
614
|
|
|
604
|
-
// Parse filters from URL query string
|
|
615
|
+
// Parse filters and sorting from URL query string
|
|
605
616
|
const urlParams = new URLSearchParams(window.location.search);
|
|
606
617
|
const page = parseInt(urlParams.get('page')) || 1;
|
|
607
618
|
state.filters = parseFilterQuery(window.location.search);
|
|
608
619
|
|
|
620
|
+
const rawSort = urlParams.get('sort');
|
|
621
|
+
const rawOrder = (urlParams.get('order') || '').toLowerCase();
|
|
622
|
+
state.sortColumn = rawSort || null;
|
|
623
|
+
state.sortDirection = rawOrder === 'asc' ? 'asc' : rawOrder === 'desc' ? 'desc' : null;
|
|
624
|
+
|
|
609
625
|
// Load model metadata first, then records
|
|
610
626
|
state.loading = true;
|
|
611
627
|
api.get('/models/' + modelName)
|
|
@@ -614,7 +630,7 @@ function initializeModelView(modelName) {
|
|
|
614
630
|
state.currentModel = modelMeta;
|
|
615
631
|
// Load bulk-updatable fields for this model
|
|
616
632
|
loadBulkFields(modelName);
|
|
617
|
-
return loadRecords(modelName, page, state.filters);
|
|
633
|
+
return loadRecords(modelName, page, state.filters, state.sortColumn, state.sortDirection);
|
|
618
634
|
})
|
|
619
635
|
.catch(err => {
|
|
620
636
|
state.error = err.message;
|
|
@@ -441,12 +441,54 @@ const RecordList = {
|
|
|
441
441
|
}),
|
|
442
442
|
]),
|
|
443
443
|
// Dynamic column headers (first column sticky left with box-shadow)
|
|
444
|
-
...displayColumns.map((col, i) =>
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
)
|
|
449
|
-
|
|
444
|
+
...displayColumns.map((col, i) => {
|
|
445
|
+
const isSortable = col.sortable !== false;
|
|
446
|
+
const isSorted = isSortable && state.sortColumn === col.name;
|
|
447
|
+
const dir = isSorted ? state.sortDirection : null;
|
|
448
|
+
const stickyStyle = i === 0 ? 'position: sticky; left: 40px; z-index: 15; box-shadow: 4px 0 8px -4px rgba(0,0,0,0.08);' : '';
|
|
449
|
+
|
|
450
|
+
if (!isSortable) {
|
|
451
|
+
return m('th.px-4.py-3.text-left.text-xs.font-medium.text-gray-500.dark:text-slate-400.uppercase.tracking-wider.whitespace-nowrap.bg-gray-50.dark:bg-slate-900.border-b.border-gray-200.dark:border-slate-700',
|
|
452
|
+
stickyStyle ? { style: stickyStyle } : {},
|
|
453
|
+
formatColumnLabel(col.name)
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return m('th.px-4.py-3.text-left.text-xs.font-medium.text-gray-500.dark:text-slate-400.uppercase.tracking-wider.whitespace-nowrap.bg-gray-50.dark:bg-slate-900.border-b.border-gray-200.dark:border-slate-700.cursor-pointer.select-none.hover:bg-gray-100.dark:hover:bg-slate-800/80.transition-colors.group',
|
|
458
|
+
{
|
|
459
|
+
style: stickyStyle || undefined,
|
|
460
|
+
onclick: () => {
|
|
461
|
+
let nextSort = col.name;
|
|
462
|
+
let nextDir = 'asc';
|
|
463
|
+
if (state.sortColumn === col.name) {
|
|
464
|
+
if (state.sortDirection === 'asc') {
|
|
465
|
+
nextDir = 'desc';
|
|
466
|
+
} else if (state.sortDirection === 'desc') {
|
|
467
|
+
nextSort = null;
|
|
468
|
+
nextDir = null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
state.sortColumn = nextSort;
|
|
472
|
+
state.sortDirection = nextDir;
|
|
473
|
+
loadRecords(modelName, 1, state.filters, nextSort, nextDir);
|
|
474
|
+
},
|
|
475
|
+
},
|
|
476
|
+
m('.flex.items-center.gap-1.5', [
|
|
477
|
+
m('span', formatColumnLabel(col.name)),
|
|
478
|
+
dir === 'asc'
|
|
479
|
+
? m('svg.w-3.5.h-3.5.text-indigo-600.dark:text-indigo-400.flex-shrink-0', { fill: 'currentColor', viewBox: '0 0 20 20' },
|
|
480
|
+
m('path', { 'fill-rule': 'evenodd', d: 'M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z', 'clip-rule': 'evenodd' })
|
|
481
|
+
)
|
|
482
|
+
: dir === 'desc'
|
|
483
|
+
? m('svg.w-3.5.h-3.5.text-indigo-600.dark:text-indigo-400.flex-shrink-0', { fill: 'currentColor', viewBox: '0 0 20 20' },
|
|
484
|
+
m('path', { 'fill-rule': 'evenodd', d: 'M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z', 'clip-rule': 'evenodd' })
|
|
485
|
+
)
|
|
486
|
+
: m('svg.w-3.5.h-3.5.text-gray-300.dark:text-slate-600.group-hover:text-gray-400.flex-shrink-0', { fill: 'currentColor', viewBox: '0 0 20 20' },
|
|
487
|
+
m('path', { d: 'M5 12a1 1 0 100 2h10a1 1 0 100-2H5zM5 6a1 1 0 100 2h10a1 1 0 100-2H5z' })
|
|
488
|
+
)
|
|
489
|
+
])
|
|
490
|
+
);
|
|
491
|
+
}),
|
|
450
492
|
// Sticky actions header (sticky right, box-shadow on left)
|
|
451
493
|
m('th.px-4.py-3.text-right.text-xs.font-medium.text-gray-500 dark:text-slate-400.uppercase.tracking-wider.bg-gray-50 dark:bg-slate-900.border-b.border-gray-200 dark:border-slate-700', {
|
|
452
494
|
style: 'position: sticky; right: 0; min-width: 120px; z-index: 15; box-shadow: -4px 0 8px -4px rgba(0,0,0,0.08);',
|
|
@@ -272,6 +272,7 @@ function registerPages(moduleId, pages, deps) {
|
|
|
272
272
|
description: page.description,
|
|
273
273
|
permission: page.permission,
|
|
274
274
|
layout: page.layout !== undefined ? page.layout : true,
|
|
275
|
+
iframe: page.iframe,
|
|
275
276
|
url: page.url || page.iframeUrl || undefined,
|
|
276
277
|
html: htmlContent || undefined,
|
|
277
278
|
});
|
|
@@ -479,6 +479,7 @@ class AdminRegistry {
|
|
|
479
479
|
description: p.description,
|
|
480
480
|
permission: p.permission,
|
|
481
481
|
layout: p.layout !== undefined ? p.layout : true,
|
|
482
|
+
iframe: p.iframe,
|
|
482
483
|
url: p.url || p.iframeUrl,
|
|
483
484
|
html: p.html,
|
|
484
485
|
hasClientComponent: this.clientComponents.has(p.id),
|
|
@@ -121,7 +121,15 @@ module.exports = {
|
|
|
121
121
|
DateField: {
|
|
122
122
|
view: (vnode) => {
|
|
123
123
|
const { name, value = '', meta = {}, required = false } = vnode.attrs;
|
|
124
|
-
|
|
124
|
+
let dateValue = '';
|
|
125
|
+
if (value) {
|
|
126
|
+
try {
|
|
127
|
+
const d = new Date(value);
|
|
128
|
+
if (!isNaN(d.getTime())) {
|
|
129
|
+
dateValue = d.toISOString().split('T')[0];
|
|
130
|
+
}
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
125
133
|
return m('.mb-4', [
|
|
126
134
|
m('label.block.text-sm.font-medium.mb-2', { for: name },
|
|
127
135
|
meta.label || name,
|
|
@@ -149,7 +157,15 @@ module.exports = {
|
|
|
149
157
|
DateTimeField: {
|
|
150
158
|
view: (vnode) => {
|
|
151
159
|
const { name, value = '', meta = {}, required = false } = vnode.attrs;
|
|
152
|
-
|
|
160
|
+
let dateTimeValue = '';
|
|
161
|
+
if (value) {
|
|
162
|
+
try {
|
|
163
|
+
const d = new Date(value);
|
|
164
|
+
if (!isNaN(d.getTime())) {
|
|
165
|
+
dateTimeValue = d.toISOString().slice(0, 16);
|
|
166
|
+
}
|
|
167
|
+
} catch {}
|
|
168
|
+
}
|
|
153
169
|
return m('.mb-4', [
|
|
154
170
|
m('label.block.text-sm.font-medium.mb-2', { for: name },
|
|
155
171
|
meta.label || name,
|
|
@@ -156,7 +156,7 @@ function generateCustomPageComponent() {
|
|
|
156
156
|
return `
|
|
157
157
|
// Custom Page Component Factory
|
|
158
158
|
function createCustomPage(pageConfig) {
|
|
159
|
-
if (pageConfig && (pageConfig.url || pageConfig.html)) {
|
|
159
|
+
if (pageConfig && (pageConfig.url || (pageConfig.html && pageConfig.iframe))) {
|
|
160
160
|
var iframeAttrs = {
|
|
161
161
|
src: pageConfig.url || undefined,
|
|
162
162
|
srcdoc: pageConfig.html || undefined,
|
|
@@ -187,6 +187,46 @@ function createCustomPage(pageConfig) {
|
|
|
187
187
|
};
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
if (pageConfig && pageConfig.html) {
|
|
191
|
+
var executeScripts = function(vnode) {
|
|
192
|
+
if (!vnode.dom) return;
|
|
193
|
+
var scripts = vnode.dom.querySelectorAll('script');
|
|
194
|
+
scripts.forEach(function(oldScript) {
|
|
195
|
+
if (oldScript.dataset && oldScript.dataset.executed) return;
|
|
196
|
+
var newScript = document.createElement('script');
|
|
197
|
+
Array.from(oldScript.attributes).forEach(function(attr) {
|
|
198
|
+
newScript.setAttribute(attr.name, attr.value);
|
|
199
|
+
});
|
|
200
|
+
newScript.dataset.executed = 'true';
|
|
201
|
+
newScript.appendChild(document.createTextNode(oldScript.innerHTML));
|
|
202
|
+
oldScript.parentNode.replaceChild(newScript, oldScript);
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
if (pageConfig.layout === false) {
|
|
206
|
+
return {
|
|
207
|
+
oncreate: executeScripts,
|
|
208
|
+
view: function() {
|
|
209
|
+
return m('div.custom-html-container', m.trust(pageConfig.html));
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
oncreate: executeScripts,
|
|
215
|
+
view: function() {
|
|
216
|
+
return m(Layout, [
|
|
217
|
+
m(Breadcrumb, { items: [{ label: pageConfig.title, href: pageConfig.path }] }),
|
|
218
|
+
m('div.mb-4.flex.items-start.justify-between.gap-4', [
|
|
219
|
+
m('div', [
|
|
220
|
+
m('h1.text-2xl.font-bold.text-gray-900.dark:text-slate-100', pageConfig.title),
|
|
221
|
+
pageConfig.description && m('p.text-gray-500.dark:text-slate-400.mt-1', pageConfig.description)
|
|
222
|
+
])
|
|
223
|
+
]),
|
|
224
|
+
m('div.custom-html-container', m.trust(pageConfig.html))
|
|
225
|
+
]);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
190
230
|
return {
|
|
191
231
|
oninit(vnode) {
|
|
192
232
|
vnode.state.data = null;
|