webspresso 0.0.78 → 0.0.79
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/package.json +1 -1
- package/plugins/admin-panel/client/parts/04-field-renderers.js +166 -0
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +32 -1
- package/plugins/admin-panel/client/parts/07-model-list.js +11 -1
- package/plugins/admin-panel/client/parts/08-record-list.js +1 -1
- package/plugins/admin-panel/index.js +29 -0
package/package.json
CHANGED
|
@@ -1,4 +1,160 @@
|
|
|
1
1
|
// Field Renderers - render appropriate input based on column type
|
|
2
|
+
|
|
3
|
+
// BelongsTo relation dropdown select component
|
|
4
|
+
const BelongsToField = {
|
|
5
|
+
oninit: (vnode) => {
|
|
6
|
+
const { col } = vnode.attrs;
|
|
7
|
+
const customField = col.customField || {};
|
|
8
|
+
const relationName = customField.relation || col.name.replace(/_id$/, '');
|
|
9
|
+
const modelName = m.route.param('model');
|
|
10
|
+
vnode.state.loading = true;
|
|
11
|
+
vnode.state.options = [];
|
|
12
|
+
|
|
13
|
+
api.get('/models/' + modelName + '/relations/' + relationName)
|
|
14
|
+
.then(result => {
|
|
15
|
+
vnode.state.options = result.data || [];
|
|
16
|
+
})
|
|
17
|
+
.catch(err => {
|
|
18
|
+
console.error('Failed to load relation data:', err);
|
|
19
|
+
})
|
|
20
|
+
.finally(() => {
|
|
21
|
+
vnode.state.loading = false;
|
|
22
|
+
m.redraw();
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
view: (vnode) => {
|
|
26
|
+
const { col, value, onChange, readonly } = vnode.attrs;
|
|
27
|
+
const customField = col.customField || {};
|
|
28
|
+
const label = col.ui?.label || formatColumnLabel(col.name);
|
|
29
|
+
const hint = col.ui?.hint || '';
|
|
30
|
+
const valueKey = customField.valueKey || 'id';
|
|
31
|
+
const displayKey = customField.displayKey ||
|
|
32
|
+
(vnode.state.options.length > 0 ? ['name', 'title', 'label', 'username', 'email'].find(k => k in vnode.state.options[0]) : null) ||
|
|
33
|
+
'id';
|
|
34
|
+
|
|
35
|
+
if (vnode.state.loading) {
|
|
36
|
+
return m('.mb-4', [
|
|
37
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', label),
|
|
38
|
+
m('.text-xs.text-gray-500.dark:text-slate-400', 'Loading options...')
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return m('.mb-4', [
|
|
43
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, [
|
|
44
|
+
label,
|
|
45
|
+
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null
|
|
46
|
+
]),
|
|
47
|
+
m('select.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
|
|
48
|
+
id: col.name,
|
|
49
|
+
name: col.name,
|
|
50
|
+
value: value !== null && value !== undefined ? String(value) : '',
|
|
51
|
+
disabled: readonly,
|
|
52
|
+
class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
|
|
53
|
+
onchange: (e) => {
|
|
54
|
+
const selectedValue = e.target.value === '' ? null : e.target.value;
|
|
55
|
+
const typedValue = (col.type === 'integer' || col.type === 'bigint' || col.type === 'float' || col.type === 'decimal') && selectedValue !== null
|
|
56
|
+
? Number(selectedValue)
|
|
57
|
+
: selectedValue;
|
|
58
|
+
onChange(typedValue);
|
|
59
|
+
},
|
|
60
|
+
}, [
|
|
61
|
+
col.nullable ? m('option', { value: '' }, '-- Select --') : m('option', { value: '' }, '-- Select --'),
|
|
62
|
+
vnode.state.options.map(item => {
|
|
63
|
+
const itemValue = item[valueKey];
|
|
64
|
+
const itemDisplay = item[displayKey] || String(itemValue);
|
|
65
|
+
return m('option', {
|
|
66
|
+
value: String(itemValue),
|
|
67
|
+
selected: value !== null && value !== undefined && String(value) === String(itemValue)
|
|
68
|
+
}, itemDisplay);
|
|
69
|
+
}),
|
|
70
|
+
]),
|
|
71
|
+
hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// HasMany relation multi-select checkbox component
|
|
77
|
+
const HasManyField = {
|
|
78
|
+
oninit: (vnode) => {
|
|
79
|
+
const { col } = vnode.attrs;
|
|
80
|
+
const customField = col.customField || {};
|
|
81
|
+
const relationName = customField.relation || col.name.replace(/_ids$/, '').replace(/_id$/, '');
|
|
82
|
+
const modelName = m.route.param('model');
|
|
83
|
+
vnode.state.loading = true;
|
|
84
|
+
vnode.state.options = [];
|
|
85
|
+
|
|
86
|
+
api.get('/models/' + modelName + '/relations/' + relationName)
|
|
87
|
+
.then(result => {
|
|
88
|
+
vnode.state.options = result.data || [];
|
|
89
|
+
})
|
|
90
|
+
.catch(err => {
|
|
91
|
+
console.error('Failed to load relation data:', err);
|
|
92
|
+
})
|
|
93
|
+
.finally(() => {
|
|
94
|
+
vnode.state.loading = false;
|
|
95
|
+
m.redraw();
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
view: (vnode) => {
|
|
99
|
+
const { col, value = [], onChange, readonly } = vnode.attrs;
|
|
100
|
+
const customField = col.customField || {};
|
|
101
|
+
const label = col.ui?.label || formatColumnLabel(col.name);
|
|
102
|
+
const hint = col.ui?.hint || '';
|
|
103
|
+
const valueKey = customField.valueKey || 'id';
|
|
104
|
+
const displayKey = customField.displayKey ||
|
|
105
|
+
(vnode.state.options.length > 0 ? ['name', 'title', 'label', 'username', 'email'].find(k => k in vnode.state.options[0]) : null) ||
|
|
106
|
+
'id';
|
|
107
|
+
const selectedIds = Array.isArray(value) ? value.map(v => String(v)) : [];
|
|
108
|
+
|
|
109
|
+
if (vnode.state.loading) {
|
|
110
|
+
return m('.mb-4', [
|
|
111
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', label),
|
|
112
|
+
m('.text-xs.text-gray-500.dark:text-slate-400', 'Loading options...')
|
|
113
|
+
]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return m('.mb-4', [
|
|
117
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', [
|
|
118
|
+
label,
|
|
119
|
+
!col.nullable && !readonly ? m('span.text-red-500', ' *') : null
|
|
120
|
+
]),
|
|
121
|
+
m('.border.border-gray-300.dark:border-slate-600.rounded-md.p-3.max-h-48.overflow-y-auto.bg-white.dark:bg-slate-900/70', [
|
|
122
|
+
vnode.state.options.map(item => {
|
|
123
|
+
const itemValue = String(item[valueKey]);
|
|
124
|
+
const itemDisplay = item[displayKey] || itemValue;
|
|
125
|
+
const isSelected = selectedIds.includes(itemValue);
|
|
126
|
+
|
|
127
|
+
return m('label.flex.items-center.mb-2.cursor-pointer.text-sm.text-gray-700.dark:text-slate-300', [
|
|
128
|
+
m('input.mr-2.rounded.border-gray-300.dark:border-slate-600.text-indigo-600.focus:ring-indigo-500', {
|
|
129
|
+
type: 'checkbox',
|
|
130
|
+
checked: isSelected,
|
|
131
|
+
disabled: readonly,
|
|
132
|
+
onchange: (e) => {
|
|
133
|
+
let newSelected = [...selectedIds];
|
|
134
|
+
if (e.target.checked) {
|
|
135
|
+
if (!newSelected.includes(itemValue)) {
|
|
136
|
+
newSelected.push(itemValue);
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
newSelected = newSelected.filter(id => id !== itemValue);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Convert type if needed
|
|
143
|
+
const typedSelected = col.type === 'array' || typeof value[0] === 'string'
|
|
144
|
+
? newSelected
|
|
145
|
+
: newSelected.map(Number);
|
|
146
|
+
onChange(typedSelected);
|
|
147
|
+
}
|
|
148
|
+
}),
|
|
149
|
+
m('span', itemDisplay),
|
|
150
|
+
]);
|
|
151
|
+
}),
|
|
152
|
+
]),
|
|
153
|
+
hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
|
|
154
|
+
]);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
2
158
|
const FieldRenderers = {
|
|
3
159
|
// Text input (string)
|
|
4
160
|
string: (col, value, onChange, readonly) => {
|
|
@@ -284,4 +440,14 @@ const FieldRenderers = {
|
|
|
284
440
|
hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
|
|
285
441
|
]);
|
|
286
442
|
},
|
|
443
|
+
|
|
444
|
+
// belongsTo relation dropdown
|
|
445
|
+
belongsTo: (col, value, onChange, readonly) => {
|
|
446
|
+
return m(BelongsToField, { col, value, onChange, readonly });
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
// hasMany relation checklist
|
|
450
|
+
hasMany: (col, value, onChange, readonly) => {
|
|
451
|
+
return m(HasManyField, { col, value, onChange, readonly });
|
|
452
|
+
},
|
|
287
453
|
};
|
|
@@ -372,7 +372,36 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
372
372
|
});
|
|
373
373
|
};
|
|
374
374
|
}
|
|
375
|
-
|
|
375
|
+
if (col.customField.type === 'belongsTo') {
|
|
376
|
+
return FieldRenderers.belongsTo;
|
|
377
|
+
}
|
|
378
|
+
if (col.customField.type === 'hasMany') {
|
|
379
|
+
return FieldRenderers.hasMany;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Check registered custom field renderers in browser
|
|
383
|
+
const custom = window.__customFieldRenderers && window.__customFieldRenderers[col.customField.type];
|
|
384
|
+
if (custom) {
|
|
385
|
+
return (col, value, onChange, readonly) => {
|
|
386
|
+
if (custom.edit && typeof custom.edit === 'object' && custom.edit.view) {
|
|
387
|
+
return m(custom.edit, { col, value, onChange, readonly });
|
|
388
|
+
} else if (typeof custom.edit === 'function') {
|
|
389
|
+
return custom.edit(value, onChange, col);
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Check schema type custom field renderers
|
|
396
|
+
if (col.type && window.__customFieldRenderers && window.__customFieldRenderers[col.type]) {
|
|
397
|
+
const custom = window.__customFieldRenderers[col.type];
|
|
398
|
+
return (col, value, onChange, readonly) => {
|
|
399
|
+
if (custom.edit && typeof custom.edit === 'object' && custom.edit.view) {
|
|
400
|
+
return m(custom.edit, { col, value, onChange, readonly });
|
|
401
|
+
} else if (typeof custom.edit === 'function') {
|
|
402
|
+
return custom.edit(value, onChange, col);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
376
405
|
}
|
|
377
406
|
|
|
378
407
|
if (col.type === 'file') {
|
|
@@ -403,6 +432,8 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
403
432
|
array: 'array',
|
|
404
433
|
uuid: 'string',
|
|
405
434
|
nanoid: 'string',
|
|
435
|
+
belongsTo: 'belongsTo',
|
|
436
|
+
hasMany: 'hasMany',
|
|
406
437
|
};
|
|
407
438
|
return FieldRenderers[typeMap[col.type] || 'string'];
|
|
408
439
|
}
|
|
@@ -65,10 +65,20 @@ const ModelList = {
|
|
|
65
65
|
};
|
|
66
66
|
|
|
67
67
|
// Format cell value based on column type
|
|
68
|
-
function formatCellValue(value, col) {
|
|
68
|
+
function formatCellValue(value, col, record = null) {
|
|
69
69
|
if (value === null || value === undefined) {
|
|
70
70
|
return m('span.text-gray-400 dark:text-slate-500.italic', 'null');
|
|
71
71
|
}
|
|
72
|
+
|
|
73
|
+
const customRendererType = (col && col.customField && col.customField.type) || (col && col.type);
|
|
74
|
+
const custom = window.__customFieldRenderers && window.__customFieldRenderers[customRendererType];
|
|
75
|
+
if (custom && custom.display) {
|
|
76
|
+
if (typeof custom.display === 'object' && custom.display.view) {
|
|
77
|
+
return m(custom.display, { value, col, record });
|
|
78
|
+
} else if (typeof custom.display === 'function') {
|
|
79
|
+
return custom.display(value, record);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
72
82
|
|
|
73
83
|
switch (col?.type) {
|
|
74
84
|
case 'boolean':
|
|
@@ -478,7 +478,7 @@ const RecordList = {
|
|
|
478
478
|
...displayColumns.map((col, i) =>
|
|
479
479
|
m('td.px-4.py-3.text-sm.whitespace-nowrap.text-gray-700 dark:text-slate-300.bg-white dark:bg-slate-800',
|
|
480
480
|
i === 0 ? { style: 'position: sticky; left: 40px; z-index: 5; box-shadow: 4px 0 8px -4px rgba(0,0,0,0.08);' } : {},
|
|
481
|
-
formatCellValue(record[col.name], col)
|
|
481
|
+
formatCellValue(record[col.name], col, record)
|
|
482
482
|
)
|
|
483
483
|
),
|
|
484
484
|
// Sticky actions cell (sticky right, box-shadow on left)
|
|
@@ -329,6 +329,25 @@ function adminPanelPlugin(options = {}) {
|
|
|
329
329
|
};
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Helper to serialize arbitrary JS structures, including functions
|
|
334
|
+
*/
|
|
335
|
+
function serializeJs(obj) {
|
|
336
|
+
if (typeof obj === 'function') {
|
|
337
|
+
return obj.toString();
|
|
338
|
+
}
|
|
339
|
+
if (Array.isArray(obj)) {
|
|
340
|
+
return '[' + obj.map(serializeJs).join(', ') + ']';
|
|
341
|
+
}
|
|
342
|
+
if (obj && typeof obj === 'object') {
|
|
343
|
+
const entries = Object.entries(obj).map(([key, val]) => {
|
|
344
|
+
return `${JSON.stringify(key)}: ${serializeJs(val)}`;
|
|
345
|
+
});
|
|
346
|
+
return '{' + entries.join(',\n') + '}';
|
|
347
|
+
}
|
|
348
|
+
return JSON.stringify(obj);
|
|
349
|
+
}
|
|
350
|
+
|
|
332
351
|
/**
|
|
333
352
|
* Generate admin panel HTML
|
|
334
353
|
* @param {string} adminPath - Admin panel path
|
|
@@ -343,6 +362,12 @@ function generateAdminPanelHtml(adminPath, registry) {
|
|
|
343
362
|
const customPageComponent = generateCustomPageComponent();
|
|
344
363
|
|
|
345
364
|
const settings = registry.settings;
|
|
365
|
+
const fieldRenderersCode = Array.from(registry.fieldRenderers.entries()).map(([type, r]) => {
|
|
366
|
+
return `window.__customFieldRenderers['${type}'] = {
|
|
367
|
+
display: ${r.display ? serializeJs(r.display) : 'null'},
|
|
368
|
+
edit: ${r.edit ? serializeJs(r.edit) : 'null'}
|
|
369
|
+
};`;
|
|
370
|
+
}).join('\n');
|
|
346
371
|
|
|
347
372
|
return `<!DOCTYPE html>
|
|
348
373
|
<html lang="en">
|
|
@@ -418,6 +443,10 @@ function generateAdminPanelHtml(adminPath, registry) {
|
|
|
418
443
|
// Custom page components container
|
|
419
444
|
window.__customPages = window.__customPages || {};
|
|
420
445
|
${registry.getClientComponents()}
|
|
446
|
+
|
|
447
|
+
// Custom field renderers container
|
|
448
|
+
window.__customFieldRenderers = window.__customFieldRenderers || {};
|
|
449
|
+
${fieldRenderersCode}
|
|
421
450
|
</script>
|
|
422
451
|
<script>${appScript}</script>
|
|
423
452
|
</body>
|