domma-cms 0.9.1 → 0.9.6

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 (43) hide show
  1. package/admin/js/templates/block-editor.html +163 -163
  2. package/admin/js/templates/form-editor.html +245 -245
  3. package/admin/js/views/action-editor.js +1 -1
  4. package/admin/js/views/block-editor.js +8 -8
  5. package/admin/js/views/collection-editor.js +4 -4
  6. package/admin/js/views/collections.js +1 -1
  7. package/admin/js/views/form-editor.js +7 -7
  8. package/admin/js/views/forms.js +1 -1
  9. package/admin/js/views/navigation.js +14 -14
  10. package/admin/js/views/page-editor.js +35 -35
  11. package/admin/js/views/pages.js +5 -5
  12. package/admin/js/views/plugins.js +13 -10
  13. package/admin/js/views/view-editor.js +1 -1
  14. package/config/plugins.json +25 -0
  15. package/package.json +1 -1
  16. package/plugins/docs/data/documents/57e003f0-68f2-47dc-9c36-ed4b10ed3deb.json +4 -4
  17. package/plugins/docs/data/folders.json +3 -3
  18. package/plugins/docs/data/versions/57e003f0-68f2-47dc-9c36-ed4b10ed3deb/1.json +5 -0
  19. package/plugins/garage/admin/templates/garage.html +30 -0
  20. package/plugins/garage/admin/views/garage.js +62 -1
  21. package/plugins/garage/plugin.json +1 -1
  22. package/plugins/notes/admin/templates/notes.html +2 -11
  23. package/plugins/notes/admin/views/notes.js +108 -129
  24. package/plugins/notes/collections/user-notes/schema.json +2 -1
  25. package/plugins/notes/plugin.json +1 -1
  26. package/plugins/site-search/admin/templates/site-search.html +174 -46
  27. package/plugins/site-search/admin/views/site-search.js +72 -1
  28. package/plugins/site-search/config.js +6 -1
  29. package/plugins/site-search/plugin.json +1 -1
  30. package/plugins/site-search/public/inject-head.html +1 -1
  31. package/plugins/site-search/public/search.css +1 -1
  32. package/plugins/site-search/public/search.js +1 -1
  33. package/plugins/todo/admin/templates/todo.html +2 -8
  34. package/plugins/todo/admin/views/todo.js +123 -106
  35. package/plugins/todo/collections/todos/schema.json +2 -1
  36. package/plugins/todo/plugin.json +1 -1
  37. package/server/routes/api/media.js +127 -118
  38. package/server/routes/api/plugins.js +15 -4
  39. package/server/server.js +288 -285
  40. package/server/services/collections.js +17 -10
  41. package/server/services/plugins.js +77 -67
  42. package/server/services/renderer.js +3 -3
  43. package/plugins/docs/data/documents/452f49b7-9c93-4a67-874d-27f882891ad2.json +0 -11
@@ -1,9 +1,9 @@
1
1
  [
2
2
  {
3
- "id": "262d8f8b-452c-49b7-82ae-e74edc13249e",
4
- "name": "Test",
3
+ "id": "d62d4e48-39c8-4602-9c46-f5235a6f169c",
4
+ "name": "Personal",
5
5
  "parentId": null,
6
6
  "userId": "2421ad8e-060d-4548-8878-af7011d5e08b",
7
- "createdAt": "2026-03-24T16:49:14.093Z"
7
+ "createdAt": "2026-04-03T15:07:14.590Z"
8
8
  }
9
9
  ]
@@ -0,0 +1,5 @@
1
+ {
2
+ "num": 1,
3
+ "content": "",
4
+ "createdAt": "2026-04-03T15:06:45.982Z"
5
+ }
@@ -33,6 +33,7 @@
33
33
  <button class="tab-item active">Results</button>
34
34
  <button class="tab-item" id="tab-btn-garage">My Garage</button>
35
35
  <button class="tab-item" id="tab-btn-history">History</button>
36
+ <button class="tab-item" id="tab-btn-settings"><span data-icon="settings"></span> Settings</button>
36
37
  </div>
37
38
  <div class="tab-content">
38
39
 
