domma-cms 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +2 -3
  2. package/admin/css/admin.css +78 -1
  3. package/admin/js/api.js +32 -0
  4. package/admin/js/app.js +24 -7
  5. package/admin/js/config/sidebar-config.js +8 -0
  6. package/admin/js/templates/collection-editor.html +80 -0
  7. package/admin/js/templates/collection-entries.html +36 -0
  8. package/admin/js/templates/collections.html +12 -0
  9. package/admin/js/templates/documentation.html +136 -0
  10. package/admin/js/templates/navigation.html +26 -4
  11. package/admin/js/templates/page-editor.html +91 -85
  12. package/admin/js/templates/settings.html +433 -172
  13. package/admin/js/views/collection-editor.js +487 -0
  14. package/admin/js/views/collection-entries.js +484 -0
  15. package/admin/js/views/collections.js +153 -0
  16. package/admin/js/views/dashboard.js +14 -6
  17. package/admin/js/views/index.js +9 -3
  18. package/admin/js/views/login.js +3 -2
  19. package/admin/js/views/navigation.js +77 -11
  20. package/admin/js/views/page-editor.js +207 -25
  21. package/admin/js/views/pages.js +14 -6
  22. package/admin/js/views/settings.js +137 -2
  23. package/admin/js/views/users.js +10 -7
  24. package/bin/cli.js +53 -17
  25. package/config/auth.json +2 -1
  26. package/config/content.json +1 -0
  27. package/config/navigation.json +14 -4
  28. package/config/plugins.json +0 -18
  29. package/config/presets.json +4 -8
  30. package/config/site.json +44 -3
  31. package/package.json +6 -2
  32. package/plugins/domma-effects/admin/templates/domma-effects.html +92 -3
  33. package/plugins/domma-effects/plugin.js +125 -0
  34. package/plugins/domma-effects/public/inject-body.html +19 -0
  35. package/plugins/example-analytics/admin/views/analytics.js +2 -2
  36. package/plugins/example-analytics/plugin.json +8 -0
  37. package/plugins/example-analytics/stats.json +15 -1
  38. package/plugins/form-builder/admin/templates/form-editor.html +19 -6
  39. package/plugins/form-builder/admin/views/form-editor.js +634 -9
  40. package/plugins/form-builder/admin/views/form-submissions.js +4 -4
  41. package/plugins/form-builder/admin/views/forms-list.js +5 -5
  42. package/plugins/form-builder/data/forms/consent.json +104 -0
  43. package/plugins/form-builder/data/forms/contacts.json +66 -0
  44. package/plugins/form-builder/data/submissions/consent.json +13 -0
  45. package/plugins/form-builder/data/submissions/contacts.json +26 -0
  46. package/plugins/form-builder/plugin.js +62 -11
  47. package/plugins/form-builder/plugin.json +12 -16
  48. package/plugins/form-builder/public/form-logic-engine.js +568 -0
  49. package/plugins/form-builder/public/inject-body.html +88 -6
  50. package/plugins/form-builder/public/inject-head.html +16 -0
  51. package/plugins/form-builder/public/package.json +1 -0
  52. package/public/css/site.css +113 -0
  53. package/public/js/btt.js +90 -0
  54. package/public/js/cookie-consent.js +61 -0
  55. package/public/js/site.js +129 -34
  56. package/scripts/build.js +129 -0
  57. package/scripts/seed.js +517 -7
  58. package/scripts/setup.js +12 -9
  59. package/server/routes/api/collections.js +301 -0
  60. package/server/routes/api/settings.js +66 -2
  61. package/server/server.js +19 -15
  62. package/server/services/collections.js +430 -0
  63. package/server/services/content.js +11 -2
  64. package/server/services/hooks.js +109 -0
  65. package/server/services/markdown.js +500 -149
  66. package/server/services/plugins.js +6 -1
  67. package/server/services/renderer.js +73 -7
  68. package/server/templates/page.html +38 -3
  69. package/plugins/back-to-top/admin/templates/back-to-top-settings.html +0 -55
  70. package/plugins/back-to-top/admin/views/back-to-top-settings.js +0 -44
  71. package/plugins/back-to-top/config.js +0 -10
  72. package/plugins/back-to-top/plugin.js +0 -24
  73. package/plugins/back-to-top/plugin.json +0 -36
  74. package/plugins/back-to-top/public/inject-body.html +0 -105
  75. package/plugins/cookie-consent/admin/templates/cookie-consent-settings.html +0 -113
  76. package/plugins/cookie-consent/admin/views/cookie-consent-settings.js +0 -73
  77. package/plugins/cookie-consent/config.js +0 -30
  78. package/plugins/cookie-consent/plugin.js +0 -24
  79. package/plugins/cookie-consent/plugin.json +0 -36
  80. package/plugins/cookie-consent/public/inject-body.html +0 -69
  81. package/plugins/custom-css/admin/templates/custom-css.html +0 -17
  82. package/plugins/custom-css/admin/views/custom-css.js +0 -35
  83. package/plugins/custom-css/config.js +0 -1
  84. package/plugins/custom-css/data/custom.css +0 -0
  85. package/plugins/custom-css/plugin.js +0 -63
  86. package/plugins/custom-css/plugin.json +0 -32
  87. package/plugins/custom-css/public/inject-head.html +0 -1
  88. package/plugins/form-builder/data/forms/contact.json +0 -52
  89. package/plugins/form-builder/data/submissions/contact.json +0 -14
