domma-cms 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/admin/css/admin.css +78 -1
- package/admin/js/api.js +32 -0
- package/admin/js/app.js +24 -7
- package/admin/js/config/sidebar-config.js +8 -0
- package/admin/js/templates/collection-editor.html +80 -0
- package/admin/js/templates/collection-entries.html +36 -0
- package/admin/js/templates/collections.html +12 -0
- package/admin/js/templates/documentation.html +136 -0
- package/admin/js/templates/navigation.html +26 -4
- package/admin/js/templates/page-editor.html +91 -85
- package/admin/js/templates/settings.html +433 -172
- package/admin/js/views/collection-editor.js +487 -0
- package/admin/js/views/collection-entries.js +484 -0
- package/admin/js/views/collections.js +153 -0
- package/admin/js/views/dashboard.js +14 -6
- package/admin/js/views/index.js +9 -3
- package/admin/js/views/login.js +3 -2
- package/admin/js/views/navigation.js +77 -11
- package/admin/js/views/page-editor.js +207 -25
- package/admin/js/views/pages.js +14 -6
- package/admin/js/views/settings.js +137 -2
- package/admin/js/views/users.js +10 -7
- package/bin/cli.js +53 -17
- package/config/auth.json +2 -1
- package/config/content.json +1 -0
- package/config/navigation.json +14 -4
- package/config/plugins.json +0 -18
- package/config/presets.json +4 -8
- package/config/site.json +44 -3
- package/package.json +6 -2
- package/plugins/domma-effects/admin/templates/domma-effects.html +92 -3
- package/plugins/domma-effects/plugin.js +125 -0
- package/plugins/domma-effects/public/inject-body.html +19 -0
- package/plugins/example-analytics/admin/views/analytics.js +2 -2
- package/plugins/example-analytics/plugin.json +8 -0
- package/plugins/example-analytics/stats.json +15 -1
- package/plugins/form-builder/admin/templates/form-editor.html +19 -6
- package/plugins/form-builder/admin/views/form-editor.js +634 -9
- package/plugins/form-builder/admin/views/form-submissions.js +4 -4
- package/plugins/form-builder/admin/views/forms-list.js +5 -5
- package/plugins/form-builder/data/forms/consent.json +104 -0
- package/plugins/form-builder/data/forms/contacts.json +66 -0
- package/plugins/form-builder/data/submissions/consent.json +13 -0
- package/plugins/form-builder/data/submissions/contacts.json +26 -0
- package/plugins/form-builder/plugin.js +62 -11
- package/plugins/form-builder/plugin.json +12 -16
- package/plugins/form-builder/public/form-logic-engine.js +568 -0
- package/plugins/form-builder/public/inject-body.html +88 -6
- package/plugins/form-builder/public/inject-head.html +16 -0
- package/plugins/form-builder/public/package.json +1 -0
- package/public/css/site.css +113 -0
- package/public/js/btt.js +90 -0
- package/public/js/cookie-consent.js +61 -0
- package/public/js/site.js +129 -34
- package/scripts/build.js +129 -0
- package/scripts/seed.js +517 -7
- package/scripts/setup.js +12 -9
- package/server/routes/api/collections.js +301 -0
- package/server/routes/api/settings.js +66 -2
- package/server/server.js +19 -15
- package/server/services/collections.js +430 -0
- package/server/services/content.js +11 -2
- package/server/services/hooks.js +109 -0
- package/server/services/markdown.js +500 -149
- package/server/services/plugins.js +6 -1
- package/server/services/renderer.js +73 -7
- package/server/templates/page.html +38 -3
- package/plugins/back-to-top/admin/templates/back-to-top-settings.html +0 -55
- package/plugins/back-to-top/admin/views/back-to-top-settings.js +0 -44
- package/plugins/back-to-top/config.js +0 -10
- package/plugins/back-to-top/plugin.js +0 -24
- package/plugins/back-to-top/plugin.json +0 -36
- package/plugins/back-to-top/public/inject-body.html +0 -105
- package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +0 -113
- package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +0 -73
- package/plugins/cookie-consent/config.js +0 -30
- package/plugins/cookie-consent/plugin.js +0 -24
- package/plugins/cookie-consent/plugin.json +0 -36
- package/plugins/cookie-consent/public/inject-body.html +0 -69
- package/plugins/custom-css/admin/templates/custom-css.html +0 -17
- package/plugins/custom-css/admin/views/custom-css.js +0 -35
- package/plugins/custom-css/config.js +0 -1
- package/plugins/custom-css/data/custom.css +0 -0
- package/plugins/custom-css/plugin.js +0 -63
- package/plugins/custom-css/plugin.json +0 -32
- package/plugins/custom-css/public/inject-head.html +0 -1
- package/plugins/form-builder/data/forms/contact.json +0 -52
- package/plugins/form-builder/data/submissions/contact.json +0 -14
|
@@ -0,0 +1,487 @@
|
|
|
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
|
+
};
|