domma-cms 0.66.0 → 0.69.3

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 (221) hide show
  1. package/CLAUDE.md +64 -1
  2. package/README.md +14 -10
  3. package/admin/css/admin.css +1 -1
  4. package/admin/index.html +1 -0
  5. package/admin/js/api.js +1 -1
  6. package/admin/js/app.js +4 -4
  7. package/admin/js/lib/analytics-shape.js +1 -0
  8. package/admin/js/lib/contacts-arrange.js +1 -0
  9. package/admin/js/lib/help-popover.js +1 -0
  10. package/admin/js/lib/notes-arrange.js +1 -0
  11. package/admin/js/lib/plugin-accent.js +1 -1
  12. package/admin/js/lib/plugin-chrome.js +1 -1
  13. package/admin/js/lib/sidebar-grouping.js +1 -1
  14. package/admin/js/lib/sidebar-grouping.test.js +1 -1
  15. package/admin/js/lib/sidebar-renderer.js +3 -3
  16. package/admin/js/lib/todo-arrange.js +1 -0
  17. package/admin/js/lib/tool-kit.js +1 -0
  18. package/admin/js/templates/analytics.html +138 -0
  19. package/admin/js/templates/contacts.html +156 -0
  20. package/admin/js/templates/notes.html +137 -0
  21. package/admin/js/templates/plugin-marketplace.html +17 -4
  22. package/admin/js/templates/plugins.html +13 -7
  23. package/admin/js/templates/theme.html +38 -0
  24. package/admin/js/templates/todo.html +113 -0
  25. package/admin/js/views/analytics.js +62 -0
  26. package/admin/js/views/contacts.js +174 -0
  27. package/admin/js/views/index.js +1 -1
  28. package/admin/js/views/notes.js +104 -0
  29. package/admin/js/views/plugin-marketplace.js +1 -1
  30. package/admin/js/views/plugins.js +24 -22
  31. package/admin/js/views/theme.js +1 -1
  32. package/admin/js/views/todo.js +96 -0
  33. package/bin/cli.js +4 -10
  34. package/bin/lib/plugin-version.js +28 -0
  35. package/bin/update.js +14 -0
  36. package/config/menus/admin-sidebar.json +1 -1
  37. package/config/server.json +3 -1
  38. package/package.json +3 -7
  39. package/plugins/_lib/admin/mail/reader-view.js +20 -0
  40. package/plugins/_lib/mail/store.js +21 -1
  41. package/plugins/blog/CLAUDE.md +237 -0
  42. package/plugins/blog/admin/css/index.css +178 -1
  43. package/plugins/blog/admin/templates/blog.html +35 -53
  44. package/plugins/blog/admin/templates/categories.html +19 -6
  45. package/plugins/blog/admin/templates/comments.html +26 -9
  46. package/plugins/blog/admin/templates/post-editor.html +36 -13
  47. package/plugins/blog/admin/templates/settings.html +2 -11
  48. package/plugins/blog/admin/views/blog.js +288 -162
  49. package/plugins/blog/admin/views/categories.js +212 -201
  50. package/plugins/blog/admin/views/comments.js +207 -158
  51. package/plugins/blog/admin/views/kit.js +372 -0
  52. package/plugins/blog/admin/views/post-editor.js +269 -21
  53. package/plugins/blog/admin/views/settings-cog.js +56 -0
  54. package/plugins/blog/admin/views/settings.js +16 -86
  55. package/plugins/blog/blocks/blog-card-compact.css +17 -0
  56. package/plugins/blog/blocks/blog-card-compact.html +8 -0
  57. package/plugins/blog/blocks/blog-card-row.css +22 -0
  58. package/plugins/blog/blocks/blog-card-row.html +11 -0
  59. package/plugins/blog/blocks/blog-card.css +35 -0
  60. package/plugins/blog/blocks/blog-card.html +16 -0
  61. package/plugins/blog/blocks/blog-post-classic.css +15 -0
  62. package/plugins/blog/blocks/blog-post-classic.html +28 -0
  63. package/plugins/blog/blocks/blog-post-feature.css +20 -0
  64. package/plugins/blog/blocks/blog-post-feature.html +30 -0
  65. package/plugins/blog/blocks/blog-post-minimal.css +10 -0
  66. package/plugins/blog/blocks/blog-post-minimal.html +23 -0
  67. package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
  68. package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
  69. package/plugins/blog/collections/posts/schema.json +3 -1
  70. package/plugins/blog/config.js +6 -0
  71. package/plugins/blog/lib/layouts.js +304 -0
  72. package/plugins/blog/lib/page.js +158 -0
  73. package/plugins/blog/lib/render.js +119 -0
  74. package/plugins/blog/lib/samples.js +234 -0
  75. package/plugins/blog/plugin.js +225 -15
  76. package/plugins/blog/plugin.json +21 -81
  77. package/plugins/blog/plugin.public.js +115 -210
  78. package/plugins/blog/public/blog.css +177 -0
  79. package/plugins/blog/public/blog.js +586 -0
  80. package/plugins/blog/public/samples/aurora.svg +7 -0
  81. package/plugins/blog/public/samples/dusk.svg +7 -0
  82. package/plugins/blog/public/samples/ember.svg +7 -0
  83. package/plugins/blog/public/samples/harbour.svg +7 -0
  84. package/plugins/blog/public/samples/meadow.svg +7 -0
  85. package/plugins/blog/public/samples/rosewood.svg +7 -0
  86. package/plugins/blog/templates/index.html +9 -8
  87. package/plugins/blog/templates/post.html +4 -17
  88. package/plugins/blog/tests/layouts.test.js +97 -0
  89. package/plugins/blog/tests/public.test.js +71 -0
  90. package/plugins/free-tier.lock.json +74 -0
  91. package/plugins/mail-reader/CLAUDE.md +78 -0
  92. package/plugins/mail-reader/plugin.json +2 -1
  93. package/public/css/theme-switcher.css +1 -0
  94. package/public/js/analytics.js +1 -0
  95. package/public/js/theme-switcher.js +1 -0
  96. package/scripts/build.js +41 -2
  97. package/scripts/run-tests.mjs +92 -0
  98. package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
  99. package/{plugins/contacts/plugin.js → server/routes/api/contacts.js} +71 -43
  100. package/server/routes/api/dashboard.js +8 -2
  101. package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
  102. package/server/routes/api/plugin-marketplace.js +78 -23
  103. package/server/routes/api/plugins.js +148 -3
  104. package/server/routes/api/todo.js +178 -0
  105. package/server/server.js +61 -1
  106. package/server/services/collections.js +13 -3
  107. package/server/services/managerClient.js +72 -136
  108. package/server/services/markdown.js +54 -1
  109. package/server/services/permissionRegistry.js +50 -0
  110. package/server/services/pluginEntitlement.js +171 -0
  111. package/server/services/pluginEnvelope.js +242 -0
  112. package/server/services/pluginInstaller.js +246 -26
  113. package/server/services/pluginScaffold.js +5 -0
  114. package/server/services/plugins.js +117 -7
  115. package/server/services/presetCollections.js +344 -0
  116. package/server/services/promoted-plugins-migration.js +254 -0
  117. package/server/services/publicCors.js +46 -0
  118. package/server/services/renderer.js +83 -3
  119. package/server/services/sidebarBadges.js +37 -0
  120. package/server/services/siteGitignore.js +307 -0
  121. package/server/services/themeSettings.js +39 -0
  122. package/config/theme.json +0 -18
  123. package/plugins/analytics/admin/css/index.css +0 -1
  124. package/plugins/analytics/admin/templates/analytics.html +0 -70
  125. package/plugins/analytics/admin/views/analytics.js +0 -176
  126. package/plugins/analytics/config.js +0 -14
  127. package/plugins/analytics/plugin.json +0 -60
  128. package/plugins/analytics/public/inject-body.html +0 -60
  129. package/plugins/analytics/public/inject-head.html +0 -1
  130. package/plugins/blog/templates/author.html +0 -9
  131. package/plugins/blog/templates/category.html +0 -9
  132. package/plugins/blog/templates/tag.html +0 -9
  133. package/plugins/contacts/admin/templates/contacts.html +0 -254
  134. package/plugins/contacts/admin/views/contacts.js +0 -943
  135. package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
  136. package/plugins/contacts/collections/user-contacts/schema.json +0 -179
  137. package/plugins/contacts/config.js +0 -6
  138. package/plugins/contacts/plugin.json +0 -36
  139. package/plugins/demo-viewer/config.js +0 -4
  140. package/plugins/demo-viewer/plugin.js +0 -36
  141. package/plugins/demo-viewer/plugin.json +0 -9
  142. package/plugins/invoice/admin/css/index.css +0 -1
  143. package/plugins/invoice/admin/templates/editor.html +0 -220
  144. package/plugins/invoice/admin/templates/index.html +0 -173
  145. package/plugins/invoice/admin/templates/issuers.html +0 -2
  146. package/plugins/invoice/admin/templates/receivers.html +0 -2
  147. package/plugins/invoice/admin/views/contacts-source.js +0 -266
  148. package/plugins/invoice/admin/views/editor.js +0 -434
  149. package/plugins/invoice/admin/views/export.js +0 -199
  150. package/plugins/invoice/admin/views/help-content.js +0 -61
  151. package/plugins/invoice/admin/views/index.js +0 -643
  152. package/plugins/invoice/admin/views/issuers.js +0 -30
  153. package/plugins/invoice/admin/views/media.js +0 -172
  154. package/plugins/invoice/admin/views/party-view.js +0 -386
  155. package/plugins/invoice/admin/views/payments.js +0 -127
  156. package/plugins/invoice/admin/views/print.js +0 -130
  157. package/plugins/invoice/admin/views/receivers.js +0 -55
  158. package/plugins/invoice/admin/views/send.js +0 -212
  159. package/plugins/invoice/admin/views/settings.js +0 -594
  160. package/plugins/invoice/admin/views/view-lifecycle.js +0 -33
  161. package/plugins/invoice/collections/invoice-issuers/schema.json +0 -82
  162. package/plugins/invoice/collections/invoice-receivers/schema.json +0 -16
  163. package/plugins/invoice/collections/invoices/schema.json +0 -33
  164. package/plugins/invoice/config.js +0 -37
  165. package/plugins/invoice/pdf.js +0 -164
  166. package/plugins/invoice/plugin.js +0 -1456
  167. package/plugins/invoice/plugin.json +0 -86
  168. package/plugins/invoice/templates/_base.css +0 -1
  169. package/plugins/invoice/templates/classic-nologo.html +0 -100
  170. package/plugins/invoice/templates/classic.html +0 -91
  171. package/plugins/invoice/templates/invoice-print.html +0 -237
  172. package/plugins/invoice/templates/minimal.html +0 -99
  173. package/plugins/invoice/templates/modern-nologo.html +0 -114
  174. package/plugins/invoice/templates/modern.html +0 -113
  175. package/plugins/invoice/templates/templates.json +0 -11
  176. package/plugins/notes/admin/templates/notes.html +0 -83
  177. package/plugins/notes/admin/views/notes.js +0 -281
  178. package/plugins/notes/collections/user-notes/schema.json +0 -54
  179. package/plugins/notes/config.js +0 -6
  180. package/plugins/notes/plugin.json +0 -33
  181. package/plugins/surveys/admin/templates/audience.html +0 -47
  182. package/plugins/surveys/admin/templates/results.html +0 -58
  183. package/plugins/surveys/admin/templates/survey-editor.html +0 -141
  184. package/plugins/surveys/admin/templates/surveys.html +0 -25
  185. package/plugins/surveys/admin/views/audience.js +0 -301
  186. package/plugins/surveys/admin/views/results.js +0 -172
  187. package/plugins/surveys/admin/views/survey-editor.js +0 -211
  188. package/plugins/surveys/admin/views/surveys.js +0 -161
  189. package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
  190. package/plugins/surveys/collections/survey-groups/schema.json +0 -11
  191. package/plugins/surveys/collections/survey-invites/schema.json +0 -16
  192. package/plugins/surveys/collections/surveys/schema.json +0 -23
  193. package/plugins/surveys/config.js +0 -8
  194. package/plugins/surveys/lib/audience.js +0 -37
  195. package/plugins/surveys/lib/campaigns.js +0 -43
  196. package/plugins/surveys/lib/ledger.js +0 -110
  197. package/plugins/surveys/lib/sending.js +0 -106
  198. package/plugins/surveys/lib/stats.js +0 -62
  199. package/plugins/surveys/lib/submit.js +0 -95
  200. package/plugins/surveys/lib/tokens.js +0 -28
  201. package/plugins/surveys/plugin.js +0 -174
  202. package/plugins/surveys/plugin.json +0 -65
  203. package/plugins/surveys/plugin.public.js +0 -149
  204. package/plugins/surveys/public/dist-shared.mjs +0 -1
  205. package/plugins/surveys/public/survey.css +0 -1
  206. package/plugins/surveys/public/survey.mjs +0 -1
  207. package/plugins/surveys/templates/survey-page.html +0 -63
  208. package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
  209. package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
  210. package/plugins/theme-switcher/config.js +0 -10
  211. package/plugins/theme-switcher/plugin.js +0 -26
  212. package/plugins/theme-switcher/plugin.json +0 -74
  213. package/plugins/theme-switcher/public/inject-body.html +0 -153
  214. package/plugins/theme-switcher/public/inject-head.html +0 -260
  215. package/plugins/todo/admin/templates/todo.html +0 -158
  216. package/plugins/todo/admin/views/todo.js +0 -343
  217. package/plugins/todo/collections/todos/schema.json +0 -60
  218. package/plugins/todo/config.js +0 -7
  219. package/plugins/todo/plugin.js +0 -102
  220. package/plugins/todo/plugin.json +0 -33
  221. /package/{plugins/contacts/admin/css/index.css → admin/css/contacts.css} +0 -0
