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
@@ -1,487 +1,3 @@
1
- /**
2
- * Collection Editor View
3
- * Create or edit a collection schema: Settings, Fields, and API Access tabs.
4
- * Field editor mirrors the form-builder pattern (same DOM ID convention) but
5
- * without page-break or spacer types — collections are pure data stores.
6
- */
7
- import { api } from '../api.js';
8
-
9
- const FIELD_TYPES = [
10
- { value: 'string', label: 'Text (single line)' },
11
- { value: 'email', label: 'Email' },
12
- { value: 'tel', label: 'Phone' },
13
- { value: 'number', label: 'Number' },
14
- { value: 'textarea', label: 'Textarea (multi-line)' },
15
- { value: 'select', label: 'Dropdown (select)' },
16
- { value: 'radio', label: 'Radio buttons' },
17
- { value: 'checkbox', label: 'Single checkbox' },
18
- { value: 'checkbox-group', label: 'Checkbox group' },
19
- { value: 'date', label: 'Date' },
20
- { value: 'time', label: 'Time' },
21
- { value: 'url', label: 'URL' },
22
- { value: 'hidden', label: 'Hidden field' }
23
- ];
24
-
25
- const OPTION_TYPES = new Set(['select', 'radio', 'checkbox-group']);
26
- const ACCESS_LEVELS = ['public', 'subscriber', 'editor', 'manager', 'admin'];
27
- const OPERATIONS = ['create', 'read', 'update', 'delete'];
28
-
29
- let fields = [];
30
- let collectionSlug = null;
31
- let isNew = true;
32
-
33
- function slugify(str) {
34
- return str.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
35
- }
36
-
37
- function getTypeLabel(type) {
38
- return FIELD_TYPES.find(t => t.value === type)?.label || type;
39
- }
40
-
41
- // ---------------------------------------------------------------------------
42
- // Collect current field state from DOM
43
- // ---------------------------------------------------------------------------
44
-
45
- function collectFieldFromDOM(idx) {
46
- const field = { ...fields[idx] };
47
-
48
- const label = document.getElementById(`fb-label-${idx}`);
49
- const name = document.getElementById(`fb-name-${idx}`);
50
- const type = document.getElementById(`fb-type-${idx}`);
51
- const required = document.getElementById(`fb-required-${idx}`);
52
- const placeholder = document.getElementById(`fb-placeholder-${idx}`);
53
- const helper = document.getElementById(`fb-helper-${idx}`);
54
-
55
- if (label) field.label = label.value.trim() || field.label;
56
- if (name) field.name = name.value.trim() || field.name;
57
- if (type) field.type = type.value || field.type;
58
- if (required) field.required = required.checked;
59
- if (placeholder) field.placeholder = placeholder.value.trim();
60
- if (helper) field.helper = helper.value.trim();
61
-
62
- if (OPTION_TYPES.has(field.type)) {
63
- const ta = document.getElementById(`fb-options-${idx}`);
64
- if (ta) {
65
- field.options = ta.value.split('\n').filter(l => l.trim()).map(line => {
66
- const [v, ...rest] = line.split(':');
67
- return { value: v.trim(), label: rest.join(':').trim() || v.trim() };
68
- });
69
- }
70
- }
71
-
72
- return field;
73
- }
74
-
75
- function collectAllFields() {
76
- return fields.map((_, idx) => collectFieldFromDOM(idx));
77
- }
78
-
79
- // ---------------------------------------------------------------------------
80
- // Build a field card DOM element
81
- // ---------------------------------------------------------------------------
82
-
83
- function buildFieldCard(field, idx) {
84
- const card = document.createElement('div');
85
- card.className = 'fb-field-card';
86
- card.dataset.index = idx;
87
- card.style.cssText = 'border:1px solid var(--border-color,#333);border-radius:8px;margin-bottom:.75rem;overflow:hidden;';
88
-
89
- // Header
90
- const header = document.createElement('div');
91
- header.className = 'fb-field-header';
92
- header.style.cssText = 'display:flex;align-items:center;gap:.5rem;padding:.6rem .75rem;background:var(--card-header-bg,rgba(255,255,255,.03));cursor:pointer;user-select:none;';
93
-
94
- const grip = document.createElement('span');
95
- grip.textContent = '⠿';
96
- grip.style.cssText = 'cursor:grab;opacity:.4;font-size:1.1rem;';
97
-
98
- const labelSpan = document.createElement('span');
99
- labelSpan.className = 'fb-field-summary';
100
- labelSpan.style.cssText = 'flex:1;font-weight:500;font-size:.9rem;';
101
- labelSpan.textContent = field.label || '(Untitled field)';
102
-
103
- const typeSpan = document.createElement('span');
104
- typeSpan.style.cssText = 'font-size:.75rem;opacity:.5;';
105
- typeSpan.textContent = getTypeLabel(field.type);
106
-
107
- const chevron = document.createElement('span');
108
- chevron.className = 'fb-field-chevron';
109
- chevron.textContent = '▾';
110
- chevron.style.cssText = 'opacity:.5;transition:transform .2s;';
111
-
112
- const delBtn = document.createElement('button');
113
- delBtn.type = 'button';
114
- delBtn.textContent = '×';
115
- delBtn.className = 'btn btn-sm';
116
- delBtn.style.cssText = 'padding:.15rem .45rem;line-height:1;font-size:1rem;opacity:.6;';
117
- delBtn.title = 'Remove field';
118
- delBtn.addEventListener('click', (e) => {
119
- e.stopPropagation();
120
- fields.splice(idx, 1);
121
- renderFields(document.getElementById('fields-list'));
122
- });
123
-
124
- header.appendChild(grip);
125
- header.appendChild(labelSpan);
126
- header.appendChild(typeSpan);
127
- header.appendChild(chevron);
128
- header.appendChild(delBtn);
129
-
130
- // Body
131
- const body = document.createElement('div');
132
- body.className = 'fb-field-body';
133
- body.style.cssText = 'padding:.75rem;display:none;';
134
-
135
- // Row 1: Label + Name + Type
136
- const row1 = document.createElement('div');
137
- row1.style.cssText = 'display:grid;grid-template-columns:1fr 1fr 1fr;gap:.6rem;margin-bottom:.6rem;';
138
-
139
- const labelWrap = document.createElement('div');
140
- const labelLbl = document.createElement('label');
141
- labelLbl.className = 'form-label';
142
- labelLbl.textContent = 'Label';
143
- const labelInput = document.createElement('input');
144
- labelInput.id = `fb-label-${idx}`;
145
- labelInput.type = 'text';
146
- labelInput.className = 'form-input';
147
- labelInput.value = field.label || '';
148
- labelInput.addEventListener('input', () => {
149
- labelSpan.textContent = labelInput.value.trim() || '(Untitled field)';
150
- const nameEl = document.getElementById(`fb-name-${idx}`);
151
- if (nameEl && !nameEl.dataset.manual) {
152
- nameEl.value = slugify(labelInput.value).replace(/-/g, '_');
153
- }
154
- });
155
- labelWrap.appendChild(labelLbl);
156
- labelWrap.appendChild(labelInput);
157
-
158
- const nameWrap = document.createElement('div');
159
- const nameLbl = document.createElement('label');
160
- nameLbl.className = 'form-label';
161
- nameLbl.textContent = 'Name (key)';
162
- const nameInput = document.createElement('input');
163
- nameInput.id = `fb-name-${idx}`;
164
- nameInput.type = 'text';
165
- nameInput.className = 'form-input';
166
- nameInput.value = field.name || '';
167
- nameInput.addEventListener('input', () => { nameInput.dataset.manual = '1'; });
168
- nameWrap.appendChild(nameLbl);
169
- nameWrap.appendChild(nameInput);
170
-
171
- const typeWrap = document.createElement('div');
172
- const typeLbl = document.createElement('label');
173
- typeLbl.className = 'form-label';
174
- typeLbl.textContent = 'Type';
175
- const typeSelect = document.createElement('select');
176
- typeSelect.id = `fb-type-${idx}`;
177
- typeSelect.className = 'form-input';
178
- FIELD_TYPES.forEach(ft => {
179
- const opt = document.createElement('option');
180
- opt.value = ft.value;
181
- opt.textContent = ft.label;
182
- if (ft.value === field.type) opt.selected = true;
183
- typeSelect.appendChild(opt);
184
- });
185
- typeSelect.addEventListener('change', () => {
186
- typeSpan.textContent = getTypeLabel(typeSelect.value);
187
- const optWrap = body.querySelector('.fb-options-wrap');
188
- if (optWrap) optWrap.style.display = OPTION_TYPES.has(typeSelect.value) ? '' : 'none';
189
- });
190
- typeWrap.appendChild(typeLbl);
191
- typeWrap.appendChild(typeSelect);
192
-
193
- row1.appendChild(labelWrap);
194
- row1.appendChild(nameWrap);
195
- row1.appendChild(typeWrap);
196
-
197
- // Row 2: Placeholder + Helper + Required
198
- const row2 = document.createElement('div');
199
- row2.style.cssText = 'display:grid;grid-template-columns:1fr 1fr auto;gap:.6rem;align-items:end;margin-bottom:.6rem;';
200
-
201
- const phWrap = document.createElement('div');
202
- const phLbl = document.createElement('label');
203
- phLbl.className = 'form-label';
204
- phLbl.textContent = 'Placeholder';
205
- const phInput = document.createElement('input');
206
- phInput.id = `fb-placeholder-${idx}`;
207
- phInput.type = 'text';
208
- phInput.className = 'form-input';
209
- phInput.value = field.placeholder || '';
210
- phWrap.appendChild(phLbl);
211
- phWrap.appendChild(phInput);
212
-
213
- const hlWrap = document.createElement('div');
214
- const hlLbl = document.createElement('label');
215
- hlLbl.className = 'form-label';
216
- hlLbl.textContent = 'Helper text';
217
- const hlInput = document.createElement('input');
218
- hlInput.id = `fb-helper-${idx}`;
219
- hlInput.type = 'text';
220
- hlInput.className = 'form-input';
221
- hlInput.value = field.helper || '';
222
- hlWrap.appendChild(hlLbl);
223
- hlWrap.appendChild(hlInput);
224
-
225
- const reqWrap = document.createElement('div');
226
- reqWrap.style.cssText = 'padding-bottom:.35rem;';
227
- const reqLabel = document.createElement('label');
228
- reqLabel.style.cssText = 'display:flex;align-items:center;gap:.4rem;cursor:pointer;white-space:nowrap;';
229
- const reqCheck = document.createElement('input');
230
- reqCheck.id = `fb-required-${idx}`;
231
- reqCheck.type = 'checkbox';
232
- reqCheck.checked = !!field.required;
233
- reqLabel.appendChild(reqCheck);
234
- reqLabel.appendChild(document.createTextNode('Required'));
235
- reqWrap.appendChild(reqLabel);
236
-
237
- row2.appendChild(phWrap);
238
- row2.appendChild(hlWrap);
239
- row2.appendChild(reqWrap);
240
-
241
- // Options (only for select/radio/checkbox-group)
242
- const optWrap = document.createElement('div');
243
- optWrap.className = 'fb-options-wrap';
244
- optWrap.style.display = OPTION_TYPES.has(field.type) ? '' : 'none';
245
- const optLbl = document.createElement('label');
246
- optLbl.className = 'form-label';
247
- optLbl.textContent = 'Options (one per line: value: Label)';
248
- const optTa = document.createElement('textarea');
249
- optTa.id = `fb-options-${idx}`;
250
- optTa.className = 'form-input';
251
- optTa.rows = 4;
252
- optTa.value = (field.options || []).map(o => `${o.value}: ${o.label}`).join('\n');
253
- optWrap.appendChild(optLbl);
254
- optWrap.appendChild(optTa);
255
-
256
- body.appendChild(row1);
257
- body.appendChild(row2);
258
- body.appendChild(optWrap);
259
-
260
- // Toggle expand/collapse
261
- header.addEventListener('click', () => {
262
- const expanded = body.style.display !== 'none';
263
- body.style.display = expanded ? 'none' : '';
264
- chevron.style.transform = expanded ? '' : 'rotate(180deg)';
265
- });
266
-
267
- card.appendChild(header);
268
- card.appendChild(body);
269
- return card;
270
- }
271
-
272
- // ---------------------------------------------------------------------------
273
- // Render all fields
274
- // ---------------------------------------------------------------------------
275
-
276
- function renderFields(listEl) {
277
- if (!listEl) return;
278
- listEl.textContent = '';
279
-
280
- if (fields.length === 0) {
281
- const msg = document.createElement('p');
282
- msg.className = 'text-muted';
283
- msg.id = 'fields-empty-msg';
284
- msg.style.cssText = 'text-align:center;padding:2rem 0;';
285
- msg.textContent = 'No fields yet. Click "Add Field" to get started.';
286
- listEl.appendChild(msg);
287
- return;
288
- }
289
-
290
- fields.forEach((field, idx) => {
291
- listEl.appendChild(buildFieldCard(field, idx));
292
- });
293
- }
294
-
295
- // ---------------------------------------------------------------------------
296
- // Build API Access rows
297
- // ---------------------------------------------------------------------------
298
-
299
- function buildApiAccessRows(apiConfig, containerEl) {
300
- containerEl.textContent = '';
301
-
302
- OPERATIONS.forEach(op => {
303
- const access = apiConfig?.[op] || { enabled: false, access: 'admin' };
304
-
305
- const row = document.createElement('div');
306
- row.style.cssText = 'display:grid;grid-template-columns:140px 1fr 160px;gap:.75rem;align-items:center;padding:.6rem 0;border-bottom:1px solid var(--border-color,#333);';
307
-
308
- const opLabel = document.createElement('strong');
309
- opLabel.textContent = op.charAt(0).toUpperCase() + op.slice(1);
310
- opLabel.style.cssText = 'font-size:.9rem;';
311
-
312
- const toggleLabel = document.createElement('label');
313
- toggleLabel.style.cssText = 'display:flex;align-items:center;gap:.45rem;cursor:pointer;font-size:.875rem;';
314
- const toggleCheck = document.createElement('input');
315
- toggleCheck.type = 'checkbox';
316
- toggleCheck.id = `api-${op}-enabled`;
317
- toggleCheck.checked = !!access.enabled;
318
- toggleLabel.appendChild(toggleCheck);
319
- toggleLabel.appendChild(document.createTextNode('Enable public access'));
320
-
321
- const accessSelect = document.createElement('select');
322
- accessSelect.id = `api-${op}-access`;
323
- accessSelect.className = 'form-input';
324
- ACCESS_LEVELS.forEach(level => {
325
- const opt = document.createElement('option');
326
- opt.value = level;
327
- opt.textContent = level.charAt(0).toUpperCase() + level.slice(1);
328
- if (level === access.access) opt.selected = true;
329
- accessSelect.appendChild(opt);
330
- });
331
-
332
- row.appendChild(opLabel);
333
- row.appendChild(toggleLabel);
334
- row.appendChild(accessSelect);
335
- containerEl.appendChild(row);
336
- });
337
- }
338
-
339
- function collectApiAccess() {
340
- const result = {};
341
- OPERATIONS.forEach(op => {
342
- const enabled = document.getElementById(`api-${op}-enabled`)?.checked ?? false;
343
- const access = document.getElementById(`api-${op}-access`)?.value || 'admin';
344
- result[op] = { enabled, access };
345
- });
346
- return result;
347
- }
348
-
349
- // ---------------------------------------------------------------------------
350
- // View export
351
- // ---------------------------------------------------------------------------
352
-
353
- export const collectionEditorView = {
354
- templateUrl: '/admin/js/templates/collection-editor.html',
355
-
356
- async onMount($container) {
357
- fields = [];
358
- collectionSlug = null;
359
- isNew = true;
360
-
361
- // Detect edit mode from URL
362
- const hash = window.location.hash;
363
- const match = hash.match(/\/collections\/edit\/([^/?#]+)/);
364
- if (match) {
365
- collectionSlug = match[1];
366
- isNew = false;
367
- }
368
-
369
- E.tabs($container.find('#collection-tabs').get(0));
370
-
371
- const listEl = $container.find('#fields-list').get(0);
372
- const apiContainer = $container.find('#api-access-rows').get(0);
373
-
374
- let apiConfig = {
375
- create: { enabled: false, access: 'admin' },
376
- read: { enabled: true, access: 'public' },
377
- update: { enabled: false, access: 'admin' },
378
- delete: { enabled: false, access: 'admin' }
379
- };
380
-
381
- if (!isNew) {
382
- try {
383
- const schema = await api.collections.get(collectionSlug);
384
- if (!schema) {
385
- E.toast('Collection not found.', { type: 'error' });
386
- R.navigate('/collections');
387
- return;
388
- }
389
-
390
- const titleText = $container.find('#editor-title-text').get(0);
391
- if (titleText) titleText.textContent = schema.title;
392
-
393
- $container.find('#field-title').val(schema.title || '');
394
- $container.find('#field-slug').val(schema.slug || '');
395
- $container.find('#field-slug').prop('readonly', true);
396
- $container.find('#slug-hint').get(0).textContent = 'Slug cannot be changed after creation.';
397
- $container.find('#field-description').val(schema.description || '');
398
-
399
- fields = schema.fields || [];
400
- apiConfig = schema.api || apiConfig;
401
- } catch {
402
- E.toast('Failed to load collection.', { type: 'error' });
403
- R.navigate('/collections');
404
- return;
405
- }
406
- } else {
407
- // Auto-populate slug from title input
408
- const titleInput = $container.find('#field-title').get(0);
409
- const slugInput = $container.find('#field-slug').get(0);
410
- if (titleInput && slugInput) {
411
- titleInput.addEventListener('input', () => {
412
- if (!slugInput.dataset.manual) {
413
- slugInput.value = slugify(titleInput.value);
414
- }
415
- });
416
- slugInput.addEventListener('input', () => { slugInput.dataset.manual = '1'; });
417
- }
418
- }
419
-
420
- renderFields(listEl);
421
- buildApiAccessRows(apiConfig, apiContainer);
422
-
423
- // Add field button
424
- $container.find('#add-field-btn').off('click').on('click', () => {
425
- fields = collectAllFields();
426
- fields.push({
427
- id: `field-${Date.now()}`,
428
- name: '',
429
- label: '',
430
- type: 'string',
431
- required: false,
432
- placeholder: '',
433
- helper: '',
434
- options: [],
435
- validation: [],
436
- logic: null
437
- });
438
- renderFields(listEl);
439
- // Auto-expand and focus the new card
440
- const cards = listEl.querySelectorAll('.fb-field-card');
441
- if (cards.length) {
442
- const last = cards[cards.length - 1];
443
- const body = last.querySelector('.fb-field-body');
444
- const chev = last.querySelector('.fb-field-chevron');
445
- if (body) body.style.display = '';
446
- if (chev) chev.style.transform = 'rotate(180deg)';
447
- last.querySelector(`#fb-label-${fields.length - 1}`)?.focus();
448
- }
449
- });
450
-
451
- // Save button
452
- $container.find('#save-collection-btn').off('click').on('click', async () => {
453
- const title = $container.find('#field-title').val().trim();
454
- const slug = $container.find('#field-slug').val().trim();
455
- const description = $container.find('#field-description').val().trim();
456
-
457
- if (!title) {
458
- E.toast('Title is required.', { type: 'warning' });
459
- return;
460
- }
461
-
462
- const finalFields = collectAllFields();
463
- const finalApi = collectApiAccess();
464
-
465
- const $btn = $container.find('#save-collection-btn');
466
- $btn.prop('disabled', true);
467
- try {
468
- if (isNew) {
469
- const created = await api.collections.create({ title, slug, description, fields: finalFields, api: finalApi });
470
- collectionSlug = created.slug;
471
- isNew = false;
472
- E.toast('Collection created.', { type: 'success' });
473
- R.navigate(`/collections/edit/${created.slug}`);
474
- } else {
475
- await api.collections.update(collectionSlug, { title, description, fields: finalFields, api: finalApi });
476
- E.toast('Collection saved.', { type: 'success' });
477
- }
478
- } catch (err) {
479
- E.toast(err.message || 'Failed to save.', { type: 'error' });
480
- } finally {
481
- $btn.prop('disabled', false);
482
- }
483
- });
484
-
485
- Domma.icons.scan();
486
- }
487
- };
1
+ import{api as D}from"../api.js";const Q=[{value:"string",label:"Text (single line)"},{value:"email",label:"Email"},{value:"tel",label:"Phone"},{value:"number",label:"Number"},{value:"textarea",label:"Textarea (multi-line)"},{value:"select",label:"Dropdown (select)"},{value:"radio",label:"Radio buttons"},{value:"checkbox",label:"Single checkbox"},{value:"checkbox-group",label:"Checkbox group"},{value:"date",label:"Date"},{value:"time",label:"Time"},{value:"url",label:"URL"},{value:"hidden",label:"Hidden field"}],J=new Set(["select","radio","checkbox-group"]),le=["public","subscriber","editor","manager","admin"],X=["create","read","update","delete"];let f=[],x=null,T=!0;function Z(e){return e.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-|-$/g,"")}function $(e){return Q.find(t=>t.value===e)?.label||e}function ne(e){const t={...f[e]},o=document.getElementById(`fb-label-${e}`),n=document.getElementById(`fb-name-${e}`),i=document.getElementById(`fb-type-${e}`),p=document.getElementById(`fb-required-${e}`),r=document.getElementById(`fb-placeholder-${e}`),l=document.getElementById(`fb-helper-${e}`);if(o&&(t.label=o.value.trim()||t.label),n&&(t.name=n.value.trim()||t.name),i&&(t.type=i.value||t.type),p&&(t.required=p.checked),r&&(t.placeholder=r.value.trim()),l&&(t.helper=l.value.trim()),J.has(t.type)){const c=document.getElementById(`fb-options-${e}`);c&&(t.options=c.value.split(`
2
+ `).filter(u=>u.trim()).map(u=>{const[g,...m]=u.split(":");return{value:g.trim(),label:m.join(":").trim()||g.trim()}}))}const a=document.getElementById(`fb-span-${e}`);if(document.getElementById(`fb-fullwidth-${e}`)?.checked)t.fullWidth=!0,delete t.span;else{delete t.fullWidth;const c=parseInt(a?.value,10);c>1?t.span=c:delete t.span}return t}function ee(){return f.map((e,t)=>ne(t))}function ae(e,t){const o=document.createElement("div");o.className="fb-field-card",o.dataset.index=t,o.style.cssText="border:1px solid var(--border-color,#333);border-radius:8px;margin-bottom:.75rem;overflow:hidden;";const n=document.createElement("div");n.className="fb-field-header",n.style.cssText="display:flex;align-items:center;gap:.5rem;padding:.6rem .75rem;background:var(--card-header-bg,rgba(255,255,255,.03));cursor:pointer;user-select:none;";const i=document.createElement("span");i.textContent="\u283F",i.style.cssText="cursor:grab;opacity:.4;font-size:1.1rem;";const p=document.createElement("span");p.className="fb-field-summary",p.style.cssText="flex:1;font-weight:500;font-size:.9rem;",p.textContent=e.label||"(Untitled field)";const r=document.createElement("span");r.style.cssText="font-size:.75rem;opacity:.5;",r.textContent=$(e.type);const l=document.createElement("span");l.className="fb-field-chevron",l.textContent="\u25BE",l.style.cssText="opacity:.5;transition:transform .2s;";const a=document.createElement("button");a.type="button",a.textContent="\xD7",a.className="btn btn-sm",a.style.cssText="padding:.15rem .45rem;line-height:1;font-size:1rem;opacity:.6;",a.title="Remove field",a.addEventListener("click",d=>{d.stopPropagation(),f.splice(t,1),K(document.getElementById("fields-list"))}),n.appendChild(i),n.appendChild(p),n.appendChild(r),n.appendChild(l),n.appendChild(a);const s=document.createElement("div");s.className="fb-field-body",s.style.cssText="padding:.75rem;display:none;";const c=document.createElement("div");c.style.cssText="display:grid;grid-template-columns:1fr 1fr 1fr;gap:.6rem;margin-bottom:.6rem;";const u=document.createElement("div"),g=document.createElement("label");g.className="form-label",g.textContent="Label";const m=document.createElement("input");m.id=`fb-label-${t}`,m.type="text",m.className="form-input",m.value=e.label||"",m.addEventListener("input",()=>{p.textContent=m.value.trim()||"(Untitled field)";const d=document.getElementById(`fb-name-${t}`);d&&!d.dataset.manual&&(d.value=Z(m.value).replace(/-/g,"_"))}),u.appendChild(g),u.appendChild(m);const w=document.createElement("div"),h=document.createElement("label");h.className="form-label",h.textContent="Name (key)";const y=document.createElement("input");y.id=`fb-name-${t}`,y.type="text",y.className="form-input",y.value=e.name||"",y.addEventListener("input",()=>{y.dataset.manual="1"}),w.appendChild(h),w.appendChild(y);const z=document.createElement("div"),P=document.createElement("label");P.className="form-label",P.textContent="Type";const v=document.createElement("select");v.id=`fb-type-${t}`,v.className="form-input",Q.forEach(d=>{const F=document.createElement("option");F.value=d.value,F.textContent=d.label,d.value===e.type&&(F.selected=!0),v.appendChild(F)}),v.addEventListener("change",()=>{r.textContent=$(v.value);const d=s.querySelector(".fb-options-wrap");d&&(d.style.display=J.has(v.value)?"":"none")}),z.appendChild(P),z.appendChild(v),c.appendChild(u),c.appendChild(w),c.appendChild(z);const N=document.createElement("div");N.style.cssText="display:grid;grid-template-columns:1fr 1fr auto;gap:.6rem;align-items:end;margin-bottom:.6rem;";const O=document.createElement("div"),U=document.createElement("label");U.className="form-label",U.textContent="Placeholder";const I=document.createElement("input");I.id=`fb-placeholder-${t}`,I.type="text",I.className="form-input",I.value=e.placeholder||"",O.appendChild(U),O.appendChild(I);const M=document.createElement("div"),_=document.createElement("label");_.className="form-label",_.textContent="Helper text";const k=document.createElement("input");k.id=`fb-helper-${t}`,k.type="text",k.className="form-input",k.value=e.helper||"",M.appendChild(_),M.appendChild(k);const j=document.createElement("div");j.style.cssText="padding-bottom:.35rem;";const B=document.createElement("label");B.style.cssText="display:flex;align-items:center;gap:.4rem;cursor:pointer;white-space:nowrap;";const q=document.createElement("input");q.id=`fb-required-${t}`,q.type="checkbox",q.checked=!!e.required,B.appendChild(q),B.appendChild(document.createTextNode("Required")),j.appendChild(B),N.appendChild(O),N.appendChild(M),N.appendChild(j);const L=document.createElement("div");L.className="fb-options-wrap",L.style.display=J.has(e.type)?"":"none";const V=document.createElement("label");V.className="form-label",V.textContent="Options (one per line: value: Label)";const S=document.createElement("textarea");S.id=`fb-options-${t}`,S.className="form-input",S.rows=4,S.value=(e.options||[]).map(d=>typeof d=="string"?`${d}: ${d}`:`${d.value??""}: ${d.label??d.value??""}`).join(`
3
+ `),L.appendChild(V),L.appendChild(S);const b=document.createElement("div");b.className="fb-grid-row",b.style.gridTemplateColumns="1fr auto",b.style.gap=".6rem",b.style.alignItems="end",b.style.marginBottom=".6rem",b.style.display=document.getElementById("collection-layout")?.value==="grid"?"grid":"none";const H=document.createElement("div"),Y=document.createElement("label");Y.className="form-label",Y.textContent="Column Span";const C=document.createElement("input");C.id=`fb-span-${t}`,C.type="number",C.className="form-input",C.min="1",C.max="6",C.value=e.span>1?String(e.span):"1",H.appendChild(Y),H.appendChild(C);const G=document.createElement("div");G.style.cssText="padding-bottom:.35rem;";const W=document.createElement("label");W.style.cssText="display:flex;align-items:center;gap:.4rem;cursor:pointer;white-space:nowrap;";const A=document.createElement("input");return A.id=`fb-fullwidth-${t}`,A.type="checkbox",A.checked=!!e.fullWidth,W.appendChild(A),W.appendChild(document.createTextNode("Full Width")),G.appendChild(W),b.appendChild(H),b.appendChild(G),s.appendChild(c),s.appendChild(N),s.appendChild(L),s.appendChild(b),n.addEventListener("click",()=>{const d=s.style.display!=="none";s.style.display=d?"none":"",l.style.transform=d?"":"rotate(180deg)"}),o.appendChild(n),o.appendChild(s),o}function K(e){if(e){if(e.textContent="",f.length===0){const t=document.createElement("p");t.className="text-muted",t.id="fields-empty-msg",t.style.cssText="text-align:center;padding:2rem 0;",t.textContent='No fields yet. Click "Add Field" to get started.',e.appendChild(t);return}f.forEach((t,o)=>{e.appendChild(ae(t,o))})}}function oe(e,t){t.textContent="",X.forEach(o=>{const n=e?.[o]||{enabled:!1,access:"admin"},i=document.createElement("div");i.style.cssText="display:grid;grid-template-columns:140px 1fr 160px;gap:.75rem;align-items:center;padding:.6rem 0;border-bottom:1px solid var(--border-color,#333);";const p=document.createElement("strong");p.textContent=o.charAt(0).toUpperCase()+o.slice(1),p.style.cssText="font-size:.9rem;";const r=document.createElement("label");r.style.cssText="display:flex;align-items:center;gap:.45rem;cursor:pointer;font-size:.875rem;";const l=document.createElement("input");l.type="checkbox",l.id=`api-${o}-enabled`,l.checked=!!n.enabled,r.appendChild(l),r.appendChild(document.createTextNode("Enable public access"));const a=document.createElement("select");a.id=`api-${o}-access`,a.className="form-input",le.forEach(s=>{const c=document.createElement("option");c.value=s,c.textContent=s.charAt(0).toUpperCase()+s.slice(1),s===n.access&&(c.selected=!0),a.appendChild(c)}),i.appendChild(p),i.appendChild(r),i.appendChild(a),t.appendChild(i)})}function se(e,t){E.dropdown("#storage-adapter-trigger",{items:[{label:"File (default)",value:"file"},{label:"MongoDB",value:"mongodb"}],onSelect:({item:n})=>{e.find("#storage-adapter").val(n.value),e.find("#storage-adapter-label").text(n.label);const i=n.value==="mongodb";e.find("#storage-connection-group").toggle(i),e.find("#storage-migration-warning").toggle(i&&!T)}});const o=t.map(n=>({label:n,value:n}));E.dropdown("#storage-connection-trigger",{items:o.length?o:[{label:"default",value:"default"}],onSelect:({item:n})=>{e.find("#storage-connection").val(n.value),e.find("#storage-connection-label").text(n.label)}})}function te(){return(document.getElementById("storage-adapter")?.value||"file")==="mongodb"?{adapter:"mongodb",connection:document.getElementById("storage-connection")?.value||"default"}:{adapter:"file"}}function ce(){const e={};return X.forEach(t=>{const o=document.getElementById(`api-${t}-enabled`)?.checked??!1,n=document.getElementById(`api-${t}-access`)?.value||"admin";e[t]={enabled:o,access:n}}),e}export const collectionEditorView={templateUrl:"/admin/js/templates/collection-editor.html",async onMount(e){f=[],x=null,T=!0;const o=window.location.hash.match(/\/collections\/edit\/([^/?#]+)/);o&&(x=o[1],T=!1),E.tabs(e.find("#collection-tabs").get(0)),e.find("#collection-layout").get(0)?.addEventListener("change",function(){const l=this.value==="grid";e.find("#collection-columns-group").get(0).style.display=l?"":"none",document.querySelectorAll(".fb-grid-row").forEach(a=>{a.style.display=l?"grid":"none"})});const n=e.find("#fields-list").get(0),i=e.find("#api-access-rows").get(0),p=await D.collections.proStatus();p?.pro&&x!=="roles"&&(e.find("#storage-tab-btn").show(),se(e,p.connections));let r={create:{enabled:!1,access:"admin"},read:{enabled:!0,access:"public"},update:{enabled:!1,access:"admin"},delete:{enabled:!1,access:"admin"}};if(T){const l=e.find("#field-title").get(0),a=e.find("#field-slug").get(0);l&&a&&(l.addEventListener("input",()=>{a.dataset.manual||(a.value=Z(l.value))}),a.addEventListener("input",()=>{a.dataset.manual="1"}))}else try{const l=await D.collections.get(x);if(!l){E.toast("Collection not found.",{type:"error"}),R.navigate("/collections");return}const a=e.find("#editor-title-text").get(0);a&&(a.textContent=l.title),e.find("#field-title").val(l.title||""),e.find("#field-slug").val(l.slug||""),e.find("#field-slug").prop("readonly",!0),e.find("#slug-hint").get(0).textContent="Slug cannot be changed after creation.",e.find("#field-description").val(l.description||""),e.find("#collection-layout").val(l.layout||"stacked"),e.find("#collection-columns").val(l.columns||2),e.find("#collection-columns-group").get(0).style.display=l.layout==="grid"?"":"none",f=l.fields||[],r=l.api||r,l.storage&&(e.find("#storage-adapter").val(l.storage.adapter||"file"),e.find("#storage-adapter-label").text(l.storage.adapter==="mongodb"?"MongoDB":"File (default)"),l.storage.adapter==="mongodb"&&(e.find("#storage-connection-group").show(),e.find("#storage-connection").val(l.storage.connection||"default"),e.find("#storage-connection-label").text(l.storage.connection||"default"))),x==="roles"&&e.find("#storage-tab-btn").hide()}catch{E.toast("Failed to load collection.",{type:"error"}),R.navigate("/collections");return}K(n),oe(r,i),e.find("#add-field-btn").off("click").on("click",()=>{f=ee(),f.push({id:`field-${Date.now()}`,name:"",label:"",type:"string",required:!1,placeholder:"",helper:"",options:[],validation:[],logic:null}),K(n);const l=n.querySelectorAll(".fb-field-card");if(l.length){const a=l[l.length-1],s=a.querySelector(".fb-field-body"),c=a.querySelector(".fb-field-chevron");s&&(s.style.display=""),c&&(c.style.transform="rotate(180deg)"),a.querySelector(`#fb-label-${f.length-1}`)?.focus()}}),e.find("#save-collection-btn").off("click").on("click",async()=>{const l=e.find("#field-title").val().trim(),a=e.find("#field-slug").val().trim(),s=e.find("#field-description").val().trim();if(!l){E.toast("Title is required.",{type:"warning"});return}const c=ee(),u=ce(),g=e.find("#collection-layout").val()||"stacked",m=parseInt(e.find("#collection-columns").val(),10)||2,w=e.find("#save-collection-btn");w.prop("disabled",!0);try{if(T){const h=await D.collections.create({title:l,slug:a,description:s,layout:g,columns:m,fields:c,api:u,storage:te()});x=h.slug,T=!1,E.toast("Collection created.",{type:"success"}),R.navigate(`/collections/edit/${h.slug}`)}else await D.collections.update(x,{title:l,description:s,layout:g,columns:m,fields:c,api:u,storage:te()}),E.toast("Collection saved.",{type:"success"})}catch(h){E.toast(h.message||"Failed to save.",{type:"error"})}finally{w.prop("disabled",!1)}}),Domma.icons.scan()}};