webspresso 0.0.98 → 0.0.100
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 +11 -1
- package/.agents/AGENTS.md +2 -2
- package/.agents/AUTH.md +415 -0
- package/.agents/CLI.md +47 -0
- package/.agents/ERRORS.md +59 -0
- package/.agents/ORM.md +178 -4
- package/.agents/PLUGINS.md +113 -8
- package/.agents/QUEUE.md +92 -0
- package/.agents/ROUTING.md +160 -7
- package/.agents/STREAMING.md +35 -0
- package/AGENTS.md +2 -0
- package/README.md +145 -93
- package/bin/commands/add-docker.js +467 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/webspresso.js +4 -0
- package/core/auth/index.js +123 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +477 -3
- package/core/auth/middleware.js +118 -5
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/session-manager.js +255 -0
- package/core/auth/sessions.js +351 -0
- package/core/auth/tokens.js +1 -0
- package/core/auth/totp.js +507 -0
- package/core/auth/user-agent.js +161 -0
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +10 -1
- package/core/orm/model.js +55 -1
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +327 -16
- package/core/orm/scopes.js +3 -2
- package/core/orm/search.js +213 -0
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +544 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +100 -1
- package/core/queue/scheduler.js +415 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +794 -6
- package/index.js +48 -0
- package/package.json +7 -5
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
- package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- 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/modules/admin-users.js +1 -1
- 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 -70
- package/plugins/audit-log/api-handlers.js +114 -0
- package/plugins/audit-log/index.js +24 -0
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/cors.js +13 -2
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/file-manager/admin-component.js +69 -53
- package/plugins/file-manager/index.js +25 -8
- package/plugins/index.js +8 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/queue/index.js +4 -0
- package/plugins/webhooks/admin-component.js +608 -0
- package/plugins/webhooks/api-handlers.js +237 -0
- package/plugins/webhooks/crypto.js +144 -0
- package/plugins/webhooks/dispatcher.js +708 -0
- package/plugins/webhooks/index.js +263 -0
- package/plugins/webhooks/matcher.js +113 -0
- package/plugins/webhooks/migration-template.js +75 -0
- package/plugins/webhooks/models.js +98 -0
- package/plugins/webhooks/sender.js +176 -0
- package/src/api/api-loader.js +1 -0
- package/src/bundle/index.js +2 -1
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +310 -104
- package/src/helpers.js +16 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/routing/cascading.js +490 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +6 -0
package/index.js
CHANGED
|
@@ -29,6 +29,8 @@ const {
|
|
|
29
29
|
mountPages,
|
|
30
30
|
filePathToRoute,
|
|
31
31
|
extractMethodFromFilename,
|
|
32
|
+
isRouteGroupSegment,
|
|
33
|
+
stripRouteGroups,
|
|
32
34
|
scanDirectory,
|
|
33
35
|
loadI18n,
|
|
34
36
|
createTranslator,
|
|
@@ -144,6 +146,8 @@ module.exports = {
|
|
|
144
146
|
mountPages,
|
|
145
147
|
filePathToRoute,
|
|
146
148
|
extractMethodFromFilename,
|
|
149
|
+
isRouteGroupSegment,
|
|
150
|
+
stripRouteGroups,
|
|
147
151
|
scanDirectory,
|
|
148
152
|
loadI18n,
|
|
149
153
|
createTranslator,
|
|
@@ -186,20 +190,56 @@ module.exports = {
|
|
|
186
190
|
compileRouteTable,
|
|
187
191
|
RouteTable,
|
|
188
192
|
mountDiscoveredRoutes,
|
|
193
|
+
resolveMiddlewares: require('./src/routing/middleware-resolver').resolveMiddlewares,
|
|
189
194
|
};
|
|
190
195
|
|
|
191
196
|
// Define lazy getters for plugins and heavy core subsystems
|
|
192
197
|
Object.defineProperties(module.exports, {
|
|
193
198
|
...Object.getOwnPropertyDescriptors(plugins),
|
|
194
199
|
|
|
200
|
+
// Cascading Middleware & Layouts
|
|
201
|
+
cascading: { get: () => require('./src/routing/cascading'), enumerable: true, configurable: true },
|
|
202
|
+
|
|
195
203
|
// SSR Streaming
|
|
196
204
|
ssr: { get: () => require('./core/ssr'), enumerable: true, configurable: true },
|
|
197
205
|
renderStream: { get: () => require('./core/ssr').renderStream, enumerable: true, configurable: true },
|
|
198
206
|
createHtmlStream: { get: () => require('./core/ssr').createHtmlStream, enumerable: true, configurable: true },
|
|
199
207
|
|
|
208
|
+
// Server-Sent Events (SSE) Streaming
|
|
209
|
+
sse: { get: () => require('./core/sse'), enumerable: true, configurable: true },
|
|
210
|
+
createSseStream: { get: () => require('./core/sse').createSseStream, enumerable: true, configurable: true },
|
|
211
|
+
ServerSentEventsStream: { get: () => require('./core/sse').ServerSentEventsStream, enumerable: true, configurable: true },
|
|
212
|
+
|
|
200
213
|
/** Event bus / plugin shell / view resolver / flows (`kernel.createApp` is distinct from SSR `createApp`) */
|
|
201
214
|
kernel: { get: () => require('./core/kernel'), enumerable: true, configurable: true },
|
|
202
215
|
|
|
216
|
+
// Authentication & OAuth
|
|
217
|
+
auth: { get: () => require('./core/auth'), enumerable: true, configurable: true },
|
|
218
|
+
createAuth: { get: () => require('./core/auth').createAuth, enumerable: true, configurable: true },
|
|
219
|
+
quickAuth: { get: () => require('./core/auth').quickAuth, enumerable: true, configurable: true },
|
|
220
|
+
|
|
221
|
+
// 2FA / TOTP (RFC 6238, RFC 4226, RFC 4648)
|
|
222
|
+
totp: { get: () => require('./core/auth/totp'), enumerable: true, configurable: true },
|
|
223
|
+
base32Encode: { get: () => require('./core/auth/totp').base32Encode, enumerable: true, configurable: true },
|
|
224
|
+
base32Decode: { get: () => require('./core/auth/totp').base32Decode, enumerable: true, configurable: true },
|
|
225
|
+
generateTotpSecret: { get: () => require('./core/auth/totp').generateTotpSecret, enumerable: true, configurable: true },
|
|
226
|
+
generateHotp: { get: () => require('./core/auth/totp').generateHotp, enumerable: true, configurable: true },
|
|
227
|
+
verifyHotp: { get: () => require('./core/auth/totp').verifyHotp, enumerable: true, configurable: true },
|
|
228
|
+
generateTotp: { get: () => require('./core/auth/totp').generateTotp, enumerable: true, configurable: true },
|
|
229
|
+
verifyTotp: { get: () => require('./core/auth/totp').verifyTotp, enumerable: true, configurable: true },
|
|
230
|
+
generateTotpUri: { get: () => require('./core/auth/totp').generateTotpUri, enumerable: true, configurable: true },
|
|
231
|
+
generateTotpQrSvg: { get: () => require('./core/auth/totp').generateTotpQrSvg, enumerable: true, configurable: true },
|
|
232
|
+
generateTotpQrDataUri: { get: () => require('./core/auth/totp').generateTotpQrDataUri, enumerable: true, configurable: true },
|
|
233
|
+
generateRecoveryCodes: { get: () => require('./core/auth/totp').generateRecoveryCodes, enumerable: true, configurable: true },
|
|
234
|
+
hashRecoveryCode: { get: () => require('./core/auth/totp').hashRecoveryCode, enumerable: true, configurable: true },
|
|
235
|
+
verifyRecoveryCode: { get: () => require('./core/auth/totp').verifyRecoveryCode, enumerable: true, configurable: true },
|
|
236
|
+
createTotpSetup: { get: () => require('./core/auth/totp').createTotpSetup, enumerable: true, configurable: true },
|
|
237
|
+
createQrMatrix: { get: () => require('./core/auth/totp').createQrMatrix, enumerable: true, configurable: true },
|
|
238
|
+
generateQrSvg: { get: () => require('./core/auth/qrcode').generateQrSvg, enumerable: true, configurable: true },
|
|
239
|
+
generateQrDataUri: { get: () => require('./core/auth/qrcode').generateQrDataUri, enumerable: true, configurable: true },
|
|
240
|
+
createAuthMiddleware: { get: () => require('./core/auth').createAuthMiddleware, enumerable: true, configurable: true },
|
|
241
|
+
setupAuthMiddleware: { get: () => require('./core/auth').setupAuthMiddleware, enumerable: true, configurable: true },
|
|
242
|
+
|
|
203
243
|
// Validation
|
|
204
244
|
z: { get: () => require('./core/validation').z, enumerable: true, configurable: true },
|
|
205
245
|
|
|
@@ -207,6 +247,10 @@ Object.defineProperties(module.exports, {
|
|
|
207
247
|
queue: { get: () => require('./core/queue'), enumerable: true, configurable: true },
|
|
208
248
|
createQueueManager: { get: () => require('./core/queue').createQueueManager, enumerable: true, configurable: true },
|
|
209
249
|
QueueManager: { get: () => require('./core/queue').QueueManager, enumerable: true, configurable: true },
|
|
250
|
+
TaskScheduler: { get: () => require('./core/queue').TaskScheduler, enumerable: true, configurable: true },
|
|
251
|
+
CronExpression: { get: () => require('./core/queue').CronExpression, enumerable: true, configurable: true },
|
|
252
|
+
parseCron: { get: () => require('./core/queue').parseCron, enumerable: true, configurable: true },
|
|
253
|
+
isValidCron: { get: () => require('./core/queue').isValidCron, enumerable: true, configurable: true },
|
|
210
254
|
|
|
211
255
|
// Schema-driven CMS core (framework-agnostic)
|
|
212
256
|
content: { get: () => require('./core/content'), enumerable: true, configurable: true },
|
|
@@ -228,5 +272,9 @@ Object.defineProperties(module.exports, {
|
|
|
228
272
|
createDevProfilerMiddleware: { get: () => require('./src/dev').createDevProfilerMiddleware, enumerable: true, configurable: true },
|
|
229
273
|
generateTypes: { get: () => require('./src/dev/typegen').generateTypes, enumerable: true, configurable: true },
|
|
230
274
|
typegen: { get: () => require('./src/dev/typegen'), enumerable: true, configurable: true },
|
|
275
|
+
|
|
276
|
+
// Webhooks plugin
|
|
277
|
+
webhooks: { get: () => require('./plugins/webhooks'), enumerable: true, configurable: true },
|
|
278
|
+
webhooksPlugin: { get: () => require('./plugins/webhooks').webhooksPlugin, enumerable: true, configurable: true },
|
|
231
279
|
});
|
|
232
280
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.100",
|
|
4
4
|
"description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"nodemailer": "^10.0.10",
|
|
127
127
|
"nunjucks": "^3.2.4",
|
|
128
128
|
"sanitize-html": "^2.17.7",
|
|
129
|
-
"sharp": "^0.35.
|
|
129
|
+
"sharp": "^0.35.5",
|
|
130
130
|
"swup": "^4.8.3",
|
|
131
131
|
"yaml": "^2.8.3",
|
|
132
132
|
"zod": "^3.23.0",
|
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
"@types/node": "^20.14.0",
|
|
168
168
|
"@vitest/coverage-v8": "^4.1.11",
|
|
169
169
|
"better-sqlite3": "^11.10.0",
|
|
170
|
-
"chokidar": "^
|
|
170
|
+
"chokidar": "^4.0.3",
|
|
171
171
|
"docboot": "^0.3.0",
|
|
172
172
|
"dotenv": "^16.3.1",
|
|
173
173
|
"express-rate-limit": "^8.4.1",
|
|
@@ -181,9 +181,11 @@
|
|
|
181
181
|
"node": ">=20.0.0"
|
|
182
182
|
},
|
|
183
183
|
"overrides": {
|
|
184
|
-
"basic-ftp": "^
|
|
184
|
+
"basic-ftp": "^6.2.1",
|
|
185
|
+
"chokidar": "^4.0.3",
|
|
186
|
+
"katex": "^0.18.2",
|
|
185
187
|
"tar": "^7.5.11",
|
|
186
|
-
"undici": "^6.
|
|
188
|
+
"undici": "^6.28.1",
|
|
187
189
|
"uuid": "^11.1.1",
|
|
188
190
|
"js-yaml": "^5.4.2",
|
|
189
191
|
"qs": "^6.16.0"
|
|
@@ -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,10 +392,167 @@ 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) {
|
|
@@ -379,6 +592,52 @@ async function handleAdminFileUpload(file, onChange, meta, state) {
|
|
|
379
592
|
}
|
|
380
593
|
}
|
|
381
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();
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
382
641
|
// Get appropriate renderer for a column type
|
|
383
642
|
function getFieldRenderer(col, modelMeta) {
|
|
384
643
|
// Check for custom field first
|
|
@@ -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,
|
|
@@ -460,11 +730,30 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
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
|
+
|
|
463
745
|
// Auto-detect belongsTo relation for foreign key columns (ends with _id or has references)
|
|
464
746
|
if (!col.primary && !col.autoIncrement && ((col.name && col.name.endsWith('_id')) || col.references)) {
|
|
465
747
|
return FieldRenderers.belongsTo;
|
|
466
748
|
}
|
|
467
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
|
+
|
|
468
757
|
// Fallback to standard type renderers
|
|
469
758
|
const typeMap = {
|
|
470
759
|
string: 'string',
|
|
@@ -480,6 +769,9 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
480
769
|
enum: 'enum',
|
|
481
770
|
json: 'json',
|
|
482
771
|
array: 'array',
|
|
772
|
+
file: 'file',
|
|
773
|
+
files: 'files',
|
|
774
|
+
image: 'image',
|
|
483
775
|
uuid: 'string',
|
|
484
776
|
nanoid: 'string',
|
|
485
777
|
belongsTo: 'belongsTo',
|
|
@@ -134,6 +134,11 @@ async function executeRecordAction(action, record, modelName, modelMeta, onSucce
|
|
|
134
134
|
if (msg) {
|
|
135
135
|
notifySuccess(msg);
|
|
136
136
|
}
|
|
137
|
+
const redirectUrl = res?.result?.redirect || res?.redirect;
|
|
138
|
+
if (redirectUrl) {
|
|
139
|
+
m.route.set(redirectUrl);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
137
142
|
if (typeof onSuccess === 'function') {
|
|
138
143
|
await onSuccess(res);
|
|
139
144
|
} else if (typeof loadRecords === 'function' && state.pagination) {
|
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const { buildComponentsBody } = require(path.join(__dirname, 'load-parts.js'));
|
|
11
11
|
|
|
12
|
+
const vm = require('vm');
|
|
12
13
|
const body = buildComponentsBody();
|
|
13
14
|
const checks = [
|
|
14
15
|
['has api helper', () => body.includes('const api =')],
|
|
15
16
|
['has Mithril globals', () => body.includes('window.__ADMIN_COMPONENTS__')],
|
|
16
17
|
['has reasonable size', () => body.length > 50_000],
|
|
18
|
+
['valid JS syntax', () => { vm.compileFunction(body); return true; }],
|
|
17
19
|
];
|
|
18
20
|
|
|
19
21
|
let failed = false;
|