domma-cms 0.55.1 → 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 (305) hide show
  1. package/CLAUDE.md +194 -3
  2. package/README.md +18 -14
  3. package/admin/css/admin.css +1 -1
  4. package/admin/css/contacts.css +1 -0
  5. package/admin/dist/domma/domma-tools.css +3 -3
  6. package/admin/dist/domma/domma-tools.min.js +3 -3
  7. package/admin/index.html +1 -0
  8. package/admin/js/api.js +1 -1
  9. package/admin/js/app.js +4 -4
  10. package/admin/js/lib/analytics-shape.js +1 -0
  11. package/admin/js/lib/contacts-arrange.js +1 -0
  12. package/admin/js/lib/help-popover.js +1 -0
  13. package/admin/js/lib/notes-arrange.js +1 -0
  14. package/admin/js/lib/page-picker.js +1 -0
  15. package/admin/js/lib/plugin-accent.js +1 -0
  16. package/admin/js/lib/plugin-chrome.js +1 -0
  17. package/admin/js/lib/shortcode-context-menu.js +2 -2
  18. package/admin/js/lib/sidebar-grouping.js +1 -1
  19. package/admin/js/lib/sidebar-grouping.test.js +1 -1
  20. package/admin/js/lib/sidebar-renderer.js +4 -4
  21. package/admin/js/lib/slideover-resizable.js +1 -0
  22. package/admin/js/lib/todo-arrange.js +1 -0
  23. package/admin/js/lib/tool-kit.js +1 -0
  24. package/admin/js/templates/analytics.html +138 -0
  25. package/admin/js/templates/contacts.html +156 -0
  26. package/admin/js/templates/context-menu-editor.html +212 -0
  27. package/admin/js/templates/context-menus.html +16 -0
  28. package/admin/js/templates/notes.html +137 -0
  29. package/admin/js/templates/plugin-code.html +1 -1
  30. package/admin/js/templates/plugin-marketplace.html +17 -4
  31. package/admin/js/templates/plugins.html +13 -7
  32. package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
  33. package/admin/js/templates/settings.html +16 -97
  34. package/admin/js/templates/theme.html +211 -0
  35. package/admin/js/templates/todo.html +113 -0
  36. package/admin/js/views/analytics.js +62 -0
  37. package/admin/js/views/contacts.js +174 -0
  38. package/admin/js/views/context-menu-editor.js +55 -0
  39. package/admin/js/views/context-menus.js +5 -0
  40. package/admin/js/views/form-editor.js +7 -7
  41. package/admin/js/views/index.js +1 -1
  42. package/admin/js/views/notes.js +104 -0
  43. package/admin/js/views/plugin-marketplace.js +1 -1
  44. package/admin/js/views/plugins.js +28 -24
  45. package/admin/js/views/search.js +1 -0
  46. package/admin/js/views/settings.js +3 -3
  47. package/admin/js/views/theme.js +1 -0
  48. package/admin/js/views/todo.js +96 -0
  49. package/bin/cli.js +6 -10
  50. package/bin/lib/plugin-version.js +28 -0
  51. package/bin/update.js +27 -2
  52. package/config/menus/admin-sidebar.json +129 -23
  53. package/config/plugins.json +5 -5
  54. package/config/search.json +13 -0
  55. package/config/server.json +3 -1
  56. package/package.json +14 -10
  57. package/plugins/_lib/admin/mail/compose-window.js +914 -0
  58. package/plugins/_lib/admin/mail/contacts.js +301 -0
  59. package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
  60. package/plugins/_lib/admin/mail/folder-tree.js +254 -0
  61. package/plugins/_lib/admin/mail/identity.js +480 -0
  62. package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
  63. package/plugins/_lib/admin/mail/keyboard.js +136 -0
  64. package/plugins/_lib/admin/mail/mail.css +1 -0
  65. package/plugins/_lib/admin/mail/mail.html +71 -0
  66. package/plugins/_lib/admin/mail/panes.js +253 -0
  67. package/plugins/_lib/admin/mail/reader-view.js +4473 -0
  68. package/plugins/_lib/admin/mail/resizable.js +26 -0
  69. package/plugins/_lib/admin/mail/rules.js +343 -0
  70. package/plugins/_lib/admin/mail/scheduling.js +203 -0
  71. package/plugins/_lib/admin/mail/section-kit.js +277 -0
  72. package/plugins/_lib/admin/mail/templates.js +238 -0
  73. package/plugins/_lib/admin/mail/threads.js +200 -0
  74. package/plugins/_lib/admin/mail/vacation.js +269 -0
  75. package/plugins/_lib/admin/ui/help.css +1 -0
  76. package/plugins/_lib/admin/ui/help.js +174 -0
  77. package/plugins/_lib/admin/ui/resizable.js +151 -0
  78. package/plugins/_lib/dataStore.js +117 -0
  79. package/plugins/_lib/mail/accounts.js +919 -0
  80. package/plugins/_lib/mail/bodyTokens.js +101 -0
  81. package/plugins/_lib/mail/compose.js +256 -0
  82. package/plugins/_lib/mail/defaults.js +57 -0
  83. package/plugins/_lib/mail/diagnostics.js +154 -0
  84. package/plugins/_lib/mail/envelope.js +161 -0
  85. package/plugins/_lib/mail/folders.js +192 -0
  86. package/plugins/_lib/mail/handoff.js +274 -0
  87. package/plugins/_lib/mail/imapPool.js +293 -0
  88. package/plugins/_lib/mail/mbox.js +74 -0
  89. package/plugins/_lib/mail/pollSchedule.js +79 -0
  90. package/plugins/_lib/mail/poller.js +135 -0
  91. package/plugins/_lib/mail/priority.js +138 -0
  92. package/plugins/_lib/mail/readRoutes.js +680 -0
  93. package/plugins/_lib/mail/render.js +291 -0
  94. package/plugins/_lib/mail/ruleRunner.js +152 -0
  95. package/plugins/_lib/mail/scheduler.js +254 -0
  96. package/plugins/_lib/mail/secretbox.js +229 -0
  97. package/plugins/_lib/mail/send.js +396 -0
  98. package/plugins/_lib/mail/store.js +1022 -0
  99. package/plugins/_lib/mail/sync.js +292 -0
  100. package/plugins/_lib/mail/syncPlan.js +126 -0
  101. package/plugins/_lib/mail/syncSelection.js +82 -0
  102. package/plugins/_lib/mail/unsubscribe.js +183 -0
  103. package/plugins/_lib/mail/vacationRunner.js +114 -0
  104. package/plugins/_lib/mail/write.js +473 -0
  105. package/plugins/_lib/schemaSync.js +83 -0
  106. package/plugins/_template/admin/css/index.css +0 -0
  107. package/plugins/_template/admin/templates/index.html +4 -4
  108. package/plugins/_template/admin/views/index.js +7 -0
  109. package/plugins/blog/CLAUDE.md +237 -0
  110. package/plugins/blog/admin/css/index.css +178 -0
  111. package/plugins/blog/admin/templates/blog.html +35 -41
  112. package/plugins/blog/admin/templates/categories.html +19 -6
  113. package/plugins/blog/admin/templates/comments.html +26 -9
  114. package/plugins/blog/admin/templates/post-editor.html +63 -40
  115. package/plugins/blog/admin/templates/settings.html +3 -9
  116. package/plugins/blog/admin/views/blog.js +288 -159
  117. package/plugins/blog/admin/views/categories.js +212 -206
  118. package/plugins/blog/admin/views/comments.js +207 -158
  119. package/plugins/blog/admin/views/kit.js +372 -0
  120. package/plugins/blog/admin/views/post-editor.js +306 -39
  121. package/plugins/blog/admin/views/settings-cog.js +56 -0
  122. package/plugins/blog/admin/views/settings.js +16 -84
  123. package/plugins/blog/blocks/blog-card-compact.css +17 -0
  124. package/plugins/blog/blocks/blog-card-compact.html +8 -0
  125. package/plugins/blog/blocks/blog-card-row.css +22 -0
  126. package/plugins/blog/blocks/blog-card-row.html +11 -0
  127. package/plugins/blog/blocks/blog-card.css +35 -0
  128. package/plugins/blog/blocks/blog-card.html +16 -0
  129. package/plugins/blog/blocks/blog-post-classic.css +15 -0
  130. package/plugins/blog/blocks/blog-post-classic.html +28 -0
  131. package/plugins/blog/blocks/blog-post-feature.css +20 -0
  132. package/plugins/blog/blocks/blog-post-feature.html +30 -0
  133. package/plugins/blog/blocks/blog-post-minimal.css +10 -0
  134. package/plugins/blog/blocks/blog-post-minimal.html +23 -0
  135. package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
  136. package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
  137. package/plugins/blog/collections/categories/schema.json +7 -6
  138. package/plugins/blog/collections/comments/schema.json +11 -10
  139. package/plugins/blog/collections/posts/schema.json +16 -13
  140. package/plugins/blog/config.js +6 -0
  141. package/plugins/blog/lib/layouts.js +304 -0
  142. package/plugins/blog/lib/page.js +158 -0
  143. package/plugins/blog/lib/render.js +119 -0
  144. package/plugins/blog/lib/samples.js +234 -0
  145. package/plugins/blog/plugin.js +259 -26
  146. package/plugins/blog/plugin.json +21 -73
  147. package/plugins/blog/plugin.public.js +217 -0
  148. package/plugins/blog/public/blog.css +177 -0
  149. package/plugins/blog/public/blog.js +586 -0
  150. package/plugins/blog/public/samples/aurora.svg +7 -0
  151. package/plugins/blog/public/samples/dusk.svg +7 -0
  152. package/plugins/blog/public/samples/ember.svg +7 -0
  153. package/plugins/blog/public/samples/harbour.svg +7 -0
  154. package/plugins/blog/public/samples/meadow.svg +7 -0
  155. package/plugins/blog/public/samples/rosewood.svg +7 -0
  156. package/plugins/blog/templates/index.html +9 -8
  157. package/plugins/blog/templates/post.html +4 -17
  158. package/plugins/blog/tests/layouts.test.js +97 -0
  159. package/plugins/blog/tests/public.test.js +71 -0
  160. package/plugins/free-tier.lock.json +74 -0
  161. package/plugins/mail-reader/CLAUDE.md +78 -0
  162. package/plugins/mail-reader/admin/views/mail.js +19 -0
  163. package/plugins/mail-reader/config.js +7 -0
  164. package/plugins/mail-reader/plugin.js +48 -0
  165. package/plugins/mail-reader/plugin.json +34 -0
  166. package/public/css/forms.css +1 -1
  167. package/public/css/search.css +1 -0
  168. package/public/css/site.css +1 -1
  169. package/public/css/theme-switcher.css +1 -0
  170. package/public/js/analytics.js +1 -0
  171. package/public/js/collection-context.js +2 -2
  172. package/public/js/context-menus.js +1 -0
  173. package/public/js/form-logic-engine.js +1 -1
  174. package/public/js/forms.js +2 -2
  175. package/public/js/search.js +1 -0
  176. package/public/js/site.js +1 -1
  177. package/public/js/theme-switcher.js +1 -0
  178. package/scripts/build.js +77 -4
  179. package/scripts/copy-domma.js +48 -0
  180. package/scripts/run-tests.mjs +92 -0
  181. package/scripts/seed.js +1996 -0
  182. package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
  183. package/server/routes/api/collections.js +34 -0
  184. package/server/routes/api/contacts.js +506 -0
  185. package/server/routes/api/context-menus.js +104 -0
  186. package/server/routes/api/dashboard.js +8 -2
  187. package/server/routes/api/forms.js +42 -3
  188. package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
  189. package/server/routes/api/notifications.js +69 -19
  190. package/server/routes/api/plugin-marketplace.js +78 -23
  191. package/server/routes/api/plugins.js +198 -9
  192. package/server/routes/api/search.js +43 -0
  193. package/server/routes/api/theme.js +69 -0
  194. package/server/routes/api/todo.js +178 -0
  195. package/server/routes/public.js +42 -7
  196. package/server/server.js +135 -1
  197. package/server/services/adapters/FileAdapter.js +6 -1
  198. package/server/services/collections.js +13 -3
  199. package/server/services/content.js +26 -0
  200. package/server/services/contextMenus.js +477 -0
  201. package/server/services/managerClient.js +72 -136
  202. package/server/services/markdown.js +124 -10
  203. package/server/services/permissionRegistry.js +74 -0
  204. package/server/services/pluginEntitlement.js +171 -0
  205. package/server/services/pluginEnvelope.js +242 -0
  206. package/server/services/pluginFiles.js +52 -11
  207. package/server/services/pluginInstaller.js +246 -26
  208. package/server/services/pluginScaffold.js +5 -0
  209. package/server/services/plugins.js +340 -7
  210. package/server/services/presetCollections.js +344 -0
  211. package/server/services/promoted-plugins-migration.js +254 -0
  212. package/server/services/publicCors.js +46 -0
  213. package/server/services/renderer.js +224 -22
  214. package/server/services/roles.js +1 -1
  215. package/server/services/search-migration.js +82 -0
  216. package/server/services/search.js +413 -0
  217. package/server/services/sidebar-migration.js +1 -0
  218. package/server/services/sidebarBadges.js +37 -0
  219. package/server/services/siteGitignore.js +307 -0
  220. package/server/services/themeSettings.js +580 -0
  221. package/server/services/users.js +8 -0
  222. package/server/templates/page.html +4 -2
  223. package/plugins/analytics/admin/templates/analytics.html +0 -61
  224. package/plugins/analytics/admin/views/analytics.js +0 -176
  225. package/plugins/analytics/config.js +0 -14
  226. package/plugins/analytics/plugin.json +0 -57
  227. package/plugins/analytics/public/inject-body.html +0 -60
  228. package/plugins/analytics/public/inject-head.html +0 -1
  229. package/plugins/blog/templates/author.html +0 -9
  230. package/plugins/blog/templates/category.html +0 -9
  231. package/plugins/blog/templates/tag.html +0 -9
  232. package/plugins/contacts/admin/templates/contacts.html +0 -126
  233. package/plugins/contacts/admin/views/contacts.js +0 -710
  234. package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
  235. package/plugins/contacts/collections/user-contacts/schema.json +0 -71
  236. package/plugins/contacts/config.js +0 -6
  237. package/plugins/contacts/data/contacts.json +0 -20
  238. package/plugins/contacts/plugin.js +0 -291
  239. package/plugins/contacts/plugin.json +0 -33
  240. package/plugins/demo-viewer/config.js +0 -4
  241. package/plugins/demo-viewer/plugin.js +0 -36
  242. package/plugins/demo-viewer/plugin.json +0 -9
  243. package/plugins/invoice/admin/templates/editor.html +0 -129
  244. package/plugins/invoice/admin/templates/index.html +0 -43
  245. package/plugins/invoice/admin/templates/issuers.html +0 -5
  246. package/plugins/invoice/admin/templates/receivers.html +0 -5
  247. package/plugins/invoice/admin/views/editor.js +0 -267
  248. package/plugins/invoice/admin/views/index.js +0 -155
  249. package/plugins/invoice/admin/views/issuers.js +0 -23
  250. package/plugins/invoice/admin/views/party-view.js +0 -148
  251. package/plugins/invoice/admin/views/receivers.js +0 -22
  252. package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
  253. package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
  254. package/plugins/invoice/collections/invoices/schema.json +0 -27
  255. package/plugins/invoice/config.js +0 -16
  256. package/plugins/invoice/plugin.js +0 -283
  257. package/plugins/invoice/plugin.json +0 -85
  258. package/plugins/invoice/templates/invoice-print.html +0 -213
  259. package/plugins/notes/admin/templates/notes.html +0 -83
  260. package/plugins/notes/admin/views/notes.js +0 -281
  261. package/plugins/notes/collections/user-notes/schema.json +0 -54
  262. package/plugins/notes/config.js +0 -6
  263. package/plugins/notes/data/notes.json +0 -1
  264. package/plugins/notes/plugin.json +0 -33
  265. package/plugins/site-search/admin/views/site-search.js +0 -116
  266. package/plugins/site-search/config.js +0 -15
  267. package/plugins/site-search/plugin.js +0 -188
  268. package/plugins/site-search/plugin.json +0 -40
  269. package/plugins/site-search/public/inject-body.html +0 -17
  270. package/plugins/site-search/public/inject-head.html +0 -1
  271. package/plugins/site-search/public/search.css +0 -1
  272. package/plugins/site-search/public/search.js +0 -1
  273. package/plugins/surveys/admin/templates/audience.html +0 -47
  274. package/plugins/surveys/admin/templates/results.html +0 -58
  275. package/plugins/surveys/admin/templates/survey-editor.html +0 -141
  276. package/plugins/surveys/admin/templates/surveys.html +0 -25
  277. package/plugins/surveys/admin/views/audience.js +0 -301
  278. package/plugins/surveys/admin/views/results.js +0 -172
  279. package/plugins/surveys/admin/views/survey-editor.js +0 -211
  280. package/plugins/surveys/admin/views/surveys.js +0 -161
  281. package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
  282. package/plugins/surveys/collections/survey-groups/schema.json +0 -11
  283. package/plugins/surveys/collections/survey-invites/schema.json +0 -16
  284. package/plugins/surveys/collections/surveys/schema.json +0 -23
  285. package/plugins/surveys/config.js +0 -8
  286. package/plugins/surveys/plugin.js +0 -174
  287. package/plugins/surveys/plugin.json +0 -65
  288. package/plugins/surveys/public/dist-shared.mjs +0 -1
  289. package/plugins/surveys/public/survey.css +0 -1
  290. package/plugins/surveys/public/survey.mjs +0 -1
  291. package/plugins/surveys/templates/survey-page.html +0 -63
  292. package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
  293. package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
  294. package/plugins/theme-switcher/config.js +0 -10
  295. package/plugins/theme-switcher/plugin.js +0 -26
  296. package/plugins/theme-switcher/plugin.json +0 -74
  297. package/plugins/theme-switcher/public/inject-body.html +0 -153
  298. package/plugins/theme-switcher/public/inject-head.html +0 -260
  299. package/plugins/todo/admin/templates/todo.html +0 -158
  300. package/plugins/todo/admin/views/todo.js +0 -343
  301. package/plugins/todo/collections/todos/schema.json +0 -60
  302. package/plugins/todo/config.js +0 -7
  303. package/plugins/todo/data/todos.json +0 -1
  304. package/plugins/todo/plugin.js +0 -102
  305. package/plugins/todo/plugin.json +0 -33
