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
@@ -69,118 +69,85 @@
69
69
  <div class="col-12">
70
70
  <div class="docs-body">
71
71
 
72
- <p>Role hierarchy governs which users can manage other users: you can only create, edit, delete or reset
73
- the password of a user whose role is less senior (a higher level number) than your own - an
74
- Admin cannot manage a Super Admin. Self-deletion is always blocked.</p>
75
-
76
- <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/users</span></h3>
77
- <p class="auth-note">Requires Bearer token + <code>users</code> read permission (Super Admin and Admin by default).</p>
78
- <p>Return all users. Passwords are stripped from the response.</p>
79
- <pre class="code-block"><code>// Response 200
80
- [ { "id": "uuid", "name": "Alice", "email": "alice@example.com", "role": "admin", "isActive": true } ]</code></pre>
81
-
82
- <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/users/:id</span>
83
- </h3>
84
- <p class="auth-note">Requires Bearer token. Accessible to the user themselves, or a user with <code>users</code>
85
- permission.</p>
86
- <p>Return a single user by ID.</p>
87
- <pre class="code-block"><code>// Response 200
88
- { "id": "uuid", "name": "Alice", "email": "alice@example.com", "role": "admin", "isActive": true }
89
-
72
+ <p>Users are managed with the <code>users</code> permission (Super Admin and Admin by default; each action -
73
+ read, create, update, delete - can be granted on its own in the role editor). The role ladder decides whom you
74
+ may manage: you can only create, edit, delete or reset the password of a user whose role is less senior (a higher
75
+ level number) than your own, so an Admin cannot manage a Super Admin or another Admin. You can always edit your
76
+ own account, but not raise your own role, and you cannot delete it. Passwords are never returned.</p>
77
+
78
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/users</span></h3>
79
+ <p class="auth-note">Requires Bearer token + <code>users</code> read permission.</p>
80
+ <p>Every user you may see (a user confined to some projects sees the users of those projects), each with their
81
+ profile fields.</p>
82
+ <pre class="code-block"><code>// Response 200
83
+ [ { "id": "uuid", "name": "Alice", "email": "alice@example.com", "role": "admin", "additionalRoles": [],
84
+ "isActive": true, "lastLogin": "...", "profile": {} } ]</code></pre>
85
+
86
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/users/:id</span></h3>
87
+ <p class="auth-note">Requires Bearer token. The user themselves, or a role with <code>users</code> read permission.</p>
88
+ <p>A single user, with their profile.</p>
89
+ <pre class="code-block"><code>// Response 200
90
+ { "id": "uuid", "name": "Alice", "email": "alice@example.com", "role": "admin", "isActive": true, "profile": {} }
90
91
  // Error 404
91
92
  { "error": "User not found" }</code></pre>
92
93
 
93
- <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/users</span>
94
- </h3>
95
- <p class="auth-note">Requires Bearer token + <code>users</code> permission.</p>
96
- <p>Create a new user. The actor cannot assign a role higher than their own level.</p>
97
- <table class="table table-sm">
98
- <thead>
99
- <tr>
100
- <th>Field</th>
101
- <th>Type</th>
102
- <th>Description</th>
103
- </tr>
104
- </thead>
105
- <tbody>
106
- <tr>
107
- <td><code>name</code></td>
108
- <td>string</td>
109
- <td>Display name</td>
110
- </tr>
111
- <tr>
112
- <td><code>email</code></td>
113
- <td>string</td>
114
- <td>Unique email address</td>
115
- </tr>
116
- <tr>
117
- <td><code>password</code></td>
118
- <td>string</td>
119
- <td>Minimum 8 characters</td>
120
- </tr>
121
- <tr>
122
- <td><code>role</code></td>
123
- <td>string</td>
124
- <td>Optional. Defaults to <code>user</code>.</td>
125
- </tr>
126
- </tbody>
127
- </table>
128
- <pre class="code-block"><code>// Response 201
94
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/users</span></h3>
95
+ <p class="auth-note">Requires Bearer token + <code>users</code> create permission.</p>
96
+ <p>Create a user. You can only give roles (primary and additional) less senior than your own.</p>
97
+ <table class="table table-sm">
98
+ <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
99
+ <tbody>
100
+ <tr><td><code>name</code></td><td>string</td><td>Display name</td></tr>
101
+ <tr><td><code>email</code></td><td>string</td><td>Unique email address</td></tr>
102
+ <tr><td><code>password</code></td><td>string</td><td>At least 8 characters, plus any rules a plugin such as
103
+ Security adds</td></tr>
104
+ <tr><td><code>role</code></td><td>string</td><td>Optional. Defaults to <code>user</code>.</td></tr>
105
+ <tr><td><code>additionalRoles</code></td><td>array</td><td>Optional. More roles; a permission check passes if
106
+ any role grants it.</td></tr>
107
+ <tr><td><code>projects</code></td><td>array</td><td>Optional. Project slugs the user is confined to.</td></tr>
108
+ <tr><td><code>isActive</code></td><td>boolean</td><td>Optional. Defaults to <code>true</code>.</td></tr>
109
+ <tr><td><code>profile</code></td><td>object</td><td>Optional. Profile field values.</td></tr>
110
+ </tbody>
111
+ </table>
112
+ <pre class="code-block"><code>// Response 201
129
113
  { "id": "uuid", "name": "Bob", "email": "bob@example.com", "role": "user", "isActive": true }
