domma-cms 0.66.0 → 0.69.4

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 +94 -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 +310 -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,33 +0,0 @@
1
- {
2
- "name": "notes",
3
- "displayName": "Notes",
4
- "version": "1.0.0",
5
- "description": "Rich note-taking with categories, search, and markdown content.",
6
- "author": "Darryl Waterhouse",
7
- "date": "2026-03-24",
8
- "icon": "edit-3",
9
- "admin": {
10
- "sidebar": [
11
- {
12
- "id": "notes",
13
- "text": "Notes",
14
- "icon": "file-text",
15
- "url": "#/plugins/notes",
16
- "section": "#/plugins/notes"
17
- }
18
- ],
19
- "routes": [
20
- {
21
- "path": "/plugins/notes",
22
- "view": "plugin-notes",
23
- "title": "Notes - Domma CMS"
24
- }
25
- ],
26
- "views": {
27
- "plugin-notes": {
28
- "entry": "notes/admin/views/notes.js?v=ecd1b254",
29
- "exportName": "notesView"
30
- }
31
- }
32
- }
33
- }
@@ -1,47 +0,0 @@
1
- <div class="survey-audience-plugin">
2
-
3
- <div class="toolbar mb-3" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
4
- <h2 style="margin:0;flex:1;">Survey Audience</h2>
5
- <button id="back-surveys" class="btn btn-outline">
6
- <span data-icon="arrow-left"></span>
7
- Surveys
8
- </button>
9
- <button id="import-contacts" class="btn btn-outline">
10
- <span data-icon="download"></span>
11
- Import from Contacts
12
- </button>
13
- <button id="add-contact" class="btn btn-primary">
14
- <span data-icon="plus"></span>
15
- Add contact
16
- </button>
17
- </div>
18
-
19
- <div class="grid grid-cols-3" style="gap:1rem;align-items:start;">
20
-
21
- <!-- Contacts table -->
22
- <div style="grid-column:span 2;">
23
- <div id="audience-table"></div>
24
- <div id="audience-empty" style="display:none;text-align:center;padding:2rem 1rem;color:var(--dm-text-muted);">
25
- <p>No audience contacts yet. Add one or import from the Contacts plugin.</p>
26
- </div>
27
- </div>
28
-
29
- <!-- Groups panel -->
30
- <div class="card">
31
- <div class="card-header"><strong>Groups</strong></div>
32
- <div class="card-body">
33
- <div style="display:flex;gap:0.5rem;margin-bottom:1rem;">
34
- <input id="new-group-name" type="text" class="form-input" placeholder="New group name…" style="flex:1;">
35
- <button id="add-group" class="btn btn-primary">
36
- <span data-icon="plus"></span>
37
- </button>
38
- </div>
39
- <div id="groups-list">
40
- <p class="text-muted">No groups yet.</p>
41
- </div>
42
- </div>
43
- </div>
44
-
45
- </div>
46
-
47
- </div>
@@ -1,58 +0,0 @@
1
- <div class="survey-results-plugin">
2
-
3
- <style>
4
- /* Layout only + real Domma theme tokens - surfaces use .card so the
5
- active theme is always respected; no hardcoded colours. */
6
- .survey-results-plugin .sr-head { display:flex; align-items:flex-start; gap:1rem; flex-wrap:wrap; margin-bottom:1.25rem; }
7
- .survey-results-plugin .sr-head h2 { margin:0 0 0.25rem; }
8
- .survey-results-plugin .sr-sub { color:var(--dm-text-muted); margin:0; }
9
- .survey-results-plugin .sr-head-actions { margin-left:auto; display:flex; gap:0.5rem; flex-wrap:wrap; }
10
-
11
- .survey-results-plugin .sr-summary { display:grid; grid-template-columns:1.4fr 1fr 1fr 1fr; gap:1rem; margin-bottom:1.5rem; }
12
- @media (max-width:760px){ .survey-results-plugin .sr-summary { grid-template-columns:1fr 1fr; } }
13
- .survey-results-plugin .sr-rate-num { font-size:2.4rem; font-weight:800; line-height:1; color:var(--dm-primary); }
14
- .survey-results-plugin .sr-kpi-num { font-size:2rem; font-weight:800; line-height:1; }
15
- .survey-results-plugin .sr-kpi-lbl { color:var(--dm-text-muted); font-size:0.85rem; margin-top:0.35rem; }
16
- .survey-results-plugin .sr-rate-bar { height:8px; border-radius:999px; background:var(--dm-border); margin-top:0.75rem; overflow:hidden; }
17
- .survey-results-plugin .sr-rate-fill { height:100%; background:var(--dm-primary); border-radius:999px; }
18
-
19
- .survey-results-plugin .sr-section-title { font-size:1.05rem; font-weight:700; margin:0 0 0.75rem; }
20
- .survey-results-plugin .sr-q-head { display:flex; align-items:baseline; gap:0.6rem; margin-bottom:0.75rem; }
21
- .survey-results-plugin .sr-q-num { display:inline-flex; align-items:center; justify-content:center; min-width:1.6rem; height:1.6rem; border-radius:50%; background:var(--dm-primary); color:var(--dm-text-inverse); font-size:0.8rem; font-weight:700; }
22
- .survey-results-plugin .sr-q-label { font-weight:700; }
23
- .survey-results-plugin .sr-q-meta { margin-left:auto; color:var(--dm-text-muted); font-size:0.8rem; text-transform:uppercase; letter-spacing:0.03em; }
24
- .survey-results-plugin .sr-avg { font-size:1.1rem; margin:0 0 0.6rem; }
25
- .survey-results-plugin .sr-avg strong { font-size:1.5rem; color:var(--dm-primary); }
26
-
27
- /* Distribution bars (survey.css isn't loaded in admin, so style here). */
28
- .survey-results-plugin .survey-dist { display:flex; flex-direction:column; gap:0.5rem; }
29
- .survey-results-plugin .survey-dist-row { display:grid; grid-template-columns:minmax(80px,160px) 1fr auto; align-items:center; gap:0.75rem; }
30
- .survey-results-plugin .survey-dist-label { font-size:0.9rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
31
- .survey-results-plugin .survey-dist-bar { height:14px; background:var(--dm-border); border-radius:999px; overflow:hidden; }
32
- .survey-results-plugin .survey-dist-fill { height:100%; background:var(--dm-primary); border-radius:999px; min-width:2px; }
33
- .survey-results-plugin .survey-dist-val { font-size:0.85rem; color:var(--dm-text-muted); white-space:nowrap; }
34
-
35
- /* Free-text answers. */
36
- .survey-results-plugin .sr-quotes { max-height:260px; overflow:auto; display:flex; flex-direction:column; gap:0.5rem; }
37
- .survey-results-plugin .sr-quote { padding:0.5rem 0.75rem; background:var(--dm-surface-raised); border-left:3px solid var(--dm-border); border-radius:0 6px 6px 0; font-size:0.92rem; }
38
- .survey-results-plugin .sr-empty { color:var(--dm-text-muted); font-style:italic; }
39
- </style>
40
-
41
- <div class="sr-head">
42
- <div>
43
- <h2 id="results-title">Survey Results</h2>
44
- <p class="sr-sub" id="results-sub">How your audience responded.</p>
45
- </div>
46
- <div class="sr-head-actions">
47
- <button id="export-csv" class="btn btn-outline"><span data-icon="download"></span> CSV</button>
48
- <button id="export-json" class="btn btn-outline"><span data-icon="download"></span> JSON</button>
49
- <button id="back-surveys" class="btn btn-outline"><span data-icon="arrow-left"></span> Surveys</button>
50
- </div>
51
- </div>
52
-
53
- <div id="summary" class="sr-summary"></div>
54
-
55
- <h3 class="sr-section-title">Question breakdown</h3>
56
- <div id="questions"></div>
57
-
58
- </div>
@@ -1,141 +0,0 @@
1
- <div class="survey-editor-plugin">
2
-
3
- <style>
4
- /* Layout only - colours/borders come from Domma's themed .card and
5
- .form-input, so the active theme is always respected. */
6
- .survey-editor-plugin .se-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem 1.25rem; }
7
- .survey-editor-plugin .se-span-2 { grid-column: 1 / -1; }
8
- .survey-editor-plugin .se-field { display: flex; flex-direction: column; gap: 0.35rem; }
9
- .survey-editor-plugin .se-field label { font-weight: 600; font-size: 0.85rem; }
10
- .survey-editor-plugin .se-field .se-hint { font-size: 0.78rem; color: var(--dm-text-muted); }
11
- .survey-editor-plugin .se-field .form-input { width: 100%; }
12
- .survey-editor-plugin .se-field textarea.form-input { min-height: 90px; resize: vertical; }
13
- .survey-editor-plugin .se-field select[multiple].form-input { min-height: 120px; }
14
- @media (max-width: 640px) { .survey-editor-plugin .se-grid { grid-template-columns: 1fr; } }
15
- </style>
16
-
17
- <div class="toolbar mb-3" style="display:flex;align-items:center;gap:0.5rem;">
18
- <h2 id="editor-title" style="margin:0;flex:1;">New Survey</h2>
19
- <button id="back-btn" class="btn btn-outline"><span data-icon="arrow-left"></span> Back</button>
20
- </div>
21
-
22
- <div class="card">
23
- <div class="card-header"><strong>Basics</strong></div>
24
- <div class="card-body">
25
- <div class="se-grid">
26
- <div class="se-field se-span-2">
27
- <label for="f-title">Title</label>
28
- <input id="f-title" name="title" type="text" class="form-input" placeholder="Customer satisfaction Q3">
29
- </div>
30
- <div class="se-field">
31
- <label for="f-slug">Slug</label>
32
- <input id="f-slug" name="slug" type="text" class="form-input" placeholder="customer-sat-q3">
33
- <span class="se-hint" id="slug-hint">URL-safe identifier.</span>
34
- </div>
35
- <div class="se-field">
36
- <label for="f-formSlug">Form</label>
37
- <select id="f-formSlug" name="formSlug" class="form-input"><option value="">Select a form…</option></select>
38
- <span class="se-hint">The Forms definition respondents complete.</span>
39
- </div>
40
- <div class="se-field se-span-2">
41
- <label for="f-description">Description</label>
42
- <textarea id="f-description" name="description" class="form-input" placeholder="Optional internal description…"></textarea>
43
- </div>
44
- </div>
45
- </div>
46
- </div>
47
-
48
- <div class="card mt-3">
49
- <div class="card-header"><strong>Setup</strong></div>
50
- <div class="card-body">
51
- <div class="se-grid">
52
- <div class="se-field">
53
- <label for="f-status">Status</label>
54
- <select id="f-status" name="status" class="form-input">
55
- <option value="draft">Draft</option>
56
- <option value="open">Open</option>
57
- <option value="closed">Closed</option>
58
- </select>
59
- </div>
60
- <div class="se-field">
61
- <label for="f-mode">Mode</label>
62
- <select id="f-mode" name="mode" class="form-input">
63
- <option value="identified">Identified</option>
64
- <option value="anonymous">Anonymous</option>
65
- </select>
66
- <span class="se-hint">Anonymous de-links responses from contacts.</span>
67
- </div>
68
- <div class="se-field se-span-2">
69
- <label for="f-groupIds">Audience groups</label>
70
- <select id="f-groupIds" name="groupIds" class="form-input" multiple></select>
71
- <span class="se-hint">Hold Ctrl/Cmd to select multiple groups.</span>
72
- </div>
73
- </div>
74
- </div>
75
- </div>
76
-
77
- <div class="card mt-3">
78
- <div class="card-header"><strong>Schedule</strong></div>
79
- <div class="card-body">
80
- <div class="se-grid">
81
- <div class="se-field"><label for="f-opensAt">Opens at</label><input id="f-opensAt" name="opensAt" type="date" class="form-input"></div>
82
- <div class="se-field"><label for="f-closesAt">Closes at</label><input id="f-closesAt" name="closesAt" type="date" class="form-input"></div>
83
- <div class="se-field"><label for="f-reminderAfterDays">Reminder after (days)</label><input id="f-reminderAfterDays" name="reminderAfterDays" type="number" min="0" value="7" class="form-input"></div>
84
- </div>
85
- </div>
86
- </div>
87
-
88
- <div class="card mt-3">
89
- <div class="card-header"><strong>Survey page content</strong></div>
90
- <div class="card-body">
91
- <div class="se-grid">
92
- <div class="se-field se-span-2">
93
- <label for="f-intro">Intro (Markdown - shown above the form)</label>
94
- <textarea id="f-intro" name="intro" class="form-input" placeholder="## Welcome&#10;Thanks for taking a moment to help us improve."></textarea>
95
- </div>
96
- <div class="se-field se-span-2">
97
- <label for="f-footer">Footer (Markdown - shown below the form)</label>
98
- <textarea id="f-footer" name="footer" class="form-input" placeholder="Questions? Contact us at hello@example.com"></textarea>
99
- </div>
100
- <div class="se-field se-span-2">
101
- <label for="f-thankYou">Thank-you message (shown after submitting)</label>
102
- <textarea id="f-thankYou" name="thankYou" class="form-input" placeholder="Thank you - your response has been recorded."></textarea>
103
- </div>
104
- </div>
105
- </div>
106
- </div>
107
-
108
- <div class="card mt-3">
109
- <div class="card-header"><strong>Invitation email</strong></div>
110
- <div class="card-body">
111
- <div class="se-grid">
112
- <div class="se-field se-span-2">
113
- <label for="f-inviteSubject">Subject</label>
114
- <input id="f-inviteSubject" name="inviteSubject" type="text" class="form-input" placeholder="We would love your feedback">
115
- </div>
116
- <div class="se-field se-span-2">
117
- <label for="f-inviteBody">Body</label>
118
- <textarea id="f-inviteBody" name="inviteBody" class="form-input" placeholder="Hi {{name}}, please complete our survey: {{link}}"></textarea>
119
- <span class="se-hint">{{name}} and {{link}} are substituted per recipient; {{link}} becomes the button.</span>
120
- </div>
121
- </div>
122
- </div>
123
- </div>
124
-
125
- <div class="card mt-3">
126
- <div class="card-body" style="display:flex;gap:0.5rem;flex-wrap:wrap;">
127
- <button id="save-btn" class="btn btn-primary">Save Survey</button>
128
- <button id="cancel-btn" class="btn btn-outline">Cancel</button>
129
- </div>
130
- </div>
131
-
132
- <div id="send-panel" class="card mt-3" style="display:none;">
133
- <div class="card-header"><strong>Invitations</strong></div>
134
- <div class="card-body" style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;">
135
- <button id="send-all-btn" class="btn btn-primary"><span data-icon="send"></span> Send invitations</button>
136
- <button id="send-reminder-btn" class="btn btn-outline"><span data-icon="bell"></span> Send reminder to non-responders</button>
137
- <span id="send-hint" class="text-muted" style="font-size:0.85rem;"></span>
138
- </div>
139
- </div>
140
-
141
- </div>
@@ -1,25 +0,0 @@
1
- <div class="surveys-plugin">
2
-
3
- <!-- Toolbar -->
4
- <div class="toolbar mb-3" style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
5
- <h2 style="margin:0;flex:1;">Surveys</h2>
6
- <button id="manage-audience" class="btn btn-outline">
7
- <span data-icon="users"></span>
8
- Audience
9
- </button>
10
- <button id="new-survey" class="btn btn-primary">
11
- <span data-icon="plus"></span>
12
- New Survey
13
- </button>
14
- </div>
15
-
16
- <!-- Surveys table -->
17
- <div id="surveys-table"></div>
18
-
19
- <!-- Empty state -->
20
- <div id="surveys-empty" style="display:none;text-align:center;padding:3rem 1rem;color:var(--dm-text-muted);">
21
- <span data-icon="clipboard-list" style="font-size:3rem;display:block;margin-bottom:1rem;"></span>
22
- <p>No surveys yet. Click <strong>New Survey</strong> to create your first campaign.</p>
23
- </div>
24
-
25
- </div>
@@ -1,301 +0,0 @@
1
- /**
2
- * Surveys plugin - shared audience view.
3
- *
4
- * Manages survey contacts and audience groups, plus an import action that pulls
5
- * the audience from the Contacts plugin.
6
- *
7
- * @module surveys/admin/views/audience
8
- */
9
-
10
- const BASE = '/api/plugins/surveys';
11
-
12
- /**
13
- * Authenticated JSON fetch helper. Mirrors plugins/contacts/admin/views/contacts.js.
14
- *
15
- * @param {string} url
16
- * @param {string} [method]
17
- * @param {object} [body]
18
- * @returns {Promise<*>}
19
- */
20
- async function api(url, method = 'GET', body) {
21
- const opts = {method, headers: {'Authorization': 'Bearer ' + (S.get('auth_token') || '')}};
22
- if (body !== undefined) {
23
- opts.headers['Content-Type'] = 'application/json';
24
- opts.body = JSON.stringify(body);
25
- }
26
- const res = await fetch(url, opts);
27
- if (!res.ok) {
28
- const err = await res.json().catch(() => ({error: res.statusText}));
29
- throw new Error(err.error || res.statusText);
30
- }
31
- const text = await res.text();
32
- return text ? JSON.parse(text) : {};
33
- }
34
-
35
- /**
36
- * Escape HTML special characters to prevent XSS in render strings.
37
- *
38
- * @param {*} str
39
- * @returns {string}
40
- */
41
- function esc(str) {
42
- return String(str ?? '')
43
- .replace(/&/g, '&amp;')
44
- .replace(/</g, '&lt;')
45
- .replace(/>/g, '&gt;')
46
- .replace(/"/g, '&quot;');
47
- }
48
-
49
- export const audienceView = {
50
- templateUrl: '/plugins/surveys/admin/templates/audience.html',
51
-
52
- /**
53
- * Mount the audience view.
54
- *
55
- * @param {object} $container - Domma-wrapped container element
56
- * @returns {Promise<void>}
57
- */
58
- async onMount($container) {
59
- let contacts = [];
60
- let groups = [];
61
- let table = null;
62
-
63
- async function load() {
64
- const [c, g] = await Promise.all([
65
- api(`${BASE}/contacts`).catch(() => []),
66
- api(`${BASE}/groups`).catch(() => [])
67
- ]);
68
- contacts = Array.isArray(c) ? c : [];
69
- groups = Array.isArray(g) ? g : [];
70
- }
71
-
72
- // ---- Contacts table ----------------------------------------------
73
- function buildTable() {
74
- const tableEl = $container.find('#audience-table').get(0);
75
- const emptyEl = $container.find('#audience-empty').get(0);
76
-
77
- if (!contacts.length) {
78
- if (tableEl) tableEl.style.display = 'none';
79
- if (emptyEl) emptyEl.style.display = 'block';
80
- return;
81
- }
82
- if (emptyEl) emptyEl.style.display = 'none';
83
- if (tableEl) tableEl.style.display = '';
84
-
85
- if (table) {
86
- table.setData(contacts);
87
- } else {
88
- // Render functions return HTML strings; delete uses delegated
89
- // listener below (not inline onclick) so we can await E.confirm.
90
- table = T.create($container.find('#audience-table').get(0), {
91
- data: contacts,
92
- columns: [
93
- {key: 'name', title: 'Name', sortable: true, render: (v) => esc(v) || '-'},
94
- {
95
- key: 'email', title: 'Email', sortable: true,
96
- render: (v) => v ? `<a href="mailto:${esc(v)}">${esc(v)}</a>` : '-'
97
- },
98
- {
99
- key: 'groups', title: 'Groups', sortable: false,
100
- render: (v) => {
101
- if (!Array.isArray(v) || !v.length) return '-';
102
- return v.map((g) => `<span class="badge badge-primary" style="margin-right:2px;">${esc(g)}</span>`).join('');
103
- }
104
- },
105
- {
106
- key: 'id', title: 'Actions', sortable: false,
107
- render: (id) =>
108
- `<button class="btn btn-sm btn-danger contact-del" data-id="${esc(id)}">` +
109
- `<span data-icon="trash" data-icon-size="14"></span> Delete</button>`
110
- }
111
- ],
112
- emptyMessage: 'No contacts.'
113
- });
114
-
115
- const container = $container.find('#audience-table').get(0);
116
- if (container) {
117
- container.addEventListener('click', async (e) => {
118
- const btn = e.target.closest('.contact-del');
119
- if (!btn) return;
120
- const id = btn.dataset.id;
121
- const contact = contacts.find((c) => c.id === id);
122
- const ok = await E.confirm(`Delete "${contact?.name || contact?.email || 'this contact'}"?`);
123
- if (!ok) return;
124
- try {
125
- await api(`${BASE}/contacts/${encodeURIComponent(id)}`, 'DELETE');
126
- E.toast('Contact deleted.', {type: 'success'});
127
- await refresh();
128
- } catch (err) {
129
- E.toast(err?.message ?? 'Failed to delete contact.', {type: 'error'});
130
- }
131
- });
132
- }
133
- }
134
- Domma.icons.scan();
135
- }
136
-
137
- // ---- Groups panel -------------------------------------------------
138
- function buildGroups() {
139
- const listEl = $container.find('#groups-list').get(0);
140
- if (!listEl) return;
141
- while (listEl.firstChild) listEl.removeChild(listEl.firstChild);
142
-
143
- if (!groups.length) {
144
- const p = document.createElement('p');
145
- p.className = 'text-muted';
146
- p.textContent = 'No groups yet.';
147
- listEl.appendChild(p);
148
- return;
149
- }
150
- groups.forEach((g) => {
151
- const row = document.createElement('div');
152
- row.style.cssText = 'padding:0.4rem 0;border-bottom:1px solid var(--dm-border);';
153
- const badge = document.createElement('span');
154
- badge.className = 'badge badge-primary';
155
- badge.textContent = g.name; // textContent - safe
156
- row.appendChild(badge);
157
- if (g.description) {
158
- const desc = document.createElement('div');
159
- desc.className = 'text-muted';
160
- desc.style.fontSize = '0.8rem';
161
- desc.textContent = g.description;
162
- row.appendChild(desc);
163
- }
164
- listEl.appendChild(row);
165
- });
166
- }
167
-
168
- // ---- Add-contact modal (light-DOM projection) ---------------------
169
- function openContactModal() {
170
- const modal = E.modal({title: 'Add Contact', size: 'md'});
171
-
172
- const wrapper = document.createElement('div');
173
- wrapper.style.cssText = 'padding:0.25rem 0 0.5rem;';
174
-
175
- const formContainer = document.createElement('div');
176
- wrapper.appendChild(formContainer);
177
-
178
- // Canonical F.create: (blueprintObject, defaultsObject, opts).renderTo(el).
179
- // Blueprint is keyed by field name; placeholder/hint live under
180
- // formConfig (proven shape - collection-entries.js). We supply our own
181
- // buttons (below) and read values from the DOM on save, so the modal's
182
- // HTML sanitiser never strips an interactive submit button.
183
- F.create(
184
- {
185
- name: {type: 'string', label: 'Name', formConfig: {placeholder: 'Full name'}},
186
- email: {
187
- type: 'string', label: 'Email', required: true,
188
- formConfig: {placeholder: 'email@example.com'}
189
- },
190
- groups: {
191
- type: 'multiselect', label: 'Groups',
192
- options: groups.map((g) => ({value: g.name, label: g.name})),
193
- formConfig: {hint: 'Hold Ctrl/Cmd to select multiple.'}
194
- }
195
- },
196
- {},
197
- {layout: 'stacked', columns: 1, showSubmitButton: false}
198
- ).renderTo(formContainer);
199
-
200
- const btnWrap = document.createElement('div');
201
- btnWrap.style.cssText = 'display:flex;justify-content:flex-end;gap:0.5rem;margin-top:0.75rem;';
202
-
203
- const cancelBtn = document.createElement('button');
204
- cancelBtn.className = 'btn btn-outline';
205
- cancelBtn.textContent = 'Cancel';
206
- cancelBtn.addEventListener('click', () => modal.close());
207
-
208
- const saveBtn = document.createElement('button');
209
- saveBtn.className = 'btn btn-primary';
210
- saveBtn.textContent = 'Save contact';
211
- saveBtn.addEventListener('click', async () => {
212
- const nameEl = formContainer.querySelector('[name="name"]');
213
- const emailEl = formContainer.querySelector('[name="email"]');
214
- const groupsEl = formContainer.querySelector('[name="groups"]');
215
- const email = (emailEl?.value ?? '').trim();
216
- if (!email) {
217
- E.toast('Email is required.', {type: 'error'});
218
- return;
219
- }
220
- const selectedGroups = groupsEl
221
- ? Array.from(groupsEl.selectedOptions).map((o) => o.value)
222
- : [];
223
- try {
224
- await api(`${BASE}/contacts`, 'POST', {
225
- name: (nameEl?.value ?? '').trim(),
226
- email,
227
- groups: selectedGroups
228
- });
229
- E.toast('Contact added.', {type: 'success'});
230
- modal.close();
231
- await refresh();
232
- } catch (err) {
233
- E.toast(err?.message ?? 'Failed to add contact.', {type: 'error'});
234
- }
235
- });
236
-
237
- btnWrap.appendChild(cancelBtn);
238
- btnWrap.appendChild(saveBtn);
239
- wrapper.appendChild(btnWrap);
240
-
241
- // Project the assembled DOM into the modal's light DOM so the
242
- // interactive buttons survive (E.modal().setContent strips them).
243
- modal.element.appendChild(wrapper);
244
- modal.open();
245
- Domma.icons.scan();
246
- }
247
-
248
- // ---- Refresh ------------------------------------------------------
249
- async function refresh() {
250
- await load();
251
- buildTable();
252
- buildGroups();
253
- }
254
-
255
- // ---- Wiring -------------------------------------------------------
256
- const addContactBtn = $container.find('#add-contact').get(0);
257
- if (addContactBtn) addContactBtn.addEventListener('click', openContactModal);
258
-
259
- const backBtn = $container.find('#back-surveys').get(0);
260
- if (backBtn) backBtn.addEventListener('click', () => { location.hash = '#/plugins/surveys'; });
261
-
262
- const importBtn = $container.find('#import-contacts').get(0);
263
- if (importBtn) {
264
- importBtn.addEventListener('click', async () => {
265
- const ok = await E.confirm('Import all contacts from the Contacts plugin?');
266
- if (!ok) return;
267
- try {
268
- const res = await api(`${BASE}/import-from-contacts`, 'POST', {});
269
- E.toast(`Imported ${res?.imported ?? 0} contact(s).`, {type: 'success'});
270
- await refresh();
271
- } catch (err) {
272
- E.toast(err?.message ?? 'Import failed.', {type: 'error'});
273
- }
274
- });
275
- }
276
-
277
- const addGroupBtn = $container.find('#add-group').get(0);
278
- const newGroupInput = $container.find('#new-group-name').get(0);
279
- async function addGroup() {
280
- const name = (newGroupInput?.value ?? '').trim();
281
- if (!name) return;
282
- try {
283
- await api(`${BASE}/groups`, 'POST', {name});
284
- if (newGroupInput) newGroupInput.value = '';
285
- E.toast('Group added.', {type: 'success'});
286
- await refresh();
287
- } catch (err) {
288
- E.toast(err?.message ?? 'Failed to add group.', {type: 'error'});
289
- }
290
- }
291
- if (addGroupBtn) addGroupBtn.addEventListener('click', addGroup);
292
- if (newGroupInput) {
293
- newGroupInput.addEventListener('keydown', (e) => {
294
- if (e.key === 'Enter') addGroup();
295
- });
296
- }
297
-
298
- await refresh();
299
- Domma.icons.scan();
300
- }
301
- };