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
|
@@ -340,12 +340,24 @@ function createExtensionApiHandlers(options) {
|
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
// Execute action
|
|
343
|
-
const
|
|
343
|
+
const context = {
|
|
344
344
|
db,
|
|
345
345
|
req,
|
|
346
346
|
user: req.session?.adminUser,
|
|
347
347
|
body: req.body,
|
|
348
|
+
model: modelName,
|
|
349
|
+
modelName,
|
|
350
|
+
name: modelName,
|
|
351
|
+
};
|
|
352
|
+
const modelArg = Object.assign(new String(modelName), context, {
|
|
353
|
+
name: modelName,
|
|
354
|
+
modelName,
|
|
355
|
+
model: modelName,
|
|
356
|
+
valueOf: () => modelName,
|
|
357
|
+
toString: () => modelName,
|
|
358
|
+
[Symbol.toPrimitive]: () => modelName,
|
|
348
359
|
});
|
|
360
|
+
const result = await action.handler(record, modelArg, context);
|
|
349
361
|
|
|
350
362
|
res.json({ success: true, result });
|
|
351
363
|
} catch (error) {
|
|
@@ -418,12 +430,24 @@ function createExtensionApiHandlers(options) {
|
|
|
418
430
|
}
|
|
419
431
|
|
|
420
432
|
// Execute action
|
|
421
|
-
const
|
|
433
|
+
const context = {
|
|
422
434
|
db,
|
|
423
435
|
req,
|
|
424
436
|
user: req.session?.adminUser,
|
|
425
437
|
body: req.body,
|
|
438
|
+
model: modelName,
|
|
439
|
+
modelName,
|
|
440
|
+
name: modelName,
|
|
441
|
+
};
|
|
442
|
+
const modelArg = Object.assign(new String(modelName), context, {
|
|
443
|
+
name: modelName,
|
|
444
|
+
modelName,
|
|
445
|
+
model: modelName,
|
|
446
|
+
valueOf: () => modelName,
|
|
447
|
+
toString: () => modelName,
|
|
448
|
+
[Symbol.toPrimitive]: () => modelName,
|
|
426
449
|
});
|
|
450
|
+
const result = await action.handler(records, modelArg, context);
|
|
427
451
|
|
|
428
452
|
res.json({ success: true, result, affected: records.length });
|
|
429
453
|
} catch (error) {
|
|
@@ -675,6 +699,10 @@ function createExtensionApiHandlers(options) {
|
|
|
675
699
|
}
|
|
676
700
|
}
|
|
677
701
|
|
|
702
|
+
if (registry && typeof registry.runHooks === 'function') {
|
|
703
|
+
await registry.runHooks('onDashboardLoad', { stats, req, user: req.session?.adminUser, db });
|
|
704
|
+
}
|
|
705
|
+
|
|
678
706
|
res.json({ stats });
|
|
679
707
|
} catch (error) {
|
|
680
708
|
res.status(500).json({ error: error.message });
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* @module plugins/admin-panel/core/registry
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* Extension types
|
|
9
11
|
*/
|
|
@@ -46,6 +48,7 @@ class AdminRegistry {
|
|
|
46
48
|
|
|
47
49
|
// Client components: JS code strings for custom page rendering
|
|
48
50
|
this.clientComponents = new Map();
|
|
51
|
+
this.clientComponentFiles = new Map();
|
|
49
52
|
|
|
50
53
|
// Custom scripts & styles for Admin Panel SSR rendering
|
|
51
54
|
this.scripts = [];
|
|
@@ -59,6 +62,8 @@ class AdminRegistry {
|
|
|
59
62
|
afterUpdate: [],
|
|
60
63
|
beforeDelete: [],
|
|
61
64
|
afterDelete: [],
|
|
65
|
+
beforeRestore: [],
|
|
66
|
+
afterRestore: [],
|
|
62
67
|
onRecordView: [],
|
|
63
68
|
onDashboardLoad: [],
|
|
64
69
|
};
|
|
@@ -66,6 +71,7 @@ class AdminRegistry {
|
|
|
66
71
|
// Settings
|
|
67
72
|
this.settings = {
|
|
68
73
|
title: 'Admin Panel',
|
|
74
|
+
titleTemplate: '%s | %b',
|
|
69
75
|
logo: null,
|
|
70
76
|
primaryColor: '#3B82F6',
|
|
71
77
|
perPage: 20,
|
|
@@ -335,17 +341,41 @@ class AdminRegistry {
|
|
|
335
341
|
* @param {string} pageId - Page ID (must match a registered page)
|
|
336
342
|
* @param {string} jsCode - JavaScript code string that defines the component.
|
|
337
343
|
* The code should assign a Mithril component to window.__customPages[pageId].
|
|
344
|
+
* @param {string} [filePath=null] - Absolute file path for dev mode live reloading
|
|
338
345
|
*/
|
|
339
|
-
registerClientComponent(pageId, jsCode) {
|
|
346
|
+
registerClientComponent(pageId, jsCode, filePath = null) {
|
|
340
347
|
this.clientComponents.set(pageId, jsCode);
|
|
348
|
+
if (filePath) {
|
|
349
|
+
this.clientComponentFiles.set(pageId, filePath);
|
|
350
|
+
}
|
|
341
351
|
return this;
|
|
342
352
|
}
|
|
343
353
|
|
|
344
354
|
/**
|
|
345
|
-
* Get all client component code strings concatenated
|
|
355
|
+
* Get all client component code strings concatenated.
|
|
356
|
+
* In development mode (NODE_ENV !== 'production'), dynamically re-reads modified
|
|
357
|
+
* component files from disk to eliminate manual server restarts.
|
|
346
358
|
*/
|
|
347
359
|
getClientComponents() {
|
|
348
|
-
|
|
360
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
361
|
+
const parts = [];
|
|
362
|
+
for (const [pageId, code] of this.clientComponents.entries()) {
|
|
363
|
+
const filePath = this.clientComponentFiles.get(pageId);
|
|
364
|
+
if (isDev && filePath) {
|
|
365
|
+
try {
|
|
366
|
+
if (fs.existsSync(filePath)) {
|
|
367
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
368
|
+
const { formatClientComponent } = require('./admin-module');
|
|
369
|
+
parts.push(formatClientComponent(pageId, raw));
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
} catch (e) {
|
|
373
|
+
console.error(`[Admin Panel] Failed to reload component for "${pageId}":`, e);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
parts.push(code || '');
|
|
377
|
+
}
|
|
378
|
+
return parts.filter(Boolean).join('\n');
|
|
349
379
|
}
|
|
350
380
|
|
|
351
381
|
/**
|
|
@@ -382,6 +412,44 @@ class AdminRegistry {
|
|
|
382
412
|
return this;
|
|
383
413
|
}
|
|
384
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Set base admin panel title
|
|
417
|
+
* @param {string} title - Base title (e.g., 'Acme Admin')
|
|
418
|
+
*/
|
|
419
|
+
setTitle(title) {
|
|
420
|
+
if (typeof title === 'string') {
|
|
421
|
+
this.settings.title = title;
|
|
422
|
+
}
|
|
423
|
+
return this;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Get base admin panel title
|
|
428
|
+
* @returns {string}
|
|
429
|
+
*/
|
|
430
|
+
getTitle() {
|
|
431
|
+
return this.settings.title;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Set admin page title template
|
|
436
|
+
* @param {string} template - Title template (e.g., '%s | %b', '%s — %b', '%s')
|
|
437
|
+
*/
|
|
438
|
+
setTitleTemplate(template) {
|
|
439
|
+
if (typeof template === 'string') {
|
|
440
|
+
this.settings.titleTemplate = template;
|
|
441
|
+
}
|
|
442
|
+
return this;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Get admin page title template
|
|
447
|
+
* @returns {string}
|
|
448
|
+
*/
|
|
449
|
+
getTitleTemplate() {
|
|
450
|
+
return this.settings.titleTemplate;
|
|
451
|
+
}
|
|
452
|
+
|
|
385
453
|
/**
|
|
386
454
|
* Enable user management
|
|
387
455
|
* @param {Object} config - User management configuration
|
|
@@ -471,19 +539,30 @@ class AdminRegistry {
|
|
|
471
539
|
toClientConfig() {
|
|
472
540
|
return {
|
|
473
541
|
settings: this.settings,
|
|
474
|
-
pages: this.getPages().map(p =>
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
542
|
+
pages: this.getPages().map(p => {
|
|
543
|
+
let html = p.html;
|
|
544
|
+
if (p.htmlFile && process.env.NODE_ENV !== 'production') {
|
|
545
|
+
try {
|
|
546
|
+
if (fs.existsSync(p.htmlFile)) {
|
|
547
|
+
html = fs.readFileSync(p.htmlFile, 'utf8');
|
|
548
|
+
}
|
|
549
|
+
} catch (e) {}
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
id: p.id,
|
|
553
|
+
title: p.title,
|
|
554
|
+
documentTitle: p.documentTitle,
|
|
555
|
+
path: p.path,
|
|
556
|
+
icon: p.icon,
|
|
557
|
+
description: p.description,
|
|
558
|
+
permission: p.permission,
|
|
559
|
+
layout: p.layout !== undefined ? p.layout : true,
|
|
560
|
+
iframe: p.iframe,
|
|
561
|
+
url: p.url || p.iframeUrl,
|
|
562
|
+
html,
|
|
563
|
+
hasClientComponent: this.clientComponents.has(p.id),
|
|
564
|
+
};
|
|
565
|
+
}),
|
|
487
566
|
|
|
488
567
|
widgets: this.getWidgets().map(w => ({
|
|
489
568
|
id: w.id,
|
|
@@ -527,6 +606,7 @@ class AdminRegistry {
|
|
|
527
606
|
this.menuGroups.clear();
|
|
528
607
|
this.fieldRenderers.clear();
|
|
529
608
|
this.clientComponents.clear();
|
|
609
|
+
this.clientComponentFiles.clear();
|
|
530
610
|
this.scripts = [];
|
|
531
611
|
this.styles = [];
|
|
532
612
|
Object.keys(this.hooks).forEach(k => this.hooks[k] = []);
|
|
@@ -27,9 +27,24 @@ function shouldShowImagePreview(url, accept) {
|
|
|
27
27
|
return isImageAccept(accept) || isImageUrl(url);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function renderUploadedFilePreview(url, { accept, readonly, onRemove, label }) {
|
|
30
|
+
function renderUploadedFilePreview(url, { accept, readonly, onRemove, onSelectLibrary, label }) {
|
|
31
31
|
if (!url) return null;
|
|
32
32
|
|
|
33
|
+
const actions = !readonly ? m('.flex.items-center.gap-3.mt-1', [
|
|
34
|
+
onSelectLibrary
|
|
35
|
+
? m('button.text-indigo-600.dark:text-indigo-400.hover:text-indigo-800.dark:hover:text-indigo-300.text-xs.font-medium', {
|
|
36
|
+
type: 'button',
|
|
37
|
+
onclick: onSelectLibrary,
|
|
38
|
+
}, '📁 Change from Library')
|
|
39
|
+
: null,
|
|
40
|
+
onRemove
|
|
41
|
+
? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-xs', {
|
|
42
|
+
type: 'button',
|
|
43
|
+
onclick: onRemove,
|
|
44
|
+
}, 'Remove')
|
|
45
|
+
: null,
|
|
46
|
+
]) : null;
|
|
47
|
+
|
|
33
48
|
if (shouldShowImagePreview(url, accept)) {
|
|
34
49
|
return m('.mt-3.flex.flex-col.items-start.gap-2', [
|
|
35
50
|
m('a.block', { href: url, target: '_blank', rel: 'noopener noreferrer' },
|
|
@@ -40,12 +55,7 @@ function renderUploadedFilePreview(url, { accept, readonly, onRemove, label }) {
|
|
|
40
55
|
})
|
|
41
56
|
),
|
|
42
57
|
m('p.text-xs.text-gray-500.dark:text-slate-400.break-all', url),
|
|
43
|
-
|
|
44
|
-
? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm', {
|
|
45
|
-
type: 'button',
|
|
46
|
-
onclick: onRemove,
|
|
47
|
-
}, 'Remove')
|
|
48
|
-
: null,
|
|
58
|
+
actions,
|
|
49
59
|
]);
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -55,12 +65,7 @@ function renderUploadedFilePreview(url, { accept, readonly, onRemove, label }) {
|
|
|
55
65
|
target: '_blank',
|
|
56
66
|
rel: 'noopener noreferrer',
|
|
57
67
|
}, url),
|
|
58
|
-
|
|
59
|
-
? m('button.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.text-sm', {
|
|
60
|
-
type: 'button',
|
|
61
|
-
onclick: onRemove,
|
|
62
|
-
}, 'Remove')
|
|
63
|
-
: null,
|
|
68
|
+
actions,
|
|
64
69
|
]);
|
|
65
70
|
}
|
|
66
71
|
|
|
@@ -141,6 +146,19 @@ module.exports = {
|
|
|
141
146
|
if (vnode.attrs.onchange) vnode.attrs.onchange('');
|
|
142
147
|
};
|
|
143
148
|
|
|
149
|
+
const openLibraryPicker = () => {
|
|
150
|
+
if (typeof window !== 'undefined' && window.__openFileManagerPicker) {
|
|
151
|
+
window.__openFileManagerPicker({
|
|
152
|
+
accept,
|
|
153
|
+
title: 'Choose from Media Library',
|
|
154
|
+
onSelect: (pickedUrl) => {
|
|
155
|
+
revokeLocalPreview(state);
|
|
156
|
+
if (vnode.attrs.onchange) vnode.attrs.onchange(pickedUrl);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
144
162
|
if (!uploadUrl) {
|
|
145
163
|
return m('.mb-4', [
|
|
146
164
|
m(
|
|
@@ -148,7 +166,14 @@ module.exports = {
|
|
|
148
166
|
label,
|
|
149
167
|
required ? m('span.text-red-500', ' *') : null
|
|
150
168
|
),
|
|
151
|
-
|
|
169
|
+
typeof window !== 'undefined' && window.__openFileManagerPicker
|
|
170
|
+
? m('.flex.items-center.gap-2.mb-3', [
|
|
171
|
+
m('button.px-3.py-1.5.bg-indigo-600.hover:bg-indigo-700.text-white.text-xs.font-medium.rounded.flex.items-center.gap-1.shadow-sm', {
|
|
172
|
+
type: 'button',
|
|
173
|
+
onclick: openLibraryPicker,
|
|
174
|
+
}, '📁 Choose from Media Library')
|
|
175
|
+
])
|
|
176
|
+
: m('p.text-xs.text-amber-700.dark:text-amber-400.mb-2', 'Upload URL is not configured.'),
|
|
152
177
|
m('input.w-full.px-3.py-2.border.rounded', {
|
|
153
178
|
type: 'text',
|
|
154
179
|
name,
|
|
@@ -161,7 +186,7 @@ module.exports = {
|
|
|
161
186
|
},
|
|
162
187
|
}),
|
|
163
188
|
value
|
|
164
|
-
? renderUploadedFilePreview(value, { accept, readonly, onRemove: clearValue, label })
|
|
189
|
+
? renderUploadedFilePreview(value, { accept, readonly, onRemove: clearValue, onSelectLibrary: openLibraryPicker, label })
|
|
165
190
|
: null,
|
|
166
191
|
]);
|
|
167
192
|
}
|
|
@@ -173,7 +198,7 @@ module.exports = {
|
|
|
173
198
|
required ? m('span.text-red-500', ' *') : null
|
|
174
199
|
),
|
|
175
200
|
displayUrl
|
|
176
|
-
? renderUploadedFilePreview(displayUrl, { accept, readonly, onRemove: clearValue, label })
|
|
201
|
+
? renderUploadedFilePreview(displayUrl, { accept, readonly, onRemove: clearValue, onSelectLibrary: openLibraryPicker, label })
|
|
177
202
|
: null,
|
|
178
203
|
m(
|
|
179
204
|
'div.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded.p-6.text-center',
|
|
@@ -200,11 +225,26 @@ module.exports = {
|
|
|
200
225
|
'p.text-gray-600.dark:text-slate-400.mb-2',
|
|
201
226
|
displayUrl ? 'Drag and drop to replace, or' : 'Drag and drop a file here, or'
|
|
202
227
|
),
|
|
203
|
-
m(
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
228
|
+
m('.flex.items-center.justify-center.gap-3.flex-wrap', [
|
|
229
|
+
m(
|
|
230
|
+
'label.text-blue-600.hover:text-blue-800.cursor-pointer',
|
|
231
|
+
{ for: 'file-input-' + name },
|
|
232
|
+
displayUrl ? 'choose another file' : 'browse'
|
|
233
|
+
),
|
|
234
|
+
typeof window !== 'undefined' && window.__openFileManagerPicker
|
|
235
|
+
? m('button.text-indigo-600.hover:text-indigo-800.dark:text-indigo-400.cursor-pointer.font-medium.flex.items-center.gap-1.text-sm', {
|
|
236
|
+
type: 'button',
|
|
237
|
+
onclick: (e) => {
|
|
238
|
+
e.preventDefault();
|
|
239
|
+
e.stopPropagation();
|
|
240
|
+
openLibraryPicker();
|
|
241
|
+
}
|
|
242
|
+
}, [
|
|
243
|
+
m('span', '•'),
|
|
244
|
+
m('span', '📁 Choose from Media Library')
|
|
245
|
+
])
|
|
246
|
+
: null,
|
|
247
|
+
]),
|
|
208
248
|
]),
|
|
209
249
|
]
|
|
210
250
|
),
|
|
@@ -280,6 +320,98 @@ module.exports = {
|
|
|
280
320
|
|
|
281
321
|
const urls = Array.isArray(value) ? value : (value ? [value] : []);
|
|
282
322
|
|
|
323
|
+
const openLibraryPicker = () => {
|
|
324
|
+
if (typeof window !== 'undefined' && window.__openFileManagerPicker) {
|
|
325
|
+
window.__openFileManagerPicker({
|
|
326
|
+
accept,
|
|
327
|
+
title: 'Add from Media Library',
|
|
328
|
+
onSelect: (pickedUrl) => {
|
|
329
|
+
const newUrls = [...urls, pickedUrl];
|
|
330
|
+
if (vnode.attrs.onchange) vnode.attrs.onchange(newUrls);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const renderGrid = () => {
|
|
337
|
+
if (urls.length === 0) return null;
|
|
338
|
+
return m('.grid.grid-cols-2.sm:grid-cols-3.md:grid-cols-4.gap-4.mb-3', urls.map((url, idx) => {
|
|
339
|
+
const isDragOver = state.dragOverIndex === idx;
|
|
340
|
+
const baseClasses = '.relative.group.border.rounded-lg.p-2.transition-all';
|
|
341
|
+
const cursorClass = readonly ? '' : '.cursor-move';
|
|
342
|
+
const borderClasses = isDragOver ? '.border-blue-500.border-2.dark:border-blue-500' : '.border-gray-200.dark:border-slate-700';
|
|
343
|
+
const bgClasses = isDragOver ? '.bg-blue-50.dark:bg-slate-800' : '.bg-gray-50.dark:bg-slate-900';
|
|
344
|
+
|
|
345
|
+
return m(baseClasses + cursorClass + borderClasses + bgClasses, {
|
|
346
|
+
draggable: !readonly,
|
|
347
|
+
ondragstart: (e) => {
|
|
348
|
+
state.draggedIndex = idx;
|
|
349
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
350
|
+
e.dataTransfer.setData('text/plain', idx);
|
|
351
|
+
},
|
|
352
|
+
ondragover: (e) => {
|
|
353
|
+
e.preventDefault();
|
|
354
|
+
e.dataTransfer.dropEffect = 'move';
|
|
355
|
+
},
|
|
356
|
+
ondragenter: (e) => {
|
|
357
|
+
state.dragOverIndex = idx;
|
|
358
|
+
},
|
|
359
|
+
ondragleave: (e) => {
|
|
360
|
+
if (state.dragOverIndex === idx) {
|
|
361
|
+
state.dragOverIndex = null;
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
ondrop: (e) => {
|
|
365
|
+
e.preventDefault();
|
|
366
|
+
const fromIdx = state.draggedIndex;
|
|
367
|
+
const toIdx = idx;
|
|
368
|
+
if (fromIdx !== null && fromIdx !== undefined && fromIdx !== toIdx) {
|
|
369
|
+
const newUrls = [...urls];
|
|
370
|
+
const [movedItem] = newUrls.splice(fromIdx, 1);
|
|
371
|
+
newUrls.splice(toIdx, 0, movedItem);
|
|
372
|
+
if (vnode.attrs.onchange) vnode.attrs.onchange(newUrls);
|
|
373
|
+
}
|
|
374
|
+
state.draggedIndex = null;
|
|
375
|
+
state.dragOverIndex = null;
|
|
376
|
+
},
|
|
377
|
+
ondragend: (e) => {
|
|
378
|
+
state.draggedIndex = null;
|
|
379
|
+
state.dragOverIndex = null;
|
|
380
|
+
}
|
|
381
|
+
}, [
|
|
382
|
+
shouldShowImagePreview(url, accept)
|
|
383
|
+
? m('a.block', { href: url, target: '_blank', rel: 'noopener noreferrer', draggable: false },
|
|
384
|
+
m('img.h-32.w-full.rounded.object-contain.bg-white.dark:bg-slate-950', {
|
|
385
|
+
src: url,
|
|
386
|
+
alt: `${label} ${idx + 1}`,
|
|
387
|
+
loading: 'lazy',
|
|
388
|
+
draggable: false,
|
|
389
|
+
})
|
|
390
|
+
)
|
|
391
|
+
: m('a.flex.items-center.justify-center.h-32.w-full.text-sm.text-indigo-600.dark:text-indigo-400.break-all.p-2', {
|
|
392
|
+
href: url,
|
|
393
|
+
target: '_blank',
|
|
394
|
+
rel: 'noopener noreferrer',
|
|
395
|
+
draggable: false,
|
|
396
|
+
}, url.substring(url.lastIndexOf('/') + 1) || url),
|
|
397
|
+
!readonly
|
|
398
|
+
? m('button.absolute.top-1.right-1.bg-red-500.text-white.rounded-full.p-1.shadow-md.opacity-0.group-hover:opacity-100.hover:bg-red-700.transition-opacity', {
|
|
399
|
+
type: 'button',
|
|
400
|
+
onclick: () => {
|
|
401
|
+
const newUrls = urls.filter((_, i) => i !== idx);
|
|
402
|
+
if (vnode.attrs.onchange) vnode.attrs.onchange(newUrls);
|
|
403
|
+
},
|
|
404
|
+
title: 'Remove file',
|
|
405
|
+
}, [
|
|
406
|
+
m('svg.w-4.h-4', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' },
|
|
407
|
+
m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M6 18L18 6M6 6l12 12' })
|
|
408
|
+
)
|
|
409
|
+
])
|
|
410
|
+
: null,
|
|
411
|
+
]);
|
|
412
|
+
}));
|
|
413
|
+
};
|
|
414
|
+
|
|
283
415
|
if (!uploadUrl) {
|
|
284
416
|
return m('.mb-4', [
|
|
285
417
|
m(
|
|
@@ -287,7 +419,19 @@ module.exports = {
|
|
|
287
419
|
label,
|
|
288
420
|
required ? m('span.text-red-500', ' *') : null
|
|
289
421
|
),
|
|
290
|
-
|
|
422
|
+
typeof window !== 'undefined' && window.__openFileManagerPicker
|
|
423
|
+
? m('.flex.items-center.gap-2.mb-3', [
|
|
424
|
+
m('button.px-3.py-1.5.bg-indigo-600.hover:bg-indigo-700.text-white.text-xs.font-medium.rounded.flex.items-center.gap-1.shadow-sm', {
|
|
425
|
+
type: 'button',
|
|
426
|
+
onclick: openLibraryPicker,
|
|
427
|
+
}, '📁 Add from Media Library')
|
|
428
|
+
])
|
|
429
|
+
: m('p.text-xs.text-amber-700.dark:text-amber-400.mb-2', 'Upload URL is not configured.'),
|
|
430
|
+
renderGrid(),
|
|
431
|
+
m('input[type=hidden]', {
|
|
432
|
+
name,
|
|
433
|
+
value: JSON.stringify(urls),
|
|
434
|
+
}),
|
|
291
435
|
]);
|
|
292
436
|
}
|
|
293
437
|
|
|
@@ -298,83 +442,7 @@ module.exports = {
|
|
|
298
442
|
required ? m('span.text-red-500', ' *') : null
|
|
299
443
|
),
|
|
300
444
|
|
|
301
|
-
|
|
302
|
-
? m('.grid.grid-cols-2.sm:grid-cols-3.md:grid-cols-4.gap-4.mb-3', urls.map((url, idx) => {
|
|
303
|
-
const isDragOver = state.dragOverIndex === idx;
|
|
304
|
-
const baseClasses = '.relative.group.border.rounded-lg.p-2.transition-all';
|
|
305
|
-
const cursorClass = readonly ? '' : '.cursor-move';
|
|
306
|
-
const borderClasses = isDragOver ? '.border-blue-500.border-2.dark:border-blue-500' : '.border-gray-200.dark:border-slate-700';
|
|
307
|
-
const bgClasses = isDragOver ? '.bg-blue-50.dark:bg-slate-800' : '.bg-gray-50.dark:bg-slate-900';
|
|
308
|
-
|
|
309
|
-
return m(baseClasses + cursorClass + borderClasses + bgClasses, {
|
|
310
|
-
draggable: !readonly,
|
|
311
|
-
ondragstart: (e) => {
|
|
312
|
-
state.draggedIndex = idx;
|
|
313
|
-
e.dataTransfer.effectAllowed = 'move';
|
|
314
|
-
e.dataTransfer.setData('text/plain', idx);
|
|
315
|
-
},
|
|
316
|
-
ondragover: (e) => {
|
|
317
|
-
e.preventDefault();
|
|
318
|
-
e.dataTransfer.dropEffect = 'move';
|
|
319
|
-
},
|
|
320
|
-
ondragenter: (e) => {
|
|
321
|
-
state.dragOverIndex = idx;
|
|
322
|
-
},
|
|
323
|
-
ondragleave: (e) => {
|
|
324
|
-
if (state.dragOverIndex === idx) {
|
|
325
|
-
state.dragOverIndex = null;
|
|
326
|
-
}
|
|
327
|
-
},
|
|
328
|
-
ondrop: (e) => {
|
|
329
|
-
e.preventDefault();
|
|
330
|
-
const fromIdx = state.draggedIndex;
|
|
331
|
-
const toIdx = idx;
|
|
332
|
-
if (fromIdx !== null && fromIdx !== undefined && fromIdx !== toIdx) {
|
|
333
|
-
const newUrls = [...urls];
|
|
334
|
-
const [movedItem] = newUrls.splice(fromIdx, 1);
|
|
335
|
-
newUrls.splice(toIdx, 0, movedItem);
|
|
336
|
-
if (vnode.attrs.onchange) vnode.attrs.onchange(newUrls);
|
|
337
|
-
}
|
|
338
|
-
state.draggedIndex = null;
|
|
339
|
-
state.dragOverIndex = null;
|
|
340
|
-
},
|
|
341
|
-
ondragend: (e) => {
|
|
342
|
-
state.draggedIndex = null;
|
|
343
|
-
state.dragOverIndex = null;
|
|
344
|
-
}
|
|
345
|
-
}, [
|
|
346
|
-
shouldShowImagePreview(url, accept)
|
|
347
|
-
? m('a.block', { href: url, target: '_blank', rel: 'noopener noreferrer', draggable: false },
|
|
348
|
-
m('img.h-32.w-full.rounded.object-contain.bg-white.dark:bg-slate-950', {
|
|
349
|
-
src: url,
|
|
350
|
-
alt: `${label} ${idx + 1}`,
|
|
351
|
-
loading: 'lazy',
|
|
352
|
-
draggable: false,
|
|
353
|
-
})
|
|
354
|
-
)
|
|
355
|
-
: m('a.flex.items-center.justify-center.h-32.w-full.text-sm.text-indigo-600.dark:text-indigo-400.break-all.p-2', {
|
|
356
|
-
href: url,
|
|
357
|
-
target: '_blank',
|
|
358
|
-
rel: 'noopener noreferrer',
|
|
359
|
-
draggable: false,
|
|
360
|
-
}, url.substring(url.lastIndexOf('/') + 1) || url),
|
|
361
|
-
!readonly
|
|
362
|
-
? m('button.absolute.top-1.right-1.bg-red-500.text-white.rounded-full.p-1.shadow-md.opacity-0.group-hover:opacity-100.hover:bg-red-700.transition-opacity', {
|
|
363
|
-
type: 'button',
|
|
364
|
-
onclick: () => {
|
|
365
|
-
const newUrls = urls.filter((_, i) => i !== idx);
|
|
366
|
-
if (vnode.attrs.onchange) vnode.attrs.onchange(newUrls);
|
|
367
|
-
},
|
|
368
|
-
title: 'Remove file',
|
|
369
|
-
}, [
|
|
370
|
-
m('svg.w-4.h-4', { fill: 'none', stroke: 'currentColor', viewBox: '0 0 24 24' },
|
|
371
|
-
m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M6 18L18 6M6 6l12 12' })
|
|
372
|
-
)
|
|
373
|
-
])
|
|
374
|
-
: null,
|
|
375
|
-
]);
|
|
376
|
-
}))
|
|
377
|
-
: null,
|
|
445
|
+
renderGrid(),
|
|
378
446
|
|
|
379
447
|
m(
|
|
380
448
|
'div.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded.p-6.text-center',
|
|
@@ -405,11 +473,26 @@ module.exports = {
|
|
|
405
473
|
'p.text-gray-600.dark:text-slate-400.mb-2',
|
|
406
474
|
'Drag and drop files here, or'
|
|
407
475
|
),
|
|
408
|
-
m(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
476
|
+
m('.flex.items-center.justify-center.gap-3.flex-wrap', [
|
|
477
|
+
m(
|
|
478
|
+
'label.text-blue-600.hover:text-blue-800.cursor-pointer',
|
|
479
|
+
{ for: 'file-input-multiple-' + name },
|
|
480
|
+
'browse'
|
|
481
|
+
),
|
|
482
|
+
typeof window !== 'undefined' && window.__openFileManagerPicker
|
|
483
|
+
? m('button.text-indigo-600.hover:text-indigo-800.dark:text-indigo-400.cursor-pointer.font-medium.flex.items-center.gap-1.text-sm', {
|
|
484
|
+
type: 'button',
|
|
485
|
+
onclick: (e) => {
|
|
486
|
+
e.preventDefault();
|
|
487
|
+
e.stopPropagation();
|
|
488
|
+
openLibraryPicker();
|
|
489
|
+
}
|
|
490
|
+
}, [
|
|
491
|
+
m('span', '•'),
|
|
492
|
+
m('span', '📁 Add from Media Library')
|
|
493
|
+
])
|
|
494
|
+
: null,
|
|
495
|
+
]),
|
|
413
496
|
]),
|
|
414
497
|
]),
|
|
415
498
|
]
|
|
@@ -79,7 +79,9 @@ function initializeDefaultRenderers() {
|
|
|
79
79
|
registerFieldRenderer('rich-text', richTextRenderer.RichTextField);
|
|
80
80
|
registerFieldRenderer('file-upload', fileUploadRenderer.FileUploadField);
|
|
81
81
|
registerFieldRenderer('file', fileUploadRenderer.FileUploadField);
|
|
82
|
+
registerFieldRenderer('image', fileUploadRenderer.FileUploadField);
|
|
82
83
|
registerFieldRenderer('files', fileUploadRenderer.FilesUploadField);
|
|
84
|
+
registerFieldRenderer('files-upload', fileUploadRenderer.FilesUploadField);
|
|
83
85
|
|
|
84
86
|
// Relation types
|
|
85
87
|
registerFieldRenderer('belongsTo', relationRenderers.BelongsToField);
|