@@ -75,6 +76,35 @@
75
76
  </div>
76
77
  </div>
77
78
 
79
+ <!-- Settings Tab -->
80
+ <div class="tab-panel" id="tab-settings">
81
+ <div style="max-width:480px;margin-top:1rem;">
82
+ <div class="card">
83
+ <div class="card-header" style="display:flex;align-items:center;gap:0.5rem;">
84
+ <span data-icon="key"></span>
85
+ <strong>DVLA API Configuration</strong>
86
+ </div>
87
+ <div class="card-body">
88
+ <p style="font-size:0.87rem;color:var(--dm-text-muted,#888);margin:0 0 1rem;">
89
+ An API key from the DVLA Vehicle Enquiry Service is required to look up vehicle details.
90
+ Obtain yours at the
91
+ <a href="https://developer-portal.driver-vehicle-licensing.api.gov.uk" target="_blank"
92
+ rel="noopener">DVLA developer portal</a>.
93
+ </p>
94
+ <div id="settings-key-status" style="margin-bottom:1rem;font-size:0.87rem;"></div>
95
+ <label style="display:block;font-size:0.9rem;font-weight:500;margin-bottom:0.35rem;">API
96
+ Key</label>
97
+ <input id="settings-api-key" type="password" class="form-input"
98
+ placeholder="Paste your DVLA API key" autocomplete="off"
99
+ style="width:100%;margin-bottom:1rem;">
100
+ <button id="settings-save-btn" class="btn btn-primary">
101
+ <span data-icon="save"></span> Save
102
+ </button>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
78
108
  </div>
79
109
  </div>
80
110
 
@@ -162,6 +162,9 @@ export const garageView = {
162
162
  const clearHistoryBtn = $container.find('#clear-history-btn').get(0);
163
163
  const tabBtnGarage = $container.find('#tab-btn-garage').get(0);
164
164
  const tabBtnHistory = $container.find('#tab-btn-history').get(0);
165
+ const settingsApiKeyInput = $container.find('#settings-api-key').get(0);
166
+ const settingsSaveBtn = $container.find('#settings-save-btn').get(0);
167
+ const settingsKeyStatus = $container.find('#settings-key-status').get(0);
165
168
 
166
169
  // ---------------------------------------------------------------
167
170
  // Tabs
@@ -552,10 +555,68 @@ export const garageView = {
552
555
  await loadHistory();
553
556
  });
554
557
 
558
+ // ---------------------------------------------------------------
559
+ // Settings
560
+ // ---------------------------------------------------------------
561
+
562
+ let currentSettings = {};
563
+
564
+ async function loadSettings() {
565
+ try {
566
+ const plugins = await api('/api/plugins');
567
+ const garage = plugins.find(p => p.name === 'garage');
568
+ currentSettings = garage?.settings || {};
569
+ const hasKey = !!(currentSettings.dvlaApiKey);
570
+ const badge = document.createElement('span');
571
+ badge.className = hasKey ? 'badge badge-success' : 'badge badge-warning';
572
+ badge.textContent = hasKey
573
+ ? 'API key configured'
574
+ : 'No API key — lookups will fail until one is saved';
575
+ settingsKeyStatus.textContent = '';
576
+ settingsKeyStatus.appendChild(badge);
577
+ } catch (err) {
578
+ settingsKeyStatus.textContent = 'Could not load settings.';
579
+ }
580
+ }
581
+
582
+ settingsSaveBtn.addEventListener('click', async () => {
583
+ const key = (settingsApiKeyInput.value || '').trim();
584
+ if (!key) {
585
+ E.toast('Please enter an API key.', {type: 'warning'});
586
+ return;
587
+ }
588
+
589
+ settingsSaveBtn.disabled = true;
590
+ try {
591
+ await api('/api/plugins/garage', 'PUT', {
592
+ enabled: true,
593
+ settings: {...currentSettings, dvlaApiKey: key}
594
+ });
595
+ currentSettings.dvlaApiKey = key;
596
+ settingsApiKeyInput.value = '';
597
+ const badge = document.createElement('span');
598
+ badge.className = 'badge badge-success';
599
+ badge.textContent = 'API key configured';
600
+ settingsKeyStatus.textContent = '';
601
+ settingsKeyStatus.appendChild(badge);
602
+ E.toast('Settings saved.', {type: 'success'});
603
+ } catch (err) {
604
+ E.toast('Failed to save: ' + err.message, {type: 'error'});
605
+ } finally {
606
+ settingsSaveBtn.disabled = false;
607
+ const icon = document.createElement('span');
608
+ icon.setAttribute('data-icon', 'save');
609
+ settingsSaveBtn.textContent = '';
610
+ settingsSaveBtn.appendChild(icon);
611
+ settingsSaveBtn.appendChild(document.createTextNode(' Save'));
612
+ Domma.icons.scan();
613
+ }
614
+ });
615
+
555
616
  // ---------------------------------------------------------------
