domma-cms 0.2.1 → 0.5.0

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.
Files changed (166) hide show
  1. package/README.md +3 -3
  2. package/admin/css/admin.css +1 -1200
  3. package/admin/dist/domma/domma-tools.css +2313 -0
  4. package/admin/dist/domma/domma-tools.min.js +10 -0
  5. package/admin/index.html +4 -0
  6. package/admin/js/api.js +1 -242
  7. package/admin/js/app.js +9 -279
  8. package/admin/js/config/sidebar-config.js +1 -115
  9. package/admin/js/lib/card.js +1 -63
  10. package/admin/js/lib/image-editor.js +1 -869
  11. package/admin/js/lib/markdown-toolbar.js +54 -421
  12. package/admin/js/templates/action-editor.html +171 -0
  13. package/admin/js/templates/actions-list.html +19 -0
  14. package/admin/js/templates/api-reference.html +1411 -0
  15. package/admin/js/templates/block-editor.html +158 -0
  16. package/admin/js/templates/blocks.html +8 -0
  17. package/admin/js/templates/collection-editor.html +47 -0
  18. package/admin/js/templates/collection-entries.html +3 -0
  19. package/admin/js/templates/collections.html +51 -4
  20. package/admin/js/templates/documentation.html +258 -0
  21. package/admin/js/templates/form-editor.html +238 -0
  22. package/{plugins/form-builder/admin → admin/js}/templates/form-submissions.html +30 -30
  23. package/{plugins/form-builder/admin/templates/forms-list.html → admin/js/templates/forms.html} +17 -17
  24. package/admin/js/templates/layouts.html +44 -7
  25. package/admin/js/templates/login.html +29 -4
  26. package/admin/js/templates/my-profile.html +17 -0
  27. package/admin/js/templates/page-editor.html +48 -0
  28. package/admin/js/templates/pages.html +6 -1
  29. package/admin/js/templates/pro-docs.html +259 -0
  30. package/admin/js/templates/role-editor.html +59 -0
  31. package/admin/js/templates/roles.html +10 -0
  32. package/admin/js/templates/settings.html +137 -18
  33. package/admin/js/templates/tutorials.html +81 -0
  34. package/admin/js/templates/user-editor.html +7 -0
  35. package/admin/js/templates/users.html +3 -1
  36. package/admin/js/templates/view-editor.html +201 -0
  37. package/admin/js/templates/view-preview.html +51 -0
  38. package/admin/js/templates/views-list.html +19 -0
  39. package/admin/js/views/action-editor.js +1 -0
  40. package/admin/js/views/actions-list.js +1 -0
  41. package/admin/js/views/api-reference.js +1 -0
  42. package/admin/js/views/block-editor.js +8 -0
  43. package/admin/js/views/blocks.js +4 -0
  44. package/admin/js/views/collection-editor.js +3 -487
  45. package/admin/js/views/collection-entries.js +1 -484
  46. package/admin/js/views/collections.js +1 -153
  47. package/admin/js/views/dashboard.js +1 -56
  48. package/admin/js/views/documentation.js +1 -12
  49. package/admin/js/views/form-editor.js +8 -0
  50. package/admin/js/views/form-submissions.js +1 -0
  51. package/admin/js/views/forms.js +1 -0
  52. package/admin/js/views/index.js +1 -39
  53. package/admin/js/views/layouts.js +9 -42
  54. package/admin/js/views/login.js +7 -251
  55. package/admin/js/views/media.js +1 -240
  56. package/admin/js/views/my-profile.js +1 -0
  57. package/admin/js/views/navigation.js +14 -212
  58. package/admin/js/views/page-editor.js +72 -661
  59. package/admin/js/views/pages.js +5 -72
  60. package/admin/js/views/plugins.js +13 -90
  61. package/admin/js/views/pro-docs.js +1 -0
  62. package/admin/js/views/role-editor.js +1 -0
  63. package/admin/js/views/roles.js +4 -0
  64. package/admin/js/views/settings.js +3 -199
  65. package/admin/js/views/tutorials.js +1 -12
  66. package/admin/js/views/user-editor.js +1 -88
  67. package/admin/js/views/users.js +4 -76
  68. package/admin/js/views/view-editor.js +1 -0
  69. package/admin/js/views/view-preview.js +1 -0
  70. package/admin/js/views/views-list.js +1 -0
  71. package/bin/cli.js +1 -1
  72. package/config/auth.json +2 -17
  73. package/config/connections.json.bak +9 -0
  74. package/config/connections.json.example +9 -0
  75. package/config/navigation.json +15 -0
  76. package/config/plugins.json +19 -29
  77. package/config/server.json +6 -6
  78. package/config/site.json +17 -6
  79. package/package.json +24 -10
  80. package/plugins/domma-effects/public/celebrations/core/canvas.js +2 -104
  81. package/plugins/domma-effects/public/celebrations/core/particles.js +1 -144
  82. package/plugins/domma-effects/public/celebrations/core/physics.js +1 -166
  83. package/plugins/domma-effects/public/celebrations/index.js +1 -535
  84. package/plugins/domma-effects/public/celebrations/themes/christmas.js +1 -1805
  85. package/plugins/domma-effects/public/celebrations/themes/guy-fawkes.js +1 -1477
  86. package/plugins/domma-effects/public/celebrations/themes/halloween.js +1 -1837
  87. package/plugins/domma-effects/public/celebrations/themes/st-andrews.js +1 -1175
  88. package/plugins/domma-effects/public/celebrations/themes/st-davids.js +1 -1258
  89. package/plugins/domma-effects/public/celebrations/themes/st-georges.js +1 -1754
  90. package/plugins/domma-effects/public/celebrations/themes/st-patricks.js +1 -1290
  91. package/plugins/domma-effects/public/celebrations/themes/valentines.js +1 -1361
  92. package/plugins/example-analytics/stats.json +21 -12
  93. package/plugins/theme-roller/admin/templates/theme-roller.html +71 -0
  94. package/plugins/theme-roller/admin/views/theme-roller-view.js +403 -0
  95. package/plugins/theme-roller/config.js +1 -0
  96. package/plugins/theme-roller/plugin.js +233 -0
  97. package/plugins/theme-roller/plugin.json +31 -0
  98. package/plugins/theme-roller/public/active-theme.css +0 -0
  99. package/plugins/theme-roller/public/inject-head-late.html +1 -0
  100. package/public/css/forms.css +1 -0
  101. package/public/css/site.css +1 -302
  102. package/public/js/btt.js +1 -90
  103. package/public/js/cookie-consent.js +1 -61
  104. package/public/js/form-logic-engine.js +1 -0
  105. package/public/js/forms.js +1 -0
  106. package/public/js/site.js +1 -204
  107. package/scripts/build.js +194 -129
  108. package/scripts/pro.js +254 -0
  109. package/scripts/reset.js +33 -8
  110. package/scripts/seed.js +343 -78
  111. package/scripts/setup.js +5 -4
  112. package/server/middleware/auth.js +136 -97
  113. package/server/routes/api/actions.js +200 -0
  114. package/server/routes/api/auth.js +292 -116
  115. package/server/routes/api/blocks.js +84 -0
  116. package/server/routes/api/collections.js +88 -23
  117. package/{plugins/form-builder/plugin.js → server/routes/api/forms.js} +483 -505
  118. package/server/routes/api/layouts.js +49 -25
  119. package/server/routes/api/media.js +118 -93
  120. package/server/routes/api/navigation.js +40 -37
  121. package/server/routes/api/pages.js +132 -118
  122. package/server/routes/api/plugins.js +6 -3
  123. package/server/routes/api/settings.js +104 -89
  124. package/server/routes/api/users.js +27 -21
  125. package/server/routes/api/views.js +148 -0
  126. package/server/routes/public.js +124 -108
  127. package/server/server.js +269 -173
  128. package/server/services/actions.js +387 -0
  129. package/server/services/adapterRegistry.js +98 -0
  130. package/server/services/adapters/FileAdapter.js +192 -0
  131. package/server/services/adapters/MongoAdapter.js +220 -0
  132. package/server/services/blocks.js +162 -0
  133. package/server/services/collections.js +74 -86
  134. package/server/services/connectionManager.js +102 -0
  135. package/server/services/content.js +312 -307
  136. package/{plugins/form-builder → server/services}/email.js +126 -103
  137. package/server/services/forms.js +173 -0
  138. package/server/services/markdown.js +1378 -648
  139. package/server/services/permissionRegistry.js +173 -0
  140. package/server/services/presetCollections.js +251 -0
  141. package/server/services/renderer.js +75 -1
  142. package/server/services/roles.js +227 -0
  143. package/server/services/rowAccess.js +104 -0
  144. package/server/services/userProfiles.js +199 -0
  145. package/server/services/users.js +281 -212
  146. package/server/services/views.js +280 -0
  147. package/server/templates/page.html +119 -113
  148. package/plugins/form-builder/admin/templates/form-editor.html +0 -171
  149. package/plugins/form-builder/admin/templates/form-settings.html +0 -29
  150. package/plugins/form-builder/admin/views/form-editor.js +0 -1442
  151. package/plugins/form-builder/admin/views/form-settings.js +0 -38
  152. package/plugins/form-builder/admin/views/form-submissions.js +0 -295
  153. package/plugins/form-builder/admin/views/forms-list.js +0 -164
  154. package/plugins/form-builder/config.js +0 -9
  155. package/plugins/form-builder/data/forms/consent.json +0 -104
  156. package/plugins/form-builder/data/forms/contact-details.json +0 -63
  157. package/plugins/form-builder/data/forms/contacts.json +0 -66
  158. package/plugins/form-builder/data/submissions/consent.json +0 -13
  159. package/plugins/form-builder/data/submissions/contact-details.json +0 -1
  160. package/plugins/form-builder/data/submissions/contacts.json +0 -26
  161. package/plugins/form-builder/plugin.json +0 -52
  162. package/plugins/form-builder/public/form-logic-engine.js +0 -568
  163. package/plugins/form-builder/public/inject-body.html +0 -352
  164. package/plugins/form-builder/public/inject-head.html +0 -58
  165. package/plugins/form-builder/public/package.json +0 -1
  166. package/scripts/copy-domma.js +0 -48