130
-
131
- // Error 409
132
- { "error": "Email already in use" }</code></pre>
133
-
134
- <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/users/:id</span>
135
- </h3>
136
- <p class="auth-note">Requires Bearer token + <code>users</code> permission.</p>
137
- <p>Update a user's details. You cannot edit a user as senior as or more senior than yourself (other
138
- than yourself). Role escalation beyond the actor's own level is blocked.</p>
139
- <table class="table table-sm">
140
- <thead>
141
- <tr>
142
- <th>Field</th>
143
- <th>Type</th>
144
- <th>Description</th>
145
- </tr>
146
- </thead>
147
- <tbody>
148
- <tr>
149
- <td><code>name</code></td>
150
- <td>string</td>
151
- <td>New display name</td>
152
- </tr>
153
- <tr>
154
- <td><code>email</code></td>
155
- <td>string</td>
156
- <td>New email address</td>
157
- </tr>
158
- <tr>
159
- <td><code>password</code></td>
160
- <td>string</td>
161
- <td>New password (min 8 chars)</td>
162
- </tr>
163
- <tr>
164
- <td><code>role</code></td>
165
- <td>string</td>
166
- <td>New role</td>
167
- </tr>
168
- <tr>
169
- <td><code>isActive</code></td>
170
- <td>boolean</td>
171
- <td>Enable or disable the account</td>
172
- </tr>
173
- </tbody>
174
- </table>
175
- <pre class="code-block"><code>// Response 200 - returns the updated user object</code></pre>
176
-
177
- <h3><span class="method-badge method-delete">DELETE</span><span
178
- class="endpoint-path">/api/users/:id</span></h3>
179
- <p class="auth-note">Requires Bearer token + <code>users</code> permission.</p>
180
- <p>Delete a user. Cannot delete your own account or a user with a higher role level.</p>
181
- <pre class="code-block"><code>// Response 200
114
+ // Error 403
115
+ { "error": "You cannot create a user with that role" }
116
+ // Error 409 - the email is already in use</code></pre>
117
+
118
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/users/:id</span></h3>
119
+ <p class="auth-note">Requires Bearer token + <code>users</code> update permission.</p>
120
+ <p>Update a user. Send only the fields to change: <code>name</code>, <code>email</code>, <code>password</code>,
121
+ <code>role</code>, <code>additionalRoles</code>, <code>projects</code>, <code>isActive</code>,
122
+ <code>profile</code>. A new password, or switching an account off, ends that user's sessions.</p>
123
+ <pre class="code-block"><code>// Response 200 - the updated user, with profile
124
+ // Error 403
125
+ { "error": "You cannot edit a user with that role" }</code></pre>
126
+
127
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/users/:id/password-reset</span></h3>
128
+ <p class="auth-note">Requires Bearer token + <code>users</code> update permission. Not for your own account.</p>
129
+ <p>Start a password reset for someone else: email them a link (the default), or with
130
+ <code>{"via": "link"}</code> get the link back to pass on yourself - for a site without email. The account must
131
+ be active and less senior than yours.</p>
132
+ <pre class="code-block"><code>// Request body (optional)
133
+ { "via": "email" } // or "link"
134
+ // Response 200
135
+ { "sent": true, "expiresAt": "...", "expiresIn": "1 hour" }
136
+ { "link": "https://example.com/admin/#/reset-password?token=...", "expiresAt": "...", "expiresIn": "1 hour" }
137
+ // Error 409 - INACTIVE, NO_SMTP (copy the link instead) or NO_ORIGIN (set the Site URL)
138
+ { "error": "...", "code": "NO_SMTP" }</code></pre>
139
+
140
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/users/:id/password-reset</span></h3>
141
+ <p class="auth-note">Requires Bearer token + <code>users</code> update permission.</p>
142
+ <p>Withdraw an open reset link, so it no longer works.</p>
143
+ <pre class="code-block"><code>// Response 200
144
+ { "ok": true }</code></pre>
145
+
146
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/users/:id</span></h3>
147
+ <p class="auth-note">Requires Bearer token + <code>users</code> delete permission.</p>
148
+ <p>Delete a user. You cannot delete your own account or anyone as senior as you.</p>
149
+ <pre class="code-block"><code>// Response 200
182
150
  { "success": true }
