domma-cms 0.92.1 → 0.94.0

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 (177) hide show
  1. package/CLAUDE.md +5 -3
  2. package/admin/css/admin.css +1 -1
  3. package/admin/js/app.js +2 -2
  4. package/admin/js/lib/action-editor-arrange.js +1 -1
  5. package/admin/js/lib/api-tokens-arrange.js +2 -2
  6. package/admin/js/lib/block-editor-arrange.js +1 -1
  7. package/admin/js/lib/blocks-arrange.js +1 -1
  8. package/admin/js/lib/collection-entries-arrange.js +1 -1
  9. package/admin/js/lib/components-arrange.js +1 -1
  10. package/admin/js/lib/dashboard-arrange.js +1 -1
  11. package/admin/js/lib/dates.js +1 -0
  12. package/admin/js/lib/forms-arrange.js +1 -1
  13. package/admin/js/lib/media-arrange.js +1 -1
  14. package/admin/js/lib/notifications-arrange.js +1 -1
  15. package/admin/js/lib/pages-arrange.js +1 -1
  16. package/admin/js/lib/related.js +1 -1
  17. package/admin/js/lib/timeline-builder.js +2 -2
  18. package/admin/js/templates/action-editor.html +6 -5
  19. package/admin/js/templates/actions-list.html +1 -1
  20. package/admin/js/templates/contacts.html +1 -1
  21. package/admin/js/templates/docs/api-actions.html +86 -60
  22. package/admin/js/templates/docs/api-authentication.html +159 -123
  23. package/admin/js/templates/docs/api-builder.html +197 -0
  24. package/admin/js/templates/docs/api-collections.html +199 -259
  25. package/admin/js/templates/docs/api-external.html +225 -0
  26. package/admin/js/templates/docs/api-forms.html +268 -0
  27. package/admin/js/templates/docs/api-layouts.html +70 -45
  28. package/admin/js/templates/docs/api-media.html +57 -80
  29. package/admin/js/templates/docs/api-navigation.html +66 -22
  30. package/admin/js/templates/docs/api-pages.html +109 -129
  31. package/admin/js/templates/docs/api-plugins.html +123 -61
  32. package/admin/js/templates/docs/api-scaffold.html +185 -0
  33. package/admin/js/templates/docs/api-settings.html +72 -64
  34. package/admin/js/templates/docs/api-users.html +74 -107
  35. package/admin/js/templates/docs/api-views.html +68 -54
  36. package/admin/js/templates/docs/components-howto.html +20 -17
  37. package/admin/js/templates/docs/components-reference.html +13 -16
  38. package/admin/js/templates/docs/components-rules.html +7 -6
  39. package/admin/js/templates/docs/components-walkthrough.html +19 -19
  40. package/admin/js/templates/docs/tutorial-crud.html +71 -40
  41. package/admin/js/templates/docs/tutorial-forms.html +51 -35
  42. package/admin/js/templates/docs/tutorial-plugin.html +132 -56
  43. package/admin/js/templates/docs/usage-actions.html +61 -15
  44. package/admin/js/templates/docs/usage-collections.html +108 -0
  45. package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
  46. package/admin/js/templates/docs/usage-dconfig.html +0 -3
  47. package/admin/js/templates/docs/usage-editions.html +213 -0
  48. package/admin/js/templates/docs/usage-media.html +22 -6
  49. package/admin/js/templates/docs/usage-navigation.html +74 -18
  50. package/admin/js/templates/docs/usage-pages.html +60 -20
  51. package/admin/js/templates/docs/usage-plugins.html +89 -17
  52. package/admin/js/templates/docs/usage-shortcodes.html +123 -70
  53. package/admin/js/templates/docs/usage-site-settings.html +50 -18
  54. package/admin/js/templates/docs/usage-tools.html +73 -0
  55. package/admin/js/templates/docs/usage-users-roles.html +99 -20
  56. package/admin/js/templates/docs/usage-views.html +36 -19
  57. package/admin/js/templates/documentation.html +153 -32
  58. package/admin/js/templates/page-editor.html +0 -5
  59. package/admin/js/templates/plugin-guide.html +15 -0
  60. package/admin/js/templates/plugin-guides.html +21 -0
  61. package/admin/js/templates/pro-docs.html +53 -234
  62. package/admin/js/templates/tutorials.html +5 -4
  63. package/admin/js/views/actions-list.js +3 -3
  64. package/admin/js/views/analytics.js +5 -5
  65. package/admin/js/views/api-endpoint-editor.js +2 -2
  66. package/admin/js/views/block-editor.js +4 -4
  67. package/admin/js/views/blocks.js +4 -4
  68. package/admin/js/views/collection-editor.js +4 -4
  69. package/admin/js/views/collection-entries.js +7 -7
  70. package/admin/js/views/component-editor.js +2 -2
  71. package/admin/js/views/contacts.js +22 -20
  72. package/admin/js/views/context-menu-editor.js +5 -5
  73. package/admin/js/views/doc-pages.js +1 -1
  74. package/admin/js/views/form-editor.js +4 -4
  75. package/admin/js/views/form-submissions.js +2 -2
  76. package/admin/js/views/index.js +1 -1
  77. package/admin/js/views/media.js +3 -3
  78. package/admin/js/views/menu-editor.js +13 -13
  79. package/admin/js/views/menu-locations.js +2 -2
  80. package/admin/js/views/my-profile.js +1 -1
  81. package/admin/js/views/page-editor.js +8 -8
  82. package/admin/js/views/plugin-guides.js +5 -0
  83. package/admin/js/views/project-detail.js +2 -2
  84. package/admin/js/views/project-settings.js +1 -1
  85. package/admin/js/views/role-editor.js +4 -4
  86. package/admin/js/views/search.js +2 -2
  87. package/admin/js/views/seo.js +17 -17
  88. package/admin/js/views/settings.js +3 -3
  89. package/admin/js/views/theme.js +3 -3
  90. package/admin/js/views/user-editor.js +1 -1
  91. package/admin/js/views/users.js +2 -2
  92. package/admin/js/views/view-editor.js +1 -1
  93. package/bin/cli.js +13 -13
  94. package/bin/lib/node-version.js +29 -0
  95. package/package.json +1 -1
  96. package/plugins/_lib/admin/mail/compose-window.js +3 -2
  97. package/plugins/_lib/admin/mail/reader-view.js +7 -6
  98. package/plugins/_lib/admin/mail/scheduling.js +4 -2
  99. package/plugins/_lib/admin/mail/templates.js +4 -4
  100. package/plugins/_lib/admin/ui/dates.js +85 -0
  101. package/plugins/blog/CLAUDE.md +31 -22
  102. package/plugins/blog/admin/views/blog.js +3 -2
  103. package/plugins/blog/admin/views/comments.js +2 -1
  104. package/plugins/blog/admin/views/post-editor.js +4 -4
  105. package/plugins/blog/blocks/blog-card-row.html +1 -1
  106. package/plugins/blog/blocks/blog-card.html +2 -2
  107. package/plugins/blog/blocks/blog-post-classic.html +2 -2
  108. package/plugins/blog/blocks/blog-post-essay.html +2 -2
  109. package/plugins/blog/blocks/blog-post-feature.html +2 -2
  110. package/plugins/blog/blocks/blog-post-minimal.html +2 -2
  111. package/plugins/blog/blocks/blog-post-sidebar.html +2 -2
  112. package/plugins/blog/blocks/blog-post-split.html +2 -2
  113. package/plugins/blog/docs/guide.md +205 -0
  114. package/plugins/blog/lib/layouts.js +3 -3
  115. package/plugins/blog/lib/page.js +2 -1
  116. package/plugins/blog/plugin.js +3 -3
  117. package/plugins/blog/plugin.json +4 -4
  118. package/plugins/blog/tests/layouts.test.js +6 -0
  119. package/plugins/feedback/CLAUDE.md +22 -3
  120. package/plugins/feedback/admin/lib/kit.js +6 -7
  121. package/plugins/feedback/admin/views/feedback.js +79 -10
  122. package/plugins/feedback/admin/views/send.js +28 -6
  123. package/plugins/feedback/docs/guide.md +95 -0
  124. package/plugins/feedback/lib/receiver.js +9 -2
  125. package/plugins/feedback/lib/sender.js +3 -2
  126. package/plugins/feedback/plugin.js +54 -6
  127. package/plugins/feedback/plugin.json +4 -4
  128. package/plugins/feedback/tests/api.test.js +74 -2
  129. package/plugins/free-tier.lock.json +49 -44
  130. package/plugins/mail-reader/CLAUDE.md +33 -18
  131. package/plugins/mail-reader/docs/guide.md +147 -0
  132. package/plugins/mail-reader/plugin.json +1 -1
  133. package/plugins/security/CLAUDE.md +4 -1
  134. package/plugins/security/admin/views/security.js +5 -5
  135. package/plugins/security/docs/guide.md +170 -0
  136. package/plugins/security/plugin.js +2 -1
  137. package/plugins/security/plugin.json +2 -1
  138. package/plugins/shopping-cart/CLAUDE.md +7 -1
  139. package/plugins/shopping-cart/admin/lib/kit.js +5 -2
  140. package/plugins/shopping-cart/admin/views/orders.js +4 -4
  141. package/plugins/shopping-cart/admin/views/overview.js +2 -2
  142. package/plugins/shopping-cart/docs/guide.md +191 -0
  143. package/plugins/shopping-cart/lib/render.js +2 -1
  144. package/plugins/shopping-cart/plugin.json +3 -3
  145. package/public/js/collection-browser.js +2 -2
  146. package/public/js/site.js +1 -1
  147. package/scripts/gen-instance-secret.js +3 -1
  148. package/scripts/setup.js +3 -1
  149. package/server/middleware/auth.js +2 -1
  150. package/server/routes/api/actions.js +47 -27
  151. package/server/routes/api/blocks.js +2 -1
  152. package/server/routes/api/collections.js +16 -52
  153. package/server/routes/api/contacts.js +66 -3
  154. package/server/routes/api/documentation.js +42 -0
  155. package/server/routes/api/notifications.js +3 -2
  156. package/server/routes/api/users.js +10 -6
  157. package/server/server.js +16 -1
  158. package/server/services/actions.js +110 -34
  159. package/server/services/adapterRegistry.js +169 -16
  160. package/server/services/adapters/FileAdapter.js +25 -0
  161. package/server/services/adapters/MongoAdapter.js +23 -0
  162. package/server/services/collections.js +104 -1
  163. package/server/services/connectionManager.js +12 -0
  164. package/server/services/dates.js +81 -0
  165. package/server/services/docs.js +13 -2
  166. package/server/services/markdown.js +75 -26
  167. package/server/services/notification-sources.js +6 -5
  168. package/server/services/passwordReset.js +2 -1
  169. package/server/services/permissionRegistry.js +3 -2
  170. package/server/services/pluginGuides.js +255 -0
  171. package/server/services/pluginInstaller.js +54 -13
  172. package/server/services/plugins.js +29 -1
  173. package/server/services/presetCollections.js +31 -5
  174. package/server/services/renderer.js +2 -2
  175. package/server/services/sidebarBadges.js +3 -1
  176. package/server/services/tools.js +4 -2
  177. package/server/templates/page.html +2 -2
