webspresso 0.0.73 → 0.0.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +44 -4
  2. package/bin/commands/orm-map.js +139 -0
  3. package/bin/commands/skill.js +22 -8
  4. package/bin/commands/upgrade.js +146 -0
  5. package/bin/utils/orm-map-html.js +689 -0
  6. package/bin/utils/orm-map-load.js +85 -0
  7. package/bin/utils/orm-map-snapshot.js +179 -0
  8. package/bin/utils/resolve-webspresso-orm.js +23 -0
  9. package/bin/webspresso.js +4 -0
  10. package/core/auth/manager.js +14 -1
  11. package/core/kernel/app.js +96 -0
  12. package/core/kernel/base-repository.js +143 -0
  13. package/core/kernel/events.js +101 -0
  14. package/core/kernel/flow.js +22 -0
  15. package/core/kernel/index.js +17 -0
  16. package/core/kernel/plugin.js +23 -0
  17. package/core/kernel/plugins/sample-seo.js +26 -0
  18. package/core/kernel/run-demo.js +58 -0
  19. package/core/kernel/view.js +167 -0
  20. package/core/openapi/build-from-api-routes.js +8 -2
  21. package/core/orm/model.js +3 -1
  22. package/core/url-path-normalize.js +30 -0
  23. package/index.d.ts +168 -1
  24. package/index.js +20 -2
  25. package/package.json +11 -1
  26. package/plugins/admin-panel/api.js +43 -15
  27. package/plugins/admin-panel/app.js +109 -0
  28. package/plugins/admin-panel/client/README.md +39 -0
  29. package/plugins/admin-panel/client/load-parts.js +74 -0
  30. package/plugins/admin-panel/client/manifest.parts.json +12 -0
  31. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +150 -0
  32. package/plugins/admin-panel/client/parts/02-filter-components.js +554 -0
  33. package/plugins/admin-panel/client/parts/03-pagination-intro.js +70 -0
  34. package/plugins/admin-panel/client/parts/04-field-renderers.js +287 -0
  35. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +335 -0
  36. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +125 -0
  37. package/plugins/admin-panel/client/parts/07-model-list.js +596 -0
  38. package/plugins/admin-panel/client/parts/08-record-list.js +536 -0
  39. package/plugins/admin-panel/client/parts/09-record-form.js +170 -0
  40. package/plugins/admin-panel/client/parts/10-export-registry.js +11 -0
  41. package/plugins/admin-panel/client/verify-spa-parts.js +32 -0
  42. package/plugins/admin-panel/client/vite.config.example.mjs +22 -0
  43. package/plugins/admin-panel/components.js +4 -2640
  44. package/plugins/admin-panel/core/api-extensions.js +100 -10
  45. package/plugins/admin-panel/index.js +3 -0
  46. package/plugins/admin-panel/lib/is-rich-text-empty.js +23 -0
  47. package/plugins/admin-panel/lib/sanitize-rich-html.js +106 -0
  48. package/plugins/admin-panel/modules/dashboard.js +17 -13
  49. package/plugins/admin-panel/modules/user-management.js +118 -27
  50. package/plugins/data-exchange/export-xlsx.js +3 -0
  51. package/plugins/data-exchange/record-selection.js +21 -5
  52. package/plugins/index.js +4 -0
  53. package/plugins/rate-limit/index.js +178 -0
  54. package/plugins/redirect/index.js +204 -0
  55. package/plugins/rest-resources/index.js +2 -1
  56. package/plugins/site-analytics/admin-component.js +88 -78
  57. package/plugins/swagger.js +2 -1
  58. package/plugins/upload/local-file-provider.js +6 -2
  59. package/src/file-router.js +270 -53
  60. package/src/njk-frontmatter.js +156 -0
  61. package/src/plugin-manager.js +4 -2
  62. package/src/server.js +28 -9
  63. package/templates/skills/webspresso-usage/REFERENCE-framework.md +276 -0
  64. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +93 -0
  65. package/templates/skills/webspresso-usage/SKILL.md +29 -275