@@ -5,7 +5,7 @@
5
5
  * Search/filter by text + date range with live count.
6
6
  * Row click opens a detail modal.
7
7
  */
8
- import { apiRequest } from '/admin/js/api.js';
8
+ import {apiRequest} from '/admin/js/api.js';
9
9
 
10
10
  let currentSlug = null;
11
11
  let currentFields = [];
@@ -169,7 +169,7 @@ function renderTable(submissions, $container) {
169
169
  // Build dynamic columns from form field definitions (excludes page-break fields)
170
170
  const fieldCols = currentFields.map(field => ({
171
171
  key: `data.${field.name}`,
172
- label: field.label || field.name,
172
+ title: field.label || field.name,
173
173
  render: (val, row) => {
174
174
  const raw = row.data?.[field.name] ?? '';
175
175
  const str = String(raw);
@@ -184,12 +184,12 @@ function renderTable(submissions, $container) {
184
184
  ...fieldCols,
185
185
  {
186
186
  key: 'meta',
187
- label: 'Date',
187
+ title: 'Date',
188
188
  render: (val) => D(val?.createdAt).format('DD MMM YYYY HH:mm')
189
189
  },
190
190
  {
191
191
  key: 'id',
192
- label: '',
192
+ title: '',
193
193
  render: (val) => {
194
194
  const btn = document.createElement('button');
195
195
  btn.className = 'btn btn-sm btn-danger js-delete-submission';
@@ -92,7 +92,7 @@ async function loadForms($container) {
92
92
  columns: [
93
93
  {
94
94
  key: 'title',
95
- label: 'Title',
95
+ title: 'Title',
96
96
  render: (val, row) => {
97
97
  const a = document.createElement('a');
98
98
  a.href = `#/plugins/form-builder/edit/${esc(row.slug)}`;
@@ -103,18 +103,18 @@ async function loadForms($container) {
103
103
  },
104
104
  {
105
105
  key: 'slug',
106
- label: 'Slug',
106
+ title: 'Slug',
107
107
  render: val => {
108
108
  const code = document.createElement('code');
109
109
  code.textContent = val;
110
110
  return code.outerHTML;
111
111
  }
112
112
  },
113
- { key: 'fields', label: 'Fields', render: val => String(val?.length ?? 0) },
114
- { key: 'submissionCount', label: 'Submissions', render: val => String(val ?? 0) },
113
+ {key: 'fields', title: 'Field Count', render: val => String(val?.length ?? 0)},
114
+ {key: 'submissionCount', title: 'Submission Count', render: val => String(val ?? 0)},
115
115
  {
116
116
  key: 'slug',
117
- label: '',
117
+ title: 'Actions',
118
118
  render: (val) => {
119
119
  const wrap = document.createElement('div');
120
120
  wrap.style.cssText = 'display:flex;gap:.4rem;justify-content:flex-end;';
@@ -0,0 +1,104 @@
1
+ {
2
+ "slug": "consent",
3
+ "title": "Consent",
4
+ "description": "",
5
+ "fields": [
6
+ {
7
+ "name": "right_to_work",
8
+ "type": "radio",
9
+ "label": "Right To Work",
10
+ "required": true,
11
+ "placeholder": "Right to work within the UK and Ireland",
12
+ "helper": "Do you have the right to work within the UK and Ireland",
13
+ "options": [
14
+ {
15
+ "value": "yes",
16
+ "label": "Yes"
17
+ },
18
+ {
19
+ "value": "no",
20
+ "label": "No"
21
+ },
22
+ {
23
+ "value": "visa",
24
+ "label": "Visa/Sponsorship Required"
25
+ }
26
+ ]
27
+ },
28
+ {
29
+ "name": "availability",
30
+ "type": "radio",
31
+ "label": "Availability",
32
+ "required": true,
33
+ "placeholder": "Are You Available Immediately?",
34
+ "helper": "",
35
+ "options": [
36
+ {
37
+ "value": "yes",
38
+ "label": "Yes"
39
+ },
40
+ {
41
+ "value": "no",
42
+ "label": "No"
43
+ }
44
+ ],
45
+ "logic": {
46
+ "visibility": {
47
+ "default": "hidden",
48
+ "conditions": [
49
+ {
50
+ "when": {
51
+ "all": [
52
+ {
53
+ "field": "right_to_work",
54
+ "operator": "equals",
55
+ "value": "yes"
56
+ }
57
+ ]
58
+ },
59
+ "then": "visible"
60
+ }
61
+ ]
62
+ },
63
+ "requirement": {
64
+ "default": false,
65
+ "conditions": [
66
+ {
67
+ "when": {
68
+ "all": [
69
+ {
70
+ "field": "right_to_work",
71
+ "operator": "equals",
72
+ "value": "yes"
73
+ }
74
+ ]
75
+ },
76
+ "then": true
77
+ }
78
+ ]
79
+ }
80
+ }
81
+ }
82
+ ],
83
+ "settings": {
84
+ "submitText": "Submit",
85
+ "successMessage": "Thank you for your submission.",
86
+ "layout": "stacked",
87
+ "honeypot": true,
88
+ "rateLimitPerMinute": 3
89
+ },
90
+ "actions": {
91
+ "email": {
92
+ "enabled": true,
93
+ "recipients": "pinpointzero@gmail.com",
94
+ "subjectPrefix": "pinpointzero@gmail.com"
95
+ },
96
+ "webhook": {
97
+ "enabled": false,
98
+ "url": "",
99
+ "method": "POST"
100
+ }
101
+ },
102
+ "createdAt": "2026-03-04T14:06:34.797Z",
103
+ "updatedAt": "2026-03-04T15:46:24.896Z"
104
+ }
@@ -0,0 +1,66 @@
1
+ {
2
+ "slug": "contacts",
3
+ "title": "Contacts",
4
+ "description": "Contact Information",
5
+ "fields": [
6
+ {
7
+ "name": "full_name",
8
+ "type": "string",
9
+ "label": "Full Name",
10
+ "required": false,
11
+ "placeholder": "Full Name",
12
+ "helper": "Full Name",
13
+ "minLength": 8,
14
+ "maxLength": 255
15
+ },
16
+ {
17
+ "type": "spacer"
18
+ },
19
+ {
20
+ "name": "phone_number",
21
+ "type": "tel",
22
+ "label": "Phone Number",
23
+ "required": false,
24
+ "placeholder": "Phone Number",
25
+ "helper": "Primary Phone Number"
26
+ },
27
+ {
28
+ "type": "spacer"
29
+ },
30
+ {
31
+ "name": "email_address",
32
+ "type": "string",
33
+ "label": "Email Address",
34
+ "required": false,
35
+ "placeholder": "Email Address",
36
+ "helper": "Email Address",
37
+ "minLength": 8,
38
+ "maxLength": 255
39
+ }
40
+ ],
41
+ "settings": {
42
+ "submitText": "Submit",
43
+ "successMessage": "Thank you for your submission.",
44
+ "layout": "stacked",
45
+ "honeypot": true,
46
+ "rateLimitPerMinute": 3
47
+ },
48
+ "actions": {
49
+ "email": {
50
+ "enabled": false,
51
+ "recipients": "",
52
+ "subjectPrefix": "[Contacts]"
53
+ },
54
+ "webhook": {
55
+ "enabled": false,
56
+ "url": "",
57
+ "method": "POST"
58
+ },
59
+ "collection": {
60
+ "enabled": true,
61
+ "slug": "contacts"
62
+ }
63
+ },
64
+ "createdAt": "2026-03-05T12:21:03.667Z",
65
+ "updatedAt": "2026-03-05T14:12:38.789Z"
66
+ }
@@ -0,0 +1,13 @@
1
+ [
2
+ {
3
+ "id": "0152b2f3-98b7-4cea-bcd1-88e0c697a609",
4
+ "data": {
5
+ "right_to_work": "yes",
6
+ "availability": "yes"
7
+ },
8
+ "meta": {
9
+ "ip": "127.0.0.1",
10
+ "createdAt": "2026-03-04T16:50:34.493Z"
11
+ }
12
+ }
13
+ ]
@@ -0,0 +1,26 @@
1
+ [
2
+ {
3
+ "id": "5f8a7059-f93e-4612-a764-7646fcac8162",
4
+ "data": {
5
+ "full_name": "Jane Smith",
6
+ "phone_number": "07700900123",
7
+ "email_address": "jane@example.com"
8
+ },
9
+ "meta": {
10
+ "ip": "127.0.0.1",
11
+ "createdAt": "2026-03-05T14:17:26.612Z"
12
+ }
13
+ },
14
+ {
15
+ "id": "57c8f836-34d5-4cab-af7b-8fd8be2c89c8",
16
+ "data": {
17
+ "full_name": "Live Test",
18
+ "phone_number": "07700900001",
19
+ "email_address": "live@test.com"
20
+ },
21
+ "meta": {
22
+ "ip": "127.0.0.1",
23
+ "createdAt": "2026-03-05T14:18:47.698Z"
24
+ }
25
+ }
26
+ ]
@@ -21,10 +21,16 @@
21
21
  import fs from 'fs/promises';
22
22
  import path from 'path';
23
23
  import crypto from 'crypto';
24
- import { fileURLToPath } from 'url';
25
- import { getPluginSettings, savePluginState } from '../../server/services/plugins.js';
26
- import { getConfig } from '../../server/config.js';
27
- import { createTransport, sendFormEmail } from './email.js';
24
+ import {fileURLToPath} from 'url';
25
+ import {createRequire} from 'module';
26
+ import {getPluginSettings, savePluginState} from '../../server/services/plugins.js';
27
+ import {getConfig} from '../../server/config.js';
28
+ import {createTransport, sendFormEmail} from './email.js';
29
+ import {createEntry, getCollection} from '../../server/services/collections.js';
30
+
31
+ // Load shared logic engine (UMD/CJS) from browser-compatible public file
32
+ const _require = createRequire(import.meta.url);
33
+ const FormLogicEngine = _require('./public/form-logic-engine.js');
28
34
 
29
35
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
30
36
  const FORMS_DIR = path.join(__dirname, 'data', 'forms');
@@ -337,12 +343,42 @@ export default async function formBuilderPlugin(fastify, options) {
337
343
  return { ok: true, message: settings.successMessage };
338
344
  }
339
345
 
340
- // Validate required fields
341
- const missingFields = (form.fields || [])
342
- .filter(f => f.required && !body[f.name]?.toString().trim())
343
- .map(f => f.label || f.name);
344
- if (missingFields.length) {
345
- return reply.status(400).send({ error: `Required fields missing: ${missingFields.join(', ')}.` });
346
+ // Build form values for engine evaluation (trim strings, default to '')
347
+ const formValues = {};
348
+ for (const field of form.fields || []) {
349
+ if (field.type === 'page-break' || field.type === 'spacer') continue;
350
+ const val = body[field.name];
351
+ formValues[field.name] = val !== undefined ? (typeof val === 'string' ? val.trim() : val) : '';
352
+ }
353
+
354
+ // Evaluate conditional logic — visibility, requirement, validation
355
+ const missingFields = [];
356
+ const validationErrors = [];
357
+ const visibleFieldNames = new Set();
358
+
359
+ for (const field of form.fields || []) {
360
+ if (field.type === 'page-break' || field.type === 'spacer') continue;
361
+ const vis = FormLogicEngine.evaluateFieldVisibility(field, formValues);
362
+ if (vis === 'hidden') continue;
363
+ visibleFieldNames.add(field.name);
364
+
365
+ const value = formValues[field.name];
366
+ const isEmpty = !value?.toString().trim();
367
+ const required = FormLogicEngine.evaluateFieldRequirement(field, formValues);
368
+ if (required && isEmpty) {
369
+ missingFields.push(field.label || field.name);
370
+ }
371
+ if (!isEmpty) {
372
+ const errors = FormLogicEngine.validateField(field, value, formValues);
373
+ if (errors.length) validationErrors.push(errors[0].message);
374
+ }
375
+ }
376
+
377
+ if (missingFields.length || validationErrors.length) {
378
+ const parts = [];
379
+ if (missingFields.length) parts.push(`Required fields missing: ${missingFields.join(', ')}`);
380
+ if (validationErrors.length) parts.push(validationErrors.join('; '));
381
+ return reply.status(400).send({ error: `${parts.join('. ')}.` });
346
382
  }
347
383
 
348
384
  // Rate limit by IP
@@ -352,9 +388,11 @@ export default async function formBuilderPlugin(fastify, options) {
352
388
  return reply.status(429).send({ error: 'Too many submissions. Please try again later.' });
353
389
  }
354
390
 
355
- // Build generic submission data map from defined fields
391
+ // Build submission data only include visible fields
356
392
  const data = {};
357
393
  for (const field of form.fields || []) {
394
+ if (field.type === 'page-break' || field.type === 'spacer') continue;
395
+ if (!visibleFieldNames.has(field.name)) continue;
358
396
  const val = body[field.name];
359
397
  if (val !== undefined) {
360
398
  data[field.name] = typeof val === 'string' ? val.trim() : val;
@@ -405,6 +443,19 @@ export default async function formBuilderPlugin(fastify, options) {
405
443
  }
406
444
  }
407
445
 
446
+ // Collection action
447
+ const collectionAction = form.actions?.collection;
448
+ if (collectionAction?.enabled && collectionAction.slug) {
449
+ try {
450
+ const col = await getCollection(collectionAction.slug);
451
+ if (col) {
452
+ await createEntry(collectionAction.slug, data, { source: `form:${slug}` });
453
+ }
454
+ } catch (err) {
455
+ fastify.log.warn(`[form-builder] Collection write failed for "${slug}": ${err.message}`);
456
+ }
457
+ }
458
+
408
459
  return { ok: true, message: settings.successMessage || 'Thank you for your submission.' };
409
460
  });
410
461
 
@@ -14,21 +14,13 @@
14
14
  "icon": "layout",
15
15
  "url": "#/plugins/form-builder",
16
16
  "section": "#/plugins/form-builder"
17
- },
18
- {
19
- "id": "form-builder-settings",
20
- "text": "Form Settings",
21
- "icon": "settings",
22
- "url": "#/plugins/form-builder/settings",
23
- "section": "#/plugins/form-builder/settings"
24
17
  }
25
18
  ],
26
19
  "routes": [
27
- { "path": "/plugins/form-builder", "view": "plugin-fb-forms-list", "title": "Forms - Domma CMS" },
28
- { "path": "/plugins/form-builder/new", "view": "plugin-fb-form-editor", "title": "New Form - Domma CMS" },
29
- { "path": "/plugins/form-builder/edit/:slug", "view": "plugin-fb-form-editor", "title": "Edit Form - Domma CMS" },
30
- { "path": "/plugins/form-builder/:slug/submissions", "view": "plugin-fb-form-submissions", "title": "Submissions - Domma CMS" },
31
- { "path": "/plugins/form-builder/settings", "view": "plugin-fb-form-settings", "title": "Form Builder Settings - Domma CMS" }
20
+ { "path": "/plugins/form-builder", "view": "plugin-fb-forms-list", "title": "Forms - Domma CMS" },
21
+ { "path": "/plugins/form-builder/new", "view": "plugin-fb-form-editor", "title": "New Form - Domma CMS" },
22
+ { "path": "/plugins/form-builder/edit/:slug", "view": "plugin-fb-form-editor", "title": "Edit Form - Domma CMS" },
23
+ { "path": "/plugins/form-builder/:slug/submissions", "view": "plugin-fb-form-submissions", "title": "Submissions - Domma CMS" }
32
24
  ],
33
25
  "views": {
34
26
  "plugin-fb-forms-list": {
@@ -42,15 +34,19 @@
42
34
  "plugin-fb-form-submissions": {
43
35
  "entry": "form-builder/admin/views/form-submissions.js",
44
36
  "exportName": "formSubmissionsView"
45
- },
46
- "plugin-fb-form-settings": {
47
- "entry": "form-builder/admin/views/form-settings.js",
48
- "exportName": "formSettingsView"
49
37
  }
50
38
  }
51
39
  },
52
40
  "inject": {
53
41
  "head": "public/inject-head.html",
54
42
  "bodyEnd": "public/inject-body.html"
43
+ },
44
+ "scaffold": {
45
+ "reset": [
46
+ {
47
+ "path": "data/submissions/contact.json",
48
+ "content": "[]"
49
+ }
50
+ ]
55
51
  }
56
52
  }