556
617
  // Initial load
557
618
  // ---------------------------------------------------------------
558
- await Promise.all([loadSaved(), loadHistory()]);
619
+ await Promise.all([loadSaved(), loadHistory(), loadSettings()]);
559
620
  Domma.icons.scan();
560
621
  }
561
622
  };
@@ -25,7 +25,7 @@
25
25
  ],
26
26
  "views": {
27
27
  "plugin-garage": {
28
- "entry": "garage/admin/views/garage.js?v=3",
28
+ "entry": "garage/admin/views/garage.js?v=4",
29
29
  "exportName": "garageView"
30
30
  }
31
31
  }
@@ -20,17 +20,8 @@
20
20
  <!-- Category filter -->
21
21
  <div id="category-filter" style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:18px;"></div>
22
22
 
23
- <!-- Notes grid -->
24
- <div
25
- id="notes-grid"
26
- style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;"
27
- ></div>
28
-
29
- <!-- Empty state -->
30
- <div id="notes-empty" style="display:none;text-align:center;padding:60px 20px;opacity:0.6;">
31
- <span data-icon="file-text" data-icon-size="48" style="display:block;margin-bottom:12px;"></span>
32
- <p style="font-size:1rem;margin:0;">No notes yet. Click <strong>New Note</strong> to get started.</p>
33
- </div>
23
+ <!-- Notes table -->
24
+ <div id="notes-table"></div>
34
25
 
35
26
  <!-- Editor overlay -->
36
27
  <div
@@ -1,22 +1,16 @@
1
1
  // ============================================================
2
2
  // Notes plugin admin view
3
- // Uses auth-aware api() helper for all API calls (Bearer token from S.get('auth_token'))
4
- // Uses $container.find() for all DOM queries
5
- // Security: all user-supplied content passes through escapeHtml()
6
- // before being assigned to innerHTML — same pattern as todo plugin.
3
+ // Uses auth-aware api() helper for all API calls
4
+ // Uses T.create() for table rendering (Domma Table component)
7
5
  // ============================================================
8
6
 