@@ -0,0 +1,287 @@
1
+ // Field Renderers - render appropriate input based on column type
2
+ const FieldRenderers = {
3
+ // Text input (string)
4
+ string: (col, value, onChange, readonly) => {
5
+ const validations = col.validations || {};
6
+ const ui = col.ui || {};
7
+ const label = ui.label || formatColumnLabel(col.name);
8
+ const inputType = ui.inputType || (validations.email ? 'email' : validations.url ? 'url' : 'text');
9
+ const placeholder = ui.placeholder || '';
10
+ const hint = ui.hint || '';
11
+
12
+ return m('.mb-4', [
13
+ m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
14
+ m('input.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.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
15
+ id: col.name,
16
+ name: col.name,
17
+ type: inputType,
18
+ value: value || '',
19
+ placeholder: placeholder,
20
+ minlength: validations.minLength || validations.min,
21
+ maxlength: validations.maxLength || validations.max || col.maxLength || 255,
22
+ pattern: validations.pattern || undefined,
23
+ required: !col.nullable && !readonly,
24
+ readonly: readonly,
25
+ disabled: readonly,
26
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
27
+ oninput: (e) => onChange(e.target.value),
28
+ }),
29
+ hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
30
+ ]);
31
+ },
32
+
33
+ // Textarea (text)
34
+ text: (col, value, onChange, readonly) => {
35
+ const validations = col.validations || {};
36
+ const ui = col.ui || {};
37
+ const label = ui.label || formatColumnLabel(col.name);
38
+ const placeholder = ui.placeholder || '';
39
+ const hint = ui.hint || '';
40
+ const rows = ui.rows || 4;
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 }, label),
44
+ m('textarea.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.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
45
+ id: col.name,
46
+ name: col.name,
47
+ rows: rows,
48
+ placeholder: placeholder,
49
+ minlength: validations.minLength || validations.min,
50
+ maxlength: validations.maxLength || validations.max,
51
+ required: !col.nullable && !readonly,
52
+ readonly: readonly,
53
+ disabled: readonly,
54
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
55
+ oninput: (e) => onChange(e.target.value),
56
+ }, value || ''),
57
+ hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
58
+ ]);
59
+ },
60
+
61
+ // Number input (integer, bigint)
62
+ integer: (col, value, onChange, readonly) => {
63
+ const validations = col.validations || {};
64
+ const ui = col.ui || {};
65
+ const label = ui.label || formatColumnLabel(col.name);
66
+ const placeholder = ui.placeholder || '';
67
+ const hint = ui.hint || '';
68
+
69
+ return m('.mb-4', [
70
+ m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
71
+ m('input.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.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
72
+ id: col.name,
73
+ name: col.name,
74
+ type: 'number',
75
+ step: validations.step || '1',
76
+ min: validations.min,
77
+ max: validations.max,
78
+ value: value !== null && value !== undefined ? value : '',
79
+ placeholder: placeholder,
80
+ required: !col.nullable && !readonly,
81
+ readonly: readonly,
82
+ disabled: readonly,
83
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
84
+ oninput: (e) => onChange(e.target.value === '' ? null : parseInt(e.target.value, 10)),
85
+ }),
86
+ hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
87
+ ]);
88
+ },
89
+
90
+ // Float/Decimal input
91
+ float: (col, value, onChange, readonly) => {
92
+ const validations = col.validations || {};
93
+ const ui = col.ui || {};
94
+ const label = ui.label || formatColumnLabel(col.name);
95
+ const placeholder = ui.placeholder || '';
96
+ const hint = ui.hint || '';
97
+
98
+ return m('.mb-4', [
99
+ m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
100
+ m('input.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.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
101
+ id: col.name,
102
+ name: col.name,
103
+ type: 'number',
104
+ step: validations.step || '0.01',
105
+ min: validations.min,
106
+ max: validations.max,
107
+ value: value !== null && value !== undefined ? value : '',
108
+ placeholder: placeholder,
109
+ required: !col.nullable && !readonly,
110
+ readonly: readonly,
111
+ disabled: readonly,
112
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
113
+ oninput: (e) => onChange(e.target.value === '' ? null : parseFloat(e.target.value)),
114
+ }),
115
+ hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
116
+ ]);
117
+ },
118
+
119
+ // Boolean checkbox
120
+ boolean: (col, value, onChange, readonly) => {
121
+ const ui = col.ui || {};
122
+ const label = ui.label || formatColumnLabel(col.name);
123
+ const hint = ui.hint || '';
124
+
125
+ return m('.mb-4', [
126
+ m('label.flex.items-center.cursor-pointer', { class: readonly ? 'cursor-not-allowed' : '' }, [
127
+ m('input.mr-2.w-4.h-4.rounded.border-gray-300.dark:border-slate-600.text-indigo-600.focus:ring-indigo-500', {
128
+ type: 'checkbox',
129
+ name: col.name,
130
+ checked: Boolean(value),
131
+ disabled: readonly,
132
+ onchange: (e) => onChange(e.target.checked),
133
+ }),
134
+ m('span.text-sm.font-medium.text-gray-700.dark:text-slate-300', label),
135
+ ]),
136
+ hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
137
+ ]);
138
+ },
139
+
140
+ // Date input
141
+ date: (col, value, onChange, readonly) => {
142
+ const validations = col.validations || {};
143
+ const ui = col.ui || {};
144
+ const label = ui.label || formatColumnLabel(col.name);
145
+ const placeholder = ui.placeholder || '';
146
+ const hint = ui.hint || '';
147
+ const dateValue = value ? new Date(value).toISOString().split('T')[0] : '';
148
+
149
+ return m('.mb-4', [
150
+ m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
151
+ m('input.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.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
152
+ id: col.name,
153
+ name: col.name,
154
+ type: 'date',
155
+ value: dateValue,
156
+ placeholder: placeholder,
157
+ min: validations.min,
158
+ max: validations.max,
159
+ required: !col.nullable && !readonly,
160
+ readonly: readonly,
161
+ disabled: readonly,
162
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
163
+ oninput: (e) => onChange(e.target.value),
164
+ }),
165
+ hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
166
+ ]);
167
+ },
168
+
169
+ // DateTime input (datetime, timestamp)
170
+ datetime: (col, value, onChange, readonly) => {
171
+ const validations = col.validations || {};
172
+ const ui = col.ui || {};
173
+ const label = ui.label || formatColumnLabel(col.name);
174
+ const placeholder = ui.placeholder || '';
175
+ const hint = ui.hint || '';
176
+ const dateTimeValue = value ? new Date(value).toISOString().slice(0, 16) : '';
177
+
178
+ return m('.mb-4', [
179
+ m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
180
+ m('input.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.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
181
+ id: col.name,
182
+ name: col.name,
183
+ type: 'datetime-local',
184
+ value: dateTimeValue,
185
+ placeholder: placeholder,
186
+ min: validations.min,
187
+ max: validations.max,
188
+ required: !col.nullable && !readonly,
189
+ readonly: readonly,
190
+ disabled: readonly,
191
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
192
+ oninput: (e) => onChange(e.target.value),
193
+ }),
194
+ hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
195
+ ]);
196
+ },
197
+
198
+ // Enum select
199
+ enum: (col, value, onChange, readonly) => {
200
+ const ui = col.ui || {};
201
+ const label = ui.label || formatColumnLabel(col.name);
202
+ const hint = ui.hint || '';
203
+ const options = col.enumValues || [];
204
+
205
+ return m('.mb-4', [
206
+ m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
207
+ 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', {
208
+ id: col.name,
209
+ name: col.name,
210
+ value: value || '',
211
+ required: !col.nullable && !readonly,
212
+ disabled: readonly,
213
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
214
+ onchange: (e) => onChange(e.target.value),
215
+ }, [
216
+ col.nullable ? m('option', { value: '' }, '-- Select --') : null,
217
+ ...options.map(opt => m('option', { value: opt, selected: value === opt }, opt)),
218
+ ]),
219
+ hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
220
+ ]);
221
+ },
222
+
223
+ // JSON textarea
224
+ json: (col, value, onChange, readonly) => {
225
+ const ui = col.ui || {};
226
+ const label = ui.label || formatColumnLabel(col.name);
227
+ const placeholder = ui.placeholder || '';
228
+ const hint = ui.hint || '';
229
+ const rows = ui.rows || 6;
230
+ const jsonString = value ? (typeof value === 'string' ? value : JSON.stringify(value, null, 2)) : '';
231
+
232
+ return m('.mb-4', [
233
+ m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
234
+ m('textarea.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.placeholder-gray-400.dark:placeholder-slate-500.font-mono.text-sm.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
235
+ id: col.name,
236
+ name: col.name,
237
+ rows: rows,
238
+ placeholder: placeholder,
239
+ required: !col.nullable && !readonly,
240
+ readonly: readonly,
241
+ disabled: readonly,
242
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
243
+ oninput: (e) => {
244
+ try {
245
+ const parsed = JSON.parse(e.target.value);
246
+ onChange(parsed);
247
+ } catch {
248
+ onChange(e.target.value);
249
+ }
250
+ },
251
+ }, jsonString),
252
+ hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
253
+ ]);
254
+ },
255
+
256
+ // Array (as JSON or tags)
257
+ array: (col, value, onChange, readonly) => {
258
+ const validations = col.validations || {};
259
+ const ui = col.ui || {};
260
+ const label = ui.label || formatColumnLabel(col.name);
261
+ const placeholder = ui.placeholder || 'Comma-separated values';
262
+ const hint = ui.hint || 'Enter comma-separated values';
263
+ const arrayValue = Array.isArray(value) ? value.join(', ') : (value || '');
264
+
265
+ return m('.mb-4', [
266
+ m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: col.name }, label),
267
+ m('input.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.placeholder-gray-400.dark:placeholder-slate-500.focus:outline-none.focus:ring-2.focus:ring-blue-500.dark:focus:ring-blue-400', {
268
+ id: col.name,
269
+ name: col.name,
270
+ type: 'text',
271
+ placeholder: placeholder,
272
+ value: arrayValue,
273
+ minlength: validations.minLength || validations.min,
274
+ maxlength: validations.maxLength || validations.max,
275
+ required: !col.nullable && !readonly,
276
+ readonly: readonly,
277
+ disabled: readonly,
278
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 cursor-not-allowed' : '',
279
+ oninput: (e) => {
280
+ const arr = e.target.value.split(',').map(s => s.trim()).filter(s => s);
281
+ onChange(arr);
282
+ },
283
+ }),
284
+ hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
285
+ ]);
286
+ },
287
+ };
@@ -0,0 +1,335 @@
1
+ // Rich Text Field Renderer Component
2
+ const RichTextField = {
3
+ oncreate: (vnode) => {
4
+ const { name, value = '', onchange, readonly = false } = vnode.attrs;
5
+
6
+ // Load Quill if not already loaded
7
+ if (typeof window.Quill === 'undefined') {
8
+ const link = document.createElement('link');
9
+ link.rel = 'stylesheet';
10
+ link.href = 'https://cdn.quilljs.com/1.3.6/quill.snow.css';
11
+ document.head.appendChild(link);
12
+
13
+ const script = document.createElement('script');
14
+ script.src = 'https://cdn.quilljs.com/1.3.6/quill.js';
15
+ script.onload = () => {
16
+ initEditor(vnode);
17
+ };
18
+ document.head.appendChild(script);
19
+ } else {
20
+ initEditor(vnode);
21
+ }
22
+
23
+ function initEditor(vnode) {
24
+ const editorId = 'quill-editor-' + name;
25
+ const editorEl = document.getElementById(editorId);
26
+ const hiddenInput = document.getElementById(name + '-value');
27
+ const isReadonly = readonly || false;
28
+
29
+ if (editorEl && !editorEl._quill) {
30
+ const quill = new window.Quill(editorEl, {
31
+ theme: 'snow',
32
+ readOnly: isReadonly,
33
+ modules: {
34
+ toolbar: isReadonly ? false : [
35
+ [{ 'header': [1, 2, 3, false] }],
36
+ ['bold', 'italic', 'underline', 'strike'],
37
+ [{ 'list': 'ordered'}, { 'list': 'bullet' }],
38
+ ['link', 'image'],
39
+ ['clean']
40
+ ]
41
+ }
42
+ });
43
+
44
+ // Set initial content
45
+ if (value) {
46
+ quill.root.innerHTML = value;
47
+ if (hiddenInput) {
48
+ hiddenInput.value = value;
49
+ }
50
+ }
51
+
52
+ // Handle content changes
53
+ if (!isReadonly) {
54
+ quill.on('text-change', () => {
55
+ const content = quill.root.innerHTML;
56
+ if (hiddenInput) {
57
+ hiddenInput.value = content;
58
+ }
59
+ if (onchange) {
60
+ onchange(content);
61
+ }
62
+ });
63
+ }
64
+
65
+ editorEl._quill = quill;
66
+ }
67
+ }
68
+ },
69
+
70
+ onupdate: (vnode) => {
71
+ // Update editor content if value changed externally
72
+ const { name, value = '', readonly = false } = vnode.attrs;
73
+ const editorId = 'quill-editor-' + name;
74
+ const editorEl = document.getElementById(editorId);
75
+ const hiddenInput = document.getElementById(name + '-value');
76
+
77
+ if (editorEl && editorEl._quill) {
78
+ const currentContent = editorEl._quill.root.innerHTML;
79
+ const newValue = value || '';
80
+ if (currentContent !== newValue) {
81
+ editorEl._quill.root.innerHTML = newValue;
82
+ if (hiddenInput) {
83
+ hiddenInput.value = newValue;
84
+ }
85
+ }
86
+ }
87
+ },
88
+
89
+ view: (vnode) => {
90
+ const { name, col, value = '', onChange, readonly } = vnode.attrs;
91
+ const ui = col.ui || {};
92
+ const label = ui.label || formatColumnLabel(col.name);
93
+ const hint = ui.hint || '';
94
+ const editorId = 'quill-editor-' + name;
95
+ const required = !col.nullable && !readonly;
96
+
97
+ return m('.mb-4', [
98
+ m('label.block.text-sm.font-medium.text-gray-700 dark:text-slate-300.mb-1', { for: name },
99
+ label,
100
+ required ? m('span.text-red-500', ' *') : null
101
+ ),
102
+ m('div.border.border-gray-300.dark:border-slate-600.rounded.bg-white.dark:bg-slate-900/50', {
103
+ id: editorId,
104
+ class: readonly ? 'bg-gray-100 dark:bg-slate-800 opacity-50' : '',
105
+ style: 'min-height: 200px;'
106
+ }),
107
+ m('input[type=hidden]', {
108
+ name,
109
+ id: name + '-value',
110
+ value: value || '',
111
+ }),
112
+ hint ? m('p.text-xs.text-gray-500 dark:text-slate-400.mt-1', hint) : null,
113
+ ]);
114
+ }
115
+ };
116
+
117
+ // File upload field (multipart POST to settings.uploadUrl; field name "file")
118
+ const FileUploadField = {
119
+ oncreate: (vnode) => {
120
+ const col = vnode.attrs.col;
121
+ const readonly = vnode.attrs.readonly;
122
+ if (readonly) return;
123
+ var cfg = window.__ADMIN_CONFIG__;
124
+ var uploadUrl = (cfg && cfg.settings && cfg.settings.uploadUrl) ? String(cfg.settings.uploadUrl) : '';
125
+ if (!uploadUrl) return;
126
+ const dropZoneId = 'drop-zone-' + col.name;
127
+ const dropZone = document.getElementById(dropZoneId);
128
+ if (!dropZone) return;
129
+ const meta = col.ui || {};
130
+ const onChange = vnode.attrs.onChange;
131
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function (eventName) {
132
+ dropZone.addEventListener(eventName, function (e) {
133
+ e.preventDefault();
134
+ e.stopPropagation();
135
+ });
136
+ });
137
+ ['dragenter', 'dragover'].forEach(function (eventName) {
138
+ dropZone.addEventListener(eventName, function () {
139
+ dropZone.classList.add('border-blue-500', 'bg-blue-50', 'dark:bg-slate-800');
140
+ });
141
+ });
142
+ ['dragleave', 'drop'].forEach(function (eventName) {
143
+ dropZone.addEventListener(eventName, function () {
144
+ dropZone.classList.remove('border-blue-500', 'bg-blue-50', 'dark:bg-slate-800');
145
+ });
146
+ });
147
+ dropZone.addEventListener('drop', function (e) {
148
+ var files = e.dataTransfer.files;
149
+ if (files.length > 0) {
150
+ handleAdminFileUpload(files[0], onChange, meta);
151
+ }
152
+ });
153
+ var fileInput = dropZone.querySelector('input[type=file]');
154
+ if (fileInput) {
155
+ fileInput.addEventListener('change', function (e) {
156
+ if (e.target.files.length > 0) {
157
+ handleAdminFileUpload(e.target.files[0], onChange, meta);
158
+ }
159
+ });
160
+ }
161
+ },
162
+ view: (vnode) => {
163
+ const col = vnode.attrs.col;
164
+ const value = vnode.attrs.value || '';
165
+ const onChange = vnode.attrs.onChange;
166
+ const readonly = vnode.attrs.readonly || false;
167
+ const meta = col.ui || {};
168
+ const label = meta.label || formatColumnLabel(col.name);
169
+ const hint = meta.hint || '';
170
+ const required = !col.nullable && !readonly;
171
+ var uploadUrl = '';
172
+ try {
173
+ var cfg2 = window.__ADMIN_CONFIG__;
174
+ uploadUrl = (cfg2 && cfg2.settings && cfg2.settings.uploadUrl) ? String(cfg2.settings.uploadUrl) : '';
175
+ } catch (e) { uploadUrl = ''; }
176
+ var maxSize = meta.maxSize || meta.maxBytes || (10 * 1024 * 1024);
177
+ var accept = meta.accept || '*/*';
178
+ var dropZoneId = 'drop-zone-' + col.name;
179
+ if (readonly) {
180
+ return m('.mb-4', [
181
+ m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', label, required ? m('span.text-red-500', ' *') : null),
182
+ value ? m('a.text-indigo-600.dark:text-indigo-400.break-all', { href: value, target: '_blank', rel: 'noopener noreferrer' }, value) : m('span.text-gray-400.dark:text-slate-500', '—'),
183
+ hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
184
+ ]);
185
+ }
186
+ if (!uploadUrl) {
187
+ return m('.mb-4', [
188
+ m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, label, required ? m('span.text-red-500', ' *') : null),
189
+ m('p.text-xs.text-amber-700.dark:text-amber-400.mb-2', 'Upload URL is not configured. Enter a public URL or path manually.'),
190
+ m('input.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.placeholder-gray-400.dark:placeholder-slate-500', {
191
+ type: 'text',
192
+ id: col.name,
193
+ name: col.name,
194
+ value: value,
195
+ placeholder: 'https://… or /uploads/…',
196
+ required: required,
197
+ oninput: function (e) { if (onChange) onChange(e.target.value); },
198
+ }),
199
+ hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
200
+ ]);
201
+ }
202
+ return m('.mb-4', [
203
+ m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', label, required ? m('span.text-red-500', ' *') : null),
204
+ m('div#' + dropZoneId + '.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded-lg.p-8.text-center.bg-gray-50.dark:bg-slate-900/40', { style: 'cursor: pointer;' }, [
205
+ m('input[type=file].hidden', {
206
+ id: 'file-input-' + col.name,
207
+ accept: accept,
208
+ onchange: function (e) {
209
+ if (e.target.files.length > 0) {
210
+ handleAdminFileUpload(e.target.files[0], onChange, meta);
211
+ }
212
+ },
213
+ }),
214
+ m('div', [
215
+ m('p.text-gray-600.dark:text-slate-400.mb-2', 'Drag and drop a file here, or'),
216
+ m('label.text-blue-600.hover:text-blue-800.dark:text-blue-400.cursor-pointer', { for: 'file-input-' + col.name }, 'browse'),
217
+ ]),
218
+ value ? m('.mt-4.text-left', [
219
+ m('p.text-sm.text-gray-600.dark:text-slate-400.break-all', 'Current: ' + value),
220
+ m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm.mt-2', {
221
+ type: 'button',
222
+ onclick: function () { if (onChange) onChange(''); },
223
+ }, 'Remove'),
224
+ ]) : null,
225
+ ]),
226
+ m('input[type=hidden]', { name: col.name, value: typeof value === 'string' ? value : '' }),
227
+ m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', 'Max ' + Math.round(maxSize / 1024 / 1024) + ' MB (server enforces limits)'),
228
+ hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
229
+ ]);
230
+ },
231
+ };
232
+
233
+ async function handleAdminFileUpload(file, onChange, meta) {
234
+ var uploadUrl = '';
235
+ try {
236
+ var cfg = window.__ADMIN_CONFIG__;
237
+ uploadUrl = (cfg && cfg.settings && cfg.settings.uploadUrl) ? String(cfg.settings.uploadUrl) : '';
238
+ } catch (e) {}
239
+ if (!uploadUrl) {
240
+ alert('Upload URL is not configured.');
241
+ return;
242
+ }
243
+ var maxSize = meta.maxSize || meta.maxBytes || (10 * 1024 * 1024);
244
+ if (file.size > maxSize) {
245
+ alert('File too large (max ' + Math.round(maxSize / 1024 / 1024) + ' MB).');
246
+ return;
247
+ }
248
+ var fd = new FormData();
249
+ fd.append('file', file);
250
+ try {
251
+ var res = await fetch(uploadUrl, { method: 'POST', body: fd, credentials: 'include' });
252
+ var data = {};
253
+ try { data = await res.json(); } catch (e2) { data = {}; }
254
+ if (!res.ok) {
255
+ alert(data.message || data.error || ('Upload failed (' + res.status + ')'));
256
+ return;
257
+ }
258
+ var url = data.url || data.publicUrl || '';
259
+ if (onChange) onChange(url);
260
+ m.redraw();
261
+ } catch (err) {
262
+ alert(err.message || 'Upload failed');
263
+ }
264
+ }
265
+
266
+ // Get appropriate renderer for a column type
267
+ function getFieldRenderer(col, modelMeta) {
268
+ // Check for custom field first
269
+ if (col.customField && col.customField.type) {
270
+ if (col.customField.type === 'rich-text') {
271
+ return (col, value, onChange, readonly) => {
272
+ return m(RichTextField, {
273
+ name: col.name,
274
+ col,
275
+ value: value || '',
276
+ onChange,
277
+ readonly: readonly || false,
278
+ });
279
+ };
280
+ }
281
+ if (col.customField.type === 'file-upload') {
282
+ return (col, value, onChange, readonly) => {
283
+ return m(FileUploadField, {
284
+ col,
285
+ value: value || '',
286
+ onChange,
287
+ readonly: readonly || false,
288
+ });
289
+ };
290
+ }
291
+ // Add other custom field types here if needed
292
+ }
293
+
294
+ if (col.type === 'file') {
295
+ return (col, value, onChange, readonly) => {
296
+ return m(FileUploadField, {
297
+ col,
298
+ value: value || '',
299
+ onChange,
300
+ readonly: readonly || false,
301
+ });
302
+ };
303
+ }
304
+
305
+ // Fallback to standard type renderers
306
+ const typeMap = {
307
+ string: 'string',
308
+ text: 'text',
309
+ integer: 'integer',
310
+ bigint: 'integer',
311
+ float: 'float',
312
+ decimal: 'float',
313
+ boolean: 'boolean',
314
+ date: 'date',
315
+ datetime: 'datetime',
316
+ timestamp: 'datetime',
317
+ enum: 'enum',
318
+ json: 'json',
319
+ array: 'array',
320
+ uuid: 'string',
321
+ nanoid: 'string',
322
+ };
323
+ return FieldRenderers[typeMap[col.type] || 'string'];
324
+ }
325
+
326
+ // Check if a column is auto-generated (readonly)
327
+ function isAutoColumn(col) {
328
+ // Primary key with auto-increment is readonly
329
+ if (col.primary || col.autoIncrement) return 'primary';
330
+ // Auto timestamps are always readonly
331
+ if (col.auto === 'create' || col.auto === 'update') return 'auto';
332
+ // Common timestamp field names
333
+ if (col.name === 'created_at' || col.name === 'updated_at') return 'auto';
334
+ return false;
335
+ }