webspresso 0.0.97 → 0.0.99
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/.agents/ADMIN.md +163 -5
- package/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/AUTH.md +169 -0
- package/.agents/CLI.md +195 -1
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +221 -9
- package/.agents/QUEUE.md +127 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/page.js +6 -3
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +21 -12
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +20 -7
- package/core/orm/model.js +53 -7
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +205 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +1188 -10
- package/index.js +88 -96
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +114 -22
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +30 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -59
- package/plugins/audit-log/api-handlers.js +148 -0
- package/plugins/audit-log/index.js +153 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +10 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +15 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +23 -0
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +4 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +347 -162
- package/src/helpers.js +967 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +635 -63
|
@@ -61,6 +61,33 @@ const RichTextField = {
|
|
|
61
61
|
onchange(content);
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
|
+
|
|
65
|
+
// Hook Quill image toolbar button to Media Library picker if available
|
|
66
|
+
try {
|
|
67
|
+
const toolbar = quill.getModule('toolbar');
|
|
68
|
+
if (toolbar && typeof window !== 'undefined') {
|
|
69
|
+
toolbar.addHandler('image', function() {
|
|
70
|
+
if (typeof window.__openFileManagerPicker === 'function') {
|
|
71
|
+
window.__openFileManagerPicker({
|
|
72
|
+
accept: 'image/*',
|
|
73
|
+
title: 'Insert Image (Media Library)',
|
|
74
|
+
onSelect: function(pickedUrl) {
|
|
75
|
+
const range = quill.getSelection(true) || { index: quill.getLength() };
|
|
76
|
+
quill.insertEmbed(range.index, 'image', pickedUrl);
|
|
77
|
+
quill.setSelection(range.index + 1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
const url = prompt('Image URL:');
|
|
82
|
+
if (url) {
|
|
83
|
+
const range = quill.getSelection(true) || { index: quill.getLength() };
|
|
84
|
+
quill.insertEmbed(range.index, 'image', url);
|
|
85
|
+
quill.setSelection(range.index + 1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
} catch (_) {}
|
|
64
91
|
}
|
|
65
92
|
|
|
66
93
|
editorEl._quill = quill;
|
|
@@ -134,9 +161,28 @@ function renderUploadedFilePreview(url, options) {
|
|
|
134
161
|
var accept = options.accept;
|
|
135
162
|
var readonly = options.readonly;
|
|
136
163
|
var onRemove = options.onRemove;
|
|
164
|
+
var onSelectLibrary = options.onSelectLibrary;
|
|
137
165
|
var label = options.label;
|
|
138
166
|
if (!url) return null;
|
|
139
167
|
|
|
168
|
+
var actionsRow = !readonly ? m('div.flex.items-center.gap-3.flex-wrap', [
|
|
169
|
+
typeof window !== 'undefined' && window.__openFileManagerPicker && onSelectLibrary
|
|
170
|
+
? m('button.text-indigo-600.dark:text-indigo-400.hover:text-indigo-800.dark:hover:text-indigo-300.text-xs.font-medium.flex.items-center.gap-1', {
|
|
171
|
+
type: 'button',
|
|
172
|
+
onclick: onSelectLibrary,
|
|
173
|
+
}, [
|
|
174
|
+
m('span', '📁'),
|
|
175
|
+
m('span', 'Change from Library')
|
|
176
|
+
])
|
|
177
|
+
: null,
|
|
178
|
+
onRemove
|
|
179
|
+
? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-xs.font-medium', {
|
|
180
|
+
type: 'button',
|
|
181
|
+
onclick: onRemove,
|
|
182
|
+
}, 'Remove')
|
|
183
|
+
: null,
|
|
184
|
+
]) : null;
|
|
185
|
+
|
|
140
186
|
if (shouldShowImagePreview(url, accept)) {
|
|
141
187
|
return m('.mt-3.flex.flex-col.items-start.gap-2', [
|
|
142
188
|
m('a.block', { href: url, target: '_blank', rel: 'noopener noreferrer' },
|
|
@@ -147,12 +193,7 @@ function renderUploadedFilePreview(url, options) {
|
|
|
147
193
|
})
|
|
148
194
|
),
|
|
149
195
|
m('p.text-xs.text-gray-500.dark:text-slate-400.break-all', url),
|
|
150
|
-
|
|
151
|
-
? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm', {
|
|
152
|
-
type: 'button',
|
|
153
|
-
onclick: onRemove,
|
|
154
|
-
}, 'Remove')
|
|
155
|
-
: null,
|
|
196
|
+
actionsRow,
|
|
156
197
|
]);
|
|
157
198
|
}
|
|
158
199
|
|
|
@@ -162,12 +203,7 @@ function renderUploadedFilePreview(url, options) {
|
|
|
162
203
|
target: '_blank',
|
|
163
204
|
rel: 'noopener noreferrer',
|
|
164
205
|
}, url),
|
|
165
|
-
|
|
166
|
-
? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm', {
|
|
167
|
-
type: 'button',
|
|
168
|
-
onclick: onRemove,
|
|
169
|
-
}, 'Remove')
|
|
170
|
-
: null,
|
|
206
|
+
actionsRow,
|
|
171
207
|
]);
|
|
172
208
|
}
|
|
173
209
|
|
|
@@ -178,7 +214,7 @@ function revokeLocalPreview(state) {
|
|
|
178
214
|
}
|
|
179
215
|
}
|
|
180
216
|
|
|
181
|
-
// File upload field (
|
|
217
|
+
// File upload field (supports local drag & drop, manual URL, and Media Library Picker)
|
|
182
218
|
const FileUploadField = {
|
|
183
219
|
oninit: function (vnode) {
|
|
184
220
|
vnode.state.localPreview = null;
|
|
@@ -190,7 +226,7 @@ const FileUploadField = {
|
|
|
190
226
|
const col = vnode.attrs.col;
|
|
191
227
|
const readonly = vnode.attrs.readonly;
|
|
192
228
|
if (readonly) return;
|
|
193
|
-
var cfg = window.__ADMIN_CONFIG__;
|
|
229
|
+
var cfg = (typeof window !== 'undefined' && window.__ADMIN_CONFIG__) ? window.__ADMIN_CONFIG__ : null;
|
|
194
230
|
var uploadUrl = (cfg && cfg.settings && cfg.settings.uploadUrl) ? String(cfg.settings.uploadUrl) : '';
|
|
195
231
|
if (!uploadUrl) return;
|
|
196
232
|
const dropZoneId = 'drop-zone-' + col.name;
|
|
@@ -242,7 +278,7 @@ const FileUploadField = {
|
|
|
242
278
|
const required = !col.nullable && !readonly;
|
|
243
279
|
var uploadUrl = '';
|
|
244
280
|
try {
|
|
245
|
-
var cfg2 = window.__ADMIN_CONFIG__;
|
|
281
|
+
var cfg2 = (typeof window !== 'undefined' && window.__ADMIN_CONFIG__) ? window.__ADMIN_CONFIG__ : null;
|
|
246
282
|
uploadUrl = (cfg2 && cfg2.settings && cfg2.settings.uploadUrl) ? String(cfg2.settings.uploadUrl) : '';
|
|
247
283
|
} catch (e) { uploadUrl = ''; }
|
|
248
284
|
var maxSize = meta.maxSize || meta.maxBytes || (10 * 1024 * 1024);
|
|
@@ -253,6 +289,18 @@ const FileUploadField = {
|
|
|
253
289
|
revokeLocalPreview(state);
|
|
254
290
|
if (onChange) onChange('');
|
|
255
291
|
};
|
|
292
|
+
var openLibraryPicker = function() {
|
|
293
|
+
if (typeof window !== 'undefined' && window.__openFileManagerPicker) {
|
|
294
|
+
window.__openFileManagerPicker({
|
|
295
|
+
accept: accept,
|
|
296
|
+
title: 'Choose from Media Library',
|
|
297
|
+
onSelect: function(pickedUrl) {
|
|
298
|
+
revokeLocalPreview(state);
|
|
299
|
+
if (onChange) onChange(pickedUrl);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
};
|
|
256
304
|
var error = vnode.attrs.error;
|
|
257
305
|
var borderClass = error ? 'border-red-500 dark:border-red-500' : 'border-gray-300 dark:border-slate-600';
|
|
258
306
|
if (readonly) {
|
|
@@ -270,7 +318,21 @@ const FileUploadField = {
|
|
|
270
318
|
label,
|
|
271
319
|
required ? m('span.text-red-500', ' *') : null,
|
|
272
320
|
]),
|
|
273
|
-
|
|
321
|
+
typeof window !== 'undefined' && window.__openFileManagerPicker
|
|
322
|
+
? m('div.mb-2.flex.items-center.gap-2.flex-wrap', [
|
|
323
|
+
m('button.px-3.py-1.5.text-xs.font-semibold.bg-indigo-600.hover:bg-indigo-700.text-white.rounded-lg.flex.items-center.gap-1.5.shadow-sm.transition', {
|
|
324
|
+
type: 'button',
|
|
325
|
+
onclick: function(e) {
|
|
326
|
+
e.preventDefault();
|
|
327
|
+
openLibraryPicker();
|
|
328
|
+
}
|
|
329
|
+
}, [
|
|
330
|
+
m('span', '📁'),
|
|
331
|
+
m('span', 'Choose from Media Library')
|
|
332
|
+
]),
|
|
333
|
+
m('span.text-xs.text-gray-400.dark:text-slate-500', 'or enter external URL:')
|
|
334
|
+
])
|
|
335
|
+
: 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.'),
|
|
274
336
|
m('input.w-full.px-3.py-2.border.rounded-md.bg-white.dark:bg-slate-900/70.text-gray-900.dark:text-slate-100.placeholder-gray-400.dark:placeholder-slate-500', {
|
|
275
337
|
type: 'text',
|
|
276
338
|
id: col.name,
|
|
@@ -281,7 +343,7 @@ const FileUploadField = {
|
|
|
281
343
|
class: borderClass,
|
|
282
344
|
oninput: function (e) { if (onChange) onChange(e.target.value); },
|
|
283
345
|
}),
|
|
284
|
-
value ? renderUploadedFilePreview(value, { accept: accept, readonly: false, onRemove: clearValue, label: label }) : null,
|
|
346
|
+
value ? renderUploadedFilePreview(value, { accept: accept, readonly: false, onRemove: clearValue, onSelectLibrary: openLibraryPicker, label: label }) : null,
|
|
285
347
|
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
286
348
|
]);
|
|
287
349
|
}
|
|
@@ -291,7 +353,7 @@ const FileUploadField = {
|
|
|
291
353
|
required ? m('span.text-red-500', ' *') : null,
|
|
292
354
|
]),
|
|
293
355
|
displayUrl
|
|
294
|
-
? renderUploadedFilePreview(displayUrl, { accept: accept, readonly: false, onRemove: clearValue, label: label })
|
|
356
|
+
? renderUploadedFilePreview(displayUrl, { accept: accept, readonly: false, onRemove: clearValue, onSelectLibrary: openLibraryPicker, label: label })
|
|
295
357
|
: null,
|
|
296
358
|
m('div#' + dropZoneId + '.border-2.border-dashed.rounded-lg.p-6.text-center.bg-gray-50.dark:bg-slate-900/40', { class: borderClass, style: 'cursor: pointer;' }, [
|
|
297
359
|
m('input[type=file].hidden', {
|
|
@@ -313,17 +375,11 @@ const FileUploadField = {
|
|
|
313
375
|
onclick: function(e) {
|
|
314
376
|
e.preventDefault();
|
|
315
377
|
e.stopPropagation();
|
|
316
|
-
|
|
317
|
-
accept: accept,
|
|
318
|
-
title: '📁 Ortam Kütüphanesinden Seç',
|
|
319
|
-
onSelect: function(pickedUrl) {
|
|
320
|
-
if (onChange) onChange(pickedUrl);
|
|
321
|
-
}
|
|
322
|
-
});
|
|
378
|
+
openLibraryPicker();
|
|
323
379
|
}
|
|
324
380
|
}, [
|
|
325
381
|
m('span', '•'),
|
|
326
|
-
m('span', '
|
|
382
|
+
m('span', 'Choose from Media Library')
|
|
327
383
|
])
|
|
328
384
|
: null
|
|
329
385
|
])
|
|
@@ -336,19 +392,176 @@ const FileUploadField = {
|
|
|
336
392
|
},
|
|
337
393
|
};
|
|
338
394
|
|
|
395
|
+
// Multiple files upload field (supports drag-and-drop, multiple uploads, and Media Library Picker)
|
|
396
|
+
const FilesUploadField = {
|
|
397
|
+
oninit: function(vnode) {
|
|
398
|
+
vnode.state.uploading = false;
|
|
399
|
+
},
|
|
400
|
+
oncreate: function(vnode) {
|
|
401
|
+
const col = vnode.attrs.col;
|
|
402
|
+
const readonly = vnode.attrs.readonly;
|
|
403
|
+
if (readonly) return;
|
|
404
|
+
const dropZoneId = 'drop-zone-multiple-' + col.name;
|
|
405
|
+
const dropZone = document.getElementById(dropZoneId);
|
|
406
|
+
if (!dropZone) return;
|
|
407
|
+
const meta = col.ui || {};
|
|
408
|
+
const onChange = vnode.attrs.onChange;
|
|
409
|
+
const state = vnode.state;
|
|
410
|
+
|
|
411
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function(eventName) {
|
|
412
|
+
dropZone.addEventListener(eventName, function(e) {
|
|
413
|
+
e.preventDefault();
|
|
414
|
+
e.stopPropagation();
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
['dragenter', 'dragover'].forEach(function(eventName) {
|
|
418
|
+
dropZone.addEventListener(eventName, function() {
|
|
419
|
+
dropZone.classList.add('border-blue-500', 'bg-blue-50', 'dark:bg-slate-800');
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
['dragleave', 'drop'].forEach(function(eventName) {
|
|
423
|
+
dropZone.addEventListener(eventName, function() {
|
|
424
|
+
dropZone.classList.remove('border-blue-500', 'bg-blue-50', 'dark:bg-slate-800');
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
dropZone.addEventListener('drop', function(e) {
|
|
428
|
+
const files = e.dataTransfer.files;
|
|
429
|
+
if (files.length > 0) {
|
|
430
|
+
handleAdminMultipleFilesUpload(files, onChange, meta, state, vnode.attrs.value);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
const fileInput = dropZone.querySelector('input[type=file]');
|
|
434
|
+
if (fileInput) {
|
|
435
|
+
fileInput.addEventListener('change', function(e) {
|
|
436
|
+
if (e.target.files.length > 0) {
|
|
437
|
+
handleAdminMultipleFilesUpload(e.target.files, onChange, meta, state, vnode.attrs.value);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
view: function(vnode) {
|
|
443
|
+
const col = vnode.attrs.col;
|
|
444
|
+
const rawVal = vnode.attrs.value;
|
|
445
|
+
const urls = Array.isArray(rawVal) ? rawVal : (typeof rawVal === 'string' && rawVal ? (function() {
|
|
446
|
+
try {
|
|
447
|
+
const parsed = JSON.parse(rawVal);
|
|
448
|
+
return Array.isArray(parsed) ? parsed : [rawVal];
|
|
449
|
+
} catch (e) {
|
|
450
|
+
return [rawVal];
|
|
451
|
+
}
|
|
452
|
+
})() : []);
|
|
453
|
+
const onChange = vnode.attrs.onChange;
|
|
454
|
+
const readonly = vnode.attrs.readonly || false;
|
|
455
|
+
const state = vnode.state;
|
|
456
|
+
const meta = col.ui || {};
|
|
457
|
+
const label = meta.label || formatColumnLabel(col.name);
|
|
458
|
+
const hint = meta.hint || '';
|
|
459
|
+
const required = !col.nullable && !readonly;
|
|
460
|
+
const maxSize = meta.maxSize || meta.maxBytes || (10 * 1024 * 1024);
|
|
461
|
+
const accept = meta.accept || '*/*';
|
|
462
|
+
const dropZoneId = 'drop-zone-multiple-' + col.name;
|
|
463
|
+
const error = vnode.attrs.error;
|
|
464
|
+
const borderClass = error ? 'border-red-500 dark:border-red-500' : 'border-gray-300 dark:border-slate-600';
|
|
465
|
+
|
|
466
|
+
const openLibraryPicker = function() {
|
|
467
|
+
if (typeof window !== 'undefined' && window.__openFileManagerPicker) {
|
|
468
|
+
window.__openFileManagerPicker({
|
|
469
|
+
accept: accept,
|
|
470
|
+
title: 'Choose from Media Library',
|
|
471
|
+
onSelect: function(pickedUrl) {
|
|
472
|
+
if (onChange) {
|
|
473
|
+
onChange([...urls, pickedUrl]);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
return m('.mb-4', [
|
|
481
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', [
|
|
482
|
+
label,
|
|
483
|
+
required ? m('span.text-red-500', ' *') : null,
|
|
484
|
+
]),
|
|
485
|
+
// Thumbnails Grid
|
|
486
|
+
urls.length > 0 ? m('div.grid.grid-cols-2.sm:grid-cols-3.md:grid-cols-4.gap-3.mb-3', urls.map(function(url, idx) {
|
|
487
|
+
return m('div.relative.group.border.border-gray-200.dark:border-slate-700.rounded-lg.p-2.bg-white.dark:bg-slate-900.shadow-sm', [
|
|
488
|
+
shouldShowImagePreview(url, accept)
|
|
489
|
+
? m('a.block', { href: url, target: '_blank', rel: 'noopener noreferrer' },
|
|
490
|
+
m('img.h-28.w-full.rounded.object-contain.bg-gray-50.dark:bg-slate-950', { src: url, alt: `${label} ${idx + 1}`, loading: 'lazy' })
|
|
491
|
+
)
|
|
492
|
+
: m('a.flex.items-center.justify-center.h-28.w-full.text-xs.text-indigo-600.dark:text-indigo-400.break-all.p-1.text-center', {
|
|
493
|
+
href: url, target: '_blank', rel: 'noopener noreferrer'
|
|
494
|
+
}, url.substring(url.lastIndexOf('/') + 1) || url),
|
|
495
|
+
!readonly && onChange ? m('button.absolute.top-1.right-1.bg-red-500.text-white.rounded-full.w-6.h-6.flex.items-center.justify-center.shadow-md.opacity-0.group-hover:opacity-100.hover:bg-red-700.transition-opacity', {
|
|
496
|
+
type: 'button',
|
|
497
|
+
title: 'Remove',
|
|
498
|
+
onclick: function() {
|
|
499
|
+
const updated = urls.filter(function(_, i) { return i !== idx; });
|
|
500
|
+
onChange(updated);
|
|
501
|
+
}
|
|
502
|
+
}, '✕') : null,
|
|
503
|
+
]);
|
|
504
|
+
})) : null,
|
|
505
|
+
|
|
506
|
+
// Dropzone & Media Library Action
|
|
507
|
+
!readonly ? m('div#' + dropZoneId + '.border-2.border-dashed.rounded-lg.p-6.text-center.bg-gray-50.dark:bg-slate-900/40', {
|
|
508
|
+
class: borderClass,
|
|
509
|
+
style: 'cursor: pointer;'
|
|
510
|
+
}, [
|
|
511
|
+
m('input[type=file].hidden', {
|
|
512
|
+
id: 'file-input-multiple-' + col.name,
|
|
513
|
+
accept: accept,
|
|
514
|
+
multiple: true,
|
|
515
|
+
onchange: function(e) {
|
|
516
|
+
if (e.target.files.length > 0) {
|
|
517
|
+
handleAdminMultipleFilesUpload(e.target.files, onChange, meta, state, urls);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}),
|
|
521
|
+
state.uploading
|
|
522
|
+
? m('p.text-indigo-600.dark:text-indigo-400.animate-pulse.text-sm.font-medium', 'Uploading files...')
|
|
523
|
+
: m('div', [
|
|
524
|
+
m('p.text-gray-600.dark:text-slate-400.mb-2.text-sm', 'Drag and drop files here, or'),
|
|
525
|
+
m('div.flex.items-center.justify-center.gap-3.flex-wrap', [
|
|
526
|
+
m('label.text-blue-600.hover:text-blue-800.dark:text-blue-400.cursor-pointer.font-medium.text-sm', {
|
|
527
|
+
for: 'file-input-multiple-' + col.name
|
|
528
|
+
}, 'browse'),
|
|
529
|
+
typeof window !== 'undefined' && window.__openFileManagerPicker
|
|
530
|
+
? m('button.text-indigo-600.hover:text-indigo-800.dark:text-indigo-400.cursor-pointer.font-medium.flex.items-center.gap-1.text-sm', {
|
|
531
|
+
type: 'button',
|
|
532
|
+
onclick: function(e) {
|
|
533
|
+
e.preventDefault();
|
|
534
|
+
e.stopPropagation();
|
|
535
|
+
openLibraryPicker();
|
|
536
|
+
}
|
|
537
|
+
}, [
|
|
538
|
+
m('span', '•'),
|
|
539
|
+
m('span', 'Add from Media Library')
|
|
540
|
+
])
|
|
541
|
+
: null
|
|
542
|
+
])
|
|
543
|
+
])
|
|
544
|
+
]) : null,
|
|
545
|
+
m('input[type=hidden]', { name: col.name, value: JSON.stringify(urls) }),
|
|
546
|
+
m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', 'Max ' + Math.round(maxSize / 1024 / 1024) + ' MB per file'),
|
|
547
|
+
error ? m('p.text-xs.text-red-600.dark:text-red-400.mt-1.font-medium', error) : (hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null),
|
|
548
|
+
]);
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
339
552
|
async function handleAdminFileUpload(file, onChange, meta, state) {
|
|
340
553
|
var uploadUrl = '';
|
|
341
554
|
try {
|
|
342
|
-
var cfg = window.__ADMIN_CONFIG__;
|
|
555
|
+
var cfg = (typeof window !== 'undefined' && window.__ADMIN_CONFIG__) ? window.__ADMIN_CONFIG__ : null;
|
|
343
556
|
uploadUrl = (cfg && cfg.settings && cfg.settings.uploadUrl) ? String(cfg.settings.uploadUrl) : '';
|
|
344
557
|
} catch (e) {}
|
|
345
558
|
if (!uploadUrl) {
|
|
346
|
-
|
|
559
|
+
notifyError('Upload URL is not configured.');
|
|
347
560
|
return;
|
|
348
561
|
}
|
|
349
562
|
var maxSize = meta.maxSize || meta.maxBytes || (10 * 1024 * 1024);
|
|
350
563
|
if (file.size > maxSize) {
|
|
351
|
-
|
|
564
|
+
notifyWarning('File too large (max ' + Math.round(maxSize / 1024 / 1024) + ' MB).');
|
|
352
565
|
return;
|
|
353
566
|
}
|
|
354
567
|
if (state && file.type && file.type.indexOf('image/') === 0) {
|
|
@@ -365,7 +578,7 @@ async function handleAdminFileUpload(file, onChange, meta, state) {
|
|
|
365
578
|
if (!res.ok) {
|
|
366
579
|
revokeLocalPreview(state);
|
|
367
580
|
m.redraw();
|
|
368
|
-
|
|
581
|
+
notifyError(data.message || data.error || ('Upload failed (' + res.status + ')'));
|
|
369
582
|
return;
|
|
370
583
|
}
|
|
371
584
|
revokeLocalPreview(state);
|
|
@@ -375,7 +588,53 @@ async function handleAdminFileUpload(file, onChange, meta, state) {
|
|
|
375
588
|
} catch (err) {
|
|
376
589
|
revokeLocalPreview(state);
|
|
377
590
|
m.redraw();
|
|
378
|
-
|
|
591
|
+
notifyError(err.message || 'Upload failed');
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function handleAdminMultipleFilesUpload(files, onChange, meta, state, currentUrls = []) {
|
|
596
|
+
var uploadUrl = '';
|
|
597
|
+
try {
|
|
598
|
+
var cfg = (typeof window !== 'undefined' && window.__ADMIN_CONFIG__) ? window.__ADMIN_CONFIG__ : null;
|
|
599
|
+
uploadUrl = (cfg && cfg.settings && cfg.settings.uploadUrl) ? String(cfg.settings.uploadUrl) : '';
|
|
600
|
+
} catch (e) {}
|
|
601
|
+
|
|
602
|
+
if (!uploadUrl) {
|
|
603
|
+
notifyError('Upload URL is not configured.');
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
var maxSize = meta.maxSize || meta.maxBytes || (10 * 1024 * 1024);
|
|
607
|
+
var baseUrls = Array.isArray(currentUrls) ? [...currentUrls] : (currentUrls ? [currentUrls] : []);
|
|
608
|
+
|
|
609
|
+
state.uploading = true;
|
|
610
|
+
if (typeof m !== 'undefined' && m.redraw) m.redraw();
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
for (var i = 0; i < files.length; i++) {
|
|
614
|
+
var file = files[i];
|
|
615
|
+
if (file.size > maxSize) {
|
|
616
|
+
notifyWarning('File ' + file.name + ' too large.');
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
var fd = new FormData();
|
|
620
|
+
fd.append('file', file);
|
|
621
|
+
try {
|
|
622
|
+
var res = await fetch(uploadUrl, { method: 'POST', body: fd, credentials: 'include' });
|
|
623
|
+
var data = {};
|
|
624
|
+
try { data = await res.json(); } catch (_) { data = {}; }
|
|
625
|
+
if (res.ok && (data.url || data.publicUrl)) {
|
|
626
|
+
baseUrls.push(data.url || data.publicUrl);
|
|
627
|
+
} else {
|
|
628
|
+
notifyError('Upload failed: ' + (data.message || data.error || res.status));
|
|
629
|
+
}
|
|
630
|
+
} catch (err) {
|
|
631
|
+
notifyError('Upload failed for ' + file.name + ': ' + err.message);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (onChange) onChange(baseUrls);
|
|
635
|
+
} finally {
|
|
636
|
+
state.uploading = false;
|
|
637
|
+
if (typeof m !== 'undefined' && m.redraw) m.redraw();
|
|
379
638
|
}
|
|
380
639
|
}
|
|
381
640
|
|
|
@@ -395,7 +654,7 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
395
654
|
});
|
|
396
655
|
};
|
|
397
656
|
}
|
|
398
|
-
if (col.customField.type === 'file-upload') {
|
|
657
|
+
if (col.customField.type === 'file-upload' || col.customField.type === 'file' || col.customField.type === 'image') {
|
|
399
658
|
return (col, value, onChange, readonly, error) => {
|
|
400
659
|
return m(FileUploadField, {
|
|
401
660
|
col,
|
|
@@ -406,6 +665,17 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
406
665
|
});
|
|
407
666
|
};
|
|
408
667
|
}
|
|
668
|
+
if (col.customField.type === 'files' || col.customField.type === 'files-upload') {
|
|
669
|
+
return (col, value, onChange, readonly, error) => {
|
|
670
|
+
return m(FilesUploadField, {
|
|
671
|
+
col,
|
|
672
|
+
value: value || [],
|
|
673
|
+
onChange,
|
|
674
|
+
readonly: readonly || false,
|
|
675
|
+
error,
|
|
676
|
+
});
|
|
677
|
+
};
|
|
678
|
+
}
|
|
409
679
|
if (col.customField.type === 'belongsTo') {
|
|
410
680
|
return FieldRenderers.belongsTo;
|
|
411
681
|
}
|
|
@@ -414,7 +684,7 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
414
684
|
}
|
|
415
685
|
|
|
416
686
|
// Check registered custom field renderers in browser
|
|
417
|
-
const custom = window.__customFieldRenderers
|
|
687
|
+
const custom = (typeof window !== 'undefined' && window.__customFieldRenderers) ? window.__customFieldRenderers[col.customField.type] : null;
|
|
418
688
|
if (custom) {
|
|
419
689
|
return (col, value, onChange, readonly, error) => {
|
|
420
690
|
try {
|
|
@@ -432,7 +702,7 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
432
702
|
}
|
|
433
703
|
|
|
434
704
|
// Check schema type custom field renderers
|
|
435
|
-
if (col.type && window.__customFieldRenderers && window.__customFieldRenderers[col.type]) {
|
|
705
|
+
if (col.type && typeof window !== 'undefined' && window.__customFieldRenderers && window.__customFieldRenderers[col.type]) {
|
|
436
706
|
const custom = window.__customFieldRenderers[col.type];
|
|
437
707
|
return (col, value, onChange, readonly, error) => {
|
|
438
708
|
try {
|
|
@@ -448,7 +718,7 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
448
718
|
};
|
|
449
719
|
}
|
|
450
720
|
|
|
451
|
-
if (col.type === 'file') {
|
|
721
|
+
if (col.type === 'file' || col.type === 'image' || (col.ui && (col.ui.widget === 'file' || col.ui.widget === 'image'))) {
|
|
452
722
|
return (col, value, onChange, readonly, error) => {
|
|
453
723
|
return m(FileUploadField, {
|
|
454
724
|
col,
|
|
@@ -459,7 +729,31 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
459
729
|
});
|
|
460
730
|
};
|
|
461
731
|
}
|
|
462
|
-
|
|
732
|
+
|
|
733
|
+
if (col.type === 'files' || (col.ui && col.ui.widget === 'files')) {
|
|
734
|
+
return (col, value, onChange, readonly, error) => {
|
|
735
|
+
return m(FilesUploadField, {
|
|
736
|
+
col,
|
|
737
|
+
value: value || [],
|
|
738
|
+
onChange,
|
|
739
|
+
readonly: readonly || false,
|
|
740
|
+
error,
|
|
741
|
+
});
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Auto-detect belongsTo relation for foreign key columns (ends with _id or has references)
|
|
746
|
+
if (!col.primary && !col.autoIncrement && ((col.name && col.name.endsWith('_id')) || col.references)) {
|
|
747
|
+
return FieldRenderers.belongsTo;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Attach file renderers to FieldRenderers object
|
|
751
|
+
FieldRenderers['file-upload'] = (col, value, onChange, readonly, error) => m(FileUploadField, { col, value: value || '', onChange, readonly: readonly || false, error });
|
|
752
|
+
FieldRenderers.file = FieldRenderers['file-upload'];
|
|
753
|
+
FieldRenderers.image = FieldRenderers['file-upload'];
|
|
754
|
+
FieldRenderers.files = (col, value, onChange, readonly, error) => m(FilesUploadField, { col, value: value || [], onChange, readonly: readonly || false, error });
|
|
755
|
+
FieldRenderers['files-upload'] = FieldRenderers.files;
|
|
756
|
+
|
|
463
757
|
// Fallback to standard type renderers
|
|
464
758
|
const typeMap = {
|
|
465
759
|
string: 'string',
|
|
@@ -475,6 +769,9 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
475
769
|
enum: 'enum',
|
|
476
770
|
json: 'json',
|
|
477
771
|
array: 'array',
|
|
772
|
+
file: 'file',
|
|
773
|
+
files: 'files',
|
|
774
|
+
image: 'image',
|
|
478
775
|
uuid: 'string',
|
|
479
776
|
nanoid: 'string',
|
|
480
777
|
belongsTo: 'belongsTo',
|