webspresso 0.0.66 → 0.0.68
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/README.md +74 -1
- package/bin/commands/doctor.js +23 -0
- package/bin/commands/new.js +106 -15
- package/core/orm/migrations/scaffold.js +6 -0
- package/core/orm/schema-helpers.js +19 -0
- package/core/orm/types.js +1 -1
- package/index.d.ts +36 -0
- package/index.js +19 -1
- package/package.json +11 -4
- package/plugins/admin-panel/api.js +5 -5
- package/plugins/admin-panel/components.js +184 -0
- package/plugins/admin-panel/core/registry.js +1 -0
- package/plugins/admin-panel/field-renderers/file-upload.js +135 -66
- package/plugins/admin-panel/field-renderers/index.js +1 -0
- package/plugins/admin-panel/index.js +8 -0
- package/plugins/index.js +3 -0
- package/plugins/upload/index.js +188 -0
- package/plugins/upload/local-file-provider.js +122 -0
- package/src/client-runtime/bootstrap-alpine-swup.js +34 -0
- package/src/client-runtime/bootstrap-swup.js +26 -0
- package/src/client-runtime/mount.js +65 -0
- package/src/client-runtime/resolve.js +40 -0
- package/src/file-router.js +16 -2
- package/src/server.js +11 -2
- package/templates/skills/webspresso-usage/SKILL.md +54 -21
- package/views/partials/webspresso-client-runtime.njk +15 -0
|
@@ -495,6 +495,7 @@ const FilterDrawer = {
|
|
|
495
495
|
if (col.primary || col.autoIncrement) return false;
|
|
496
496
|
if (col.auto === 'create' || col.auto === 'update') return false;
|
|
497
497
|
if (col.type === 'json') return false;
|
|
498
|
+
if (col.type === 'file') return false;
|
|
498
499
|
return true;
|
|
499
500
|
});
|
|
500
501
|
|
|
@@ -1049,6 +1050,155 @@ const RichTextField = {
|
|
|
1049
1050
|
}
|
|
1050
1051
|
};
|
|
1051
1052
|
|
|
1053
|
+
// File upload field (multipart POST to settings.uploadUrl; field name "file")
|
|
1054
|
+
const FileUploadField = {
|
|
1055
|
+
oncreate: (vnode) => {
|
|
1056
|
+
const col = vnode.attrs.col;
|
|
1057
|
+
const readonly = vnode.attrs.readonly;
|
|
1058
|
+
if (readonly) return;
|
|
1059
|
+
var cfg = window.__ADMIN_CONFIG__;
|
|
1060
|
+
var uploadUrl = (cfg && cfg.settings && cfg.settings.uploadUrl) ? String(cfg.settings.uploadUrl) : '';
|
|
1061
|
+
if (!uploadUrl) return;
|
|
1062
|
+
const dropZoneId = 'drop-zone-' + col.name;
|
|
1063
|
+
const dropZone = document.getElementById(dropZoneId);
|
|
1064
|
+
if (!dropZone) return;
|
|
1065
|
+
const meta = col.ui || {};
|
|
1066
|
+
const onChange = vnode.attrs.onChange;
|
|
1067
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function (eventName) {
|
|
1068
|
+
dropZone.addEventListener(eventName, function (e) {
|
|
1069
|
+
e.preventDefault();
|
|
1070
|
+
e.stopPropagation();
|
|
1071
|
+
});
|
|
1072
|
+
});
|
|
1073
|
+
['dragenter', 'dragover'].forEach(function (eventName) {
|
|
1074
|
+
dropZone.addEventListener(eventName, function () {
|
|
1075
|
+
dropZone.classList.add('border-blue-500', 'bg-blue-50', 'dark:bg-slate-800');
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
['dragleave', 'drop'].forEach(function (eventName) {
|
|
1079
|
+
dropZone.addEventListener(eventName, function () {
|
|
1080
|
+
dropZone.classList.remove('border-blue-500', 'bg-blue-50', 'dark:bg-slate-800');
|
|
1081
|
+
});
|
|
1082
|
+
});
|
|
1083
|
+
dropZone.addEventListener('drop', function (e) {
|
|
1084
|
+
var files = e.dataTransfer.files;
|
|
1085
|
+
if (files.length > 0) {
|
|
1086
|
+
handleAdminFileUpload(files[0], onChange, meta);
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
var fileInput = dropZone.querySelector('input[type=file]');
|
|
1090
|
+
if (fileInput) {
|
|
1091
|
+
fileInput.addEventListener('change', function (e) {
|
|
1092
|
+
if (e.target.files.length > 0) {
|
|
1093
|
+
handleAdminFileUpload(e.target.files[0], onChange, meta);
|
|
1094
|
+
}
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
view: (vnode) => {
|
|
1099
|
+
const col = vnode.attrs.col;
|
|
1100
|
+
const value = vnode.attrs.value || '';
|
|
1101
|
+
const onChange = vnode.attrs.onChange;
|
|
1102
|
+
const readonly = vnode.attrs.readonly || false;
|
|
1103
|
+
const meta = col.ui || {};
|
|
1104
|
+
const label = meta.label || formatColumnLabel(col.name);
|
|
1105
|
+
const hint = meta.hint || '';
|
|
1106
|
+
const required = !col.nullable && !readonly;
|
|
1107
|
+
var uploadUrl = '';
|
|
1108
|
+
try {
|
|
1109
|
+
var cfg2 = window.__ADMIN_CONFIG__;
|
|
1110
|
+
uploadUrl = (cfg2 && cfg2.settings && cfg2.settings.uploadUrl) ? String(cfg2.settings.uploadUrl) : '';
|
|
1111
|
+
} catch (e) { uploadUrl = ''; }
|
|
1112
|
+
var maxSize = meta.maxSize || meta.maxBytes || (10 * 1024 * 1024);
|
|
1113
|
+
var accept = meta.accept || '*/*';
|
|
1114
|
+
var dropZoneId = 'drop-zone-' + col.name;
|
|
1115
|
+
if (readonly) {
|
|
1116
|
+
return m('.mb-4', [
|
|
1117
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', label, required ? m('span.text-red-500', ' *') : null),
|
|
1118
|
+
value ? m('a.text-indigo-600.break-all', { href: value, target: '_blank', rel: 'noopener noreferrer' }, value) : m('span.text-gray-400', '—'),
|
|
1119
|
+
hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
|
|
1120
|
+
]);
|
|
1121
|
+
}
|
|
1122
|
+
if (!uploadUrl) {
|
|
1123
|
+
return m('.mb-4', [
|
|
1124
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', { for: col.name }, label, required ? m('span.text-red-500', ' *') : null),
|
|
1125
|
+
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.'),
|
|
1126
|
+
m('input.w-full.px-3.py-2.border.border-gray-300.dark:border-slate-600.rounded.bg-white.dark:bg-slate-800', {
|
|
1127
|
+
type: 'text',
|
|
1128
|
+
id: col.name,
|
|
1129
|
+
name: col.name,
|
|
1130
|
+
value: value,
|
|
1131
|
+
placeholder: 'https://… or /uploads/…',
|
|
1132
|
+
required: required,
|
|
1133
|
+
oninput: function (e) { if (onChange) onChange(e.target.value); },
|
|
1134
|
+
}),
|
|
1135
|
+
hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
|
|
1136
|
+
]);
|
|
1137
|
+
}
|
|
1138
|
+
return m('.mb-4', [
|
|
1139
|
+
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-1', label, required ? m('span.text-red-500', ' *') : null),
|
|
1140
|
+
m('div#' + dropZoneId + '.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded.p-8.text-center', { style: 'cursor: pointer;' }, [
|
|
1141
|
+
m('input[type=file].hidden', {
|
|
1142
|
+
id: 'file-input-' + col.name,
|
|
1143
|
+
accept: accept,
|
|
1144
|
+
onchange: function (e) {
|
|
1145
|
+
if (e.target.files.length > 0) {
|
|
1146
|
+
handleAdminFileUpload(e.target.files[0], onChange, meta);
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1149
|
+
}),
|
|
1150
|
+
m('div', [
|
|
1151
|
+
m('p.text-gray-600.dark:text-slate-400.mb-2', 'Drag and drop a file here, or'),
|
|
1152
|
+
m('label.text-blue-600.hover:text-blue-800.dark:text-blue-400.cursor-pointer', { for: 'file-input-' + col.name }, 'browse'),
|
|
1153
|
+
]),
|
|
1154
|
+
value ? m('.mt-4.text-left', [
|
|
1155
|
+
m('p.text-sm.text-gray-600.dark:text-slate-400.break-all', 'Current: ' + value),
|
|
1156
|
+
m('button.text-red-600.hover:text-red-800.text-sm.mt-2', {
|
|
1157
|
+
type: 'button',
|
|
1158
|
+
onclick: function () { if (onChange) onChange(''); },
|
|
1159
|
+
}, 'Remove'),
|
|
1160
|
+
]) : null,
|
|
1161
|
+
]),
|
|
1162
|
+
m('input[type=hidden]', { name: col.name, value: typeof value === 'string' ? value : '' }),
|
|
1163
|
+
m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', 'Max ' + Math.round(maxSize / 1024 / 1024) + ' MB (server enforces limits)'),
|
|
1164
|
+
hint ? m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1', hint) : null,
|
|
1165
|
+
]);
|
|
1166
|
+
},
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
async function handleAdminFileUpload(file, onChange, meta) {
|
|
1170
|
+
var uploadUrl = '';
|
|
1171
|
+
try {
|
|
1172
|
+
var cfg = window.__ADMIN_CONFIG__;
|
|
1173
|
+
uploadUrl = (cfg && cfg.settings && cfg.settings.uploadUrl) ? String(cfg.settings.uploadUrl) : '';
|
|
1174
|
+
} catch (e) {}
|
|
1175
|
+
if (!uploadUrl) {
|
|
1176
|
+
alert('Upload URL is not configured.');
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
var maxSize = meta.maxSize || meta.maxBytes || (10 * 1024 * 1024);
|
|
1180
|
+
if (file.size > maxSize) {
|
|
1181
|
+
alert('File too large (max ' + Math.round(maxSize / 1024 / 1024) + ' MB).');
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
var fd = new FormData();
|
|
1185
|
+
fd.append('file', file);
|
|
1186
|
+
try {
|
|
1187
|
+
var res = await fetch(uploadUrl, { method: 'POST', body: fd, credentials: 'include' });
|
|
1188
|
+
var data = {};
|
|
1189
|
+
try { data = await res.json(); } catch (e2) { data = {}; }
|
|
1190
|
+
if (!res.ok) {
|
|
1191
|
+
alert(data.message || data.error || ('Upload failed (' + res.status + ')'));
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
var url = data.url || data.publicUrl || '';
|
|
1195
|
+
if (onChange) onChange(url);
|
|
1196
|
+
m.redraw();
|
|
1197
|
+
} catch (err) {
|
|
1198
|
+
alert(err.message || 'Upload failed');
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1052
1202
|
// Get appropriate renderer for a column type
|
|
1053
1203
|
function getFieldRenderer(col, modelMeta) {
|
|
1054
1204
|
// Check for custom field first
|
|
@@ -1064,8 +1214,29 @@ function getFieldRenderer(col, modelMeta) {
|
|
|
1064
1214
|
});
|
|
1065
1215
|
};
|
|
1066
1216
|
}
|
|
1217
|
+
if (col.customField.type === 'file-upload') {
|
|
1218
|
+
return (col, value, onChange, readonly) => {
|
|
1219
|
+
return m(FileUploadField, {
|
|
1220
|
+
col,
|
|
1221
|
+
value: value || '',
|
|
1222
|
+
onChange,
|
|
1223
|
+
readonly: readonly || false,
|
|
1224
|
+
});
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1067
1227
|
// Add other custom field types here if needed
|
|
1068
1228
|
}
|
|
1229
|
+
|
|
1230
|
+
if (col.type === 'file') {
|
|
1231
|
+
return (col, value, onChange, readonly) => {
|
|
1232
|
+
return m(FileUploadField, {
|
|
1233
|
+
col,
|
|
1234
|
+
value: value || '',
|
|
1235
|
+
onChange,
|
|
1236
|
+
readonly: readonly || false,
|
|
1237
|
+
});
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1069
1240
|
|
|
1070
1241
|
// Fallback to standard type renderers
|
|
1071
1242
|
const typeMap = {
|
|
@@ -1316,6 +1487,19 @@ function formatCellValue(value, col) {
|
|
|
1316
1487
|
case 'text':
|
|
1317
1488
|
const textStr = String(value);
|
|
1318
1489
|
return textStr.length > 50 ? textStr.substring(0, 50) + '...' : textStr;
|
|
1490
|
+
|
|
1491
|
+
case 'file': {
|
|
1492
|
+
const s = String(value);
|
|
1493
|
+
const short = s.length > 72 ? s.substring(0, 72) + '…' : s;
|
|
1494
|
+
if (/^https?:\/\//.test(s) || s.startsWith('/')) {
|
|
1495
|
+
return m('a.text-indigo-600.dark:text-indigo-400.hover:underline.break-all', {
|
|
1496
|
+
href: s,
|
|
1497
|
+
target: '_blank',
|
|
1498
|
+
rel: 'noopener noreferrer',
|
|
1499
|
+
}, short);
|
|
1500
|
+
}
|
|
1501
|
+
return short || m('span.text-gray-400', '—');
|
|
1502
|
+
}
|
|
1319
1503
|
|
|
1320
1504
|
default:
|
|
1321
1505
|
const str = String(value);
|
|
@@ -1,124 +1,193 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* File Upload Field Renderer
|
|
3
|
-
*
|
|
3
|
+
* POST multipart field "file" to window.__ADMIN_CONFIG__.settings.uploadUrl
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
function getUploadUrlFromAdminConfig() {
|
|
7
|
+
try {
|
|
8
|
+
const cfg = typeof window !== 'undefined' ? window.__ADMIN_CONFIG__ : null;
|
|
9
|
+
const u = cfg && cfg.settings && cfg.settings.uploadUrl;
|
|
10
|
+
return u ? String(u) : '';
|
|
11
|
+
} catch {
|
|
12
|
+
return '';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
module.exports = {
|
|
7
17
|
FileUploadField: {
|
|
8
18
|
oncreate: (vnode) => {
|
|
9
|
-
const { name,
|
|
19
|
+
const { name, meta = {} } = vnode.attrs;
|
|
20
|
+
if (!getUploadUrlFromAdminConfig()) return;
|
|
21
|
+
|
|
10
22
|
const dropZoneId = 'drop-zone-' + name;
|
|
11
|
-
|
|
12
23
|
const dropZone = document.getElementById(dropZoneId);
|
|
13
24
|
if (!dropZone) return;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
|
|
25
|
+
|
|
26
|
+
['dragenter', 'dragover', 'dragleave', 'drop'].forEach((eventName) => {
|
|
17
27
|
dropZone.addEventListener(eventName, (e) => {
|
|
18
28
|
e.preventDefault();
|
|
19
29
|
e.stopPropagation();
|
|
20
30
|
});
|
|
21
31
|
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
['dragenter', 'dragover'].forEach(eventName => {
|
|
32
|
+
|
|
33
|
+
['dragenter', 'dragover'].forEach((eventName) => {
|
|
25
34
|
dropZone.addEventListener(eventName, () => {
|
|
26
|
-
dropZone.classList.add('border-blue-500', 'bg-blue-50');
|
|
35
|
+
dropZone.classList.add('border-blue-500', 'bg-blue-50', 'dark:bg-slate-800');
|
|
27
36
|
});
|
|
28
37
|
});
|
|
29
|
-
|
|
30
|
-
['dragleave', 'drop'].forEach(eventName => {
|
|
38
|
+
|
|
39
|
+
['dragleave', 'drop'].forEach((eventName) => {
|
|
31
40
|
dropZone.addEventListener(eventName, () => {
|
|
32
|
-
dropZone.classList.remove('border-blue-500', 'bg-blue-50');
|
|
41
|
+
dropZone.classList.remove('border-blue-500', 'bg-blue-50', 'dark:bg-slate-800');
|
|
33
42
|
});
|
|
34
43
|
});
|
|
35
|
-
|
|
36
|
-
// Handle dropped files
|
|
44
|
+
|
|
37
45
|
dropZone.addEventListener('drop', (e) => {
|
|
38
46
|
const files = e.dataTransfer.files;
|
|
39
47
|
if (files.length > 0) {
|
|
40
|
-
|
|
48
|
+
handleFileUpload(files[0], vnode, meta);
|
|
41
49
|
}
|
|
42
50
|
});
|
|
43
|
-
|
|
44
|
-
// Handle file input change
|
|
51
|
+
|
|
45
52
|
const fileInput = dropZone.querySelector('input[type=file]');
|
|
46
53
|
if (fileInput) {
|
|
47
54
|
fileInput.addEventListener('change', (e) => {
|
|
48
55
|
if (e.target.files.length > 0) {
|
|
49
|
-
|
|
56
|
+
handleFileUpload(e.target.files[0], vnode, meta);
|
|
50
57
|
}
|
|
51
58
|
});
|
|
52
59
|
}
|
|
53
60
|
},
|
|
54
|
-
|
|
61
|
+
|
|
55
62
|
view: (vnode) => {
|
|
56
63
|
const { name, value = '', meta = {}, required = false } = vnode.attrs;
|
|
57
64
|
const dropZoneId = 'drop-zone-' + name;
|
|
58
|
-
const maxSize = meta.maxSize ||
|
|
65
|
+
const maxSize = meta.maxSize || meta.maxBytes || 10 * 1024 * 1024;
|
|
59
66
|
const accept = meta.accept || '*/*';
|
|
60
|
-
|
|
67
|
+
const uploadUrl = getUploadUrlFromAdminConfig();
|
|
68
|
+
|
|
69
|
+
if (!uploadUrl) {
|
|
70
|
+
return m('.mb-4', [
|
|
71
|
+
m(
|
|
72
|
+
'label.block.text-sm.font-medium.mb-2',
|
|
73
|
+
meta.label || name,
|
|
74
|
+
required ? m('span.text-red-500', ' *') : null
|
|
75
|
+
),
|
|
76
|
+
m('p.text-xs.text-amber-700.dark:text-amber-400.mb-2', 'Upload URL is not configured.'),
|
|
77
|
+
m('input.w-full.px-3.py-2.border.rounded', {
|
|
78
|
+
type: 'text',
|
|
79
|
+
name,
|
|
80
|
+
value: typeof value === 'string' ? value : '',
|
|
81
|
+
placeholder: 'https://… or /uploads/…',
|
|
82
|
+
required,
|
|
83
|
+
oninput: (e) => {
|
|
84
|
+
if (vnode.attrs.onchange) vnode.attrs.onchange(e.target.value);
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
|
|
61
90
|
return m('.mb-4', [
|
|
62
|
-
m(
|
|
91
|
+
m(
|
|
92
|
+
'label.block.text-sm.font-medium.mb-2',
|
|
63
93
|
meta.label || name,
|
|
64
94
|
required ? m('span.text-red-500', ' *') : null
|
|
65
95
|
),
|
|
66
|
-
m(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
m('p.text-gray-600 dark:text-slate-400.mb-2', 'Drag and drop a file here, or'),
|
|
81
|
-
m('label.text-blue-600.hover:text-blue-800.cursor-pointer', {
|
|
82
|
-
for: 'file-input-' + name
|
|
83
|
-
}, 'browse'),
|
|
84
|
-
]),
|
|
85
|
-
value ? m('.mt-4', [
|
|
86
|
-
m('p.text-sm.text-gray-600', 'Current file: ' + (typeof value === 'string' ? value : value.name || 'uploaded')),
|
|
87
|
-
m('button.text-red-600.hover:text-red-800.text-sm.mt-2', {
|
|
88
|
-
onclick: () => {
|
|
89
|
-
if (vnode.attrs.onchange) {
|
|
90
|
-
vnode.attrs.onchange('');
|
|
96
|
+
m(
|
|
97
|
+
'div.border-2.border-dashed.border-gray-300.dark:border-slate-600.rounded.p-8.text-center',
|
|
98
|
+
{
|
|
99
|
+
id: dropZoneId,
|
|
100
|
+
style: 'cursor: pointer;',
|
|
101
|
+
},
|
|
102
|
+
[
|
|
103
|
+
m('input[type=file]', {
|
|
104
|
+
class: 'hidden',
|
|
105
|
+
id: 'file-input-' + name,
|
|
106
|
+
accept,
|
|
107
|
+
onchange: (e) => {
|
|
108
|
+
if (e.target.files.length > 0) {
|
|
109
|
+
handleFileUpload(e.target.files[0], vnode, meta);
|
|
91
110
|
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
|
|
111
|
+
},
|
|
112
|
+
}),
|
|
113
|
+
m('div', [
|
|
114
|
+
m('p.text-gray-600.dark:text-slate-400.mb-2', 'Drag and drop a file here, or'),
|
|
115
|
+
m(
|
|
116
|
+
'label.text-blue-600.hover:text-blue-800.cursor-pointer',
|
|
117
|
+
{ for: 'file-input-' + name },
|
|
118
|
+
'browse'
|
|
119
|
+
),
|
|
120
|
+
]),
|
|
121
|
+
value
|
|
122
|
+
? m('.mt-4', [
|
|
123
|
+
m(
|
|
124
|
+
'p.text-sm.text-gray-600.break-all',
|
|
125
|
+
'Current: ' + (typeof value === 'string' ? value : value.name || 'uploaded')
|
|
126
|
+
),
|
|
127
|
+
m(
|
|
128
|
+
'button.text-red-600.hover:text-red-800.text-sm.mt-2',
|
|
129
|
+
{
|
|
130
|
+
type: 'button',
|
|
131
|
+
onclick: () => {
|
|
132
|
+
if (vnode.attrs.onchange) vnode.attrs.onchange('');
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
'Remove'
|
|
136
|
+
),
|
|
137
|
+
])
|
|
138
|
+
: null,
|
|
139
|
+
]
|
|
140
|
+
),
|
|
96
141
|
m('input[type=hidden]', {
|
|
97
142
|
name,
|
|
98
143
|
value: typeof value === 'string' ? value : '',
|
|
99
144
|
}),
|
|
145
|
+
m(
|
|
146
|
+
'p.text-xs.text-gray-500.mt-1',
|
|
147
|
+
'Max ' + Math.round(maxSize / 1024 / 1024) + ' MB (server enforces limits)'
|
|
148
|
+
),
|
|
100
149
|
]);
|
|
101
|
-
}
|
|
150
|
+
},
|
|
102
151
|
},
|
|
103
152
|
};
|
|
104
153
|
|
|
105
154
|
/**
|
|
106
|
-
*
|
|
107
|
-
* @param {
|
|
108
|
-
* @param {Object}
|
|
109
|
-
* @param {Object} meta - Field metadata
|
|
155
|
+
* @param {File} file
|
|
156
|
+
* @param {import('mithril').Vnode} vnode
|
|
157
|
+
* @param {Object} meta
|
|
110
158
|
*/
|
|
111
|
-
function
|
|
112
|
-
const maxSize = meta.maxSize ||
|
|
113
|
-
|
|
159
|
+
async function handleFileUpload(file, vnode, meta) {
|
|
160
|
+
const maxSize = meta.maxSize || meta.maxBytes || 10 * 1024 * 1024;
|
|
114
161
|
if (file.size > maxSize) {
|
|
115
|
-
alert(`File
|
|
162
|
+
alert(`File too large (max ${Math.round(maxSize / 1024 / 1024)} MB).`);
|
|
116
163
|
return;
|
|
117
164
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
165
|
+
|
|
166
|
+
const uploadUrl = getUploadUrlFromAdminConfig();
|
|
167
|
+
if (!uploadUrl) {
|
|
168
|
+
alert('Upload URL is not configured.');
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const fd = new FormData();
|
|
173
|
+
fd.append('file', file);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const res = await fetch(uploadUrl, { method: 'POST', body: fd, credentials: 'include' });
|
|
177
|
+
let data = {};
|
|
178
|
+
try {
|
|
179
|
+
data = await res.json();
|
|
180
|
+
} catch {
|
|
181
|
+
data = {};
|
|
182
|
+
}
|
|
183
|
+
if (!res.ok) {
|
|
184
|
+
alert(data.message || data.error || `Upload failed (${res.status})`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const url = data.url || data.publicUrl || '';
|
|
188
|
+
if (vnode.attrs.onchange) vnode.attrs.onchange(url);
|
|
189
|
+
if (typeof m !== 'undefined' && m.redraw) m.redraw();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
alert(err.message || 'Upload failed');
|
|
123
192
|
}
|
|
124
193
|
}
|
|
@@ -78,6 +78,7 @@ function initializeDefaultRenderers() {
|
|
|
78
78
|
// Custom field types
|
|
79
79
|
registerFieldRenderer('rich-text', richTextRenderer.RichTextField);
|
|
80
80
|
registerFieldRenderer('file-upload', fileUploadRenderer.FileUploadField);
|
|
81
|
+
registerFieldRenderer('file', fileUploadRenderer.FileUploadField);
|
|
81
82
|
|
|
82
83
|
// Relation types
|
|
83
84
|
registerFieldRenderer('belongsTo', relationRenderers.BelongsToField);
|
|
@@ -30,6 +30,7 @@ const { registerModule } = require('./core/admin-module');
|
|
|
30
30
|
* @param {string} [options.userManagement.model='User'] - User model name
|
|
31
31
|
* @param {Object} [options.userManagement.fields] - Field mappings
|
|
32
32
|
* @param {Function} [options.configure] - Configuration callback (registry) => void
|
|
33
|
+
* @param {string} [options.uploadUrl] - POST URL for file uploads (overrides app.get('webspresso.uploadPath') from uploadPlugin)
|
|
33
34
|
* @returns {Object} Plugin definition
|
|
34
35
|
*/
|
|
35
36
|
function adminPanelPlugin(options = {}) {
|
|
@@ -41,6 +42,7 @@ function adminPanelPlugin(options = {}) {
|
|
|
41
42
|
auth,
|
|
42
43
|
userManagement: userMgmtConfig,
|
|
43
44
|
configure,
|
|
45
|
+
uploadUrl: uploadUrlOption,
|
|
44
46
|
} = options;
|
|
45
47
|
|
|
46
48
|
// Validate required options
|
|
@@ -128,6 +130,12 @@ function adminPanelPlugin(options = {}) {
|
|
|
128
130
|
this.api._ctx = ctx;
|
|
129
131
|
const { app } = ctx;
|
|
130
132
|
|
|
133
|
+
const uploadUrlResolved =
|
|
134
|
+
uploadUrlOption || app.get('webspresso.uploadPath') || null;
|
|
135
|
+
if (uploadUrlResolved) {
|
|
136
|
+
registry.configure({ uploadUrl: uploadUrlResolved });
|
|
137
|
+
}
|
|
138
|
+
|
|
131
139
|
// Check if admin_users table exists (migration run)
|
|
132
140
|
db.knex.schema.hasTable('admin_users').then((hasAdminTable) => {
|
|
133
141
|
if (!hasAdminTable) {
|
package/plugins/index.js
CHANGED
|
@@ -16,6 +16,7 @@ const swaggerPlugin = require('./swagger');
|
|
|
16
16
|
const healthCheckPlugin = require('./health-check');
|
|
17
17
|
const restResourcePlugin = require('./rest-resources');
|
|
18
18
|
const ormCacheAdminPlugin = require('./orm-cache-admin');
|
|
19
|
+
const { uploadPlugin, createLocalFileProvider } = require('./upload');
|
|
19
20
|
|
|
20
21
|
module.exports = {
|
|
21
22
|
sitemapPlugin,
|
|
@@ -31,5 +32,7 @@ module.exports = {
|
|
|
31
32
|
healthCheckPlugin,
|
|
32
33
|
restResourcePlugin,
|
|
33
34
|
ormCacheAdminPlugin,
|
|
35
|
+
uploadPlugin,
|
|
36
|
+
createLocalFileProvider,
|
|
34
37
|
};
|
|
35
38
|
|