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,3 +1,188 @@
|
|
|
1
|
+
// Helper to extract custom actions for a model
|
|
2
|
+
function getModelActions(modelName, modelMeta) {
|
|
3
|
+
const fromMeta = Array.isArray(modelMeta?.actions) ? modelMeta.actions : [];
|
|
4
|
+
const fromConfig = Array.isArray(window.__ADMIN_CONFIG__?.actions)
|
|
5
|
+
? window.__ADMIN_CONFIG__.actions.filter(a => {
|
|
6
|
+
if (!a.models || a.models === '*') return true;
|
|
7
|
+
if (Array.isArray(a.models)) return a.models.includes(modelName);
|
|
8
|
+
return a.models === modelName;
|
|
9
|
+
})
|
|
10
|
+
: [];
|
|
11
|
+
|
|
12
|
+
const map = new Map();
|
|
13
|
+
for (const a of fromConfig) map.set(a.id, a);
|
|
14
|
+
for (const a of fromMeta) map.set(a.id, a);
|
|
15
|
+
return Array.from(map.values());
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Helper to extract custom bulk actions for a model
|
|
19
|
+
function getModelBulkActions(modelName, modelMeta) {
|
|
20
|
+
const fromMeta = Array.isArray(modelMeta?.bulkActions) ? modelMeta.bulkActions : [];
|
|
21
|
+
const fromConfig = Array.isArray(window.__ADMIN_CONFIG__?.bulkActions)
|
|
22
|
+
? window.__ADMIN_CONFIG__.bulkActions.filter(a => {
|
|
23
|
+
if (a.id === 'bulk-delete' || a.id === 'bulk-restore' || a.id === 'bulk-update') return false;
|
|
24
|
+
if (!a.models || a.models === '*') return true;
|
|
25
|
+
if (Array.isArray(a.models)) return a.models.includes(modelName);
|
|
26
|
+
return a.models === modelName;
|
|
27
|
+
})
|
|
28
|
+
: [];
|
|
29
|
+
|
|
30
|
+
const map = new Map();
|
|
31
|
+
for (const a of fromConfig) map.set(a.id, a);
|
|
32
|
+
for (const a of fromMeta) map.set(a.id, a);
|
|
33
|
+
return Array.from(map.values());
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Evaluate condition whether an action should be shown for a record
|
|
37
|
+
function shouldShowAction(action, record) {
|
|
38
|
+
if (!action || !action.showWhen) return true;
|
|
39
|
+
if (!record) return true;
|
|
40
|
+
if (typeof action.showWhen === 'function') {
|
|
41
|
+
try {
|
|
42
|
+
return !!action.showWhen(record);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (typeof action.showWhen === 'object' && action.showWhen !== null) {
|
|
48
|
+
return Object.entries(action.showWhen).every(([k, v]) => record[k] === v);
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Render icon for actions (SVG string or common icon names)
|
|
54
|
+
function renderActionIcon(icon) {
|
|
55
|
+
if (!icon) return null;
|
|
56
|
+
if (typeof icon === 'string' && (icon.startsWith('<svg') || icon.includes('</svg>'))) {
|
|
57
|
+
return m.trust(icon);
|
|
58
|
+
}
|
|
59
|
+
const iconPaths = {
|
|
60
|
+
check: 'M5 13l4 4L19 7',
|
|
61
|
+
copy: 'M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z',
|
|
62
|
+
duplicate: 'M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z',
|
|
63
|
+
play: 'M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z',
|
|
64
|
+
refresh: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15',
|
|
65
|
+
eye: 'M15 12a3 3 0 11-6 0 3 3 0 016 0z M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z',
|
|
66
|
+
trash: 'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16',
|
|
67
|
+
archive: 'M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4',
|
|
68
|
+
mail: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
|
69
|
+
star: 'M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z',
|
|
70
|
+
};
|
|
71
|
+
const pathD = iconPaths[icon];
|
|
72
|
+
if (pathD) {
|
|
73
|
+
return m('svg.w-3.5.h-3.5', { fill: 'none', viewBox: '0 0 24 24', stroke: 'currentColor' },
|
|
74
|
+
m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: pathD })
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
return m('span.text-xs', icon);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getActionColorClass(color) {
|
|
81
|
+
switch (color) {
|
|
82
|
+
case 'green':
|
|
83
|
+
return 'text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-300 hover:bg-green-50 dark:hover:bg-green-950/40';
|
|
84
|
+
case 'red':
|
|
85
|
+
return 'text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-950/40';
|
|
86
|
+
case 'blue':
|
|
87
|
+
case 'indigo':
|
|
88
|
+
return 'text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-300 hover:bg-indigo-50 dark:hover:bg-indigo-950/40';
|
|
89
|
+
case 'amber':
|
|
90
|
+
case 'yellow':
|
|
91
|
+
return 'text-amber-600 dark:text-amber-400 hover:text-amber-800 dark:hover:text-amber-300 hover:bg-amber-50 dark:hover:bg-amber-950/40';
|
|
92
|
+
case 'purple':
|
|
93
|
+
case 'violet':
|
|
94
|
+
return 'text-purple-600 dark:text-purple-400 hover:text-purple-800 dark:hover:text-purple-300 hover:bg-purple-50 dark:hover:bg-purple-950/40';
|
|
95
|
+
default:
|
|
96
|
+
return 'text-gray-600 dark:text-slate-400 hover:text-gray-800 dark:hover:text-slate-200 hover:bg-gray-100 dark:hover:bg-slate-700/60';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getBulkActionColorClass(color) {
|
|
101
|
+
switch (color) {
|
|
102
|
+
case 'green':
|
|
103
|
+
return 'text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 hover:bg-green-50 dark:hover:bg-green-950/40';
|
|
104
|
+
case 'red':
|
|
105
|
+
return 'text-red-600 dark:text-red-400 border-red-200 dark:border-red-800 hover:bg-red-50 dark:hover:bg-red-950/40';
|
|
106
|
+
case 'blue':
|
|
107
|
+
case 'indigo':
|
|
108
|
+
return 'text-indigo-600 dark:text-indigo-400 border-indigo-200 dark:border-indigo-800 hover:bg-indigo-50 dark:hover:bg-indigo-950/40';
|
|
109
|
+
case 'amber':
|
|
110
|
+
case 'yellow':
|
|
111
|
+
return 'text-amber-600 dark:text-amber-400 border-amber-200 dark:border-amber-800 hover:bg-amber-50 dark:hover:bg-amber-950/40';
|
|
112
|
+
case 'purple':
|
|
113
|
+
case 'violet':
|
|
114
|
+
return 'text-purple-600 dark:text-purple-400 border-purple-200 dark:border-purple-800 hover:bg-purple-50 dark:hover:bg-purple-950/40';
|
|
115
|
+
default:
|
|
116
|
+
return 'text-gray-700 dark:text-slate-300 border-gray-300 dark:border-slate-600 hover:bg-gray-50 dark:hover:bg-slate-700/40';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function executeRecordAction(action, record, modelName, modelMeta, onSuccess) {
|
|
121
|
+
if (action.confirm) {
|
|
122
|
+
const msg = action.confirmMessage || ('Are you sure you want to perform "' + action.label + '"?');
|
|
123
|
+
const isConfirmed = await confirmAction({ title: action.label, message: msg, danger: action.color === 'red' });
|
|
124
|
+
if (!isConfirmed) return;
|
|
125
|
+
}
|
|
126
|
+
const primaryKey = modelMeta?.primaryKey || 'id';
|
|
127
|
+
const recId = record[primaryKey];
|
|
128
|
+
const busyKey = action.id + '-' + recId;
|
|
129
|
+
state.actionInProgress = busyKey;
|
|
130
|
+
m.redraw();
|
|
131
|
+
try {
|
|
132
|
+
const res = await api.post('/extensions/actions/' + action.id + '/' + modelName + '/' + recId);
|
|
133
|
+
const msg = res?.result?.message || res?.message;
|
|
134
|
+
if (msg) {
|
|
135
|
+
notifySuccess(msg);
|
|
136
|
+
}
|
|
137
|
+
const redirectUrl = res?.result?.redirect || res?.redirect;
|
|
138
|
+
if (redirectUrl) {
|
|
139
|
+
m.route.set(redirectUrl);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (typeof onSuccess === 'function') {
|
|
143
|
+
await onSuccess(res);
|
|
144
|
+
} else if (typeof loadRecords === 'function' && state.pagination) {
|
|
145
|
+
loadRecords(modelName, state.pagination.page);
|
|
146
|
+
}
|
|
147
|
+
} catch (err) {
|
|
148
|
+
const errText = 'Action failed: ' + (err.message || err);
|
|
149
|
+
notifyError(errText);
|
|
150
|
+
} finally {
|
|
151
|
+
state.actionInProgress = null;
|
|
152
|
+
m.redraw();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function executeBulkAction(action, modelName, modelMeta) {
|
|
157
|
+
const count = state.selectAllMode ? state.pagination.total : state.selectedRecords.size;
|
|
158
|
+
if (action.confirm) {
|
|
159
|
+
const msg = action.confirmMessage || ('Perform "' + action.label + '" on ' + count + ' records?');
|
|
160
|
+
const isConfirmed = await confirmAction({ title: action.label, message: msg, danger: action.color === 'red' });
|
|
161
|
+
if (!isConfirmed) return;
|
|
162
|
+
}
|
|
163
|
+
state.bulkActionInProgress = true;
|
|
164
|
+
m.redraw();
|
|
165
|
+
try {
|
|
166
|
+
const payload = state.selectAllMode
|
|
167
|
+
? { selectAll: true, filters: state.filters, trashed: state.trashedView }
|
|
168
|
+
: { ids: Array.from(state.selectedRecords), trashed: state.trashedView };
|
|
169
|
+
const res = await api.post('/extensions/bulk-actions/' + action.id + '/' + modelName, payload);
|
|
170
|
+
state.selectedRecords = new Set();
|
|
171
|
+
state.selectAllMode = false;
|
|
172
|
+
loadRecords(modelName, state.pagination.page);
|
|
173
|
+
if (res?.result?.message || res?.message) {
|
|
174
|
+
const msg = res.result?.message || res.message;
|
|
175
|
+
notifySuccess(msg);
|
|
176
|
+
}
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const errText = 'Bulk action failed: ' + (err.message || err);
|
|
179
|
+
notifyError(errText);
|
|
180
|
+
} finally {
|
|
181
|
+
state.bulkActionInProgress = false;
|
|
182
|
+
m.redraw();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
1
186
|
// Record List Component - displays records with dynamic columns
|
|
2
187
|
const RecordList = {
|
|
3
188
|
oninit(vnode) {
|
|
@@ -25,8 +210,10 @@ const RecordList = {
|
|
|
25
210
|
view: (vnode) => {
|
|
26
211
|
const modelName = m.route.param('model');
|
|
27
212
|
const modelMeta = state.currentModelMeta;
|
|
28
|
-
const displayColumns = modelMeta ? getDisplayColumns(modelMeta.columns) : [];
|
|
213
|
+
const displayColumns = modelMeta ? getDisplayColumns(modelMeta.columns, modelMeta) : [];
|
|
29
214
|
const primaryKey = modelMeta?.primaryKey || 'id';
|
|
215
|
+
const customActions = getModelActions(modelName, modelMeta);
|
|
216
|
+
const customBulkActions = getModelBulkActions(modelName, modelMeta);
|
|
30
217
|
|
|
31
218
|
// Count active filters
|
|
32
219
|
const activeFilterCount = Object.values(state.filters || {}).filter(filterPayloadIsActive).length;
|
|
@@ -101,7 +288,7 @@ const RecordList = {
|
|
|
101
288
|
if (state.trashedView) payload.trashed = 'only';
|
|
102
289
|
await downloadDataExchangeXlsx(modelName, payload);
|
|
103
290
|
} catch (err) {
|
|
104
|
-
|
|
291
|
+
notifyError('Error: ' + err.message);
|
|
105
292
|
}
|
|
106
293
|
},
|
|
107
294
|
}, [
|
|
@@ -119,7 +306,12 @@ const RecordList = {
|
|
|
119
306
|
e.target.value = '';
|
|
120
307
|
if (!file) return;
|
|
121
308
|
const adminPath = window.__ADMIN_PATH__ || '/_admin';
|
|
122
|
-
const mode =
|
|
309
|
+
const mode = (await confirmAction({
|
|
310
|
+
title: 'Import Mode',
|
|
311
|
+
message: 'Confirm to upsert records matching existing IDs, or cancel to insert as new records only.',
|
|
312
|
+
confirmText: 'Upsert by ID',
|
|
313
|
+
cancelText: 'Insert only'
|
|
314
|
+
})) ? 'upsert' : 'insert';
|
|
123
315
|
const upsertKey = 'id';
|
|
124
316
|
const fd = new FormData();
|
|
125
317
|
fd.append('file', file);
|
|
@@ -137,10 +329,10 @@ const RecordList = {
|
|
|
137
329
|
if (body.errors && body.errors.length) {
|
|
138
330
|
msg += 'First errors: ' + body.errors.slice(0, 3).map(function (x) { return 'row ' + x.row + ': ' + x.message; }).join('; ');
|
|
139
331
|
}
|
|
140
|
-
|
|
332
|
+
notifySuccess(msg);
|
|
141
333
|
loadRecords(modelName, state.pagination.page, state.filters);
|
|
142
334
|
} catch (err) {
|
|
143
|
-
|
|
335
|
+
notifyError('Error: ' + err.message);
|
|
144
336
|
}
|
|
145
337
|
},
|
|
146
338
|
}) : null,
|
|
@@ -155,7 +347,7 @@ const RecordList = {
|
|
|
155
347
|
),
|
|
156
348
|
'Import',
|
|
157
349
|
]) : null,
|
|
158
|
-
!state.trashedView ? m('button.inline-flex.items-center.gap-2.px-4.py-2.text-sm.font-medium.text-white.bg-indigo-600.rounded-lg.hover:bg-indigo-700.focus:outline-none.focus:ring-2.focus:ring-indigo-500', {
|
|
350
|
+
!state.trashedView && !modelMeta?.readOnly && modelMeta?.permissions?.create !== false ? m('button.inline-flex.items-center.gap-2.px-4.py-2.text-sm.font-medium.text-white.bg-indigo-600.rounded-lg.hover:bg-indigo-700.focus:outline-none.focus:ring-2.focus:ring-indigo-500', {
|
|
159
351
|
onclick: () => {
|
|
160
352
|
state.currentRecord = null;
|
|
161
353
|
state.editing = true;
|
|
@@ -259,7 +451,7 @@ const RecordList = {
|
|
|
259
451
|
? m('button.inline-flex.items-center.gap-1.px-3.py-1.5.text-sm.font-medium.text-green-600.bg-white dark:bg-slate-800.border.border-green-200.rounded.hover:bg-green-50.transition-colors', {
|
|
260
452
|
disabled: state.bulkActionInProgress,
|
|
261
453
|
onclick: async () => {
|
|
262
|
-
if (!
|
|
454
|
+
if (!(await confirmAction({ title: 'Restore Records', message: 'Restore the selected records?', confirmText: 'Restore' }))) return;
|
|
263
455
|
state.bulkActionInProgress = true;
|
|
264
456
|
m.redraw();
|
|
265
457
|
try {
|
|
@@ -269,9 +461,10 @@ const RecordList = {
|
|
|
269
461
|
await api.post('/extensions/bulk-actions/bulk-restore/' + modelName, payload);
|
|
270
462
|
state.selectedRecords = new Set();
|
|
271
463
|
state.selectAllMode = false;
|
|
464
|
+
notifySuccess('Records restored successfully');
|
|
272
465
|
loadRecords(modelName, 1);
|
|
273
466
|
} catch (err) {
|
|
274
|
-
|
|
467
|
+
notifyError('Error: ' + err.message);
|
|
275
468
|
} finally {
|
|
276
469
|
state.bulkActionInProgress = false;
|
|
277
470
|
m.redraw();
|
|
@@ -283,11 +476,11 @@ const RecordList = {
|
|
|
283
476
|
),
|
|
284
477
|
'Restore',
|
|
285
478
|
])
|
|
286
|
-
: m('button.inline-flex.items-center.gap-1.px-3.py-1.5.text-sm.font-medium.text-red-600.bg-white dark:bg-slate-800.border.border-red-200.rounded.hover:bg-red-50.transition-colors', {
|
|
479
|
+
: (!modelMeta?.readOnly && modelMeta?.permissions?.delete !== false ? m('button.inline-flex.items-center.gap-1.px-3.py-1.5.text-sm.font-medium.text-red-600.bg-white dark:bg-slate-800.border.border-red-200.rounded.hover:bg-red-50.transition-colors', {
|
|
287
480
|
disabled: state.bulkActionInProgress,
|
|
288
481
|
onclick: async () => {
|
|
289
482
|
const count = state.selectAllMode ? state.pagination.total : state.selectedRecords.size;
|
|
290
|
-
if (!
|
|
483
|
+
if (!(await confirmAction({ title: 'Delete Records', message: 'Are you sure you want to delete ' + count + ' records? This action cannot be undone.', danger: true, confirmText: 'Delete' }))) return;
|
|
291
484
|
state.bulkActionInProgress = true;
|
|
292
485
|
m.redraw();
|
|
293
486
|
try {
|
|
@@ -297,9 +490,10 @@ const RecordList = {
|
|
|
297
490
|
await api.post('/extensions/bulk-actions/bulk-delete/' + modelName, payload);
|
|
298
491
|
state.selectedRecords = new Set();
|
|
299
492
|
state.selectAllMode = false;
|
|
493
|
+
notifySuccess('Records deleted successfully');
|
|
300
494
|
loadRecords(modelName, 1);
|
|
301
495
|
} catch (err) {
|
|
302
|
-
|
|
496
|
+
notifyError('Error: ' + err.message);
|
|
303
497
|
} finally {
|
|
304
498
|
state.bulkActionInProgress = false;
|
|
305
499
|
m.redraw();
|
|
@@ -310,7 +504,17 @@ const RecordList = {
|
|
|
310
504
|
m('path', { 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'stroke-width': '2', d: 'M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16' })
|
|
311
505
|
),
|
|
312
506
|
'Delete',
|
|
313
|
-
]),
|
|
507
|
+
]) : null),
|
|
508
|
+
// Custom bulk actions
|
|
509
|
+
!state.trashedView && customBulkActions.map(action =>
|
|
510
|
+
m('button.inline-flex.items-center.gap-1.px-3.py-1.5.text-sm.font-medium.bg-white.dark:bg-slate-800.border.rounded.transition-colors.' + getBulkActionColorClass(action.color), {
|
|
511
|
+
disabled: state.bulkActionInProgress,
|
|
512
|
+
onclick: () => executeBulkAction(action, modelName, modelMeta),
|
|
513
|
+
}, [
|
|
514
|
+
action.icon ? renderActionIcon(action.icon) : null,
|
|
515
|
+
action.label,
|
|
516
|
+
])
|
|
517
|
+
),
|
|
314
518
|
!state.trashedView ? m('button.inline-flex.items-center.gap-1.px-3.py-1.5.text-sm.font-medium.text-blue-600.bg-white dark:bg-slate-800.border.border-blue-200.rounded.hover:bg-blue-50.transition-colors', {
|
|
315
519
|
disabled: state.bulkActionInProgress,
|
|
316
520
|
onclick: async () => {
|
|
@@ -335,7 +539,7 @@ const RecordList = {
|
|
|
335
539
|
URL.revokeObjectURL(url);
|
|
336
540
|
}, 1000);
|
|
337
541
|
} catch (err) {
|
|
338
|
-
|
|
542
|
+
notifyError('Error: ' + err.message);
|
|
339
543
|
} finally {
|
|
340
544
|
state.bulkActionInProgress = false;
|
|
341
545
|
m.redraw();
|
|
@@ -357,7 +561,7 @@ const RecordList = {
|
|
|
357
561
|
? { selectAll: true, filters: state.filters }
|
|
358
562
|
: { ids: Array.from(state.selectedRecords) };
|
|
359
563
|
if (!state.selectAllMode && (!payload.ids || payload.ids.length === 0)) {
|
|
360
|
-
|
|
564
|
+
notifyWarning('Please select at least one record to export');
|
|
361
565
|
return;
|
|
362
566
|
}
|
|
363
567
|
const response = await api.post('/extensions/export?model=' + modelName + '&format=csv', payload);
|
|
@@ -375,7 +579,7 @@ const RecordList = {
|
|
|
375
579
|
URL.revokeObjectURL(url);
|
|
376
580
|
}, 1000);
|
|
377
581
|
} catch (err) {
|
|
378
|
-
|
|
582
|
+
notifyError('Error: ' + err.message);
|
|
379
583
|
} finally {
|
|
380
584
|
state.bulkActionInProgress = false;
|
|
381
585
|
m.redraw();
|
|
@@ -397,13 +601,13 @@ const RecordList = {
|
|
|
397
601
|
? { selectAll: true, filters: state.filters }
|
|
398
602
|
: { ids: Array.from(state.selectedRecords) };
|
|
399
603
|
if (!state.selectAllMode && (!payload.ids || payload.ids.length === 0)) {
|
|
400
|
-
|
|
604
|
+
notifyWarning('Please select at least one record to export');
|
|
401
605
|
return;
|
|
402
606
|
}
|
|
403
607
|
if (state.trashedView) payload.trashed = 'only';
|
|
404
608
|
await downloadDataExchangeXlsx(modelName, payload);
|
|
405
609
|
} catch (err) {
|
|
406
|
-
|
|
610
|
+
notifyError('Error: ' + err.message);
|
|
407
611
|
} finally {
|
|
408
612
|
state.bulkActionInProgress = false;
|
|
409
613
|
m.redraw();
|
|
@@ -543,32 +747,53 @@ const RecordList = {
|
|
|
543
747
|
onclick: async () => {
|
|
544
748
|
try {
|
|
545
749
|
await api.post('/models/' + modelName + '/records/' + record[primaryKey] + '/restore');
|
|
750
|
+
notifySuccess('Record restored successfully');
|
|
546
751
|
loadRecords(modelName, state.pagination.page);
|
|
547
752
|
} catch (err) {
|
|
548
|
-
|
|
753
|
+
notifyError('Error: ' + err.message);
|
|
549
754
|
}
|
|
550
755
|
},
|
|
551
756
|
}, 'Restore')
|
|
552
757
|
: [
|
|
758
|
+
// Custom row actions
|
|
759
|
+
...customActions
|
|
760
|
+
.filter(action => shouldShowAction(action, record))
|
|
761
|
+
.map(action =>
|
|
762
|
+
m('button.inline-flex.items-center.px-2.py-1.text-sm.rounded.mr-1.transition-colors.' + getActionColorClass(action.color), {
|
|
763
|
+
disabled: state.actionInProgress === (action.id + '-' + record[primaryKey]),
|
|
764
|
+
onclick: () => executeRecordAction(action, record, modelName, modelMeta),
|
|
765
|
+
}, [
|
|
766
|
+
action.icon ? m('span.mr-1', renderActionIcon(action.icon)) : null,
|
|
767
|
+
state.actionInProgress === (action.id + '-' + record[primaryKey]) ? '...' : action.label,
|
|
768
|
+
])
|
|
769
|
+
),
|
|
553
770
|
m('button.inline-flex.items-center.px-2.py-1.text-sm.text-indigo-600.dark:text-indigo-400.hover:text-indigo-800.dark:hover:text-indigo-300.hover:bg-indigo-50.dark:hover:bg-indigo-950/40.rounded.mr-1.transition-colors', {
|
|
554
771
|
onclick: () => {
|
|
555
772
|
state.currentRecord = record;
|
|
556
773
|
state.editing = true;
|
|
557
774
|
m.route.set('/models/' + modelName + '/edit/' + record[primaryKey]);
|
|
558
775
|
},
|
|
559
|
-
}, 'Edit'),
|
|
560
|
-
m('button.inline-flex.items-center.px-2.py-1.text-sm.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.hover:bg-red-50.dark:hover:bg-red-950/40.rounded.transition-colors', {
|
|
776
|
+
}, modelMeta?.readOnly || modelMeta?.permissions?.edit === false ? 'View' : 'Edit'),
|
|
777
|
+
!modelMeta?.readOnly && modelMeta?.permissions?.delete !== false ? m('button.inline-flex.items-center.px-2.py-1.text-sm.text-red-600.dark:text-red-400.hover:text-red-800.dark:hover:text-red-300.hover:bg-red-50.dark:hover:bg-red-950/40.rounded.transition-colors', {
|
|
561
778
|
onclick: async () => {
|
|
562
|
-
|
|
779
|
+
const isConfirmed = await confirmAction({
|
|
780
|
+
title: 'Delete Record',
|
|
781
|
+
message: 'Are you sure you want to delete this record?',
|
|
782
|
+
danger: true,
|
|
783
|
+
confirmText: 'Delete'
|
|
784
|
+
});
|
|
785
|
+
if (isConfirmed) {
|
|
563
786
|
try {
|
|
564
787
|
await api.delete('/models/' + modelName + '/records/' + record[primaryKey]);
|
|
788
|
+
notifySuccess('Record deleted successfully');
|
|
565
789
|
loadRecords(modelName, state.pagination.page);
|
|
566
790
|
} catch (err) {
|
|
567
|
-
|
|
791
|
+
const errMsg = 'Error: ' + err.message;
|
|
792
|
+
notifyError(errMsg);
|
|
568
793
|
}
|
|
569
794
|
}
|
|
570
795
|
},
|
|
571
|
-
}, 'Delete'),
|
|
796
|
+
}, 'Delete') : null,
|
|
572
797
|
],
|
|
573
798
|
]),
|
|
574
799
|
])
|
|
@@ -144,6 +144,10 @@ const RecordForm = {
|
|
|
144
144
|
api.get('/models/' + modelName)
|
|
145
145
|
.then(modelMeta => {
|
|
146
146
|
state.currentModelMeta = modelMeta;
|
|
147
|
+
if (typeof setAdminPageTitle === 'function') {
|
|
148
|
+
const label = modelMeta?.label || modelName;
|
|
149
|
+
setAdminPageTitle((id && id !== 'new' ? 'Edit ' : 'New ') + label);
|
|
150
|
+
}
|
|
147
151
|
|
|
148
152
|
// Initialize form data with defaults
|
|
149
153
|
modelMeta.columns.forEach(col => {
|
|
@@ -179,16 +183,26 @@ const RecordForm = {
|
|
|
179
183
|
const id = m.route.param('id');
|
|
180
184
|
const isNew = !id || id === 'new';
|
|
181
185
|
const modelMeta = state.currentModelMeta;
|
|
186
|
+
const isFormReadOnly = modelMeta?.readOnly === true ||
|
|
187
|
+
(isNew && modelMeta?.permissions?.create === false) ||
|
|
188
|
+
(!isNew && modelMeta?.permissions?.edit === false);
|
|
182
189
|
|
|
190
|
+
const customActions = !isNew && state.currentRecord && typeof getModelActions === 'function'
|
|
191
|
+
? getModelActions(modelName, modelMeta).filter(action => typeof shouldShowAction === 'function' ? shouldShowAction(action, state.currentRecord) : true)
|
|
192
|
+
: [];
|
|
193
|
+
|
|
183
194
|
const breadcrumbs = [
|
|
184
195
|
{ label: modelMeta?.label || modelName, href: '/models/' + modelName },
|
|
185
|
-
{ label: isNew ? 'New' : 'Edit #' + id, href: '#' },
|
|
196
|
+
{ label: isNew ? 'New' : (isFormReadOnly ? 'View #' + id : 'Edit #' + id), href: '#' },
|
|
186
197
|
];
|
|
187
198
|
|
|
188
199
|
return m(Layout, { breadcrumbs }, [
|
|
189
200
|
m('.flex.items-center.justify-between.mb-6', [
|
|
190
|
-
m('h2.text-2xl.font-bold.text-gray-900.dark:text-slate-100', isNew ? 'New Record' : 'Edit Record'),
|
|
191
|
-
|
|
201
|
+
m('h2.text-2xl.font-bold.text-gray-900.dark:text-slate-100', isNew ? 'New Record' : (isFormReadOnly ? 'View Record' : 'Edit Record')),
|
|
202
|
+
m('.flex.items-center.gap-2', [
|
|
203
|
+
isFormReadOnly ? m('span.px-2.py-0.5.text-xs.font-semibold.rounded.bg-amber-100.text-amber-800.dark:bg-amber-900/40.dark:text-amber-300', 'Read-Only') : null,
|
|
204
|
+
modelMeta ? m('span.text-gray-500.dark:text-slate-400', modelMeta.label || modelMeta.name) : null,
|
|
205
|
+
]),
|
|
192
206
|
]),
|
|
193
207
|
|
|
194
208
|
state.loading ? m('p.text-gray-600.dark:text-slate-400', 'Loading...') :
|
|
@@ -198,6 +212,7 @@ const RecordForm = {
|
|
|
198
212
|
style: 'min-height: calc(100vh - 280px);',
|
|
199
213
|
onsubmit: async (e) => {
|
|
200
214
|
e.preventDefault();
|
|
215
|
+
if (isFormReadOnly) return;
|
|
201
216
|
state.error = null;
|
|
202
217
|
state.fieldErrors = {};
|
|
203
218
|
|
|
@@ -261,6 +276,9 @@ const RecordForm = {
|
|
|
261
276
|
} else {
|
|
262
277
|
await api.put('/models/' + modelName + '/records/' + id, payload);
|
|
263
278
|
}
|
|
279
|
+
if (typeof window !== 'undefined' && window.Admin && typeof window.Admin.toast === 'function') {
|
|
280
|
+
window.Admin.toast.success(isNew ? 'Record created successfully' : 'Record updated successfully');
|
|
281
|
+
}
|
|
264
282
|
m.route.set('/models/' + modelName);
|
|
265
283
|
} catch (err) {
|
|
266
284
|
state.error = err.message || 'Failed to save record';
|
|
@@ -287,12 +305,13 @@ const RecordForm = {
|
|
|
287
305
|
// Hide hidden fields
|
|
288
306
|
if (col.ui && col.ui.hidden) return null;
|
|
289
307
|
|
|
290
|
-
const isReadonly = !!autoType || (col.ui && col.ui.readonly);
|
|
308
|
+
const isReadonly = isFormReadOnly || !!autoType || (col.ui && col.ui.readonly);
|
|
291
309
|
const renderer = getFieldRenderer(col, modelMeta);
|
|
292
310
|
const value = state.formData[col.name];
|
|
293
311
|
const fieldError = state.fieldErrors ? state.fieldErrors[col.name] : null;
|
|
294
312
|
|
|
295
313
|
const onChange = (newValue) => {
|
|
314
|
+
if (isFormReadOnly) return;
|
|
296
315
|
state.formData[col.name] = newValue;
|
|
297
316
|
if (state.fieldErrors && state.fieldErrors[col.name]) {
|
|
298
317
|
delete state.fieldErrors[col.name];
|
|
@@ -304,14 +323,42 @@ const RecordForm = {
|
|
|
304
323
|
]),
|
|
305
324
|
|
|
306
325
|
// Sticky footer buttons
|
|
307
|
-
m('.flex.gap-4.p-4.border-t.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900.sticky.bottom-0', [
|
|
308
|
-
m('
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
326
|
+
m('.flex.items-center.justify-between.gap-4.p-4.border-t.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900.sticky.bottom-0', [
|
|
327
|
+
m('.flex.items-center.gap-3', [
|
|
328
|
+
isFormReadOnly
|
|
329
|
+
? m('button.bg-gray-600.dark:bg-slate-700.text-white.px-6.py-2.rounded-lg.hover:bg-gray-700.dark:hover:bg-slate-600[type=button]', {
|
|
330
|
+
onclick: () => m.route.set('/models/' + modelName),
|
|
331
|
+
}, 'Back')
|
|
332
|
+
: [
|
|
333
|
+
m('button.bg-blue-600.dark:bg-blue-500.text-white.px-6.py-2.rounded-lg.hover:bg-blue-700.dark:hover:bg-blue-600.disabled:opacity-50', {
|
|
334
|
+
type: 'submit',
|
|
335
|
+
disabled: state.loading,
|
|
336
|
+
}, state.loading ? 'Saving...' : 'Save'),
|
|
337
|
+
m('button.bg-gray-200.dark:bg-slate-700.text-gray-800.dark:text-slate-200.px-6.py-2.rounded.hover:bg-gray-300.dark:hover:bg-slate-600[type=button]', {
|
|
338
|
+
onclick: () => m.route.set('/models/' + modelName),
|
|
339
|
+
}, 'Cancel'),
|
|
340
|
+
],
|
|
341
|
+
]),
|
|
342
|
+
!isNew && state.currentRecord && customActions.length > 0 ? m('.flex.items-center.gap-2', customActions.map(action => {
|
|
343
|
+
const isBusy = state.actionInProgress === (action.id + '-' + id);
|
|
344
|
+
return m('button.px-3.py-2.text-xs.font-medium.rounded-lg.border.flex.items-center.gap-1.5.transition-colors[type=button]', {
|
|
345
|
+
class: typeof getBulkActionColorClass === 'function' ? getBulkActionColorClass(action.color) : 'text-gray-700 dark:text-slate-300 border-gray-300 dark:border-slate-600',
|
|
346
|
+
disabled: isBusy || state.loading,
|
|
347
|
+
title: action.label,
|
|
348
|
+
onclick: () => executeRecordAction(action, state.currentRecord, modelName, modelMeta, async () => {
|
|
349
|
+
const refreshed = await api.get('/models/' + modelName + '/records/' + id);
|
|
350
|
+
if (refreshed?.data) {
|
|
351
|
+
state.currentRecord = refreshed.data;
|
|
352
|
+
Object.keys(refreshed.data).forEach(key => {
|
|
353
|
+
state.formData[key] = refreshed.data[key];
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}),
|
|
357
|
+
}, [
|
|
358
|
+
isBusy ? m('span.inline-block.animate-spin', '⟳') : (action.icon && typeof renderActionIcon === 'function' ? renderActionIcon(action.icon) : null),
|
|
359
|
+
m('span', action.label),
|
|
360
|
+
]);
|
|
361
|
+
})) : null,
|
|
315
362
|
]),
|
|
316
363
|
]),
|
|
317
364
|
]);
|
|
@@ -8,4 +8,27 @@ window.__ADMIN_COMPONENTS__ = {
|
|
|
8
8
|
RecordForm,
|
|
9
9
|
api,
|
|
10
10
|
state,
|
|
11
|
+
notify: typeof window !== 'undefined' ? window.notify : undefined,
|
|
12
|
+
confirmModal: typeof window !== 'undefined' ? window.confirmModal : undefined,
|
|
13
|
+
toastState: typeof window !== 'undefined' ? window.toastState : undefined,
|
|
14
|
+
confirmState: typeof window !== 'undefined' ? window.confirmState : undefined,
|
|
15
|
+
AdminUI: typeof window !== 'undefined' ? window.AdminUI : undefined,
|
|
16
|
+
Admin: typeof window !== 'undefined' ? window.Admin : undefined,
|
|
17
|
+
ToastContainer: typeof window !== 'undefined' ? window.ToastContainer : undefined,
|
|
18
|
+
ConfirmModalContainer: typeof window !== 'undefined' ? window.ConfirmModalContainer : undefined,
|
|
11
19
|
};
|
|
20
|
+
|
|
21
|
+
if (typeof window !== 'undefined') {
|
|
22
|
+
window.Admin = window.Admin || {};
|
|
23
|
+
window.Admin.api = api;
|
|
24
|
+
window.Admin.state = state;
|
|
25
|
+
if (typeof window.AdminUI !== 'undefined') {
|
|
26
|
+
window.Admin.ui = window.AdminUI;
|
|
27
|
+
}
|
|
28
|
+
if (typeof window.notify !== 'undefined') {
|
|
29
|
+
window.Admin.toast = window.notify;
|
|
30
|
+
}
|
|
31
|
+
if (typeof window.confirmModal !== 'undefined') {
|
|
32
|
+
window.Admin.confirm = window.confirmModal;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -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;
|
|
@@ -193,19 +193,22 @@ function registerModule(config, deps) {
|
|
|
193
193
|
menuList = menuList.concat(dirMenuItems);
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
const moduleTitle = config.title;
|
|
197
|
+
|
|
196
198
|
for (const p of pagesList) {
|
|
197
199
|
if (p.menu && !menuList.some(m => m.id === p.id)) {
|
|
200
|
+
const pageLabel = p.title || moduleTitle || p.id;
|
|
198
201
|
if (p.menu === true) {
|
|
199
202
|
menuList.push({
|
|
200
203
|
id: p.id,
|
|
201
|
-
label:
|
|
204
|
+
label: pageLabel,
|
|
202
205
|
path: p.path,
|
|
203
206
|
icon: p.icon,
|
|
204
207
|
});
|
|
205
208
|
} else if (typeof p.menu === 'object') {
|
|
206
209
|
menuList.push({
|
|
207
210
|
id: p.id,
|
|
208
|
-
label:
|
|
211
|
+
label: pageLabel,
|
|
209
212
|
path: p.path,
|
|
210
213
|
icon: p.icon,
|
|
211
214
|
...p.menu,
|
|
@@ -215,7 +218,7 @@ function registerModule(config, deps) {
|
|
|
215
218
|
}
|
|
216
219
|
|
|
217
220
|
if (pagesList.length > 0) {
|
|
218
|
-
registerPages(config.id, pagesList, { registry, adminPath, ctx, optionalAuth, serveAdminPanel });
|
|
221
|
+
registerPages(config.id, pagesList, { registry, adminPath, ctx, optionalAuth, serveAdminPanel }, moduleTitle);
|
|
219
222
|
}
|
|
220
223
|
|
|
221
224
|
if (menuList.length > 0) {
|
|
@@ -246,7 +249,7 @@ function registerPageDir(dirPath, deps) {
|
|
|
246
249
|
}
|
|
247
250
|
}
|
|
248
251
|
|
|
249
|
-
function registerPages(moduleId, pages, deps) {
|
|
252
|
+
function registerPages(moduleId, pages, deps, moduleTitle) {
|
|
250
253
|
if (!Array.isArray(pages)) {
|
|
251
254
|
throw new Error(`Module "${moduleId}": pages must be an array`);
|
|
252
255
|
}
|
|
@@ -257,8 +260,9 @@ function registerPages(moduleId, pages, deps) {
|
|
|
257
260
|
const pageId = page.id || moduleId;
|
|
258
261
|
|
|
259
262
|
let htmlContent = page.html || null;
|
|
263
|
+
let resolvedHtmlPath = null;
|
|
260
264
|
if (page.htmlFile) {
|
|
261
|
-
|
|
265
|
+
resolvedHtmlPath = path.resolve(page.htmlFile);
|
|
262
266
|
if (!fs.existsSync(resolvedHtmlPath)) {
|
|
263
267
|
throw new Error(`Module "${moduleId}", page "${pageId}": htmlFile not found at ${page.htmlFile}`);
|
|
264
268
|
}
|
|
@@ -266,7 +270,8 @@ function registerPages(moduleId, pages, deps) {
|
|
|
266
270
|
}
|
|
267
271
|
|
|
268
272
|
registry.registerPage(pageId, {
|
|
269
|
-
title: page.title,
|
|
273
|
+
title: page.title || moduleTitle,
|
|
274
|
+
documentTitle: page.documentTitle,
|
|
270
275
|
path: page.path,
|
|
271
276
|
icon: page.icon,
|
|
272
277
|
description: page.description,
|
|
@@ -275,6 +280,7 @@ function registerPages(moduleId, pages, deps) {
|
|
|
275
280
|
iframe: page.iframe,
|
|
276
281
|
url: page.url || page.iframeUrl || undefined,
|
|
277
282
|
html: htmlContent || undefined,
|
|
283
|
+
htmlFile: resolvedHtmlPath || (page.htmlFile ? path.resolve(page.htmlFile) : undefined),
|
|
278
284
|
});
|
|
279
285
|
|
|
280
286
|
if (page.componentFile) {
|
|
@@ -284,7 +290,7 @@ function registerPages(moduleId, pages, deps) {
|
|
|
284
290
|
}
|
|
285
291
|
const rawCode = fs.readFileSync(resolvedPath, 'utf8');
|
|
286
292
|
const formattedCode = formatClientComponent(pageId, rawCode);
|
|
287
|
-
registry.registerClientComponent(pageId, formattedCode);
|
|
293
|
+
registry.registerClientComponent(pageId, formattedCode, resolvedPath);
|
|
288
294
|
} else if (page.component) {
|
|
289
295
|
const formattedCode = formatClientComponent(pageId, page.component);
|
|
290
296
|
registry.registerClientComponent(pageId, formattedCode);
|