package/admin/index.html CHANGED
@@ -10,6 +10,7 @@
10
10
  <link rel="stylesheet" href="/dist/domma/grid.css">
11
11
  <link rel="stylesheet" href="/dist/domma/elements.css">
12
12
  <link rel="stylesheet" href="/dist/domma/themes/domma-themes.css">
13
+ <link rel="stylesheet" href="/admin/dist/domma/domma-tools.css">
13
14
 
14
15
  <!-- Cropper.js - image editor -->
15
16
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/cropperjs@1/dist/cropper.min.css">
@@ -60,6 +61,9 @@
60
61
  <!-- DommaJS Syntax Highlighter (adds Domma.syntax) -->
61
62
  <script src="/dist/domma/domma-syntax.min.js"></script>
62
63
 
64
+ <!-- DommaJS Tools (adds E.editor, E.schemaBuilder etc.) -->
65
+ <script src="/admin/dist/domma/domma-tools.min.js"></script>
66
+
63
67
  <!-- Cropper.js - image editor -->
64
68
  <script src="https://cdn.jsdelivr.net/npm/cropperjs@1/dist/cropper.min.js"></script>
65
69
 
package/admin/js/api.js CHANGED
@@ -1,242 +1 @@
1
- /**
2
- * Admin API Client
3
- * Authenticated requests using Bearer tokens with automatic refresh on 401.
4
- * All existing api.* namespaces are preserved — views need no changes.
5
- */
6
-
7
- const BASE = '/api';
8
-
9
- // ---------------------------------------------------------------------------
10
- // Token helpers
11
- // ---------------------------------------------------------------------------
12
-
13
- function getToken() { return S.get('auth_token'); }
14
- function getRefreshToken() { return S.get('auth_refresh_token'); }
15
- function setToken(t) { S.set('auth_token', t); }
16
- function clearAuth() {
17
- S.remove('auth_token');
18
- S.remove('auth_refresh_token');
19
- S.remove('auth_user');
20
- }
21
-
22
- /**
23
- * Attempt to refresh the access token using the stored refresh token.
24
- * Clears auth and redirects to login if refresh fails.
25
- *
26
- * @returns {Promise<string>} New access token
27
- */
28
- async function refreshAccessToken() {
29
- const refreshToken = getRefreshToken();
30
- if (!refreshToken) throw new Error('No refresh token');
31
-
32
- const res = await fetch(`${BASE}/auth/refresh`, {
33
- method: 'POST',
34
- headers: { 'Content-Type': 'application/json' },
35
- body: JSON.stringify({ refreshToken })
36
- });
37
-
38
- if (!res.ok) {
39
- clearAuth();
40
- R.navigate('/login');
41
- throw new Error('Token refresh failed');
42
- }
43
-
44
- const { token } = await res.json();
45
- setToken(token);
46
- return token;
47
- }
48
-
49
- // ---------------------------------------------------------------------------
50
- // Core request function
51
- // ---------------------------------------------------------------------------
52
-
53
- /**
54
- * Make an authenticated API request.
55
- * Automatically retries once after refreshing the access token on 401.
56
- *
57
- * @param {string} endpoint
58
- * @param {RequestInit} options
59
- * @returns {Promise<any>}
60
- */
61
- async function apiRequest(endpoint, options = {}) {
62
- let token = getToken();
63
-
64
- const buildHeaders = (t) => ({
65
- ...(options.body !== undefined ? { 'Content-Type': 'application/json' } : {}),
66
- ...options.headers,
67
- ...(t ? { Authorization: `Bearer ${t}` } : {})
68
- });
69
-
70
- let res = await fetch(`${BASE}${endpoint}`, { ...options, headers: buildHeaders(token) });
71
-
72
- // On 401, try refreshing once
73
- if (res.status === 401 && getRefreshToken()) {
74
- try {
75
- token = await refreshAccessToken();
76
- res = await fetch(`${BASE}${endpoint}`, { ...options, headers: buildHeaders(token) });
77
- } catch {
78
- return; // Already redirected to login
79
- }
80
- }
81
-
82
- if (!res.ok) {
83
- const body = await res.json().catch(() => ({ error: 'Request failed' }));
84
- throw new Error(body.error || body.message || `HTTP ${res.status}`);
85
- }
86
-
87
- // 204 No Content
88
- if (res.status === 204) return null;
89
-
90
- return res.json();
91
- }
92
-
93
- /**
94
- * Upload a file with Bearer token (multipart — no Content-Type override).
95
- *
96
- * @param {string} endpoint
97
- * @param {FormData} formData
98
- * @returns {Promise<any>}
99
- */
100
- async function uploadRequest(endpoint, formData) {
101
- const token = getToken();
102
- const headers = token ? { Authorization: `Bearer ${token}` } : {};
103
-
104
- const res = await fetch(`${BASE}${endpoint}`, { method: 'POST', headers, body: formData });
105
- if (!res.ok) {
106
- const body = await res.json().catch(() => ({ error: 'Upload failed' }));
107
- throw new Error(body.error || body.message || `HTTP ${res.status}`);
108
- }
109
- return res.json();
110
- }
111
-
112
- // ---------------------------------------------------------------------------
113
- // Domain API namespaces
114
- // ---------------------------------------------------------------------------
115
-
116
- export const api = {
117
- // Auth
118
- auth: {
119
- setupStatus: () => apiRequest('/auth/setup-status', { method: 'GET' }),
120
- setup: (data) => apiRequest('/auth/setup', { method: 'POST', body: JSON.stringify(data) }),
121
- login: (data) => apiRequest('/auth/login', { method: 'POST', body: JSON.stringify(data) }),
122
- me: () => apiRequest('/auth/me', { method: 'GET' }),
123
- refresh: (refreshToken) => apiRequest('/auth/refresh', { method: 'POST', body: JSON.stringify({ refreshToken }) })
124
- },
125
-
126
- // Pages
127
- pages: {
128
- list: () => apiRequest('/pages', { method: 'GET' }),
129
- get: (urlPath) => apiRequest(`/pages${urlPath}`, { method: 'GET' }),
130
- create: (payload) => apiRequest('/pages', { method: 'POST', body: JSON.stringify(payload) }),
131
- update: (urlPath, payload) => apiRequest(`/pages${urlPath}`, { method: 'PUT', body: JSON.stringify(payload) }),
132
- delete: (urlPath) => apiRequest(`/pages${urlPath}`, {method: 'DELETE'}),
133
- preview: (markdown) => apiRequest('/pages/preview', {method: 'POST', body: JSON.stringify({markdown})})
134
- },
135
-
136
- // Settings
137
- settings: {
138
- get: () => apiRequest('/settings', { method: 'GET' }),
139
- save: (data) => apiRequest('/settings', { method: 'PUT', body: JSON.stringify(data) })
140
- },
141
-
142
- // Navigation
143
- navigation: {
144
- get: () => apiRequest('/navigation', { method: 'GET' }),
145
- save: (data) => apiRequest('/navigation', { method: 'PUT', body: JSON.stringify(data) })
146
- },
147
-
148
- // Layouts (presets)
149
- layouts: {
150
- get: () => apiRequest('/layouts', { method: 'GET' }),
151
- save: (data) => apiRequest('/layouts', { method: 'PUT', body: JSON.stringify(data) })
152
- },
153
-
154
- // Media
155
- media: {
156
- list: () => apiRequest('/media', { method: 'GET' }),
157
- upload: (formData) => uploadRequest('/media', formData),
158
- delete: (name) => apiRequest(`/media/${encodeURIComponent(name)}`, {method: 'DELETE'}),
159
- rename: (name, newName) => apiRequest(`/media/${encodeURIComponent(name)}`, {
160
- method: 'PATCH',
161
- body: JSON.stringify({newName})
162
- }),
163
- info: (name) => apiRequest(`/media/${encodeURIComponent(name)}/info`, {method: 'GET'}),
164
- transform: (name, body) => apiRequest(`/media/${encodeURIComponent(name)}/transform`, {
165
- method: 'POST',
166
- body: JSON.stringify(body)
167
- }),
168
- },
169
-
170
- // Users
171
- users: {
172
- list: () => apiRequest('/users', { method: 'GET' }),
173
- get: (id) => apiRequest(`/users/${id}`, { method: 'GET' }),
174
- create: (data) => apiRequest('/users', { method: 'POST', body: JSON.stringify(data) }),
175
- update: (id, data) => apiRequest(`/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
176
- delete: (id) => apiRequest(`/users/${id}`, { method: 'DELETE' })
177
- },
178
-
179
- // Plugins
180
- plugins: {
181
- list: () => apiRequest('/plugins', { method: 'GET' }),
182
- update: (name, data) => apiRequest(`/plugins/${name}`, { method: 'PUT', body: JSON.stringify(data) }),
183
- adminConfig: () => apiRequest('/plugins/admin-config', { method: 'GET' })
184
- },
185
-
186
- // Collections
187
- collections: {
188
- list: () => apiRequest('/collections', { method: 'GET' }),
189
- get: (slug) => apiRequest(`/collections/${slug}`, { method: 'GET' }),
190
- create: (data) => apiRequest('/collections', { method: 'POST', body: JSON.stringify(data) }),
191
- update: (slug, data) => apiRequest(`/collections/${slug}`, { method: 'PUT', body: JSON.stringify(data) }),
192
- delete: (slug) => apiRequest(`/collections/${slug}`, { method: 'DELETE' }),
193
-
194
- listEntries: (slug, params = {}) => {
195
- const qs = new URLSearchParams(params).toString();
196
- return apiRequest(`/collections/${slug}/entries${qs ? '?' + qs : ''}`, { method: 'GET' });
197
- },
198
- getEntry: (slug, id) => apiRequest(`/collections/${slug}/entries/${id}`, { method: 'GET' }),
199
- createEntry: (slug, data) => apiRequest(`/collections/${slug}/entries`, { method: 'POST', body: JSON.stringify({ data }) }),
200
- updateEntry: (slug, id, data) => apiRequest(`/collections/${slug}/entries/${id}`, { method: 'PUT', body: JSON.stringify({ data }) }),
201
- deleteEntry: (slug, id) => apiRequest(`/collections/${slug}/entries/${id}`, { method: 'DELETE' }),
202
- clearEntries: (slug) => apiRequest(`/collections/${slug}/entries`, { method: 'DELETE' }),
203
-
204
- import: (slug, entries) => apiRequest(`/collections/${slug}/import`, { method: 'POST', body: JSON.stringify({ entries }) }),
205
-
206
- // Public API (no auth needed when collection is public)
207
- publicList: (slug, params = {}) => {
208
- const qs = new URLSearchParams(params).toString();
209
- return apiRequest(`/collections/${slug}/public${qs ? '?' + qs : ''}`, { method: 'GET' });
210
- }
211
- },
212
-
213
- // Settings (extended)
214
- settingsExt: {
215
- testEmail: (to) => apiRequest('/settings/test-email', { method: 'POST', body: JSON.stringify({ to }) })
216
- }
217
- };
218
-
219
- // ---------------------------------------------------------------------------
220
- // Auth state helpers (used by app.js)
221
- // ---------------------------------------------------------------------------
222
-
223
- export function isAuthenticated() {
224
- return !!getToken();
225
- }
226
-
227
- export function getUser() {
228
- return S.get('auth_user');
229
- }
230
-
231
- export function setAuthData({ token, refreshToken, user }) {
232
- if (token) setToken(token);
233
- if (refreshToken) S.set('auth_refresh_token', refreshToken);
234
- if (user) S.set('auth_user', user);
235
- }
236
-
237
- export function logout() {
238
- clearAuth();
239
- R.navigate('/login');
240
- }
241
-
242
- export {apiRequest};
1
+ const r="/api";function a(){return S.get("auth_token")}function h(){return S.get("auth_refresh_token")}function m(e){S.set("auth_token",e)}function c(){S.remove("auth_token"),S.remove("auth_refresh_token"),S.remove("auth_user")}async function l(){const e=h();if(!e)throw new Error("No refresh token");const o=await fetch(`${r}/auth/refresh`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})});if(!o.ok)throw c(),R.navigate("/login"),new Error("Token refresh failed");const{token:s}=await o.json();return m(s),s}async function t(e,o={}){let s=a();const d=i=>({...o.body!==void 0?{"Content-Type":"application/json"}:{},...o.headers,...i?{Authorization:`Bearer ${i}`}:{}});let n=await fetch(`${r}${e}`,{...o,headers:d(s)});if(n.status===401&&h())try{s=await l(),n=await fetch(`${r}${e}`,{...o,headers:d(s)})}catch{return}if(!n.ok){const i=await n.json().catch(()=>({error:"Request failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.status===204?null:n.json()}async function u(e,o){const s=a(),d=s?{Authorization:`Bearer ${s}`}:{},n=await fetch(`${r}${e}`,{method:"POST",headers:d,body:o});if(!n.ok){const i=await n.json().catch(()=>({error:"Upload failed"}));throw new Error(i.error||i.message||`HTTP ${n.status}`)}return n.json()}export const api={auth:{setupStatus:()=>t("/auth/setup-status",{method:"GET"}),setup:e=>t("/auth/setup",{method:"POST",body:JSON.stringify(e)}),login:e=>t("/auth/login",{method:"POST",body:JSON.stringify(e)}),me:()=>t("/auth/me",{method:"GET"}),updateMe:e=>t("/auth/me",{method:"PUT",body:JSON.stringify(e)}),refresh:e=>t("/auth/refresh",{method:"POST",body:JSON.stringify({refreshToken:e})}),forgotPassword:e=>t("/auth/forgot-password",{method:"POST",body:JSON.stringify({email:e})}),resetPassword:(e,o)=>t("/auth/reset-password",{method:"POST",body:JSON.stringify({token:e,password:o})})},pages:{list:()=>t("/pages",{method:"GET"}),get:e=>t(`/pages${e}`,{method:"GET"}),create:e=>t("/pages",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/pages${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/pages${e}`,{method:"DELETE"}),preview:e=>t("/pages/preview",{method:"POST",body:JSON.stringify({markdown:e})}),tags:()=>t("/pages/tags",{method:"GET"}).then(e=>e.tags||[])},settings:{get:()=>t("/settings",{method:"GET"}),save:e=>t("/settings",{method:"PUT",body:JSON.stringify(e)})},navigation:{get:()=>t("/navigation",{method:"GET"}),save:e=>t("/navigation",{method:"PUT",body:JSON.stringify(e)})},layouts:{get:()=>t("/layouts",{method:"GET"}),save:e=>t("/layouts",{method:"PUT",body:JSON.stringify(e)}),getOptions:()=>t("/layouts/options",{method:"GET"}),saveOptions:e=>t("/layouts/options",{method:"PUT",body:JSON.stringify(e)})},media:{list:()=>t("/media",{method:"GET"}),upload:e=>u("/media",e),delete:e=>t(`/media/${encodeURIComponent(e)}`,{method:"DELETE"}),rename:(e,o)=>t(`/media/${encodeURIComponent(e)}`,{method:"PATCH",body:JSON.stringify({newName:o})}),info:e=>t(`/media/${encodeURIComponent(e)}/info`,{method:"GET"}),transform:(e,o)=>t(`/media/${encodeURIComponent(e)}/transform`,{method:"POST",body:JSON.stringify(o)})},users:{list:()=>t("/users",{method:"GET"}),get:e=>t(`/users/${e}`,{method:"GET"}),create:e=>t("/users",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/users/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/users/${e}`,{method:"DELETE"})},plugins:{list:()=>t("/plugins",{method:"GET"}),update:(e,o)=>t(`/plugins/${e}`,{method:"PUT",body:JSON.stringify(o)}),adminConfig:()=>t("/plugins/admin-config",{method:"GET"})},collections:{list:()=>t("/collections",{method:"GET"}),proStatus:()=>t("/collections/pro-status",{method:"GET"}),get:e=>t(`/collections/${e}`,{method:"GET"}),create:e=>t("/collections",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/collections/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/collections/${e}`,{method:"DELETE"}),listEntries:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/collections/${e}/entries${s?"?"+s:""}`,{method:"GET"})},getEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"GET"}),createEntry:(e,o)=>t(`/collections/${e}/entries`,{method:"POST",body:JSON.stringify({data:o})}),updateEntry:(e,o,s)=>t(`/collections/${e}/entries/${o}`,{method:"PUT",body:JSON.stringify({data:s})}),deleteEntry:(e,o)=>t(`/collections/${e}/entries/${o}`,{method:"DELETE"}),clearEntries:e=>t(`/collections/${e}/entries`,{method:"DELETE"}),import:(e,o)=>t(`/collections/${e}/import`,{method:"POST",body:JSON.stringify({entries:o})}),publicList:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/collections/${e}/public${s?"?"+s:""}`,{method:"GET"})},getConnections:()=>t("/collections/connections",{method:"GET"}),saveConnections:e=>t("/collections/connections",{method:"PUT",body:JSON.stringify(e)})},forms:{list:()=>t("/forms",{method:"GET"}),create:e=>t("/forms",{method:"POST",body:JSON.stringify(e)}),get:e=>t(`/forms/${e}`,{method:"GET"}),update:(e,o)=>t(`/forms/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/forms/${e}`,{method:"DELETE"}),listSubmissions:e=>t(`/forms/${e}/submissions`,{method:"GET"}),clearSubmissions:e=>t(`/forms/${e}/submissions`,{method:"DELETE"}),deleteSubmission:(e,o)=>t(`/forms/${e}/submissions/${o}`,{method:"DELETE"}),testEmail:e=>t("/forms/test-email",{method:"POST",body:JSON.stringify({to:e})})},views:{list:()=>t("/views",{method:"GET"}),get:e=>t(`/views/${e}`,{method:"GET"}),create:e=>t("/views",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/views/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/views/${e}`,{method:"DELETE"}),execute:(e,o={})=>{const s=new URLSearchParams(o).toString();return t(`/views/${e}/execute${s?"?"+s:""}`,{method:"GET"})},forCollection:e=>t(`/views/collection/${e}`,{method:"GET"})},actions:{list:()=>t("/actions",{method:"GET"}),get:e=>t(`/actions/${e}`,{method:"GET"}),create:e=>t("/actions",{method:"POST",body:JSON.stringify(e)}),update:(e,o)=>t(`/actions/${e}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/actions/${e}`,{method:"DELETE"}),execute:(e,o)=>t(`/actions/${e}/execute`,{method:"POST",body:JSON.stringify({entryId:o})}),forCollection:e=>t(`/actions/collection/${e}`,{method:"GET"}),checkAccess:(e,o)=>t(`/actions/${e}/check-access`,{method:"POST",body:JSON.stringify({entryIds:o})})},blocks:{list:()=>t("/blocks",{method:"GET"}),get:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"GET"}),put:(e,o)=>t(`/blocks/${encodeURIComponent(e)}`,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(`/blocks/${encodeURIComponent(e)}`,{method:"DELETE"})},get:e=>t(e,{method:"GET"}),post:(e,o)=>t(e,{method:"POST",body:JSON.stringify(o)}),put:(e,o)=>t(e,{method:"PUT",body:JSON.stringify(o)}),delete:e=>t(e,{method:"DELETE"}),themes:{list:()=>t("/plugins/theme-roller/themes",{method:"GET"})},settingsExt:{testEmail:e=>t("/settings/test-email",{method:"POST",body:JSON.stringify({to:e})})}};export function isAuthenticated(){return!!a()}export function getUser(){return S.get("auth_user")}export function setAuthData({token:e,refreshToken:o,user:s}){e&&m(e),o&&S.set("auth_refresh_token",o),s&&S.set("auth_user",s)}export function logout(){const e=h();e&&fetch(`${r}/auth/logout`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:e})}).catch(()=>{}),c(),R.navigate("/login")}export{t as apiRequest};
package/admin/js/app.js CHANGED
@@ -1,231 +1,11 @@
1
- /**
2
- * Domma CMS Admin - SPA Entry Point
3
- * Mirrors the domma-board candidate/app.js pattern.
4
- * Adds JWT auth guard, role-aware sidebar, and dynamic plugin loading.
5
- */
6
- import {getSidebarConfig} from './config/sidebar-config.js';
7
- import {views as coreViews} from './views/index.js';
8
- import {api, getUser, isAuthenticated, logout} from './api.js';
9
-
10
- $(() => {
11
- // Fetch admin theme preference from server config; fall back to charcoal-dark
12
- (async () => {
13
- try {
14
- const site = await api.settings.get();
15
- Domma.theme.init({ theme: site.adminTheme || 'charcoal-dark', persist: true });
16
- } catch {
17
- Domma.theme.init({ theme: 'charcoal-dark', persist: true });
18
- }
19
- })();
20
-
21
- // -------------------------------------------------------------------------
22
- // Auth guard — redirect to login unless already on /login
23
- // -------------------------------------------------------------------------
24
- R.use(async (to, from, next) => {
25
- if (to.path === '/login') return next();
26
-
27
- if (!isAuthenticated()) {
28
- R.navigate('/login');
29
- return;
30
- }
31
-
32
- next();
33
- });
34
-
35
- // -------------------------------------------------------------------------
36
- // Sidebar (role-aware — re-built after login lands on dashboard)
37
- // -------------------------------------------------------------------------
38
- let sidebar = null;
39
-
40
- async function fetchSidebarCounts() {
41
- if (!isAuthenticated()) return {};
42
- try {
43
- const [pages, media, users, plugins, collections] = await Promise.all([
44
- api.pages.list().catch(() => []),
45
- api.media.list().catch(() => []),
46
- api.users.list().catch(() => []),
47
- api.plugins.list().catch(() => []),
48
- api.collections.list().catch(() => [])
49
- ]);
50
- return {
51
- pages: pages.length,
52
- media: media.length,
53
- users: users.length,
54
- plugins: plugins.filter(p => p.enabled).length || plugins.length,
55
- collections: collections.length
56
- };
57
- } catch {
58
- return {};
59
- }
60
- }
61
-
62
- async function buildSidebar(role) {
63
- if (sidebar) {
64
- $('#admin-sidebar').empty();
65
- }
66
- const counts = await fetchSidebarCounts();
67
- sidebar = Domma.elements.sidebar('#admin-sidebar', {
68
- header: { title: 'CMS Admin', icon: 'layout' },
69
- items: getSidebarConfig(role, counts, pluginSidebarItems),
70
- collapsible: true,
71
- collapseAt: 992,
72
- push: true,
73
- contentSelector: '.dashboard-main',
74
- top: '60px'
75
- });
76
- attachSidebarTooltips();
77
- attachCollapsibleHeadings();
78
- }
79
-
80
- function attachSidebarTooltips() {
81
- $('#admin-sidebar .sidebar-link').each(function () {
82
- const label = $(this).find('.sidebar-text').text().trim();
83
- if (label) $(this).attr('data-tooltip', label);
84
- });
85
- E.tooltip('#admin-sidebar [data-tooltip]', { placement: 'right' });
86
- }
87
-
88
- function attachCollapsibleHeadings() {
89
- $('#admin-sidebar .sidebar-heading').each(function () {
90
- const $heading = $(this);
91
- $heading.addClass('sidebar-heading--collapsible');
92
- $heading.append('<span class="sidebar-heading-toggle"><span data-icon="chevron-down"></span></span>');
93
-
94
- $heading.on('click', function () {
95
- const collapsing = !$heading.hasClass('is-collapsed');
96
- $heading.toggleClass('is-collapsed', collapsing);
97
-
98
- let $el = $heading.next();
99
- while ($el.length && !$el.hasClass('sidebar-heading') && !$el.hasClass('sidebar-divider')) {
100
- $el.toggle(!collapsing);
101
- $el = $el.next();
102
- }
103
- });
104
- });
105
-
106
- Domma.icons.scan('#admin-sidebar');
107
- }
108
-
109
- // Build with no role initially (login page has no sidebar)
110
- buildSidebar(null);
111
-
112
- // Keep sidebar active item in sync with route
113
- M.subscribe('router:afterChange', ({ to }) => {
114
- if (sidebar && to.path !== '/login') {
115
- sidebar.setActive('#' + to.path);
116
- }
117
- });
118
-
119
- // -------------------------------------------------------------------------
120
- // Router — core routes + login
121
- // -------------------------------------------------------------------------
122
- const coreRoutes = [
123
- { path: '/', view: 'dashboard', title: 'Dashboard - Domma CMS' },
124
- { path: '/pages', view: 'pages', title: 'Pages - Domma CMS' },
125
- { path: '/pages/new', view: 'pageEditor', title: 'New Page - Domma CMS' },
126
- { path: '/pages/edit/*', view: 'pageEditor', title: 'Edit Page - Domma CMS' },
127
- { path: '/media', view: 'media', title: 'Media - Domma CMS' },
128
- { path: '/navigation', view: 'navigation', title: 'Navigation - Domma CMS' },
129
- { path: '/layouts', view: 'layouts', title: 'Layouts - Domma CMS' },
130
- { path: '/settings', view: 'settings', title: 'Settings - Domma CMS' },
131
- { path: '/users', view: 'users', title: 'Users - Domma CMS' },
132
- { path: '/users/new', view: 'userEditor', title: 'New User - Domma CMS' },
133
- { path: '/users/edit/:id', view: 'userEditor', title: 'Edit User - Domma CMS' },
134
- {path: '/plugins', view: 'plugins', title: 'Plugins - Domma CMS'},
135
- {path: '/documentation', view: 'documentation', title: 'Usage - Domma CMS'},
136
- {path: '/tutorials', view: 'tutorials', title: 'Tutorials - Domma CMS'},
137
- {path: '/collections', view: 'collections', title: 'Collections - Domma CMS'},
138
- {path: '/collections/new', view: 'collectionEditor', title: 'New Collection - Domma CMS'},
139
- {path: '/collections/edit/:slug', view: 'collectionEditor', title: 'Edit Collection - Domma CMS'},
140
- {path: '/collections/:slug/entries',view: 'collectionEntries', title: 'Entries - Domma CMS'},
141
- { path: '/login', view: 'login', title: 'Sign in - Domma CMS',
142
- onEnter: () => {
143
- $('#admin-sidebar').hide();
144
- $('#admin-topbar').hide();
145
- }
146
- }
147
- ];
148
-
149
- // After login, re-show chrome and rebuild sidebar with correct role
150
- M.subscribe('router:afterChange', ({ to, from }) => {
151
- if (to.path !== '/login') {
152
- $('#admin-sidebar').show();
153
- $('#admin-topbar').show();
154
- if (from?.path === '/login') {
155
- const user = getUser();
156
- if (user) buildSidebar(user.role);
157
- }
158
- injectUserMenu();
159
- }
160
- });
161
-
162
- // Reveal primary/danger buttons after each view renders
163
- M.subscribe('router:afterChange', () => {
164
- setTimeout(() => {
165
- if ($('.btn-primary, .btn-danger').length) {
166
- Domma.effects.reveal('.btn-primary, .btn-danger', {animation: 'fade', stagger: 40, duration: 300});
167
- }
168
- }, 50);
169
- });
170
-
171
- // Collapsible cards — event delegation handles all views including plugins
172
- $('#view-container').on('click', '.card-collapsible .card-header', function (e) {
173
- if ($(e.target).closest('button, a').length) return;
174
- $(this).closest('.card').toggleClass('card-collapsed');
175
- });
176
-
177
- // Global Ctrl+S / Cmd+S — click the primary save button in any view
178
- document.addEventListener('keydown', (e) => {
179
- if (!(e.ctrlKey || e.metaKey) || e.key !== 's') return;
180
- if (window.location.hash === '#/login') return;
181
- const saveBtn = document.querySelector('#view-container .view-header button.btn-primary');
182
- if (saveBtn) {
183
- e.preventDefault();
184
- saveBtn.click();
185
- }
186
- });
187
-
188
- // -------------------------------------------------------------------------
189
- // Dynamic plugin routes (loaded before R.init)
190
- // -------------------------------------------------------------------------
191
- const allViews = { ...coreViews };
192
- const allRoutes = [...coreRoutes];
193
- let pluginSidebarItems = [];
194
-
195
- async function loadPlugins() {
196
- if (!isAuthenticated()) return;
197
- try {
198
- const pluginConfig = await api.plugins.adminConfig();
199
- pluginSidebarItems = pluginConfig.sidebar || [];
200
- if (pluginConfig.routes?.length) allRoutes.push(...pluginConfig.routes);
201
- for (const [viewName, viewDef] of Object.entries(pluginConfig.views || {})) {
202
- try {
203
- const mod = await import(`/plugins/${viewDef.entry}`);
204
- allViews[viewName] = mod[viewDef.exportName];
205
- } catch { /* plugin view load failure is non-fatal */ }
206
- }
207
- } catch { /* plugins endpoint unreachable — continue without plugins */ }
208
- }
209
-
210
- // -------------------------------------------------------------------------
211
- // Inject user info + sign out into topbar
212
- // -------------------------------------------------------------------------
213
- function injectUserMenu() {
214
- const user = getUser();
215
- if (!user) return;
216
- if ($('#topbar-user-name').length) return; // already injected
217
-
218
- const roleLabelMap = {
219
- admin: 'Admin', manager: 'Manager', editor: 'Editor', subscriber: 'Subscriber'
220
- };
221
- const roleLabel = roleLabelMap[user.role] || user.role;
222
-
223
- $('#topbar-user').html(`
224
- <span id="topbar-user-name" class="topbar-user-name">${escapeHtml(user.name)}</span>
225
- <span class="topbar-role-badge topbar-role-badge--${escapeHtml(user.role)}">${roleLabel}</span>
226
- `);
227
-
228
- $('#topbar-actions').html(`
1
+ import{getSidebarConfig as L}from"./config/sidebar-config.js";import{views as U}from"./views/index.js";import{api as s,getUser as b,isAuthenticated as n,logout as V}from"./api.js";$(()=>{(async()=>{try{const t=n()?await s.settings.get():null;Domma.theme.init({theme:t?.adminTheme||"charcoal-dark",persist:!0})}catch{Domma.theme.init({theme:"charcoal-dark",persist:!0})}})(),R.use(async(t,a,e)=>{if(t.path==="/login"||t.path==="/reset-password")return e();if(!n()){R.navigate("/login");return}e()});let r=null;async function f(){if(!n())return{};try{const[t,a,e,i,o,c,D,y,k,P,N,x,A]=await Promise.all([s.pages.list().catch(()=>[]),s.media.list().catch(()=>[]),s.users.list().catch(()=>[]),s.plugins.list().catch(()=>[]),s.collections.list().catch(()=>[]),s.forms.list().catch(()=>[]),s.themes.list().catch(()=>[]),s.views.list().catch(()=>[]),s.actions.list().catch(()=>[]),s.blocks.list().catch(()=>[]),s.navigation.get().catch(()=>({})),s.layouts.get().catch(()=>({})),s.get("/collections/roles/entries?limit=100").catch(()=>({entries:[]}))]);return{pages:t.length,media:a.length,users:e.length,plugins:i.filter(W=>W.enabled).length||i.length,collections:o.length,forms:c.length,themes:D.length,views:y.length,actions:k.length,blocks:P.length,navigation:(N.items||[]).length,layouts:Object.keys(x).length,roles:(A.entries||[]).length}}catch{return{}}}async function m(){try{return(await s.get("/auth/permissions")).permissions||[]}catch{return[]}}async function d(t){r&&$("#admin-sidebar").empty();const a=await f(),e=h.map(i=>{if(!i.countKey)return i;const o=a[i.countKey];return{...i,badge:o!=null&&o>0?String(o):null}});r=Domma.elements.sidebar("#admin-sidebar",{header:{title:"CMS Admin",icon:"layout"},items:L(t,a,e),collapsible:!0,collapseAt:992,push:!0,contentSelector:".dashboard-main",top:"60px"}),v(),C()}function v(){$("#admin-sidebar .sidebar-link").each(function(){const t=$(this).find(".sidebar-text").text().trim();t&&$(this).attr("data-tooltip",t)}),E.tooltip("#admin-sidebar [data-tooltip]",{placement:"right"})}function C(){$("#admin-sidebar .sidebar-heading").each(function(){const t=$(this);t.addClass("sidebar-heading--collapsible"),t.append('<span class="sidebar-heading-toggle"><span data-icon="chevron-down"></span></span>'),t.on("click",function(){const a=!t.hasClass("is-collapsed");t.toggleClass("is-collapsed",a);let e=t.next();for(;e.length&&!e.hasClass("sidebar-heading")&&!e.hasClass("sidebar-divider");)e.toggle(!a),e=e.next()})}),Domma.icons.scan("#admin-sidebar")}M.subscribe("router:afterChange",({to:t})=>{r&&t.path!=="/login"&&t.path!=="/reset-password"&&r.setActive("#"+t.path)});const S=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/navigation",view:"navigation",title:"Navigation - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:a})=>{if(t.path!=="/login"&&t.path!=="/reset-password"){if($("#admin-sidebar").show(),$("#admin-topbar").show(),a?.path==="/login"||a?.path==="/reset-password"){await u();const e=await m();d(e)}g()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){$(t.target).closest("button, a").length||$(this).closest(".card").toggleClass("card-collapsed")}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login"||window.location.hash.startsWith("#/reset-password"))return;const a=document.querySelector("#view-container .view-header button.btn-primary");a&&(t.preventDefault(),a.click())});const p={...U},l=[...S];let h=[];async function u(){if(n())try{const t=await s.plugins.adminConfig();h=t.sidebar||[],t.routes?.length&&l.push(...t.routes);for(const[a,e]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${e.entry}`);p[a]=i[e.exportName]}catch{}}catch{}}function g(){const t=b();if(!t||$("#topbar-user-name").length)return;const e={admin:"Admin",manager:"Manager",editor:"Editor",subscriber:"Subscriber"}[t.role]||t.role;$("#topbar-user").html(`
2
+ <span id="topbar-user-name" class="topbar-user-name">${w(t.name)}</span>
3
+ <span class="topbar-role-badge topbar-role-badge--${w(t.role)}">${e}</span>
4
+ `),$("#topbar-actions").html(`
5
+ <a href="#/my-profile" class="topbar-action-link" data-tooltip="My Profile" data-tooltip-placement="bottom">
6
+ <span data-icon="user"></span>
7
+ <span>My Profile</span>
8
+ </a>
229
9
  <a href="#/settings" class="topbar-action-link" data-tooltip="Settings" data-tooltip-placement="bottom">
230
10
  <span data-icon="settings"></span>
231
11
  <span>Settings</span>
@@ -234,54 +14,4 @@ $(() => {
234
14
  <span data-icon="log-out"></span>
235
15
  <span>Sign out</span>
236
16
  </a>
237
- `);
238
-
239
- $('#topbar-logout-btn').on('click', (e) => { e.preventDefault(); logout(); });
240
- Domma.icons.scan('#admin-topbar');
241
- E.tooltip('#topbar-actions [data-tooltip]', { placement: 'bottom' });
242
- }
243
-
244
- function escapeHtml(str) {
245
- return String(str)
246
- .replace(/&/g, '&amp;')
247
- .replace(/</g, '&lt;')
248
- .replace(/>/g, '&gt;');
249
- }
250
-
251
- // -------------------------------------------------------------------------
252
- // Boot sequence
253
- // -------------------------------------------------------------------------
254
- (async () => {
255
- await loadPlugins();
256
-
257
- const user = getUser();
258
- if (user) {
259
- await buildSidebar(user.role);
260
- injectUserMenu();
261
- }
262
-
263
- R.init({
264
- container: '#view-container',
265
- routes: allRoutes,
266
- views: allViews,
267
- default: '/',
268
- transitions: { enter: 'fadeIn', leave: 'fadeOut', duration: 150 }
269
- });
270
-
271
- // Patch Domma router to support /* wildcard routes (multi-segment catch-all)
272
- const origExtract = R._extractParams.bind(R);
273
- R._extractParams = function(routePath, actualPath) {
274
- if (routePath.endsWith('/*')) {
275
- const prefix = routePath.slice(0, -2);
276
- return actualPath.startsWith(prefix + '/') ? {} : null;
277
- }
278
- return origExtract(routePath, actualPath);
279
- };
280
- // Only re-trigger if the current URL needs the wildcard patch (e.g. /pages/edit/about)
281
- const currentPath = (window.location.hash || '#/').slice(1) || '/';
282
- const needsWildcard = allRoutes
283
- .filter(r => r.path.endsWith('/*'))
284
- .some(r => currentPath.startsWith(r.path.slice(0, -2) + '/'));
285
- if (needsWildcard) R._handleRouteChange();
286
- })();
287
- });
17
+ `),$("#topbar-logout-btn").on("click",i=>{i.preventDefault(),V()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"})}function w(t){return String(t).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}(async()=>{if(!n()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash="#/login";else if(await u(),b()){const o=await m();await d(o),g()}R.init({container:"#view-container",routes:l,views:p,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}});const t=R._extractParams.bind(R);R._extractParams=function(i,o){if(i.endsWith("/*")){const c=i.slice(0,-2);return o.startsWith(c+"/")?{}:null}return t(i,o)};const a=(window.location.hash||"#/").slice(1)||"/";l.filter(i=>i.path.endsWith("/*")).some(i=>a.startsWith(i.path.slice(0,-2)+"/"))&&R._handleRouteChange()})()});