@@ -19,13 +19,14 @@
19
19
  We'll build one component end-to-end: a <strong>star rating</strong> widget you can drop onto any
20
20
  page with <code>[component name="star-rating" max="5" value="3" /]</code>. By the end you'll have
21
21
  used every section of a <code>.dmc</code> file, reactive state, an event listener, a custom event,
22
- and the live preview. Follow along in a real <strong>New Component</strong> editor.
22
+ and the live preview. Follow along in a real <strong>New component</strong> editor.
23
23
  </p>
24
24
 
25
25
  <hr>
26
26
 
27
27
  <h2>Step 1 - Create the component</h2>
28
- <p>Open <a href="#/components">Data → Components</a> → <strong>New Component</strong> and name it
28
+ <p>Open <a href="#/components">Data &gt; Components</a>, click <strong>New component</strong> in the banner
29
+ and name it
29
30
  <code>star-rating</code>. Remember: lowercase + hyphens, and the name is permanent because it
30
31
  becomes the element tag <code>&lt;dm-star-rating&gt;</code>. The four source tabs
31
32
  (<code>&lt;template&gt;</code>, <code>&lt;props&gt;</code>, <code>&lt;script&gt;</code>,
@@ -47,13 +48,13 @@
47
48
 
48
49
  <h2>Step 3 - Write the template</h2>
49
50
  <p>We render <code>max</code> stars and mark each as filled if its index is below the current rating.
50
- Because <code>{{#each}}</code> needs a list, we'll build a <code>stars</code> array in state
51
+ Because <code>&#123;&#123;#each&#125;&#125;</code> needs a list, we'll build a <code>stars</code> array in state
51
52
  (next step) where each item knows whether it's <code>on</code>. For now, the markup:</p>
52
53
  <pre class="code-block"><code>&lt;template&gt;
53
- &lt;div class="dm-stars" role="img" aria-label="{{value}} of {{max}}"&gt;
54
- {{#each stars}}
55
- &lt;button class="star {{#if on}}on{{/if}}" data-action="rate" data-index="{{n}}"&gt;★&lt;/button&gt;
56
- {{/each}}
54
+ &lt;div class="dm-stars" role="img" aria-label="&#123;&#123;value&#125;&#125; of &#123;&#123;max&#125;&#125;"&gt;
55
+ &#123;&#123;#each stars&#125;&#125;
56
+ &lt;button class="star &#123;&#123;#if on&#125;&#125;on&#123;&#123;/if&#125;&#125;" data-action="rate" data-index="&#123;&#123;n&#125;&#125;"&gt;★&lt;/button&gt;
57
+ &#123;&#123;/each&#125;&#125;
57
58
  &lt;/div&gt;
58
59
  &lt;/template&gt;</code></pre>
59
60
  <p>Each star is a button carrying its 1-based position in <code>data-index</code>, so a single click
@@ -68,7 +69,6 @@
68
69
  <pre class="code-block"><code>&lt;script&gt;
69
70
  export default {
70
71
  data() { return { stars: [], value: 0 }; },
71
-
72
72
  methods: {
73
73
  // Build the star list for a given rating.
74
74
  render(value) {
@@ -86,7 +86,6 @@ export default {
86
86
  }));
87
87
  }
88
88
  },
89
-
90
89
  onMount() {
91
90
  this.render(this.props.value);
92
91
  this.el.shadowRoot.addEventListener('click', (e) =&gt; {
@@ -119,7 +118,7 @@ export default {
119
118
  <h2>Step 6 - Watch the preview, then save</h2>
120
119
  <p>As you typed, the editor recompiled and re-mounted the component in the preview iframe. Toggle
121
120
  <code>readonly</code> and change <code>value</code> in the <em>Preview props</em> panel to sanity-check
122
- both modes. When it looks right, hit <strong>Save Component</strong> - the source compiles before it
121
+ both modes. When it looks right, press <strong>Save</strong> in the banner (or Ctrl+S) - the source compiles before it
123
122
  is written, so a typo surfaces as a clear error rather than a broken page.</p>
124
123
 
125
124
  <hr>
@@ -127,19 +126,20 @@ export default {
127
126
  <h2>Step 7 - Use it on a page</h2>
128
127
  <p>Display-only, in any Markdown page:</p>
129
128
  <pre class="code-block"><code>[component name="star-rating" max="5" value="4" readonly="true" /]</code></pre>
130
- <p>Interactive, reacting to the custom event with a little page script:</p>
131
- <pre class="code-block"><code>&lt;dm-star-rating max="5" value="0" id="r1"&gt;&lt;/dm-star-rating&gt;
132
- &lt;script&gt;
133
- document.getElementById('r1').addEventListener('rating-change', (e) =&gt; {
129
+ <p>Interactive, as a raw tag with an id:</p>
130
+ <pre class="code-block"><code>&lt;dm-star-rating max="5" value="0" id="r1"&gt;&lt;/dm-star-rating&gt;</code></pre>
131
+ <p>A page's content cannot carry a <code>&lt;script&gt;</code> - the sanitiser strips it - so the code
132
+ that reacts to <code>rating-change</code> belongs in site-wide JavaScript, such as a plugin's
133
+ <code>inject.bodyEnd</code> snippet, or in another component that wraps this one:</p>
134
+ <pre class="code-block"><code>document.getElementById('r1')?.addEventListener('rating-change', (e) =&gt; {
134
135
  console.log('User picked', e.detail.value);
135
- });
136
- &lt;/script&gt;</code></pre>
136
+ });</code></pre>
137
137
 
138
138
  <hr>
139
139
 
140
140
  <h2>Step 8 - Share it</h2>
141
- <p>Back on <a href="#/components">Components</a>, the <span data-icon="download"></span> Export button
142
- gives you <code>star-rating.dmcomponent.json</code> - import that on another Domma site to reuse the
141
+ <p>Back on <a href="#/components">Components</a>, right-click the row and choose
142
+ <span data-icon="download"></span> <strong>Export .dmcomponent.json</strong> - it gives you <code>star-rating.dmcomponent.json</code> - import that on another Domma site to reuse the
143
143
  widget verbatim.</p>
144
144
 
145
145
  <hr>
@@ -147,7 +147,7 @@ document.getElementById('r1').addEventListener('rating-change', (e) =&gt; {
147
147
  <h2>What you used</h2>
148
148
  <ul>
149
149
  <li><strong>Props</strong> with types + defaults, coerced from attributes</li>
150
- <li><strong>Template</strong> interpolation, <code>{{#each}}</code> and <code>{{#if}}</code></li>
150
+ <li><strong>Template</strong> interpolation, <code>&#123;&#123;#each&#125;&#125;</code> and <code>&#123;&#123;#if&#125;&#125;</code></li>
151
151
  <li><strong>State</strong> via <code>data()</code> + <code>this.set()</code>, seeded from props in <code>onMount()</code></li>
152
152
  <li>A single delegated <strong>shadow-root listener</strong> and <code>data-*</code> dispatch</li>
153
153
  <li>A bubbling <strong>CustomEvent</strong> for page-level integration</li>
@@ -39,16 +39,17 @@
39
39
 
40
40
  <div class="alert alert-info">
41
41
  <strong>Why not a database?</strong> Collections store as flat JSON files on disk by default -
42
- no database to install, no SQL to learn, no migrations. When you outgrow that, switch
43
- individual collections to MongoDB without changing any of your pages or forms. The
44
- compatibility layer is the point.
42
+ no database to install, no SQL to learn, no migrations. When you outgrow that, move
43
+ individual collections to MongoDB (a Pro feature) from the collection's <strong>Storage</strong> tab without
44
+ changing any of your pages or forms. The move keeps every entry's id, data and dates, so references and
45
+ ownership still point at the right rows. The compatibility layer is the point.
45
46
  </div>
46
47
 
47
48
  <hr>
48
49
 
49
50
  <h2>Step 1 - Create the Collection (your data store)</h2>
50
51
 
51
- <p>Open <a href="#/collections">Collections</a> → <strong>New collection</strong>. You'll give it a
52
+ <p>Open <a href="#/collections">Data &gt; Collections</a> &gt; <strong>New collection</strong>. You'll give it a
52
53
  slug (the URL-safe name we use in shortcodes), a title, and a list of fields.</p>
53
54
 
54
55
  <p><strong>The slug matters more than you'd think.</strong> It's what every Form, Action, and shortcode
@@ -60,14 +61,18 @@
60
61
  just for show - it drives every downstream behaviour:</p>
61
62
 
62
63
  <ul>
63
- <li><code>text</code> renders a text input on forms and is searchable in the Browser</li>
64
+ <li><strong>Text</strong> (<code>string</code>) renders a text input on forms and is searchable in the Browser</li>
64
65
  <li><code>number</code> renders a number input, gets a min/max range filter automatically</li>
65
- <li><code>select</code> with options becomes a dropdown both on forms AND in the filter rail</li>
66
- <li><code>multiselect</code> becomes a checkbox group AND a filterable tag chip set</li>
66
+ <li><strong>Dropdown</strong> (<code>select</code>) with options becomes a dropdown both on forms AND in the filter rail</li>
67
67
  <li><code>date</code> renders a date picker AND gets a from/to range filter</li>
68
+ <li><code>multiselect</code> becomes a checkbox group AND a filterable tag chip set</li>
68
69
  <li><code>file</code> renders a file upload with mime/size validation</li>
69
70
  <li><code>reference</code> stores a link to another collection's entry - auto-renders as a populated dropdown</li>
70
71
  </ul>
72
+ <p>The collection editor's Fields tab offers text, email, phone, number, textarea, dropdown, radio buttons,
73
+ checkbox, checkbox group, date, time, URL and hidden. <code>multiselect</code>, <code>file</code>,
74
+ <code>reference</code> and a few others are supported everywhere else but are set by editing the
75
+ collection's <code>content/collections/&lt;slug&gt;/schema.json</code>.</p>
71
76
 
72
77
  <p>Pick the right type at design time and you get the right form input, the right filter UI, and the
73
78
  right validation, all for free. Pick <code>text</code> for everything and you have to recreate all
@@ -93,7 +98,7 @@
93
98
  visitor gets the same instant render. The page is cached per role, so a million visitors viewing
94
99
  a public page hit the cache and never touch the data store.</p>
95
100
 
96
- <p><strong>Five display modes are built in:</strong></p>
101
+ <p><strong>Eight display modes are built in:</strong></p>
97
102
 
98
103
  <ul>
99
104
  <li><code>display="table"</code> - sortable, paginated, with a search box</li>
@@ -101,16 +106,20 @@
101
106
  <li><code>display="list"</code> - vertical stack with title + meta</li>
102
107
  <li><code>display="accordion"</code> - expandable rows, title visible, body hidden until clicked</li>
103
108
  <li><code>display="timeline"</code> - chronological with dates and statuses</li>
109
+ <li><code>display="carousel"</code> - one slide per entry</li>
110
+ <li><code>display="listgroup"</code> - a compact list-group row per entry</li>
111
+ <li><code>display="block"</code> - each entry through a reusable block template you design</li>
104
112
  </ul>
105
113
 
106
- <p>All five display the same data; you pick the right shape for the page. A directory might use
114
+ <p>Visitors can right-click any of them to filter, sort, copy or export (export follows the collection's
115
+ export setting). All eight display the same data; you pick the right shape for the page. A directory might use
107
116
  <code>cards</code>; an admin overview might use <code>table</code>; a history page might use
108
117
  <code>timeline</code>.</p>
109
118
 
110
119
  <h3>Make it interactive - flip on the Browser</h3>
111
120
 
112
- <p>Adding any of <code>searchable</code>, <code>filterable</code>, or <code>sortable</code> upgrades the
113
- static list to a full <strong>Collection Browser</strong>:</p>
121
+ <p>Adding any of <code>searchable</code>, <code>filterable</code>, <code>sortable</code> or
122
+ <code>paginate</code> upgrades the static list to a full <strong>Collection Browser</strong>:</p>
114
123
 
115
124
  <pre class="code-block"><code>[collection slug="jobs" display="cards" columns="3"
116
125
  searchable
@@ -133,7 +142,7 @@
133
142
 
134
143
  <h2>Step 3 - Create new records (the Form)</h2>
135
144
 
136
- <p>Open <a href="#/forms">Forms</a> → <strong>New form</strong>. The slug here is the form's identity -
145
+ <p>Open <a href="#/forms">Data &gt; Forms</a> &gt; <strong>New form</strong>. The slug here is the form's identity -
137
146
  what you embed on a page with <code>[form name="..." /]</code>.</p>
138
147
 
139
148
  <p>Form fields can mirror the collection fields exactly, or they can be a subset. <strong>You almost
@@ -142,8 +151,11 @@
142
151
  <code>internalNotes</code> shouldn't be visible to the submitter at all. The form is your
143
152
  audience-facing slice of the collection; design it for who's filling it in.</p>
144
153
 
145
- <p>Wire the form to the collection in <strong>Actions → Collection</strong>: enable it and pick the
146
- target collection slug. Every submission becomes one new entry in that collection. Done.</p>
154
+ <p>A form stores each submission in the collection with the same slug as the form, and creates that
155
+ collection from its fields if it does not exist yet. The other way round, every collection you make in the
156
+ admin gets a matching form of its own - so for <code>jobs</code> you usually open the <code>jobs</code>
157
+ form and trim it, rather than starting a new one. Every submission becomes one new entry in that
158
+ collection. Done.</p>
147
159
 
148
160
  <h3>Identity capture (the small thing that makes everything work)</h3>
149
161
 
@@ -160,7 +172,9 @@
160
172
 
161
173
  <h2>Step 4 - Change records over time (the Action)</h2>
162
174
 
163
- <p>This is where most no-code platforms top out. An Action is a server-side button: visitors click it
175
+ <p>This is where most no-code platforms top out. Actions are defined in <a href="#/actions">Data &gt;
176
+ Actions</a> and are stored in MongoDB, so they need a MongoDB connection (a Pro feature); everything
177
+ before this step works without one. An Action is a server-side button: visitors click it
164
178
  on a page, the server runs a sequence of steps against the entry they clicked on, and the page
165
179
  refreshes.</p>
166
180
 
@@ -168,7 +182,8 @@
168
182
  (remove the entry), <code>createInCollection</code> (write a related entry to another collection
169
183
  - this is how "apply for this job" creates an application without losing the job), <code>email</code>
170
184
  (send a notification using the entry's fields as template variables), and <code>webhook</code>
171
- (POST to an external service).</p>
185
+ (POST to an external service), <code>moveToCollection</code> and <code>notify</code> (a notification in the
186
+ admin bell).</p>
172
187
 
173
188
  <h3>The transition is the magic word</h3>
174
189
 
@@ -180,8 +195,9 @@
180
195
  and the server refuses with HTTP 409. The illegal move can't happen, even if someone copies
181
196
  and edits the URL.
182
197
  </li>
183
- <li><strong>Per-row UI.</strong> Add <code>transitions</code> to a <code>[collection]</code> block
184
- and each row sprouts buttons for exactly the transitions legally available given that row's
198
+ <li><strong>Per-row UI.</strong> Add <code>transitions</code> to an interactive <code>[collection]</code>
199
+ block (one with <code>searchable</code>, <code>sortable</code>, <code>filterable</code> or
200
+ <code>paginate</code>; <code>scope="mine"</code> too) and each row sprouts buttons for exactly the transitions legally available given that row's
185
201
  current status AND the viewer's role. A candidate sees "Withdraw" on their pending application;
186
202
  an admin sees "Move to reviewing"; a candidate viewing a rejected application sees nothing
187
203
  to click. No client-side conditionals; the rules live entirely in the action definitions.
@@ -199,8 +215,10 @@
199
215
  ]
200
216
  }</code></pre>
201
217
 
202
- <p>Read that JSON like a sentence: <em>"Anyone with the candidate role can withdraw their own
203
- application as long as it's currently submitted or reviewing."</em> The platform enforces every
218
+ <p>Read that JSON like a sentence: <em>"Anyone with the candidate role (or a more senior one) can
219
+ withdraw their own application as long as it's currently submitted or reviewing."</em> When an action
220
+ lists several roles, any one of them is enough. A role name the site does not have admits nobody but the
221
+ level-0 role, and an action that names no roles is for admins (levels 0 and 1). The platform enforces every
204
222
  clause: <code>access.roles</code> for the role check, <code>rowLevel.mode: 'owner'</code> so
205
223
  candidates can only touch their own entries, <code>transition.from</code> for the status guard.
206
224
  Together they're the whole authorisation policy for this one button.</p>
@@ -209,10 +227,15 @@
209
227
 
210
228
  <h2>Step 5 - Who sees what (Visibility + scope)</h2>
211
229
 
212
- <p>Pages have a <code>visibility</code> frontmatter field that decides who can load the page at all.
213
- It accepts a single role (<em>"editor and everyone above"</em>) or an array of roles
214
- (<em>"candidates OR employers"</em>). Roles are hierarchical - higher-privilege roles inherit
215
- access to lower-privilege gated pages without you adding them explicitly.</p>
230
+ <p>Pages have a <code>visibility</code> setting (in the page editor, stored in the frontmatter) that
231
+ decides who can load the page at all. It accepts a single role (<code>candidate</code> means <em>"candidate
232
+ and everyone more senior"</em>) or an array of roles (<em>"candidates OR employers"</em>). Roles are
233
+ a ladder by level - more senior roles inherit access to less senior gated pages without you adding them
234
+ explicitly. To keep a page to exactly one role, prefix it with <code>=</code>: <code>=candidate</code>
235
+ admits only people who hold the candidate role (and the level-0 role), not employers or admins. The same
236
+ rules apply to menu items. Roles are managed in <a href="#/roles">System &gt; Roles</a>; a site starts
237
+ with <code>super-admin</code>, <code>admin</code> and <code>user</code>, and plugins can add their
238
+ own.</p>
216
239
 
217
240
  <p>For per-user data <em>within</em> a page that mixed-role users share - like a "My applications"
218
241
  block on a dashboard that both candidates and employers visit - use <code>scope="mine"</code>:</p>
@@ -222,16 +245,20 @@
222
245
  fields="jobId,status,submittedAt"
223
246
  empty="You haven't applied yet." /]</code></pre>
224
247
 
225
- <p>The page itself stays cached per role; the per-user block renders client-side via a small hydration
248
+ <p>The page itself stays cached per role; the per-user block renders in the browser via a small hydration
226
249
  request that injects <code>createdBy = current-user-id</code> server-side (the client can never
227
250
  tamper with which user's data they see). Anonymous visitors see a sign-in prompt where the block
228
- would render.</p>
251
+ would render. Make the block interactive (add <code>paginate</code>, <code>searchable</code>,
252
+ <code>sortable</code> or <code>filterable</code>) and it becomes a Collection Browser of the viewer's own
253
+ rows, so <code>transitions</code> works on it too; the server refuses a transition on anyone else's
254
+ row.</p>
229
255
 
230
256
  <p>For cross-collection scoping - <em>"recruiter sees only applications for jobs they posted"</em> -
231
257
  use the <code>reference</code> row-access mode in your action's <code>access.rowLevel</code>. The
232
- platform resolves the reference to check ownership on the target. <a
233
- href="/docs/configuration.md" target="_blank">Full row-access reference</a> covers all three
234
- modes (<code>owner</code>, <code>field</code>, <code>reference</code>) with worked examples.</p>
258
+ platform resolves the reference to check ownership on the target. The row-access section of
259
+ <code>docs/configuration.md</code> (in the Domma CMS package) covers all three modes
260
+ (<code>owner</code>, <code>field</code>, <code>reference</code>) with worked examples. Saved
261
+ <a href="#/views">Views</a> apply the same row-level rules per viewer.</p>
235
262
 
236
263
  <hr>
237
264
 
@@ -251,12 +278,13 @@
251
278
  entries (showing the <code>displayField</code>); the page display resolves to the readable label
252
279
  everywhere; with a <code>linkTemplate</code> the label becomes a clickable link to the target's
253
280
  detail page. Validation refuses to save an entry pointing at a non-existent target. Dangling
254
- references (target deleted later) render as "<em>id</em> (missing)" instead of crashing the page.</p>
281
+ references (target deleted later) render as "<em>id</em> (missing)" instead of crashing the page. Imports
282
+ are checked the same way: an imported entry pointing at a missing target is skipped and reported.</p>
255
283
 
256
284
  <p><strong>Status fields</strong> with <code>options</code> + paired <code>transition</code>-bearing
257
285
  actions = a workflow. We covered this in Step 4 but it deserves repeating: the combination of a
258
286
  typed status field, a few actions with <code>transition.from</code>, and the <code>transitions</code>
259
- attribute on a <code>[collection]</code> shortcode gives you a real state machine that's enforced
287
+ attribute on an interactive <code>[collection]</code> shortcode gives you a real state machine that's enforced
260
288
  everywhere (UI, API, audit) with no JavaScript.</p>
261
289
 
262
290
  <hr>
@@ -264,7 +292,7 @@
264
292
  <h2>Step 7 - The Browser tour (advanced reading UI)</h2>
265
293
 
266
294
  <p>We touched on the Browser in Step 2. Here's the rest of what it does for free once you turn it
267
- on with <code>searchable filterable=... sortable</code>:</p>
295
+ on with <code>searchable filterable=... sortable</code> (or just <code>paginate</code>):</p>
268
296
 
269
297
  <ul>
270
298
  <li><strong>Faceted filter counts</strong> - every filter chip shows the count of entries that
@@ -281,7 +309,7 @@
281
309
  <li><strong>URL state sync</strong> - every change writes to the URL query string, so deep-links
282
310
  and the browser back button work.</li>
283
311
  <li><strong>Relevance sort during search</strong> - when the search box has a term, results
284
- re-order by match count with a 3× boost on matches in the title field.</li>
312
+ re-order by match count with a 3x boost on matches in the title field.</li>
285
313
  <li><strong>Keyboard shortcuts</strong> - <code>/</code> focuses search, <code>←</code>/<code>→</code>
286
314
  pages.</li>
287
315
  <li><strong>Infinite scroll</strong> - <code>pagination="scroll"</code> replaces the pager with
@@ -303,20 +331,22 @@
303
331
  <li>Two collections: <code>jobs</code> (employer posts roles) and <code>applications</code> (candidates apply)</li>
304
332
  <li><code>applications</code> has a <code>reference</code> field <code>jobId</code> pointing at <code>jobs</code></li>
305
333
  <li><code>applications</code> has a <code>file</code> field <code>resume</code> for the candidate's PDF</li>
334
+ <li><code>candidate</code> and <code>employer</code> roles, added in System &gt; Roles</li>
306
335
  <li>A public <code>[collection slug="jobs"]</code> on <code>/jobs</code> with the Browser turned on</li>
307
- <li>An apply form that writes to <code>applications</code> + an action that emails HR</li>
336
+ <li>An apply form that writes to <code>applications</code> + an action that emails HR (actions need
337
+ MongoDB)</li>
308
338
  <li>A dashboard at <code>/dashboard</code> with <code>visibility: [candidate, employer]</code> and
309
339
  two <code>[collection scope="mine"]</code> blocks (one per role)</li>
310
340
  <li>Transition actions: <em>start-review</em>, <em>invite-to-interview</em>, <em>make-offer</em>,
311
341
  <em>reject</em>, <em>withdraw</em> - each with its own role + state guards</li>
312
- <li>The candidate dashboard adds <code>transitions</code> and each row gets the right buttons
313
- for its current status</li>
342
+ <li>The candidate dashboard's <code>scope="mine"</code> block adds <code>paginate transitions</code> and each
343
+ row gets the right buttons for its current status</li>
314
344
  <li>Recruiters get <code>access.rowLevel: { mode: 'reference', field: 'jobId', targetCollection: 'jobs' }</code>
315
345
  on their transition actions so they only ever see applications for jobs they posted</li>
316
346
  </ol>
317
347
 
318
348
  <p>That's a complete recruitment platform built in pure JSON + Markdown. The full annotated build
319
- lives at <a href="/docs/recruitment-recipe.md" target="_blank">docs/recruitment-recipe.md</a>.</p>
349
+ is <code>docs/recruitment-recipe.md</code> in the Domma CMS package.</p>
320
350
 
321
351
  <hr>
322
352
 
@@ -330,13 +360,14 @@
330
360
 
331
361
  <p class="text-muted" style="margin-top:1rem;font-size:.9rem;">
332
362
  Recipes are JSON files under <code>server/services/recipes/</code> - copy one as a starting
333
- point for your own. The <a href="/docs/scaffolding.md" target="_blank">scaffolding docs</a>
334
- cover the recipe format in full.
363
+ point for your own. <code>docs/scaffolding.md</code> in the Domma CMS package covers the recipe
364
+ format in full. Applying a recipe whose collection, form, action or menu already exists is refused
365
+ (HTTP 409) rather than overwriting it.
335
366
  </p>
336
367
 
337
368
  <hr>
338
369
  <p class="text-muted" style="font-size:.9rem;">
339
- Next: <a href="#/tutorials/plugin">Writing a Plugin →</a>
370
+ Next: <a href="#/tutorials/plugin">Writing a Plugin</a>
340
371
  </p>
341
372
 
342
373
  </div>
@@ -7,28 +7,33 @@
7
7
  <div class="col-12">
8
8
  <div class="docs-body">
9
9
 
10
- <p>Every form in Domma CMS can trigger up to four things after a submission is stored:</p>
10
+ <p>Every form in Domma CMS stores each submission as an entry in a collection first. After that it can do
11
+ up to four more things:</p>
11
12
  <ol>
12
13
  <li>Send an <strong>email notification</strong> to one or more recipients</li>
13
- <li>POST to a <strong>webhook URL</strong></li>
14
- <li>Execute a <strong>CMS Action</strong> (Pro)</li>
15
- <li>Redirect the visitor to a <strong>success page</strong> (or show an inline message)</li>
14
+ <li>Send the submission to a <strong>webhook URL</strong></li>
15
+ <li>Run a <strong>CMS Action</strong> (needs MongoDB)</li>
16
+ <li>Show an inline <strong>success message</strong> or redirect the visitor to a <strong>success page</strong></li>
16
17
  </ol>
17
- <p>These are configured per-form in the admin. Open any form in <a href="#/forms">Forms</a> and go to
18
- the <strong>Settings</strong> and <strong>Actions</strong> tabs.</p>
18
+ <p>These are set per form. Open any form in <a href="#/forms">Data &gt; Forms</a>: the success message and
19
+ redirect are on the <strong>Settings</strong> tab; email, webhook, CMS Action and spam protection are on the
20
+ <strong>Actions</strong> tab.</p>
19
21
 
20
22
  <hr>
21
23
 
22
24
  <h3>1. Email notification</h3>
23
- <p>Go to the <strong>Actions</strong> tab of your form and enable <em>Send email on submit</em>. Enter
24
- one or more comma-separated recipient addresses. This uses the SMTP settings configured in
25
- <a href="#/settings">Site Settings → Email / SMTP</a>.</p>
25
+ <p>On the <strong>Actions</strong> tab, in the <em>Email Action</em> card, tick <em>Send email on submit</em>
26
+ and enter one or more comma-separated recipient addresses. The email goes through the SMTP settings in
27
+ <a href="#/settings">System &gt; Site Settings &gt; Email</a>; without them nothing is delivered.</p>
26
28
 
27
29
  <pre class="code-block"><code>Recipients: admin@example.com, team@example.com
28
30
  Subject Prefix: [Contact Form]</code></pre>
31
+ <p>The subject reads <em>&lt;prefix&gt; New submission</em>. With no prefix, the form's title in square
32
+ brackets is used.</p>
29
33
 
30
34
  <h3>2. Webhook</h3>
31
- <p>Enable <em>POST to webhook on submit</em> and enter a URL. Domma will POST the following JSON body:</p>
35
+ <p>In the <em>Webhook Action</em> card, tick <em>POST to webhook on submit</em>, enter a URL and choose
36
+ <code>POST</code> or <code>PUT</code>. Domma sends this JSON body:</p>
32
37
  <pre class="code-block"><code>{
33
38
  "form": "enquiries",
34
39
  "data": {
@@ -37,55 +42,66 @@ Subject Prefix: [Contact Form]</code></pre>
37
42
  "message": "Hello!"
38
43
  }
39
44
  }</code></pre>
40
- <p>Use this to integrate with Zapier, Make, Slack, or any HTTP endpoint.</p>
45
+ <p>Use this to integrate with Zapier, Make, Slack, or any HTTP endpoint. The URL is not kept in the editor's
46
+ draft, in case it carries a secret token.</p>
41
47
 
42
- <h3>3. CMS Action (Pro)</h3>
43
- <p>Actions are reusable workflow steps defined in <a href="#/actions">Actions</a>. A single action can
44
- chain multiple steps: update a field, move an entry to another collection, send an email, call a
45
- webhook, or delete an entry.</p>
48
+ <h3>3. CMS Action (needs MongoDB)</h3>
49
+ <p>Actions are reusable workflow steps defined in <a href="#/actions">Data &gt; Actions</a>. A single action
50
+ can chain several steps: update a field, move the entry to another collection, create an entry in another
51
+ collection, send an email, call a webhook, raise a notification, or delete the entry. Actions are stored in
52
+ MongoDB, so they need a MongoDB connection.</p>
46
53
  <p>To wire an Action to a form:</p>
47
54
  <ol>
48
- <li>Create an Action in <a href="#/actions">Actions</a> targeting the same collection as your form.</li>
49
- <li>Open the form in <a href="#/forms">Forms</a> → <strong>Actions</strong> tab → <strong>CMS
55
+ <li>Create an Action in <a href="#/actions">Data &gt; Actions</a> targeting the collection your form stores
56
+ into.</li>
57
+ <li>Open the form in <a href="#/forms">Forms</a> &gt; <strong>Actions</strong> tab &gt; <strong>CMS
50
58
  Action</strong> card.
51
59
  </li>
52
- <li>Select the Action from the dropdown and save.</li>
60
+ <li>Pick it under <em>Action on Submit</em> and save.</li>
53
61
  </ol>
54
- <p>The Action runs server-side, after the entry is saved. If it fails (e.g. MongoDB is not
55
- configured), the submission is still stored - the action failure is non-fatal and logged as a
56
- warning.</p>
62
+ <p>The Action runs on the server after the entry is saved, as the signed-in visitor if there is one (so
63
+ <code>&#123;&#123;user.id&#125;&#125;</code> and <code>&#123;&#123;user.email&#125;&#125;</code> work in its steps). If it fails - for example
64
+ because MongoDB is not connected - the submission is still stored.</p>
65
+ <p>A field's <strong>triggers</strong> (Fields tab) can also run an action, notify the admins or redirect
66
+ after submit, only when the answers meet the trigger's condition.</p>
57
67
 
58
68
  <h3>4. Success message vs. redirect</h3>
59
69
  <p>After a successful submission the visitor sees one of two things:</p>
60
70
  <ul>
61
71
  <li><strong>Inline success message</strong> - the form is replaced by the text set in
62
- <em>Settings → Success Message</em>. Good for simple acknowledgements.
72
+ <em>Settings &gt; Success Message</em> (default: "Thank you for your submission."). Good for simple
73
+ acknowledgements.
63
74
  </li>
64
75
  <li><strong>Page redirect</strong> - the visitor is sent to the URL set in
65
- <em>Settings → Success Redirect URL</em>. Good for registration flows, checkouts, or when you
66
- want a full thank-you page with additional content. <strong>Takes priority</strong> if both are
67
- set.
76
+ <em>Settings &gt; Success Redirect URL</em>. Good for registration flows or a full thank-you page.
77
+ <strong>Takes priority</strong> if both are set, and a trigger's "Redirect after submit" takes priority
78
+ over it. <code>&#123;&#123;entryId&#125;&#125;</code> in the URL is replaced with the new entry's id.
68
79
  </li>
69
80
  </ul>
70
81
  <p>Example: set <em>Success Redirect URL</em> to <code>/thank-you</code> and create a
71
- <code>thank-you.md</code> page in the CMS with any content you like.</p>
82
+ <code>thank-you</code> page in <a href="#/pages">Pages</a> with any content you like.</p>
72
83
 
73
84
  <h3>Execution order</h3>
74
85
  <p>On every submission, the pipeline runs in this fixed order:</p>
75
86
  <ol>
76
- <li>Validate fields + honeypot + rate limit</li>
77
- <li>Store entry to collection</li>
78
- <li>Send email (if enabled)</li>
79
- <li>Call webhook (if enabled)</li>
80
- <li>Execute CMS Action (if set)</li>
81
- <li>Return success response → client redirects or shows message</li>
87
+ <li>Spam checks (honeypot and a too-fast submission are accepted silently and not stored)</li>
88
+ <li>Triggers: a "Block submission" trigger refuses it; an "End the form here" trigger decides what is kept</li>
89
+ <li>Field validation (required fields, conditional logic), then the per-minute rate limit</li>
90
+ <li>Store the entry in the collection - if this fails, the visitor is told and nothing else runs</li>
91
+ <li>Send the email (if enabled)</li>
92
+ <li>Call the webhook (if enabled)</li>
93
+ <li>Run the CMS Action (if set)</li>
94
+ <li>Run trigger events: run an action, notify the admins, redirect</li>
95
+ <li>Return success - the browser redirects or shows the message</li>
82
96
  </ol>
83
- <p>Steps 3-5 are non-fatal: a failure in any of them is logged as a warning but does not prevent the
84
- submission from being stored or the success response from being returned.</p>
97
+ <p>Steps 5-8 cannot lose the submission: a failure is logged and raises a warning in
98
+ <a href="#/system/notifications">Notifications</a> ("Form ...: webhook failed"), but the entry is already
99
+ stored and the visitor still sees success. An "End the form here" trigger set not to record stores nothing,
100
+ so no email, webhook or action runs.</p>
85
101
 
86
102
  <hr>
87
103
  <p class="text-muted" style="font-size:.9rem;">
88
- <a href="#/tutorials">← Back to all tutorials</a>
104
+ <a href="#/tutorials">Back to all tutorials</a>
89
105
  </p>
90
106
 
91
107
  </div>