@@ -1,35 +0,0 @@
1
- {
2
- "slug": "contacts-groups",
3
- "title": "Contact Groups",
4
- "description": "Groups for the Contacts plugin.",
5
- "plugin": "contacts",
6
- "fields": [
7
- {
8
- "name": "name",
9
- "label": "Group Name",
10
- "type": "text",
11
- "required": true
12
- }
13
- ],
14
- "api": {
15
- "create": {
16
- "enabled": true,
17
- "access": "admin"
18
- },
19
- "read": {
20
- "enabled": true,
21
- "access": "admin"
22
- },
23
- "update": {
24
- "enabled": false,
25
- "access": "admin"
26
- },
27
- "delete": {
28
- "enabled": false,
29
- "access": "admin"
30
- }
31
- },
32
- "storage": {
33
- "adapter": "file"
34
- }
35
- }
@@ -1,71 +0,0 @@
1
- {
2
- "slug": "contacts-contacts",
3
- "title": "Contacts",
4
- "description": "Contacts managed by the Contacts plugin.",
5
- "plugin": "contacts",
6
- "fields": [
7
- {
8
- "name": "name",
9
- "label": "Name",
10
- "type": "text",
11
- "required": true
12
- },
13
- {
14
- "name": "email",
15
- "label": "Email",
16
- "type": "text",
17
- "required": false
18
- },
19
- {
20
- "name": "phone",
21
- "label": "Phone",
22
- "type": "text",
23
- "required": false
24
- },
25
- {
26
- "name": "groups",
27
- "label": "Groups",
28
- "type": "text",
29
- "required": false
30
- },
31
- {
32
- "name": "notes",
33
- "label": "Notes",
34
- "type": "textarea",
35
- "required": false
36
- },
37
- {
38
- "name": "favourite",
39
- "label": "Favourite",
40
- "type": "text",
41
- "required": false
42
- },
43
- {
44
- "name": "userId",
45
- "label": "User ID",
46
- "type": "text",
47
- "required": false
48
- }
49
- ],
50
- "api": {
51
- "create": {
52
- "enabled": true,
53
- "access": "admin"
54
- },
55
- "read": {
56
- "enabled": true,
57
- "access": "admin"
58
- },
59
- "update": {
60
- "enabled": false,
61
- "access": "admin"
62
- },
63
- "delete": {
64
- "enabled": false,
65
- "access": "admin"
66
- }
67
- },
68
- "storage": {
69
- "adapter": "file"
70
- }
71
- }
@@ -1,6 +0,0 @@
1
- export default {
2
- scope: 'user',
3
- maxPerUser: 5000,
4
- // storage.adapter: 'mongodb' uses config/connections.json; 'file' uses local JSON
5
- storage: {adapter: 'file'}
6
- };
@@ -1,20 +0,0 @@
1
- {
2
- "contacts": [
3
- {
4
- "id": "97bbf711-84ce-41cc-898f-811936e051bc",
5
- "name": "Darryl Waterhouse",
6
- "email": "",
7
- "phone": "+447835449292",
8
- "groups": [
9
- "Coders"
10
- ],
11
- "notes": "Ledge",
12
- "favourite": true,
13
- "userId": "2421ad8e-060d-4548-8878-af7011d5e08b",
14
- "createdAt": "2026-03-24T16:43:39.907Z"
15
- }
16
- ],
17
- "groups": [
18
- "Coders"
19
- ]
20
- }
@@ -1,291 +0,0 @@
1
- import defaultConfig from './config.js';
2
- import {createEntry, deleteEntry, getEntry, listEntries, updateEntry} from '../../server/services/collections.js';
3
-
4
- const CONTACTS_SLUG = 'contacts-contacts';
5
- const GROUPS_SLUG = 'contacts-groups';
6
-
7
- /** Map a collection entry to the shape the admin view expects. */
8
- function toContact(entry) {
9
- let groups = entry.data.groups;
10
- if (typeof groups === 'string') {
11
- try {
12
- groups = JSON.parse(groups);
13
- } catch {
14
- groups = [];
15
- }
16
- }
17
- return {
18
- id: entry.id,
19
- name: entry.data.name ?? '',
20
- email: entry.data.email ?? '',
21
- phone: entry.data.phone ?? '',
22
- groups: Array.isArray(groups) ? groups : [],
23
- notes: entry.data.notes ?? '',
24
- favourite: entry.data.favourite === true || entry.data.favourite === 'true',
25
- userId: entry.data.userId ?? null,
26
- createdAt: entry.meta.createdAt,
27
- updatedAt: entry.meta.updatedAt
28
- };
29
- }
30
-
31
- function toGroupName(entry) {
32
- return entry.data.name ?? '';
33
- }
34
-
35
- export default async function contactsPlugin(fastify, options) {
36
- const {authenticate} = options.auth;
37
- const config = {...defaultConfig, ...(options.settings || {})};
38
- const scope = config.scope ?? 'user';
39
-
40
- function userId(request) {
41
- return scope === 'user' ? (request.user?.id ?? request.user?.sub ?? null) : null;
42
- }
43
-
44
- async function loadContacts(uid) {
45
- const {entries} = await listEntries(CONTACTS_SLUG, {limit: 100000, sort: 'createdAt', order: 'asc'});
46
- let items = entries.map(toContact);
47
- if (uid) items = items.filter(c => c.userId === uid);
48
- return items;
49
- }
50
-
51
- async function loadGroups() {
52
- const {entries} = await listEntries(GROUPS_SLUG, {limit: 10000, sort: 'createdAt', order: 'asc'});
53
- return entries.map(e => ({id: e.id, name: toGroupName(e)}));
54
- }
55
-
56
- // -------------------------------------------------------------------------
57
- // Contacts CRUD
58
- // -------------------------------------------------------------------------
59
-
60
- /** GET /api/plugins/contacts/contacts - supports ?q=, ?group=, ?favourites=true */
61
- fastify.get('/contacts', {preHandler: [authenticate]}, async (request, reply) => {
62
- const uid = userId(request);
63
- const { q, group, favourites } = request.query;
64
-
65
- let result = await loadContacts(uid);
66
-
67
- if (q) {
68
- const query = q.toLowerCase();
69
- result = result.filter(c =>
70
- c.name.toLowerCase().includes(query) ||
71
- c.email.toLowerCase().includes(query) ||
72
- c.phone.includes(query)
73
- );
74
- }
75
- if (group) {
76
- result = result.filter(c => Array.isArray(c.groups) && c.groups.includes(group));
77
- }
78
- if (favourites === 'true') {
79
- result = result.filter(c => c.favourite === true);
80
- }
81
-
82
- result.sort((a, b) => a.name.localeCompare(b.name));
83
- return reply.send(result);
84
- });
85
-
86
- /** POST /api/plugins/contacts/contacts */
87
- fastify.post('/contacts', {preHandler: [authenticate]}, async (request, reply) => {
88
- const uid = userId(request);
89
- const { name, email, phone, groups, notes, favourite } = request.body ?? {};
90
-
91
- if (!name || typeof name !== 'string' || !name.trim()) {
92
- return reply.code(400).send({ error: 'name is required' });
93
- }
94
-
95
- const entry = await createEntry(CONTACTS_SLUG, {
96
- name: name.trim(),
97
- email: email?.trim() ?? '',
98
- phone: phone?.trim() ?? '',
99
- groups: Array.isArray(groups) ? groups : [],
100
- notes: notes?.trim() ?? '',
101
- favourite: favourite === true,
102
- userId: uid ?? null
103
- }, {createdBy: uid, source: 'admin'});
104
-
105
- return reply.code(201).send(toContact(entry));
106
- });
107
-
108
- /** PUT /api/plugins/contacts/contacts/:id */
109
- fastify.put('/contacts/:id', {preHandler: [authenticate]}, async (request, reply) => {
110
- const uid = userId(request);
111
- const { id } = request.params;
112
- const updates = request.body ?? {};
113
-
114
- const entry = await getEntry(CONTACTS_SLUG, id);
115
- if (!entry) return reply.code(404).send({error: 'Contact not found'});
116
- if (uid && entry.data.userId !== uid) return reply.code(404).send({error: 'Contact not found'});
117
-
118
- const merged = {...entry.data};
119
- for (const key of ['name', 'email', 'phone', 'groups', 'notes', 'favourite']) {
120
- if (key in updates) merged[key] = updates[key];
121
- }
122
-
123
- const updated = await updateEntry(CONTACTS_SLUG, id, merged);
124
- return reply.send(toContact(updated));
125
- });
126
-
127
- /** DELETE /api/plugins/contacts/contacts/bulk - must be before /:id */
128
- fastify.delete('/contacts/bulk', {preHandler: [authenticate]}, async (request, reply) => {
129
- const uid = userId(request);
130
- const { ids } = request.body ?? {};
131
-
132
- if (!Array.isArray(ids) || ids.length === 0) {
133
- return reply.code(400).send({ error: 'ids array is required' });
134
- }
135
-
136
- let deleted = 0;
137
- for (const id of ids) {
138
- const entry = await getEntry(CONTACTS_SLUG, id).catch(() => null);
139
- if (!entry) continue;
140
- if (uid && entry.data.userId !== uid) continue;
141
- await deleteEntry(CONTACTS_SLUG, id).catch(() => {
142
- });
143
- deleted++;
144
- }
145
-
146
- return reply.send({ deleted });
147
- });
148
-
149
- /** DELETE /api/plugins/contacts/contacts/:id */
150
- fastify.delete('/contacts/:id', {preHandler: [authenticate]}, async (request, reply) => {
151
- const uid = userId(request);
152
- const { id } = request.params;
153
-
154
- const entry = await getEntry(CONTACTS_SLUG, id);
155
- if (!entry) return reply.code(404).send({error: 'Contact not found'});
156
- if (uid && entry.data.userId !== uid) return reply.code(404).send({error: 'Contact not found'});
157
-
158
- await deleteEntry(CONTACTS_SLUG, id);
159
- return reply.send({ ok: true });
160
- });
161
-
162
- // -------------------------------------------------------------------------
163
- // Groups CRUD
164
- // -------------------------------------------------------------------------
165
-
166
- /** GET /api/plugins/contacts/groups */
167
- fastify.get('/groups', {preHandler: [authenticate]}, async (request, reply) => {
168
- const groups = await loadGroups();
169
- return reply.send(groups.map(g => g.name));
170
- });
171
-
172
- /** POST /api/plugins/contacts/groups */
173
- fastify.post('/groups', {preHandler: [authenticate]}, async (request, reply) => {
174
- const { name } = request.body ?? {};
175
-
176
- if (!name || typeof name !== 'string' || !name.trim()) {
177
- return reply.code(400).send({ error: 'name is required' });
178
- }
179
-
180
- const trimmed = name.trim();
181
- const existing = await loadGroups();
182
- if (existing.some(g => g.name === trimmed)) {
183
- return reply.code(400).send({ error: 'Group already exists' });
184
- }
185
-
186
- await createEntry(GROUPS_SLUG, {name: trimmed}, {source: 'admin'});
187
- return reply.code(201).send({ name: trimmed });
188
- });
189
-
190
- /** PUT /api/plugins/contacts/groups/:name - rename + cascade */
191
- fastify.put('/groups/:name', {preHandler: [authenticate]}, async (request, reply) => {
192
- const oldName = decodeURIComponent(request.params.name);
193
- const { newName } = request.body ?? {};
194
-
195
- if (!newName || typeof newName !== 'string' || !newName.trim()) {
196
- return reply.code(400).send({ error: 'newName is required' });
197
- }
198
-
199
- const groups = await loadGroups();
200
- const grpEntry = groups.find(g => g.name === oldName);
201
- if (!grpEntry) return reply.code(404).send({error: 'Group not found'});
202
-
203
- const trimmed = newName.trim();
204
- if (groups.some(g => g.name === trimmed)) {
205
- return reply.code(400).send({ error: 'A group with that name already exists' });
206
- }
207
-
208
- // Rename the group entry
209
- await updateEntry(GROUPS_SLUG, grpEntry.id, {name: trimmed});
210
-
211
- // Cascade rename across all contacts
212
- const {entries} = await listEntries(CONTACTS_SLUG, {limit: 100000});
213
- for (const entry of entries) {
214
- const grps = Array.isArray(entry.data.groups) ? entry.data.groups : [];
215
- if (grps.includes(oldName)) {
216
- await updateEntry(CONTACTS_SLUG, entry.id, {
217
- ...entry.data,
218
- groups: grps.map(g => (g === oldName ? trimmed : g))
219
- });
220
- }
221
- }
222
-
223
- return reply.send({ name: trimmed });
224
- });
225
-
226
- /** DELETE /api/plugins/contacts/groups/:name - delete + cascade */
227
- fastify.delete('/groups/:name', {preHandler: [authenticate]}, async (request, reply) => {
228
- const name = decodeURIComponent(request.params.name);
229
-
230
- const groups = await loadGroups();
231
- const grpEntry = groups.find(g => g.name === name);
232
- if (!grpEntry) return reply.code(404).send({error: 'Group not found'});
233
-
234
- // Remove the group entry
235
- await deleteEntry(GROUPS_SLUG, grpEntry.id);
236
-
237
- // Cascade: strip group from all contacts
238
- const {entries} = await listEntries(CONTACTS_SLUG, {limit: 100000});
239
- for (const entry of entries) {
240
- const grps = Array.isArray(entry.data.groups) ? entry.data.groups : [];
241
- if (grps.includes(name)) {
242
- await updateEntry(CONTACTS_SLUG, entry.id, {
243
- ...entry.data,
244
- groups: grps.filter(g => g !== name)
245
- });
246
- }
247
- }
248
-
249
- return reply.send({ ok: true });
250
- });
251
-
252
- // -------------------------------------------------------------------------
253
- // Import / Export
254
- // -------------------------------------------------------------------------
255
-
256
- /** GET /api/plugins/contacts/export */
257
- fastify.get('/export', {preHandler: [authenticate]}, async (request, reply) => {
258
- const uid = userId(request);
259
- const contacts = await loadContacts(uid);
260
- return reply
261
- .header('Content-Disposition', 'attachment; filename="contacts.json"')
262
- .send(contacts);
263
- });
264
-
265
- /** POST /api/plugins/contacts/import */
266
- fastify.post('/import', {preHandler: [authenticate]}, async (request, reply) => {
267
- const uid = userId(request);
268
- const { contacts: incoming } = request.body ?? {};
269
-
270
- if (!Array.isArray(incoming)) {
271
- return reply.code(400).send({ error: 'contacts array is required' });
272
- }
273
-
274
- let imported = 0;
275
- for (const raw of incoming) {
276
- if (!raw || typeof raw.name !== 'string') continue;
277
- await createEntry(CONTACTS_SLUG, {
278
- name: raw.name?.trim() ?? '',
279
- email: raw.email?.trim() ?? '',
280
- phone: raw.phone?.trim() ?? '',
281
- groups: Array.isArray(raw.groups) ? raw.groups : [],
282
- notes: raw.notes?.trim() ?? '',
283
- favourite: raw.favourite === true,
284
- userId: uid ?? null
285
- }, {source: 'import'});
286
- imported++;
287
- }
288
-
289
- return reply.send({ imported });
290
- });
291
- }
@@ -1,33 +0,0 @@
1
- {
2
- "name": "contacts",
3
- "displayName": "Contacts",
4
- "version": "1.0.0",
5
- "description": "Contact manager with groups, favourites, search, and import/export.",
6
- "author": "Darryl Waterhouse",
7
- "date": "2026-03-24",
8
- "icon": "users",
9
- "admin": {
10
- "sidebar": [
11
- {
12
- "id": "contacts",
13
- "text": "Contacts",
14
- "icon": "users",
15
- "url": "#/plugins/contacts",
16
- "section": "#/plugins/contacts"
17
- }
18
- ],
19
- "routes": [
20
- {
21
- "path": "/plugins/contacts",
22
- "view": "plugin-contacts",
23
- "title": "Contacts - Domma CMS"
24
- }
25
- ],
26
- "views": {
27
- "plugin-contacts": {
28
- "entry": "contacts/admin/views/contacts.js?v=b7ed0c5c",
29
- "exportName": "contactsView"
30
- }
31
- }
32
- }
33
- }
@@ -1,4 +0,0 @@
1
- export default {
2
- basePath: '/public/demos',
3
- defaultHeight: '500px'
4
- };
@@ -1,36 +0,0 @@
1
- /**
2
- * Demo Viewer Plugin
3
- *
4
- * Registers a [demo] shortcode that embeds interactive Domma JS demos via iframe.
5
- *
6
- * Usage:
7
- * [demo src="elements/accordion" /]
8
- * [demo src="effects/ambient" height="600px" title="Ambient Effect" /]
9
- */
10
-
11
- export default async function demoViewerPlugin(fastify, opts) {
12
- const {hooks, settings} = opts;
13
- const basePath = settings.basePath || '/public/demos';
14
- const defaultHeight = settings.defaultHeight || '500px';
15
-
16
- hooks.registerShortcode('demo', (attrStr, body, context) => {
17
- const attrs = context.parseShortcodeAttrs(attrStr);
18
- const src = attrs.src || '';
19
- const height = attrs.height || defaultHeight;
20
- const title = attrs.title || '';
21
- const iframeSrc = `${basePath}/${src}.html`;
22
-
23
- const titleBar = title
24
- ? `<div class="demo-viewer-title">${context.escapeAttr(title)}</div>`
25
- : '';
26
-
27
- return `<div class="demo-viewer">${titleBar}<iframe src="${context.escapeAttr(iframeSrc)}" style="width:100%;height:${context.escapeAttr(height)};border:none;border-radius:var(--dm-radius, 8px);background:var(--dm-surface, #1a1a2e);" loading="lazy" allowfullscreen></iframe></div>`;
28
- }, {priority: 5});
29
-
30
- hooks.registerSanitizeRules({
31
- tags: ['iframe'],
32
- attributes: {
33
- iframe: ['src', 'height', 'width', 'style', 'loading', 'allowfullscreen']
34
- }
35
- });
36
- }
@@ -1,9 +0,0 @@
1
- {
2
- "name": "demo-viewer",
3
- "displayName": "Demo Viewer",
4
- "version": "1.0.0",
5
- "description": "Embeds interactive Domma JS demos via iframe shortcode",
6
- "author": "Domma",
7
- "date": "2026-03-31",
8
- "icon": "play-circle"
9
- }
@@ -1,129 +0,0 @@
1
- <div class="view-header d-flex align-items-center justify-content-between mb-4">
2
- <h1 class="h3 mb-0" id="editor-title">New Invoice</h1>
3
- <div class="d-flex gap-2">
4
- <a id="btn-print" class="btn btn-secondary" href="#" target="_blank" style="display:none;">
5
- <span data-icon="printer"></span> Print / PDF
6
- </a>
7
- <button id="btn-save" class="btn btn-primary">
8
- <span data-icon="save"></span> Save
9
- </button>
10
- </div>
11
- </div>
12
-
13
- <div class="d-flex gap-4" style="align-items:flex-start;">
14
-
15
- <div style="flex:1; min-width:0;">
16
-
17
- <div class="card mb-3">
18
- <div class="card-body">
19
- <div class="grid grid-cols-2 gap-3">
20
- <div>
21
- <label class="form-label">Invoice Number</label>
22
- <input id="f-number" type="text" class="form-control font-monospace" placeholder="auto-generated">
23
- <div class="text-muted small">Leave blank to auto-number</div>
24
- </div>
25
- <div>
26
- <label class="form-label">Status</label>
27
- <select id="f-status" class="form-select">
28
- <option value="draft">Draft</option>
29
- <option value="sent">Sent</option>
30
- <option value="paid">Paid</option>
31
- <option value="overdue">Overdue</option>
32
- <option value="cancelled">Cancelled</option>
33
- </select>
34
- </div>
35
- <div>
36
- <label class="form-label">Issue Date</label>
37
- <input id="f-issue-date" type="date" class="form-control">
38
- </div>
39
- <div>
40
- <label class="form-label">Due Date</label>
41
- <input id="f-due-date" type="date" class="form-control">
42
- </div>
43
- <div>
44
- <label class="form-label">Issuer</label>
45
- <select id="f-issuer" class="form-select"></select>
46
- </div>
47
- <div>
48
- <label class="form-label">Receiver</label>
49
- <select id="f-receiver" class="form-select"></select>
50
- </div>
51
- </div>
52
- </div>
53
- </div>
54
-
55
- <div class="card mb-3">
56
- <div class="card-header d-flex justify-content-between align-items-center">
57
- <strong>Line Items</strong>
58
- <button id="btn-add-line" type="button" class="btn btn-sm btn-outline">
59
- <span data-icon="plus"></span> Add Line
60
- </button>
61
- </div>
62
- <div class="card-body">
63
- <table class="table mb-0" id="line-items-table">
64
- <thead>
65
- <tr>
66
- <th style="width:55%;">Description</th>
67
- <th style="width:10%;">Qty</th>
68
- <th style="width:15%;">Unit (net)</th>
69
- <th style="width:15%; text-align:right;">Total</th>
70
- <th style="width:5%;"></th>
71
- </tr>
72
- </thead>
73
- <tbody id="line-items-body"></tbody>
74
- </table>
75
- </div>
76
- </div>
77
-
78
- <div class="card mb-3">
79
- <div class="card-header"><strong>Notes</strong></div>
80
- <div class="card-body">
81
- <textarea id="f-notes" class="form-control" rows="3" placeholder="Optional notes printed on the invoice"></textarea>
82
- </div>
83
- </div>
84
- </div>
85
-
86
- <div style="width:300px; flex-shrink:0;">
87
-
88
- <div class="card mb-3">
89
- <div class="card-header"><strong>Totals</strong></div>
90
- <div class="card-body">
91
- <div class="d-flex justify-content-between mb-2">
92
- <span class="text-muted">Subtotal</span>
93
- <span id="t-subtotal" style="font-variant-numeric:tabular-nums;">-</span>
94
- </div>
95
- <div class="d-flex justify-content-between mb-2" id="vat-row">
96
- <span class="text-muted">VAT (<span id="t-vat-rate">20</span>%)</span>
97
- <span id="t-vat" style="font-variant-numeric:tabular-nums;">-</span>
98
- </div>
99
- <hr>
100
- <div class="d-flex justify-content-between">
101
- <strong>Total</strong>
102
- <strong id="t-total" style="font-variant-numeric:tabular-nums;">-</strong>
103
- </div>
104
- </div>
105
- </div>
106
-
107
- <div class="card mb-3">
108
- <div class="card-header"><strong>VAT</strong></div>
109
- <div class="card-body">
110
- <label class="d-flex align-items-center gap-2 mb-2" style="cursor:pointer;">
111
- <input id="f-vat-enabled" type="checkbox" checked> Apply VAT
112
- </label>
113
- <label class="form-label">Rate %</label>
114
- <input id="f-vat-rate" type="number" min="0" max="100" step="0.01" class="form-control" value="20">
115
- </div>
116
- </div>
117
-
118
- <div class="card mb-3">
119
- <div class="card-header"><strong>Currency</strong></div>
120
- <div class="card-body">
121
- <select id="f-currency" class="form-select">
122
- <option value="GBP">GBP - British Pound</option>
123
- <option value="EUR">EUR - Euro</option>
124
- <option value="USD">USD - US Dollar</option>
125
- </select>
126
- </div>
127
- </div>
128
- </div>
129
- </div>