183
-
184
151
  // Error 403
185
152
  { "error": "You cannot delete your own account" }</code></pre>
186
153
 
@@ -69,27 +69,31 @@
69
69
  <div class="col-12">
70
70
  <div class="docs-body">
71
71
 
72
- <p>Views work on every storage adapter - no MongoDB connection is needed. All admin endpoints require
73
- authentication and the <code>views</code> permission. View configs are stored as JSON files in
74
- <code>content/views/&lt;slug&gt;.json</code>.</p>
75
-
76
- <h3 style="margin-top:16px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
77
- Admin Endpoints</h3>
78
-
79
- <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views</span></h3>
80
- <p class="auth-note">Requires: <code>views</code> permission</p>
81
- <p>List all view configs, sorted by creation date descending.</p>
82
-
83
- <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/views</span>
84
- </h3>
85
- <p class="auth-note">Requires: <code>views</code> permission</p>
86
- <p>Create a new view config. Returns <code>201</code> on success.</p>
87
- <pre class="code-block"><code>{
88
- "title": "Active Premium Users",
89
- "slug": "active-premium-users",
72
+ <p>Views work on every storage adapter - no MongoDB connection is needed: a view over a MongoDB collection runs a
73
+ native aggregation, and one over a file collection runs the built-in pipeline evaluator. View configs are JSON
74
+ files in <code>content/views/&lt;slug&gt;.json</code>. The admin endpoints need the <code>views</code> permission
75
+ for the action named (read, create, update, delete).</p>
76
+
77
+ <h3 style="margin-top:16px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
78
+ Admin Endpoints</h3>
79
+
80
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views</span></h3>
81
+ <p class="auth-note">Requires: <code>views</code> read permission</p>
82
+ <p>Every view you may see, newest first.</p>
83
+
84
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/views</span></h3>
85
+ <p class="auth-note">Requires: <code>views</code> create permission</p>
86
+ <p>Create a view. Answers <code>201</code>. Pipeline stages may be <code>$match</code>, <code>$lookup</code>,
87
+ <code>$sort</code>, <code>$project</code>, <code>$unwind</code>, <code>$addFields</code>, <code>$group</code>,
88
+ <code>$count</code>, <code>$skip</code> and <code>$limit</code>; <code>$out</code>, <code>$merge</code>,
89
+ <code>$function</code>, <code>$accumulator</code> and <code>$graphLookup</code> are refused. Field paths are
90
+ document paths such as <code>data.status</code> or <code>meta.createdAt</code>.</p>
91
+ <pre class="code-block"><code>{
92
+ "title": "Active Members",
93
+ "slug": "active-members",
90
94
  "connection": "default",
91
95
  "pipeline": {
92
- "source": "users",
96
+ "source": "members",
93
97
  "stages": [
94
98
  { "type": "$match", "config": { "data.status": "active" } },
95
99
  { "type": "$sort", "config": { "meta.createdAt": -1 } },
@@ -97,46 +101,56 @@
97
101
  ]
98
102
  },
99
103
  "display": { "mode": "table", "columns": [ { "key": "data.name", "label": "Name" } ], "pageSize": 25 },
100
- "access": { "roles": ["admin", "manager"], "public": false }
104
+ "access": { "roles": ["admin"], "public": false, "rowLevel": null }
101
105
  }</code></pre>
102
106
 
103
- <h3><span class="method-badge method-get">GET</span><span
104
- class="endpoint-path">/api/views/:slug</span></h3>
105
- <p class="auth-note">Requires: <code>views</code> permission</p>
106
- <p>Return a single view config by slug.</p>
107
-
108
- <h3><span class="method-badge method-put">PUT</span><span
109
- class="endpoint-path">/api/views/:slug</span></h3>
110
- <p class="auth-note">Requires: <code>views</code> permission</p>
111
- <p>Update a view config. Accepts the same body shape as POST; all fields are optional.</p>
112
-
113
- <h3><span class="method-badge method-delete">DELETE</span><span
114
- class="endpoint-path">/api/views/:slug</span></h3>
115
- <p class="auth-note">Requires: <code>views</code> permission</p>
116
- <p>Delete a view config.</p>
117
-
118
- <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views/:slug/execute</span>
119
- </h3>
120
- <p class="auth-note">Requires: <code>views</code> permission</p>
121
- <p>Execute the view's aggregation pipeline and return paginated results. Query params:
122
- <code>page</code> (default 1), <code>limit</code> (default 25).</p>
123
- <pre class="code-block"><code>// Response
124
- { "results": [ ... ], "total": 142, "page": 1, "limit": 25 }</code></pre>
107
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views/:slug</span></h3>
108
+ <p class="auth-note">Requires: <code>views</code> read permission</p>
109
+ <p>One view config.</p>
110
+
111
+ <h3><span class="method-badge method-put">PUT</span><span class="endpoint-path">/api/views/:slug</span></h3>
112
+ <p class="auth-note">Requires: <code>views</code> update permission</p>
113
+ <p>Update a view. The same body as POST; every field is optional.</p>
125
114
 
126
- <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views/collection/:slug</span>
127
- </h3>
128
- <p class="auth-note">Requires: <code>views</code> permission</p>
129
- <p>List all view configs whose <code>pipeline.source</code> matches the given collection slug.</p>
115
+ <h3><span class="method-badge method-delete">DELETE</span><span class="endpoint-path">/api/views/:slug</span></h3>
116
+ <p class="auth-note">Requires: <code>views</code> delete permission</p>
117
+ <p>Delete a view.</p>
130
118
 
131
- <h3 style="margin-top:24px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
132
- Public Endpoint</h3>
119
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views/:slug/execute</span></h3>
120
+ <p class="auth-note">Requires: <code>views</code> read permission</p>
121
+ <p>Run the view and return a page of results. Query params: <code>page</code> (default 1), <code>limit</code>
122
+ (default 25).</p>
123
+ <pre class="code-block"><code>// Response
124
+ { "results": [ ... ], "total": 142, "page": 1, "limit": 25 }</code></pre>
133
125
 
134
- <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views/:slug/public</span>
135
- </h3>
136
- <p class="auth-note">Access level: per view <code>access</code> config</p>
137
- <p>Execute the view publicly. If <code>access.public</code> is <code>false</code>, a valid JWT and a
138
- role listed in <code>access.roles</code> (or a more privileged one) is required; with no roles
139
- listed, only the admin tier (role levels 0 and 1) may read it.</p>
126
+ <h3><span class="method-badge method-post">POST</span><span class="endpoint-path">/api/views/_preview</span></h3>
127
+ <p class="auth-note">Requires: <code>views</code> update permission</p>
128
+ <p>Run an unsaved pipeline (the view editor's Results tab) and return up to 50 rows.</p>
129
+ <pre class="code-block"><code>// Request body
130
+ { "source": "members", "connection": "default", "stages": [ ... ], "limit": 10 }</code></pre>
131
+
132
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views/collection/:slug</span></h3>
133
+ <p class="auth-note">Requires: <code>views</code> read permission</p>
134
+ <p>The views whose <code>pipeline.source</code> is the given collection.</p>
135
+
136
+ <h3 style="margin-top:24px;font-size:15px;text-transform:uppercase;letter-spacing:.5px;opacity:.6">
137
+ Public Endpoint</h3>
138
+
139
+ <h3><span class="method-badge method-get">GET</span><span class="endpoint-path">/api/views/:slug/public</span></h3>
140
+ <p class="auth-note">Access level: per view <code>access</code> config</p>
141
+ <p>Run the view for a visitor. This is the same decision <code>[view]</code> on a page makes:</p>
142
+ <ul>
143
+ <li><code>access.public: true</code> - anyone.</li>
144
+ <li>Otherwise a signed-in user holding any role in <code>access.roles</code>, or a more senior one (the page
145
+ visibility ladder; <code>=role</code> means that role exactly). A role name the site does not have admits
146
+ only the level-0 role.</li>
147
+ <li>No roles listed - the admin tier (role levels 0 and 1).</li>
148
+ </ul>
149
+ <p>A row-level rule (<code>access.rowLevel</code>: <code>owner</code>, <code>field</code> or <code>reference</code>)
150
+ is applied as the viewer, so each person sees only their rows and the totals count only those; an anonymous
151
+ visitor gets no rows. Refused: 401 when not signed in, 403 otherwise.</p>
152
+ <pre class="code-block"><code>// Response
153
+ { "results": [ ... ], "total": 12, "page": 1, "limit": 25 }</code></pre>
140
154
 
141
155
  </div>
142
156
  </div>
@@ -23,11 +23,12 @@
23
23
 
24
24
  <h2>Create a component</h2>
25
25
  <ol>
26
- <li>Open <a href="#/components">Data → Components</a> → <strong>New Component</strong>.</li>
26
+ <li>Open <a href="#/components">Data &gt; Components</a> and click <strong>New component</strong> in the
27
+ banner.</li>
27
28
  <li>Name it in lowercase-with-hyphens (e.g. <code>price-tag</code>). The name is permanent and
28
29
  becomes the tag <code>&lt;dm-price-tag&gt;</code>.</li>
29
30
  <li>Fill the <code>&lt;template&gt;</code>, <code>&lt;props&gt;</code> and <code>&lt;script&gt;</code>
30
- tabs (the editor watches the live preview as you type).</li>
31
+ tabs, and <code>&lt;style&gt;</code> if you want one (the live preview updates as you type).</li>
31
32
  <li><strong>Save</strong>. It compiles first - fix any reported errors and it lands on disk.</li>
32
33
  </ol>
33
34
 
@@ -35,7 +36,7 @@
35
36
 
36
37
  <h2>Render a value</h2>
37
38
  <p>Interpolate a prop or state value with double braces:</p>
38
- <pre class="code-block"><code>&lt;template&gt;&lt;span class="dm-price"&gt;{{currency}}{{amount}}&lt;/span&gt;&lt;/template&gt;
39
+ <pre class="code-block"><code>&lt;template&gt;&lt;span class="dm-price"&gt;&#123;&#123;currency&#125;&#125;&#123;&#123;amount&#125;&#125;&lt;/span&gt;&lt;/template&gt;
39
40
  &lt;props&gt;
40
41
  {
41
42
  "currency": { "type": "string", "default": "£" },
@@ -53,7 +54,7 @@
53
54
  template re-renders automatically.</p>
54
55
  <pre class="code-block"><code>&lt;template&gt;
55
56
  &lt;div class="dm-counter"&gt;
56
- &lt;strong&gt;{{count}}&lt;/strong&gt;
57
+ &lt;strong&gt;&#123;&#123;count&#125;&#125;&lt;/strong&gt;
57
58
  &lt;button data-action="inc"&gt;+&lt;/button&gt;
58
59
  &lt;/div&gt;
59
60
  &lt;/template&gt;
@@ -83,15 +84,15 @@ onMount() { this.set({ count: this.props.initial }); }</code></pre>
83
84
  <hr>
84
85
 
85
86
  <h2>Show something conditionally</h2>
86
- <pre class="code-block"><code>{{#if showReset}}&lt;button data-action="reset"&gt;Reset&lt;/button&gt;{{/if}}
87
- {{#unless soldOut}}&lt;button&gt;Buy&lt;/button&gt;{{/unless}}</code></pre>
87
+ <pre class="code-block"><code>&#123;&#123;#if showReset&#125;&#125;&lt;button data-action="reset"&gt;Reset&lt;/button&gt;&#123;&#123;/if&#125;&#125;
88
+ &#123;&#123;#unless soldOut&#125;&#125;&lt;button&gt;Buy&lt;/button&gt;&#123;&#123;/unless&#125;&#125;</code></pre>
88
89
 
89
90
  <hr>
90
91
 
91
92
  <h2>Loop over a list</h2>
92
- <p>Pass an array prop (attribute value is JSON) and iterate with <code>{{#each}}</code>:</p>
93
+ <p>Pass an array prop (attribute value is JSON) and iterate with <code>&#123;&#123;#each&#125;&#125;</code>:</p>
93
94
  <pre class="code-block"><code>&lt;template&gt;
94
- &lt;ul&gt;{{#each items}}&lt;li&gt;{{@index}}. {{.}}&lt;/li&gt;{{/each}}&lt;/ul&gt;
95
+ &lt;ul&gt;&#123;&#123;#each items&#125;&#125;&lt;li&gt;&#123;&#123;@index&#125;&#125;. &#123;&#123;.&#125;&#125;&lt;/li&gt;&#123;&#123;/each&#125;&#125;&lt;/ul&gt;
95
96
  &lt;/template&gt;
96
97
  &lt;props&gt;{ "items": { "type": "array", "default": [] } }&lt;/props&gt;
97
98
  &lt;script&gt;export default {};&lt;/script&gt;</code></pre>
@@ -109,7 +110,8 @@ onMount() { this.set({ count: this.props.initial }); }</code></pre>
109
110
 
110
111
  <h2>Fetch data from the API</h2>
111
112
  <p>Domma's HTTP helper <code>H</code> is available inside the script. Fetch in <code>onMount</code> and
112
- push results into state:</p>
113
+ push results into state. <code>/api/v1/&lt;slug&gt;</code> answers only if the collection's API read
114
+ access allows the visitor (collection editor, <strong>API &amp; Export</strong> tab):</p>
113
115
  <pre class="code-block"><code>export default {
114
116
  data() { return { entries: [] }; },
115
117
  async onMount() {
@@ -126,7 +128,7 @@ onMount() { this.set({ count: this.props.initial }); }</code></pre>
126
128
  <pre class="code-block"><code>export default {
127
129
  data() { return { now: '' }; },
128
130
  _timer: null,
129
- methods: { tick() { this.set({ now: new Date().toLocaleTimeString() }); } },
131
+ methods: { tick() { this.set({ now: new Date().toTimeString().slice(0, 8) }); } }, // HH:mm:ss
130
132
  onMount() { this.tick(); this._timer = setInterval(() =&gt; this.tick(), 1000); },
131
133
  onUnmount() { if (this._timer) clearInterval(this._timer); }
132
134
  };</code></pre>
@@ -145,24 +147,25 @@ onMount() { this.set({ count: this.props.initial }); }</code></pre>
145
147
  <h2>Use it on a page</h2>
146
148
  <p>Either form works and auto-loads the component's module:</p>
147
149
  <pre class="code-block"><code>[component name="counter" step="2" /]
148
-
149
150
  &lt;dm-counter step="2"&gt;&lt;/dm-counter&gt;</code></pre>
150
151
 
151
152
  <hr>
152
153
 
153
154
  <h2>Move a component between sites</h2>
154
155
  <ol>
155
- <li>On <a href="#/components">Components</a>, click <span data-icon="download"></span> Export on the
156
- row - you'll download <code>name.dmcomponent.json</code>.</li>
157
- <li>On the other site, click <strong>Import</strong> and pick that file. On a name clash you'll be
158
- asked to confirm an overwrite.</li>
156
+ <li>On <a href="#/components">Components</a>, right-click the row (or use its menu) and choose
157
+ <span data-icon="download"></span> <strong>Export .dmcomponent.json</strong> - you'll download
158
+ <code>name.dmcomponent.json</code>. The editor has the same Export in its banner.</li>
159
+ <li>On the other site, click <strong>Import</strong> in the Components banner and pick that file. On a
160
+ name clash you'll be asked to confirm an overwrite.</li>
159
161
  </ol>
160
162
 
161
163
  <hr>
162
164
 
163
165
  <h2>File it under a project</h2>
164
- <p>In the editor, set the <strong>Project</strong> dropdown. That writes <code>meta.project</code> to
165
- the sidecar so the component shows under that project's filtered list and is scoped accordingly.</p>
166
+ <p>In the editor, set the <strong>Project</strong> dropdown. That writes <code>project</code> to the
167
+ component's <code>.meta.json</code> sidecar, so it shows in that project's Components list
168
+ (<code>#/projects/&lt;slug&gt;/components</code>).</p>
166
169
 
167
170
  <hr>
168
171
  <p class="text-muted" style="font-size:.9rem;">
@@ -38,19 +38,16 @@
38
38
  Block order does not matter.</p>
39
39
 
40
40
  <pre class="code-block"><code>&lt;template&gt;
41
- &lt;div class="dm-greeting"&gt;Hello, {{name}}!&lt;/div&gt;
41
+ &lt;div class="dm-greeting"&gt;Hello, &#123;&#123;name&#125;&#125;!&lt;/div&gt;
42
42
  &lt;/template&gt;
43
-
44
43
  &lt;props&gt;
45
44
  {
46
45
  "name": { "type": "string", "default": "world", "label": "Name to greet" }
47
46
  }
48
47
  &lt;/props&gt;
49
-
50
48
  &lt;script&gt;
51
49
  export default {};
52
50
  &lt;/script&gt;
53
-
54
51
  &lt;style&gt;
55
52
  .dm-greeting { font-weight: 600; }
56
53
  &lt;/style&gt;</code></pre>
@@ -71,22 +68,22 @@ export default {};
71
68
  <tr><th>Syntax</th><th>Purpose</th></tr>
72
69
  </thead>
73
70
  <tbody>
74
- <tr><td><code>{{ value }}</code></td><td>Interpolate a prop or state value (HTML-escaped).</td></tr>
75
- <tr><td><code>{{#if cond}}…{{/if}}</code></td><td>Render the block only when <code>cond</code> is truthy.</td></tr>
76
- <tr><td><code>{{#unless cond}}…{{/unless}}</code></td><td>Render the block only when <code>cond</code> is falsy.</td></tr>
77
- <tr><td><code>{{#each list}}…{{/each}}</code></td><td>Repeat the block for each item. Inside, <code>{{.}}</code> is the item (for primitives), <code>{{@index}}</code>, <code>{{@first}}</code> and <code>{{@last}}</code> are available; object items expose their own keys.</td></tr>
78
- <tr><td><code>{{#with obj}}…{{/with}}</code></td><td>Scope the block to an object's keys.</td></tr>
71
+ <tr><td><code>&#123;&#123; value &#125;&#125;</code></td><td>Interpolate a prop or state value (HTML-escaped).</td></tr>
72
+ <tr><td><code>&#123;&#123;#if cond&#125;&#125;…&#123;&#123;/if&#125;&#125;</code></td><td>Render the block only when <code>cond</code> is truthy.</td></tr>
73
+ <tr><td><code>&#123;&#123;#unless cond&#125;&#125;…&#123;&#123;/unless&#125;&#125;</code></td><td>Render the block only when <code>cond</code> is falsy.</td></tr>
74
+ <tr><td><code>&#123;&#123;#each list&#125;&#125;…&#123;&#123;/each&#125;&#125;</code></td><td>Repeat the block for each item. Inside, <code>&#123;&#123;.&#125;&#125;</code> is the item (for primitives), <code>&#123;&#123;@index&#125;&#125;</code>, <code>&#123;&#123;@first&#125;&#125;</code> and <code>&#123;&#123;@last&#125;&#125;</code> are available; object items expose their own keys.</td></tr>
75
+ <tr><td><code>&#123;&#123;#with obj&#125;&#125;…&#123;&#123;/with&#125;&#125;</code></td><td>Scope the block to an object's keys.</td></tr>
79
76
  <tr><td><code>&lt;slot&gt;&lt;/slot&gt;</code></td><td>Project the light-DOM content placed between the element's tags.</td></tr>
80
77
  </tbody>
81
78
  </table>
82
79
 
83
80
  <pre class="code-block"><code>&lt;template&gt;
84
81
  &lt;ul class="dm-list"&gt;
85
- {{#each items}}
86
- &lt;li class="{{#if @first}}first{{/if}}"&gt;{{@index}}. {{.}}&lt;/li&gt;
87
- {{/each}}
82
+ &#123;&#123;#each items&#125;&#125;
83
+ &lt;li class="&#123;&#123;#if @first&#125;&#125;first&#123;&#123;/if&#125;&#125;"&gt;&#123;&#123;@index&#125;&#125;. &#123;&#123;.&#125;&#125;&lt;/li&gt;
84
+ &#123;&#123;/each&#125;&#125;
88
85
  &lt;/ul&gt;
89
- {{#if title}}&lt;h4&gt;{{title}}&lt;/h4&gt;{{/if}}
86
+ &#123;&#123;#if title&#125;&#125;&lt;h4&gt;&#123;&#123;title&#125;&#125;&lt;/h4&gt;&#123;&#123;/if&#125;&#125;
90
87
  &lt;slot&gt;&lt;/slot&gt;
91
88
  &lt;/template&gt;</code></pre>
92
89
 
@@ -122,12 +119,10 @@ export default {};
122
119
  <pre class="code-block"><code>export default {
123
120
  // Reactive state. Called WITHOUT `this` - do not read props here.
124
121
  data() { return { count: 0 }; },
125
-
126
122
  // Methods are bound to the component context (`this`).
127
123
  methods: {
128
124
  inc() { this.set({ count: this.data.count + this.props.step }); }
129
125
  },
130
-
131
126
  // Lifecycle - bound to `this`; props and the element are available here.
132
127
  onMount() { this.set({ count: this.props.initial }); },
133
128
  onUnmount() { /* clean up timers, listeners, etc. */ }
@@ -215,6 +210,7 @@ export default {};
215
210
  </thead>
216
211
  <tbody>
217
212
  <tr><td><code>GET /api/components</code></td><td>read</td><td>List components with metadata + parsed props.</td></tr>
213
+ <tr><td><code>GET /api/components/_count</code></td><td>read</td><td>The count shown on the sidebar badge.</td></tr>
218
214
  <tr><td><code>GET /api/components/:name</code></td><td>read</td><td>Raw <code>.dmc</code> source + parsed props.</td></tr>
219
215
  <tr><td><code>GET /api/components/:name.js</code></td><td>public</td><td>Compiled browser module (what pages load).</td></tr>
220
216
  <tr><td><code>POST /api/components/compile</code></td><td>update</td><td>Transient compile for the live preview (nothing written).</td></tr>
@@ -231,7 +227,8 @@ export default {};
231
227
 
232
228
  <h3>Bundled &amp; plugin components</h3>
233
229
  <p>The <code>bundled</code> flag marks a component as part of fresh-install seed data. Plugins may
234
- also contribute components via the <code>registerComponent()</code> hook - these appear in the list
230
+ also contribute components with <code>options.hooks.registerComponent({name, source})</code> in their
231
+ <code>plugin.js</code> - these appear in the list
235
232
  with a <span class="badge badge-secondary">plugin</span> badge, are served from memory, and are
236
233
  <strong>read-only</strong> from the admin (you can't edit or delete them, and you can't save a
237
234
  component whose name a plugin already owns).</p>
@@ -77,9 +77,10 @@
77
77
 
78
78
  <div class="alert alert-warning">
79
79
  <strong>2. Boolean coercion is string-based.</strong> Attribute values are strings. A boolean prop
80
- is true only for the string <code>"true"</code>; <code>showReset="false"</code> is still a non-empty
81
- string and coerces to <code>true</code>. To switch a boolean off, set it to <code>"true"</code> or
82
- omit the attribute entirely.
80
+ is false only for the string <code>"false"</code> (or when the attribute is left out and the default is
81
+ false). A bare attribute (<code>show-reset</code>), <code>"true"</code> and any other value, including
82
+ <code>"no"</code> and <code>"0"</code>, coerce to <code>true</code>. To switch a boolean off, write
83
+ <code>"false"</code> or omit the attribute.
83
84
  </div>
84
85
 
85
86
  <div class="alert alert-warning">
@@ -109,8 +110,8 @@
109
110
 
110
111
  <h2>Plugin-owned components are read-only</h2>
111
112
  <ul>
112
- <li>Components contributed by a plugin show a <span class="badge badge-secondary">plugin</span>
113
- badge, are served from memory, and <strong>cannot be edited or deleted</strong> from the admin
113
+ <li>Components contributed by a plugin (through <code>options.hooks.registerComponent({name, source})</code>
114
+ in its <code>plugin.js</code>) show a <span class="badge badge-secondary">plugin</span> badge, are served from memory, and <strong>cannot be edited or deleted</strong> from the admin
114
115
  (attempting it returns <code>PLUGIN_OWNED</code>).</li>
115
116
  <li>You also can't save a disk component whose name a plugin already owns - pick a different name.</li>
116
117
  </ul>
@@ -130,7 +131,7 @@
130
131
 
131
132
  <h2>Rendering &amp; safety notes</h2>
132
133
  <ul>
133
- <li><code>{{ }}</code> interpolation is HTML-escaped, so prop values can't inject markup.</li>
134
+ <li><code>&#123;&#123; &#125;&#125;</code> interpolation is HTML-escaped, so prop values can't inject markup.</li>
134
135
  <li>The markdown sanitiser keeps a live allowlist of <code>dm-*</code> tags, refreshed whenever you
135
136
  save or delete a component - a component you just created is immediately usable in page content.
136
137
  <code>on*</code> handler attributes and <code>javascript:</code> URLs are still stripped globally.</li>