@@ -1,343 +0,0 @@
1
- // ============================================================
2
- // Todo plugin admin view
3
- // Uses auth-aware api() helper for all API calls
4
- // Uses T.create() for table rendering (Domma Table component)
5
- // ============================================================
6
-
7
- const STATUS = {
8
- NOT_STARTED: 'not-started',
9
- IN_PROGRESS: 'in-progress',
10
- COMPLETED: 'completed'
11
- };
12
-
13
- const PRIORITY = {
14
- HIGHEST: 'highest',
15
- HIGH: 'high',
16
- MEDIUM: 'medium',
17
- LOW: 'low',
18
- LOWEST: 'lowest'
19
- };
20
-
21
- function formatStatus(status) {
22
- return status.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
23
- }
24
-
25
- function formatPriority(priority) {
26
- return priority.charAt(0).toUpperCase() + priority.slice(1);
27
- }
28
-
29
- const esc = (str) => String(str ?? '')
30
- .replace(/&/g, '&')
31
- .replace(/</g, '&lt;')
32
- .replace(/>/g, '&gt;')
33
- .replace(/"/g, '&quot;')
34
- .replace(/'/g, '&#39;');
35
-
36
- async function api(url, method = 'GET', body) {
37
- const opts = {method, headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}};
38
- if (body !== undefined) {
39
- opts.headers['Content-Type'] = 'application/json';
40
- opts.body = JSON.stringify(body);
41
- }
42
- const res = await fetch(url, opts);
43
- if (!res.ok) {
44
- const err = await res.json().catch(() => ({error: res.statusText}));
45
- throw new Error(err.error || res.statusText);
46
- }
47
- const text = await res.text();
48
- return text ? JSON.parse(text) : {};
49
- }
50
-
51
- export const todoView = {
52
- templateUrl: '/plugins/todo/admin/templates/todo.html',
53
-
54
- async onMount($container) {
55
- // ---- State ----
56
- let todos = [];
57
- let activeFilter = 'all';
58
-
59
- // ---- Fetch ----
60
- async function loadTodos() {
61
- try {
62
- const res = await api('/api/plugins/todo/items');
63
- todos = Array.isArray(res) ? res : (res.data ?? []);
64
- } catch {
65
- E.toast('Failed to load todos', { type: 'error' });
66
- todos = [];
67
- }
68
- }
69
-
70
- // ---- Render table ----
71
- function render() {
72
- const wrap = $container.find('#todo-table').get(0);
73
- if (!wrap) return;
74
-
75
- const filtered = activeFilter === 'all'
76
- ? todos
77
- : todos.filter((t) => t.status === activeFilter);
78
-
79
- T.create(wrap, {
80
- data: filtered,
81
- columns: [
82
- {
83
- key: 'status',
84
- title: '',
85
- sortable: false,
86
- render: (status, row) => {
87
- const checked = status === STATUS.COMPLETED ? 'checked' : '';
88
- return `<input type="checkbox" class="form-check-input todo-checkbox" data-id="${esc(row.id)}" ${checked} style="cursor:pointer;" />`;
89
- }
90
- },
91
- {
92
- key: 'text',
93
- title: 'Task',
94
- sortable: true,
95
- render: (text, row) => {
96
- const isCompleted = row.status === STATUS.COMPLETED;
97
- const style = isCompleted ? 'text-decoration:line-through;opacity:0.45;' : '';
98
- let dueBadge = '';
99
- if (row.dueAt) {
100
- const due = new Date(row.dueAt);
101
- const today = new Date();
102
- today.setHours(0, 0, 0, 0);
103
- const overdue = !isCompleted && due < today;
104
- const dueLabel = D(row.dueAt).format('D MMM');
105
- dueBadge = ` <span class="badge ${overdue ? 'badge-danger' : 'badge-outline'}" `
106
- + `style="font-size:0.68rem;padding:1px 6px;" title="Due ${esc(dueLabel)}">`
107
- + `${overdue ? '\u26a0 ' : ''}${esc(dueLabel)}</span>`;
108
- }
109
- return `<span style="font-size:0.9rem;${style}">${esc(text)}${dueBadge}</span>`;
110
- }
111
- },
112
- {
113
- key: 'status',
114
- title: 'Status',
115
- sortable: true,
116
- render: (status, row) => {
117
- const opts = [
118
- {v: 'not-started', l: 'Not Started'},
119
- {v: 'in-progress', l: 'In Progress'},
120
- {v: 'completed', l: 'Completed'}
121
- ].map(({v, l}) =>
122
- `<option value="${v}"${status === v ? ' selected' : ''}>${l}</option>`
123
- ).join('');
124
- return `<select class="todo-select-pill status-select status-pill-${esc(status)}" data-id="${esc(row.id)}">${opts}</select>`;
125
- }
126
- },
127
- {
128
- key: 'priority',
129
- title: 'Priority',
130
- sortable: true,
131
- render: (priority, row) => {
132
- const opts = [
133
- {v: 'highest', l: 'Highest'},
134
- {v: 'high', l: 'High'},
135
- {v: 'medium', l: 'Medium'},
136
- {v: 'low', l: 'Low'},
137
- {v: 'lowest', l: 'Lowest'}
138
- ].map(({v, l}) =>
139
- `<option value="${v}"${priority === v ? ' selected' : ''}>${l}</option>`
140
- ).join('');
141
- return `<select class="todo-select-pill priority-select priority-pill-${esc(priority)}" data-id="${esc(row.id)}">${opts}</select>`;
142
- }
143
- },
144
- {
145
- key: 'id',
146
- title: 'Actions',
147
- sortable: false,
148
- render: (id) =>
149
- `<div style="display:flex;gap:.4rem;">` +
150
- `<button class="btn btn-sm btn-outline edit-todo" data-id="${esc(id)}">Edit</button>` +
151
- `<button class="btn btn-sm btn-danger delete-todo" data-id="${esc(id)}">Delete</button>` +
152
- `</div>`
153
- }
154
- ],
155
- emptyMessage: 'No tasks yet. Add one above to get started.'
156
- });
157
-
158
- wrap.querySelectorAll('.edit-todo').forEach((btn) => {
159
- btn.addEventListener('click', () => editTodo(btn.dataset.id));
160
- });
161
-
162
- wrap.querySelectorAll('.delete-todo').forEach((btn) => {
163
- btn.addEventListener('click', () => deleteTodo(btn.dataset.id));
164
- });
165
-
166
- wrap.querySelectorAll('.todo-checkbox').forEach((cb) => {
167
- cb.addEventListener('change', () => toggleTodo(cb.dataset.id));
168
- });
169
-
170
- wrap.querySelectorAll('.status-select').forEach((sel) => {
171
- sel.addEventListener('change', () => {
172
- sel.className = sel.className.replace(/status-pill-\S+/g, `status-pill-${sel.value}`);
173
- updateStatus(sel.dataset.id, sel.value);
174
- });
175
- });
176
-
177
- wrap.querySelectorAll('.priority-select').forEach((sel) => {
178
- sel.addEventListener('change', () => {
179
- sel.className = sel.className.replace(/priority-pill-\S+/g, `priority-pill-${sel.value}`);
180
- updatePriority(sel.dataset.id, sel.value);
181
- });
182
- });
183
-
184
- Domma.icons.scan(wrap);
185
- }
186
-
187
- // ---- Add todo ----
188
- async function addTodo() {
189
- const $input = $container.find('#todo-input');
190
- const $priority = $container.find('#priority-input');
191
- const $due = $container.find('#due-date-input');
192
- const text = $input.get(0).value.trim();
193
- const priority = $priority.get(0).value;
194
- const dueAt = $due.get(0)?.value || null;
195
-
196
- if (!text) {
197
- E.toast('Please enter a task', { type: 'warning' });
198
- return;
199
- }
200
-
201
- try {
202
- const item = await api('/api/plugins/todo/items', 'POST', {
203
- text,
204
- priority,
205
- status: STATUS.NOT_STARTED,
206
- dueAt
207
- });
208
- todos.push(item);
209
- $input.get(0).value = '';
210
- $priority.get(0).value = PRIORITY.MEDIUM;
211
- if ($due.get(0)) $due.get(0).value = '';
212
- render();
213
- E.toast('Task added', { type: 'success' });
214
- } catch {
215
- E.toast('Failed to add task', { type: 'error' });
216
- }
217
- }
218
-
219
- // ---- Toggle (checkbox) ----
220
- async function toggleTodo(id) {
221
- const todo = todos.find((t) => t.id === id);
222
- if (!todo) return;
223
-
224
- const newStatus = todo.status === STATUS.COMPLETED ? STATUS.NOT_STARTED : STATUS.COMPLETED;
225
- try {
226
- const updated = await api(`/api/plugins/todo/items/${id}`, 'PUT', {status: newStatus});
227
- Object.assign(todo, updated);
228
- render();
229
- E.toast(newStatus === STATUS.COMPLETED ? 'Task completed' : 'Task reopened', { type: 'success' });
230
- } catch {
231
- E.toast('Failed to update task', { type: 'error' });
232
- }
233
- }
234
-
235
- // ---- Edit todo text ----
236
- async function editTodo(id) {
237
- const todo = todos.find((t) => t.id === id);
238
- if (!todo) return;
239
-
240
- const newText = await E.prompt('Edit your task:', { inputValue: todo.text });
241
- if (!newText || !newText.trim()) return;
242
-
243
- try {
244
- const updated = await api(`/api/plugins/todo/items/${id}`, 'PUT', {text: newText.trim()});
245
- Object.assign(todo, updated);
246
- render();
247
- E.toast('Task updated', { type: 'success' });
248
- } catch {
249
- E.toast('Failed to update task', { type: 'error' });
250
- }
251
- }
252
-
253
- // ---- Delete single ----
254
- async function deleteTodo(id) {
255
- const confirmed = await E.confirm('Delete this task?');
256
- if (!confirmed) return;
257
-
258
- try {
259
- await api(`/api/plugins/todo/items/${id}`, 'DELETE');
260
- todos = todos.filter((t) => t.id !== id);
261
- render();
262
- E.toast('Task deleted', { type: 'success' });
263
- } catch {
264
- E.toast('Failed to delete task', { type: 'error' });
265
- }
266
- }
267
-
268
- // ---- Update status ----
269
- async function updateStatus(id, status) {
270
- const todo = todos.find((t) => t.id === id);
271
- if (!todo) return;
272
-
273
- try {
274
- const updated = await api(`/api/plugins/todo/items/${id}`, 'PUT', {status});
275
- Object.assign(todo, updated);
276
- render();
277
- E.toast(`Status: ${formatStatus(status)}`, { type: 'success' });
278
- } catch {
279
- E.toast('Failed to update status', { type: 'error' });
280
- }
281
- }
282
-
283
- // ---- Update priority ----
284
- async function updatePriority(id, priority) {
285
- const todo = todos.find((t) => t.id === id);
286
- if (!todo) return;
287
-
288
- try {
289
- const updated = await api(`/api/plugins/todo/items/${id}`, 'PUT', {priority});
290
- Object.assign(todo, updated);
291
- render();
292
- E.toast(`Priority: ${formatPriority(priority)}`, { type: 'success' });
293
- } catch {
294
- E.toast('Failed to update priority', { type: 'error' });
295
- }
296
- }
297
-
298
- // ---- Clear completed ----
299
- async function clearCompleted() {
300
- const hasCompleted = todos.some((t) => t.status === STATUS.COMPLETED);
301
- if (!hasCompleted) {
302
- E.toast('No completed tasks to clear', { type: 'warning' });
303
- return;
304
- }
305
-
306
- const confirmed = await E.confirm('Clear all completed tasks?');
307
- if (!confirmed) return;
308
-
309
- try {
310
- await api('/api/plugins/todo/completed', 'DELETE');
311
- todos = todos.filter((t) => t.status !== STATUS.COMPLETED);
312
- render();
313
- E.toast('Completed tasks cleared', { type: 'success' });
314
- } catch {
315
- E.toast('Failed to clear completed tasks', { type: 'error' });
316
- }
317
- }
318
-
319
- // ---- Event bindings ----
320
-
321
- $container.find('#add-todo-btn').get(0).addEventListener('click', addTodo);
322
-
323
- $container.find('#todo-input').get(0).addEventListener('keydown', (e) => {
324
- if (e.key === 'Enter') addTodo();
325
- });
326
-
327
- $container.find('#clear-completed-btn').get(0).addEventListener('click', clearCompleted);
328
-
329
- $container.find('#filter-bar').get(0).querySelectorAll('.filter-btn').forEach((btn) => {
330
- btn.addEventListener('click', () => {
331
- activeFilter = btn.dataset.filter;
332
- $container.find('#filter-bar').get(0).querySelectorAll('.filter-btn').forEach((b) => b.classList.remove('active'));
333
- btn.classList.add('active');
334
- render();
335
- });
336
- });
337
-
338
- // ---- Initial load ----
339
- await loadTodos();
340
- render();
341
- Domma.icons.scan($container.get(0));
342
- }
343
- };
@@ -1,60 +0,0 @@
1
- {
2
- "slug": "todo-items",
3
- "title": "Todos",
4
- "description": "Todo items managed by the Todo plugin.",
5
- "plugin": "todo",
6
- "fields": [
7
- {
8
- "name": "text",
9
- "label": "Task",
10
- "type": "text",
11
- "required": true
12
- },
13
- {
14
- "name": "status",
15
- "label": "Status",
16
- "type": "text",
17
- "required": false
18
- },
19
- {
20
- "name": "priority",
21
- "label": "Priority",
22
- "type": "text",
23
- "required": false
24
- },
25
- {
26
- "name": "dueAt",
27
- "label": "Due Date",
28
- "type": "text",
29
- "required": false
30
- },
31
- {
32
- "name": "userId",
33
- "label": "User ID",
34
- "type": "text",
35
- "required": false
36
- }
37
- ],
38
- "api": {
39
- "create": {
40
- "enabled": true,
41
- "access": "admin"
42
- },
43
- "read": {
44
- "enabled": true,
45
- "access": "admin"
46
- },
47
- "update": {
48
- "enabled": false,
49
- "access": "admin"
50
- },
51
- "delete": {
52
- "enabled": false,
53
- "access": "admin"
54
- }
55
- },
56
- "storage": {
57
- "adapter": "mongodb",
58
- "connection": "default"
59
- }
60
- }
@@ -1,7 +0,0 @@
1
- export default {
2
- scope: 'user',
3
- defaultPriority: 'medium',
4
- maxPerUser: 500,
5
- // storage.adapter: 'mongodb' uses config/connections.json; 'file' uses local JSON
6
- storage: {adapter: 'mongodb', connection: 'default'}
7
- };
@@ -1,102 +0,0 @@
1
- import defaultConfig from './config.js';
2
- import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../server/services/collections.js';
3
-
4
- const SLUG = 'todo-items';
5
-
6
- /** Flatten a collection entry into the shape the admin view expects. */
7
- function toTodo(entry) {
8
- return {
9
- id: entry.id,
10
- ...entry.data,
11
- createdAt: entry.meta.createdAt,
12
- updatedAt: entry.meta.updatedAt
13
- };
14
- }
15
-
16
- export default async function todoPlugin(fastify, options) {
17
- const { authenticate } = options.auth;
18
- const config = {...defaultConfig, ...(options.settings || {})};
19
- const scope = config.scope ?? 'user';
20
-
21
- function userId(request) {
22
- return scope === 'user' ? (request.user?.id ?? request.user?.sub ?? null) : null;
23
- }
24
-
25
- async function loadAll(uid) {
26
- const {entries} = await listEntries(SLUG, {limit: 10000, sort: 'createdAt', order: 'asc'});
27
- let items = entries.map(toTodo);
28
- if (uid) items = items.filter(t => t.userId === uid);
29
- return items;
30
- }
31
-
32
- /** GET /api/plugins/todo/items */
33
- fastify.get('/items', { preHandler: [authenticate] }, async (request, reply) => {
34
- const uid = userId(request);
35
- const { status } = request.query;
36
- let items = await loadAll(uid);
37
- if (status) items = items.filter(t => t.status === status);
38
- return reply.send(items);
39
- });
40
-
41
- /** POST /api/plugins/todo/items */
42
- fastify.post('/items', { preHandler: [authenticate] }, async (request, reply) => {
43
- const uid = userId(request);
44
- const { text, priority, status } = request.body ?? {};
45
-
46
- if (!text || typeof text !== 'string' || !text.trim()) {
47
- return reply.code(400).send({ error: 'text is required' });
48
- }
49
-
50
- const {dueAt} = request.body ?? {};
51
- const entry = await createEntry(SLUG, {
52
- text: text.trim(),
53
- status: status ?? 'not-started',
54
- priority: priority ?? config.defaultPriority ?? 'medium',
55
- dueAt: dueAt ?? null,
56
- userId: uid ?? null
57
- }, {createdBy: uid, source: 'admin'});
58
-
59
- return reply.code(201).send(toTodo(entry));
60
- });
61
-
62
- /** PUT /api/plugins/todo/items/:id */
63
- fastify.put('/items/:id', { preHandler: [authenticate] }, async (request, reply) => {
64
- const uid = userId(request);
65
- const { id } = request.params;
66
- const patch = request.body ?? {};
67
-
68
- const entry = await getEntry(SLUG, id);
69
- if (!entry) return reply.code(404).send({error: 'Todo not found'});
70
- if (uid && entry.data.userId !== uid) return reply.code(404).send({error: 'Todo not found'});
71
-
72
- const merged = {...entry.data};
73
- for (const key of ['text', 'status', 'priority', 'dueAt']) {
74
- if (key in patch) merged[key] = key === 'text' ? String(patch[key]).trim() : patch[key];
75
- }
76
-
77
- const updated = await updateEntry(SLUG, id, merged);
78
- return reply.send(toTodo(updated));
79
- });
80
-
81
- /** DELETE /api/plugins/todo/completed - must be registered before /:id */
82
- fastify.delete('/completed', {preHandler: [authenticate]}, async (request, reply) => {
83
- const uid = userId(request);
84
- const all = await loadAll(uid);
85
- const completed = all.filter(t => t.status === 'completed');
86
- await Promise.all(completed.map(t => deleteEntry(SLUG, t.id)));
87
- return reply.send({success: true, removed: completed.length});
88
- });
89
-
90
- /** DELETE /api/plugins/todo/items/:id */
91
- fastify.delete('/items/:id', { preHandler: [authenticate] }, async (request, reply) => {
92
- const uid = userId(request);
93
- const { id } = request.params;
94
-
95
- const entry = await getEntry(SLUG, id);
96
- if (!entry) return reply.code(404).send({error: 'Todo not found'});
97
- if (uid && entry.data.userId !== uid) return reply.code(404).send({error: 'Todo not found'});
98
-
99
- await deleteEntry(SLUG, id);
100
- return reply.send({ success: true });
101
- });
102
- }
@@ -1,33 +0,0 @@
1
- {
2
- "name": "todo",
3
- "displayName": "Todo",
4
- "version": "1.0.0",
5
- "description": "Personal task manager with priorities and status tracking.",
6
- "author": "Darryl Waterhouse",
7
- "date": "2026-03-24",
8
- "icon": "check-square",
9
- "admin": {
10
- "sidebar": [
11
- {
12
- "id": "todo",
13
- "text": "Todo",
14
- "icon": "check-square",
15
- "url": "#/plugins/todo",
16
- "section": "#/plugins/todo"
17
- }
18
- ],
19
- "routes": [
20
- {
21
- "path": "/plugins/todo",
22
- "view": "plugin-todo",
23
- "title": "Todo - Domma CMS"
24
- }
25
- ],
26
- "views": {
27
- "plugin-todo": {
28
- "entry": "todo/admin/views/todo.js?v=5ab5af5f",
29
- "exportName": "todoView"
30
- }
31
- }
32
- }
33
- }