9
- /** Escape user-supplied text before inserting into HTML markup */
10
- function escapeHtml(str) {
11
- return String(str ?? '')
12
- .replace(/&/g, '&amp;')
13
- .replace(/</g, '&lt;')
14
- .replace(/>/g, '&gt;')
15
- .replace(/"/g, '&quot;')
16
- .replace(/'/g, '&#39;');
17
- }
7
+ const esc = (str) => String(str ?? '')
8
+ .replace(/&/g, '&amp;')
9
+ .replace(/</g, '&lt;')
10
+ .replace(/>/g, '&gt;')
11
+ .replace(/"/g, '&quot;')
12
+ .replace(/'/g, '&#39;');
18
13
 
19
- /** Debounce a function call */
20
14
  function debounce(fn, delay) {
21
15
  let timer;
22
16
  return (...args) => {
@@ -49,11 +43,10 @@ export const notesView = {
49
43
  let categories = [];
50
44
  let activeCategory = '';
51
45
  let searchQuery = '';
52
- let editingNoteId = null; // null = new note
46
+ let editingNoteId = null;
47
+ let notesTable = null;
53
48
 
54
- // ---- DOM refs (resolved once after template renders) ----
55
- const gridEl = $container.find('#notes-grid').get(0);
56
- const emptyEl = $container.find('#notes-empty').get(0);
49
+ // ---- DOM refs ----
57
50
  const editorEl = $container.find('#note-editor').get(0);
58
51
  const titleInput = $container.find('#note-title').get(0);
59
52
  const contentInput = $container.find('#note-content').get(0);
@@ -86,85 +79,112 @@ export const notesView = {
86
79
  }
87
80
  }
88
81
 
89
- // ---- Render notes grid ----
90
- // Safe: all user values are run through escapeHtml() before string interpolation.
91
- function renderGrid() {
92
- if (notes.length === 0) {
93
- gridEl.textContent = '';
94
- emptyEl.style.display = 'block';
82
+ // ---- Render table (T.create on first call, setData on subsequent) ----
83
+ function renderTable() {
84
+ if (notesTable) {
85
+ notesTable.setData(notes);
95
86
  return;
96
87
  }
97
88
 
98
- emptyEl.style.display = 'none';
99
-
100
- const cards = notes.map((note) => {
101
- const safeId = escapeHtml(note.id);
102
- const safeTitle = escapeHtml(note.title || 'Untitled');
103
- const preview = escapeHtml((note.content || '').slice(0, 120));
104
- const ellipsis = note.content && note.content.length > 120 ? '\u2026' : '';
105
- const safeFromNow = escapeHtml(D(note.updatedAt).fromNow());
106
- const safeFullDt = escapeHtml(D(note.updatedAt).format('DD MMM YYYY HH:mm'));
107
-
108
- const catTags = Array.isArray(note.categories) && note.categories.length > 0
109
- ? note.categories.map((c) =>
110
- '<span class="badge badge-outline notes-cat-tag"'
111
- + ' style="font-size:0.7rem;cursor:pointer;"'
112
- + ' data-cat="' + escapeHtml(c) + '">'
113
- + escapeHtml(c)
114
- + '</span>'
115
- ).join(' ')
116
- : '';
117
-
118
- const previewHtml = preview
119
- ? '<p class="notes-card-preview" style="margin:0 0 8px;font-size:0.875rem;opacity:0.8;white-space:pre-wrap;">'
120
- + preview + ellipsis + '</p>'
121
- : '';
122
-
123
- return '<div class="card notes-card" data-id="' + safeId + '" style="cursor:default;">'
124
- + '<div class="card-body">'
125
- + '<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:8px;margin-bottom:6px;">'
126
- + '<h4 class="notes-card-title" style="margin:0;font-size:1rem;font-weight:600;flex:1;">' + safeTitle + '</h4>'
127
- + '<button class="btn btn-sm btn-danger delete-note-card" data-id="' + safeId + '" title="Delete note" style="flex-shrink:0;">'
128
- + '<span data-icon="trash" data-icon-size="13"></span>'
129
- + '</button>'
130
- + '</div>'
131
- + previewHtml
132
- + '<div class="notes-card-footer" style="display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:4px;">'
133
- + '<div class="notes-card-cats">' + catTags + '</div>'
134
- + '<small style="opacity:0.6;" title="' + safeFullDt + '">' + safeFromNow + '</small>'
135
- + '</div>'
136
- + '</div>'
137
- + '</div>';
138
- }).join('');
139
-
140
- // All values in `cards` have been escaped — safe assignment.
141
- gridEl.innerHTML = cards;
142
- Domma.icons.scan(gridEl);
89
+ notesTable = T.create('#notes-table', {
90
+ data: notes,
91
+ onRender: (el) => Domma.icons.scan(el),
92
+ columns: [
93
+ {
94
+ key: 'title',
95
+ title: 'Title',
96
+ sortable: true,
97
+ render: (v) => `<strong>${esc(v || 'Untitled')}</strong>`
98
+ },
99
+ {
100
+ key: 'content',
101
+ title: 'Content',
102
+ sortable: false,
103
+ render: (v) => {
104
+ const preview = (v || '').slice(0, 100);
105
+ const ellipsis = v && v.length > 100 ? '\u2026' : '';
106
+ return preview
107
+ ? `<span style="font-size:0.87rem;opacity:0.8;">${esc(preview)}${ellipsis}</span>`
108
+ : '<span style="opacity:0.35;">\u2014</span>';
109
+ }
110
+ },
111
+ {
112
+ key: 'categories',
113
+ title: 'Categories',
114
+ sortable: false,
115
+ render: (v) => {
116
+ if (!Array.isArray(v) || v.length === 0) return '<span style="opacity:0.35;">\u2014</span>';
117
+ return v.map((c) =>
118
+ `<span class="badge badge-outline notes-cat-tag" data-cat="${esc(c)}" `
119
+ + `style="font-size:0.7rem;margin-right:2px;cursor:pointer;">${esc(c)}</span>`
120
+ ).join('');
121
+ }
122
+ },
123
+ {
124
+ key: 'updatedAt',
125
+ title: 'Updated',
126
+ sortable: true,
127
+ render: (v) => v
128
+ ? `<span title="${esc(D(v).format('DD MMM YYYY HH:mm'))}">${esc(D(v).fromNow())}</span>`
129
+ : '\u2014'
130
+ },
131
+ {
132
+ key: 'id',
133
+ title: 'Actions',
134
+ sortable: false,
135
+ render: (id) =>
136
+ `<button class="btn btn-sm btn-outline note-edit-btn" data-id="${esc(id)}" style="margin-right:4px;">` +
137
+ `<span data-icon="edit" data-icon-size="13"></span> Edit</button>` +
138
+ `<button class="btn btn-sm btn-danger note-delete-btn" data-id="${esc(id)}">` +
139
+ `<span data-icon="trash" data-icon-size="13"></span></button>`
140
+ }
141
+ ],
142
+ emptyMessage: 'No notes yet. Click New Note to get started.'
143
+ });
144
+
145
+ // Event delegation on the table container
146
+ const tableContainer = $container.find('#notes-table').get(0);
147
+ tableContainer.addEventListener('click', (e) => {
148
+ const editBtn = e.target.closest('.note-edit-btn');
149
+ const deleteBtn = e.target.closest('.note-delete-btn');
150
+ const catTag = e.target.closest('.notes-cat-tag');
151
+
152
+ if (editBtn) {
153
+ const note = notes.find((n) => n.id === editBtn.dataset.id);
154
+ if (note) openEditor(note);
155
+ } else if (deleteBtn) {
156
+ deleteNote(deleteBtn.dataset.id);
157
+ } else if (catTag) {
158
+ activeCategory = catTag.dataset.cat;
159
+ renderCategoryFilter();
160
+ loadNotes().then(renderTable);
161
+ }
162
+ });
143
163
  }
144
164
 
145
- // ---- Render category filter bar ----
146
- // Safe: all category strings are run through escapeHtml().
165
+ // ---- Render category filter bar (DOM methods — no innerHTML with user data) ----
147
166
  function renderCategoryFilter() {
148
- const allActive = activeCategory === '';
149
- let html = '<button class="btn btn-sm ' + (allActive ? 'btn-primary' : 'btn-secondary')
150
- + ' cat-filter-btn" data-cat="" style="margin-right:4px;">All</button>';
151
-
167
+ catFilterEl.textContent = '';
168
+
169
+ const makeBtn = (label, cat) => {
170
+ const btn = document.createElement('button');
171
+ btn.className = 'btn btn-sm cat-filter-btn ' + (activeCategory === cat ? 'btn-primary' : 'btn-secondary');
172
+ btn.dataset.cat = cat;
173
+ btn.style.marginRight = '4px';
174
+ btn.textContent = label;
175
+ return btn;
176
+ };
177
+
178
+ catFilterEl.appendChild(makeBtn('All', ''));
152
179
  for (const c of categories) {
153
- const safeC = escapeHtml(c);
154
- const isActive = activeCategory === c;
155
- html += '<button class="btn btn-sm ' + (isActive ? 'btn-primary' : 'btn-secondary')
156
- + ' cat-filter-btn" data-cat="' + safeC + '" style="margin-right:4px;">'
157
- + safeC + '</button>';
180
+ catFilterEl.appendChild(makeBtn(c, c));
158
181
  }
159
-
160
- // All values in `html` have been escaped — safe assignment.
161
- catFilterEl.innerHTML = html;
162
182
  }
163
183
 
164
184
  // ---- Full reload ----
165
185
  async function reload() {
166
186
  await Promise.all([loadNotes(), loadCategories()]);
167
- renderGrid();
187
+ renderTable();
168
188
  renderCategoryFilter();
169
189
  }
170
190
 
@@ -188,10 +208,7 @@ export const notesView = {
188
208
  }
189
209
 
190
210
  function parseCategoriesInput() {
191
- return catsInput.value
192
- .split(',')
193
- .map((c) => c.trim())
194
- .filter(Boolean);
211
+ return catsInput.value.split(',').map((c) => c.trim()).filter(Boolean);
195
212
  }
196
213
 
197
214
  // ---- Save (create or update) ----
@@ -232,69 +249,31 @@ export const notesView = {
232
249
 
233
250
  // ---- Event bindings ----
234
251
 
235
- // New note button
236
252
  $container.find('#new-note-btn').get(0).addEventListener('click', () => openEditor());
237
-
238
- // Save button
239
253
  $container.find('#save-note-btn').get(0).addEventListener('click', saveNote);
240
-
241
- // Cancel buttons (header X and footer Cancel both call closeEditor)
242
254
  $container.find('#cancel-note-btn').get(0).addEventListener('click', closeEditor);
243
255
  $container.find('#cancel-note-btn-footer').get(0).addEventListener('click', closeEditor);
244
-
245
- // Delete button (inside editor)
246
256
  deleteBtnEl.addEventListener('click', () => {
247
257
  if (editingNoteId) deleteNote(editingNoteId);
248
258
  });
249
259
 
250
- // Search input (debounced)
251
260
  $container.find('#notes-search').get(0).addEventListener('input', debounce(async (e) => {
252
261
  searchQuery = e.target.value.trim();
253
262
  await loadNotes();
254
- renderGrid();
263
+ renderTable();
255
264
  }, 300));
256
265
 
257
- // Category filter — native delegation on catFilterEl
258
266
  catFilterEl.addEventListener('click', async (e) => {
259
267
  const btn = e.target.closest('.cat-filter-btn');
260
268
  if (!btn) return;
261
269
  activeCategory = btn.dataset.cat;
262
270
  renderCategoryFilter();
263
271
  await loadNotes();
264
- renderGrid();
272
+ renderTable();
265
273
  });
266
274
 
267
- // Notes grid — native delegation
268
- gridEl.addEventListener('click', (e) => {
269
- // Delete button on card
270
- const deleteBtn = e.target.closest('.delete-note-card');
271
- if (deleteBtn) {
272
- deleteNote(deleteBtn.dataset.id);
273
- return;
274
- }
275
-
276
- // Category tag — filter by that category
277
- const catTag = e.target.closest('.notes-cat-tag');
278
- if (catTag) {
279
- activeCategory = catTag.dataset.cat;
280
- renderCategoryFilter();
281
- loadNotes().then(renderGrid);
282
- return;
283
- }
284
-
285
- // Click anywhere else on card — open editor
286
- const card = e.target.closest('.notes-card');
287
- if (card) {
288
- const note = notes.find((n) => n.id === card.dataset.id);
289
- if (note) openEditor(note);
290
- }
291
- });
292
-
293
- // Ctrl+Enter / Cmd+Enter to save from editor
294
275
  editorEl.addEventListener('keydown', (e) => {
295
- if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
296
- saveNote();
297
- }
276
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') saveNote();
298
277
  });
299
278
 
300
279
  // ---- Initial load ----
@@ -48,6 +48,7 @@
48
48
  }
49
49
  },
50
50
  "storage": {
51
- "adapter": "file"
51
+ "adapter": "mongodb",
52
+ "connection": "default"
52
53
  }
53
54
  }
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "views": {
17
17
  "plugin-notes": {
18
- "entry": "notes/admin/views/notes.js",
18
+ "entry": "notes/admin/views/notes.js?v=2",
19
19
  "exportName": "notesView"
20
20
  }
21
21
  }