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
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
// Login Form Component
|
|
2
2
|
const LoginForm = {
|
|
3
|
+
oninit: () => {
|
|
4
|
+
state.loading = false;
|
|
5
|
+
state.error = null;
|
|
6
|
+
state.notice = null;
|
|
7
|
+
var reason = null;
|
|
8
|
+
try {
|
|
9
|
+
if (typeof m !== 'undefined' && m.route && typeof m.route.param === 'function') {
|
|
10
|
+
reason = m.route.param('reason');
|
|
11
|
+
}
|
|
12
|
+
if (!reason && typeof window !== 'undefined' && window.location && window.location.search) {
|
|
13
|
+
var match = window.location.search.match(/[?&]reason=([^&]+)/);
|
|
14
|
+
if (match && match[1]) reason = decodeURIComponent(match[1]);
|
|
15
|
+
}
|
|
16
|
+
} catch (e) {}
|
|
17
|
+
if (reason === 'expired') {
|
|
18
|
+
state.notice = 'Your session has expired. Please sign in again.';
|
|
19
|
+
}
|
|
20
|
+
},
|
|
3
21
|
view: () => m('.min-h-screen.flex.items-center.justify-center.p-4.sm:p-6.bg-gradient-to-br.from-blue-600.via-indigo-600.to-purple-700', [
|
|
22
|
+
typeof ToastContainer !== 'undefined' ? m(ToastContainer) : null,
|
|
23
|
+
typeof ConfirmModalContainer !== 'undefined' ? m(ConfirmModalContainer) : null,
|
|
4
24
|
m('.w-full.max-w-md', [
|
|
5
|
-
m('.bg-white
|
|
25
|
+
m('.bg-white.dark:bg-slate-800.rounded-2xl.shadow-2xl.p-6.sm:p-8', [
|
|
6
26
|
m('div.text-center.mb-6', [
|
|
7
27
|
m('h1.text-2xl.sm:text-3xl.font-bold.text-gray-900.dark:text-slate-50', 'Admin Login'),
|
|
8
28
|
m('p.text-gray-500.dark:text-slate-400.text-sm.mt-1', 'Sign in to your account'),
|
|
@@ -12,22 +32,41 @@ const LoginForm = {
|
|
|
12
32
|
e.preventDefault();
|
|
13
33
|
state.loading = true;
|
|
14
34
|
state.error = null;
|
|
35
|
+
m.redraw();
|
|
15
36
|
try {
|
|
16
37
|
const data = new FormData(e.target);
|
|
38
|
+
const email = (data.get('email') || '').trim();
|
|
39
|
+
const password = data.get('password') || '';
|
|
40
|
+
if (!email || !password) {
|
|
41
|
+
state.error = 'Please enter both email and password';
|
|
42
|
+
state.loading = false;
|
|
43
|
+
m.redraw();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
17
46
|
const result = await api.post('/auth/login', {
|
|
18
|
-
email
|
|
19
|
-
password
|
|
47
|
+
email,
|
|
48
|
+
password,
|
|
20
49
|
});
|
|
21
50
|
state.user = result.user;
|
|
51
|
+
state.error = null;
|
|
52
|
+
state.notice = null;
|
|
22
53
|
m.route.set(consumeIntendedRoute('/'));
|
|
23
54
|
} catch (err) {
|
|
24
|
-
state.error = err.message;
|
|
55
|
+
state.error = err.message || 'Invalid email or password';
|
|
25
56
|
} finally {
|
|
26
57
|
state.loading = false;
|
|
58
|
+
m.redraw();
|
|
27
59
|
}
|
|
28
60
|
}
|
|
29
61
|
}, [
|
|
30
|
-
state.
|
|
62
|
+
state.notice ? m('.bg-amber-50.border.border-amber-200.text-amber-800.dark:bg-amber-950/50.dark:border-amber-800/80.dark:text-amber-200.px-4.py-3.rounded-lg.mb-4.text-sm.flex.items-center.gap-2', [
|
|
63
|
+
m('span.text-base.select-none', 'ℹ️'),
|
|
64
|
+
m('span.flex-1', state.notice),
|
|
65
|
+
]) : null,
|
|
66
|
+
state.error ? m('.bg-red-50.border.border-red-200.text-red-700.dark:bg-red-950/50.dark:border-red-800/80.dark:text-red-200.px-4.py-3.rounded-lg.mb-4.text-sm.flex.items-center.gap-2', [
|
|
67
|
+
m('span.text-base.select-none', '⚠️'),
|
|
68
|
+
m('span.flex-1', state.error),
|
|
69
|
+
]) : null,
|
|
31
70
|
m('.mb-4', [
|
|
32
71
|
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-2', { for: 'email' }, 'Email'),
|
|
33
72
|
m('input#email.w-full.px-3.py-2.5.bg-white.text-gray-900.border.border-gray-300.rounded-lg.placeholder-gray-400.focus:ring-2.focus:ring-blue-500.focus:border-blue-500.transition-colors.dark:bg-slate-900/80.dark:border-slate-500.dark:text-slate-100.dark:placeholder-slate-500', {
|
|
@@ -35,6 +74,12 @@ const LoginForm = {
|
|
|
35
74
|
name: 'email',
|
|
36
75
|
required: true,
|
|
37
76
|
placeholder: 'admin@example.com',
|
|
77
|
+
oninput: () => {
|
|
78
|
+
if (state.error) {
|
|
79
|
+
state.error = null;
|
|
80
|
+
m.redraw();
|
|
81
|
+
}
|
|
82
|
+
},
|
|
38
83
|
}),
|
|
39
84
|
]),
|
|
40
85
|
m('.mb-6', [
|
|
@@ -43,6 +88,12 @@ const LoginForm = {
|
|
|
43
88
|
type: 'password',
|
|
44
89
|
name: 'password',
|
|
45
90
|
required: true,
|
|
91
|
+
oninput: () => {
|
|
92
|
+
if (state.error) {
|
|
93
|
+
state.error = null;
|
|
94
|
+
m.redraw();
|
|
95
|
+
}
|
|
96
|
+
},
|
|
46
97
|
}),
|
|
47
98
|
]),
|
|
48
99
|
m('button.w-full.bg-blue-600.text-white.py-2.5.px-4.rounded-lg.font-medium.hover:bg-blue-700.focus:ring-2.focus:ring-blue-500.focus:ring-offset-2.dark:focus:ring-offset-slate-800.disabled:opacity-50.transition-colors', {
|
|
@@ -57,42 +108,70 @@ const LoginForm = {
|
|
|
57
108
|
|
|
58
109
|
// Setup Form Component
|
|
59
110
|
const SetupForm = {
|
|
111
|
+
oninit: () => {
|
|
112
|
+
state.loading = false;
|
|
113
|
+
state.error = null;
|
|
114
|
+
state.notice = null;
|
|
115
|
+
},
|
|
60
116
|
view: () => m('.min-h-screen.flex.items-center.justify-center.p-4.sm:p-6.bg-gradient-to-br.from-blue-600.via-indigo-600.to-purple-700', [
|
|
117
|
+
typeof ToastContainer !== 'undefined' ? m(ToastContainer) : null,
|
|
118
|
+
typeof ConfirmModalContainer !== 'undefined' ? m(ConfirmModalContainer) : null,
|
|
61
119
|
m('.w-full.max-w-md', [
|
|
62
|
-
m('.bg-white
|
|
120
|
+
m('.bg-white.dark:bg-slate-800.rounded-2xl.shadow-2xl.p-6.sm:p-8', [
|
|
63
121
|
m('div.text-center.mb-6', [
|
|
64
122
|
m('h1.text-2xl.sm:text-3xl.font-bold.text-gray-900.dark:text-slate-50', 'Setup Admin Account'),
|
|
65
123
|
m('p.text-gray-500.dark:text-slate-400.text-sm.mt-1', 'Create the first admin user account.'),
|
|
66
124
|
]),
|
|
67
125
|
m('form', {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
126
|
+
onsubmit: async (e) => {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
state.loading = true;
|
|
129
|
+
state.error = null;
|
|
130
|
+
m.redraw();
|
|
131
|
+
try {
|
|
132
|
+
const data = new FormData(e.target);
|
|
133
|
+
const name = (data.get('name') || '').trim();
|
|
134
|
+
const email = (data.get('email') || '').trim();
|
|
135
|
+
const password = data.get('password') || '';
|
|
136
|
+
if (!name || !email || !password) {
|
|
137
|
+
state.error = 'Please fill in all fields';
|
|
138
|
+
state.loading = false;
|
|
139
|
+
m.redraw();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const result = await api.post('/auth/setup', {
|
|
143
|
+
email,
|
|
144
|
+
password,
|
|
145
|
+
name,
|
|
146
|
+
});
|
|
147
|
+
state.user = result.user;
|
|
148
|
+
state.needsSetup = false;
|
|
149
|
+
state.error = null;
|
|
150
|
+
m.route.set(consumeIntendedRoute('/'));
|
|
151
|
+
} catch (err) {
|
|
152
|
+
state.error = err.message || 'Failed to setup admin account';
|
|
153
|
+
} finally {
|
|
154
|
+
state.loading = false;
|
|
155
|
+
m.redraw();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}, [
|
|
159
|
+
state.error ? m('.bg-red-50.border.border-red-200.text-red-700.dark:bg-red-950/50.dark:border-red-800/80.dark:text-red-200.px-4.py-3.rounded-lg.mb-4.text-sm.flex.items-center.gap-2', [
|
|
160
|
+
m('span.text-base.select-none', '⚠️'),
|
|
161
|
+
m('span.flex-1', state.error),
|
|
162
|
+
]) : null,
|
|
90
163
|
m('.mb-4', [
|
|
91
164
|
m('label.block.text-sm.font-medium.text-gray-700.dark:text-slate-300.mb-2', { for: 'name' }, 'Name'),
|
|
92
165
|
m('input#name.w-full.px-3.py-2.5.bg-white.text-gray-900.border.border-gray-300.rounded-lg.focus:ring-2.focus:ring-blue-500.focus:border-blue-500.transition-colors.dark:bg-slate-900/80.dark:border-slate-500.dark:text-slate-100', {
|
|
93
166
|
type: 'text',
|
|
94
167
|
name: 'name',
|
|
95
168
|
required: true,
|
|
169
|
+
oninput: () => {
|
|
170
|
+
if (state.error) {
|
|
171
|
+
state.error = null;
|
|
172
|
+
m.redraw();
|
|
173
|
+
}
|
|
174
|
+
},
|
|
96
175
|
}),
|
|
97
176
|
]),
|
|
98
177
|
m('.mb-4', [
|
|
@@ -102,6 +181,12 @@ const SetupForm = {
|
|
|
102
181
|
name: 'email',
|
|
103
182
|
required: true,
|
|
104
183
|
placeholder: 'admin@example.com',
|
|
184
|
+
oninput: () => {
|
|
185
|
+
if (state.error) {
|
|
186
|
+
state.error = null;
|
|
187
|
+
m.redraw();
|
|
188
|
+
}
|
|
189
|
+
},
|
|
105
190
|
}),
|
|
106
191
|
]),
|
|
107
192
|
m('.mb-6', [
|
|
@@ -110,6 +195,12 @@ const SetupForm = {
|
|
|
110
195
|
type: 'password',
|
|
111
196
|
name: 'password',
|
|
112
197
|
required: true,
|
|
198
|
+
oninput: () => {
|
|
199
|
+
if (state.error) {
|
|
200
|
+
state.error = null;
|
|
201
|
+
m.redraw();
|
|
202
|
+
}
|
|
203
|
+
},
|
|
113
204
|
}),
|
|
114
205
|
]),
|
|
115
206
|
m('button.w-full.bg-blue-600.text-white.py-2.5.px-4.rounded-lg.font-medium.hover:bg-blue-700.focus:ring-2.focus:ring-blue-500.focus:ring-offset-2.dark:focus:ring-offset-slate-800.disabled:opacity-50.transition-colors', {
|
|
@@ -84,7 +84,27 @@ function formatCellValue(value, col, record = null) {
|
|
|
84
84
|
return String(value);
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
|
|
87
|
+
|
|
88
|
+
// Badge formatting for status / enum / mapped values
|
|
89
|
+
const badgeMap = (col && col.badge) || (col && col.customField && col.customField.badge);
|
|
90
|
+
if (badgeMap && typeof badgeMap === 'object') {
|
|
91
|
+
const valStr = String(value);
|
|
92
|
+
const color = badgeMap[valStr] || badgeMap['*'] || 'gray';
|
|
93
|
+
const colorClasses = {
|
|
94
|
+
green: 'bg-green-100 text-green-800 dark:bg-green-950/60 dark:text-green-300 border border-green-200 dark:border-green-800',
|
|
95
|
+
red: 'bg-red-100 text-red-800 dark:bg-red-950/60 dark:text-red-300 border border-red-200 dark:border-red-800',
|
|
96
|
+
blue: 'bg-blue-100 text-blue-800 dark:bg-blue-950/60 dark:text-blue-300 border border-blue-200 dark:border-blue-800',
|
|
97
|
+
amber: 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 border border-amber-200 dark:border-amber-800',
|
|
98
|
+
yellow: 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300 border border-amber-200 dark:border-amber-800',
|
|
99
|
+
purple: 'bg-purple-100 text-purple-800 dark:bg-purple-950/60 dark:text-purple-300 border border-purple-200 dark:border-purple-800',
|
|
100
|
+
violet: 'bg-purple-100 text-purple-800 dark:bg-purple-950/60 dark:text-purple-300 border border-purple-200 dark:border-purple-800',
|
|
101
|
+
indigo: 'bg-indigo-100 text-indigo-800 dark:bg-indigo-950/60 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-800',
|
|
102
|
+
gray: 'bg-gray-100 text-gray-800 dark:bg-slate-800 dark:text-slate-300 border border-gray-200 dark:border-slate-700',
|
|
103
|
+
}[color] || 'bg-gray-100 text-gray-800 dark:bg-slate-800 dark:text-slate-300 border border-gray-200 dark:border-slate-700';
|
|
104
|
+
|
|
105
|
+
return m('span.inline-flex.items-center.px-2.5.py-0.5.rounded-full.text-xs.font-medium.' + colorClasses, valStr);
|
|
106
|
+
}
|
|
107
|
+
|
|
88
108
|
switch (col?.type) {
|
|
89
109
|
case 'boolean':
|
|
90
110
|
return value
|
|
@@ -298,7 +318,7 @@ const BulkFieldUpdateDropdown = {
|
|
|
298
318
|
vnode.state._bulkFieldName = null;
|
|
299
319
|
if (onComplete) onComplete();
|
|
300
320
|
} catch (err) {
|
|
301
|
-
|
|
321
|
+
notifyError('Error: ' + err.message);
|
|
302
322
|
} finally {
|
|
303
323
|
state.bulkActionInProgress = false;
|
|
304
324
|
m.redraw();
|
|
@@ -319,9 +339,9 @@ const BulkFieldUpdateDropdown = {
|
|
|
319
339
|
const trimmed = vnode.state.bulkTemporalInput && String(vnode.state.bulkTemporalInput).trim();
|
|
320
340
|
if (!trimmed) {
|
|
321
341
|
if (state.selectedBulkField.nullable) {
|
|
322
|
-
|
|
342
|
+
notifyWarning('Enter a date/time or use Clear (set null).');
|
|
323
343
|
} else {
|
|
324
|
-
|
|
344
|
+
notifyWarning('Please enter a value.');
|
|
325
345
|
}
|
|
326
346
|
return;
|
|
327
347
|
}
|
|
@@ -342,7 +362,7 @@ const BulkFieldUpdateDropdown = {
|
|
|
342
362
|
vnode.state._bulkFieldName = null;
|
|
343
363
|
if (onComplete) onComplete();
|
|
344
364
|
} catch (err) {
|
|
345
|
-
|
|
365
|
+
notifyError('Error: ' + err.message);
|
|
346
366
|
} finally {
|
|
347
367
|
state.bulkActionInProgress = false;
|
|
348
368
|
m.redraw();
|
|
@@ -363,7 +383,7 @@ const BulkFieldUpdateDropdown = {
|
|
|
363
383
|
state.selectedBulkField = null;
|
|
364
384
|
if (onComplete) onComplete();
|
|
365
385
|
} catch (err) {
|
|
366
|
-
|
|
386
|
+
notifyError('Error: ' + err.message);
|
|
367
387
|
} finally {
|
|
368
388
|
state.bulkActionInProgress = false;
|
|
369
389
|
m.redraw();
|
|
@@ -414,13 +434,27 @@ const BulkFieldUpdateDropdown = {
|
|
|
414
434
|
},
|
|
415
435
|
};
|
|
416
436
|
|
|
417
|
-
// Get columns to display in table (
|
|
418
|
-
function getDisplayColumns(columns) {
|
|
437
|
+
// Get columns to display in table (respects listColumns if configured, else limits to reasonable number)
|
|
438
|
+
function getDisplayColumns(columns, modelMeta = null) {
|
|
419
439
|
if (!columns || columns.length === 0) return [];
|
|
420
440
|
|
|
421
441
|
// Filter out hidden columns (password_hash, api_token, etc.)
|
|
422
442
|
const visible = [...columns].filter((col) => !col.hidden);
|
|
423
443
|
|
|
444
|
+
// If model defines listColumns, respect the explicit column list and ordering
|
|
445
|
+
if (modelMeta?.listColumns && Array.isArray(modelMeta.listColumns) && modelMeta.listColumns.length > 0) {
|
|
446
|
+
const colMap = new Map(visible.map((c) => [c.name, c]));
|
|
447
|
+
const customList = [];
|
|
448
|
+
for (const name of modelMeta.listColumns) {
|
|
449
|
+
if (colMap.has(name)) {
|
|
450
|
+
customList.push(colMap.get(name));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (customList.length > 0) {
|
|
454
|
+
return customList;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
424
458
|
// Prioritize: id, name/title, then others (excluding long text/json fields)
|
|
425
459
|
const priority = ['id', 'name', 'title', 'email', 'slug', 'status', 'published', 'created_at'];
|
|
426
460
|
const exclude = ['password', 'content', 'body', 'description']; // Usually too long
|
|
@@ -633,6 +667,13 @@ function initializeModelView(modelName) {
|
|
|
633
667
|
.then(modelMeta => {
|
|
634
668
|
state.currentModelMeta = modelMeta;
|
|
635
669
|
state.currentModel = modelMeta;
|
|
670
|
+
if (typeof setAdminPageTitle === 'function') {
|
|
671
|
+
setAdminPageTitle(modelMeta?.label || modelName);
|
|
672
|
+
}
|
|
673
|
+
if (!state.sortColumn && modelMeta?.defaultSort?.column) {
|
|
674
|
+
state.sortColumn = modelMeta.defaultSort.column;
|
|
675
|
+
state.sortDirection = (modelMeta.defaultSort.order || 'desc').toLowerCase();
|
|
676
|
+
}
|
|
636
677
|
// Load bulk-updatable fields for this model
|
|
637
678
|
loadBulkFields(modelName);
|
|
638
679
|
return loadRecords(modelName, page, state.filters, state.sortColumn, state.sortDirection);
|