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.
- package/CLAUDE.md +5 -3
- package/admin/css/admin.css +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/action-editor-arrange.js +1 -1
- package/admin/js/lib/api-tokens-arrange.js +2 -2
- package/admin/js/lib/block-editor-arrange.js +1 -1
- package/admin/js/lib/blocks-arrange.js +1 -1
- package/admin/js/lib/collection-entries-arrange.js +1 -1
- package/admin/js/lib/components-arrange.js +1 -1
- package/admin/js/lib/dashboard-arrange.js +1 -1
- package/admin/js/lib/dates.js +1 -0
- package/admin/js/lib/forms-arrange.js +1 -1
- package/admin/js/lib/media-arrange.js +1 -1
- package/admin/js/lib/notifications-arrange.js +1 -1
- package/admin/js/lib/pages-arrange.js +1 -1
- package/admin/js/lib/related.js +1 -1
- package/admin/js/lib/timeline-builder.js +2 -2
- package/admin/js/templates/action-editor.html +6 -5
- package/admin/js/templates/actions-list.html +1 -1
- package/admin/js/templates/contacts.html +1 -1
- package/admin/js/templates/docs/api-actions.html +86 -60
- package/admin/js/templates/docs/api-authentication.html +159 -123
- package/admin/js/templates/docs/api-builder.html +197 -0
- package/admin/js/templates/docs/api-collections.html +199 -259
- package/admin/js/templates/docs/api-external.html +225 -0
- package/admin/js/templates/docs/api-forms.html +268 -0
- package/admin/js/templates/docs/api-layouts.html +70 -45
- package/admin/js/templates/docs/api-media.html +57 -80
- package/admin/js/templates/docs/api-navigation.html +66 -22
- package/admin/js/templates/docs/api-pages.html +109 -129
- package/admin/js/templates/docs/api-plugins.html +123 -61
- package/admin/js/templates/docs/api-scaffold.html +185 -0
- package/admin/js/templates/docs/api-settings.html +72 -64
- package/admin/js/templates/docs/api-users.html +74 -107
- package/admin/js/templates/docs/api-views.html +68 -54
- package/admin/js/templates/docs/components-howto.html +20 -17
- package/admin/js/templates/docs/components-reference.html +13 -16
- package/admin/js/templates/docs/components-rules.html +7 -6
- package/admin/js/templates/docs/components-walkthrough.html +19 -19
- package/admin/js/templates/docs/tutorial-crud.html +71 -40
- package/admin/js/templates/docs/tutorial-forms.html +51 -35
- package/admin/js/templates/docs/tutorial-plugin.html +132 -56
- package/admin/js/templates/docs/usage-actions.html +61 -15
- package/admin/js/templates/docs/usage-collections.html +108 -0
- package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
- package/admin/js/templates/docs/usage-dconfig.html +0 -3
- package/admin/js/templates/docs/usage-editions.html +213 -0
- package/admin/js/templates/docs/usage-media.html +22 -6
- package/admin/js/templates/docs/usage-navigation.html +74 -18
- package/admin/js/templates/docs/usage-pages.html +60 -20
- package/admin/js/templates/docs/usage-plugins.html +89 -17
- package/admin/js/templates/docs/usage-shortcodes.html +123 -70
- package/admin/js/templates/docs/usage-site-settings.html +50 -18
- package/admin/js/templates/docs/usage-tools.html +73 -0
- package/admin/js/templates/docs/usage-users-roles.html +99 -20
- package/admin/js/templates/docs/usage-views.html +36 -19
- package/admin/js/templates/documentation.html +153 -32
- package/admin/js/templates/page-editor.html +0 -5
- package/admin/js/templates/plugin-guide.html +15 -0
- package/admin/js/templates/plugin-guides.html +21 -0
- package/admin/js/templates/pro-docs.html +53 -234
- package/admin/js/templates/tutorials.html +5 -4
- package/admin/js/views/actions-list.js +3 -3
- package/admin/js/views/analytics.js +5 -5
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/block-editor.js +4 -4
- package/admin/js/views/blocks.js +4 -4
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collection-entries.js +7 -7
- package/admin/js/views/component-editor.js +2 -2
- package/admin/js/views/contacts.js +22 -20
- package/admin/js/views/context-menu-editor.js +5 -5
- package/admin/js/views/doc-pages.js +1 -1
- package/admin/js/views/form-editor.js +4 -4
- package/admin/js/views/form-submissions.js +2 -2
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/media.js +3 -3
- package/admin/js/views/menu-editor.js +13 -13
- package/admin/js/views/menu-locations.js +2 -2
- package/admin/js/views/my-profile.js +1 -1
- package/admin/js/views/page-editor.js +8 -8
- package/admin/js/views/plugin-guides.js +5 -0
- package/admin/js/views/project-detail.js +2 -2
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/role-editor.js +4 -4
- package/admin/js/views/search.js +2 -2
- package/admin/js/views/seo.js +17 -17
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +3 -3
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +2 -2
- package/admin/js/views/view-editor.js +1 -1
- package/bin/cli.js +13 -13
- package/bin/lib/node-version.js +29 -0
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +3 -2
- package/plugins/_lib/admin/mail/reader-view.js +7 -6
- package/plugins/_lib/admin/mail/scheduling.js +4 -2
- package/plugins/_lib/admin/mail/templates.js +4 -4
- package/plugins/_lib/admin/ui/dates.js +85 -0
- package/plugins/blog/CLAUDE.md +31 -22
- package/plugins/blog/admin/views/blog.js +3 -2
- package/plugins/blog/admin/views/comments.js +2 -1
- package/plugins/blog/admin/views/post-editor.js +4 -4
- package/plugins/blog/blocks/blog-card-row.html +1 -1
- package/plugins/blog/blocks/blog-card.html +2 -2
- package/plugins/blog/blocks/blog-post-classic.html +2 -2
- package/plugins/blog/blocks/blog-post-essay.html +2 -2
- package/plugins/blog/blocks/blog-post-feature.html +2 -2
- package/plugins/blog/blocks/blog-post-minimal.html +2 -2
- package/plugins/blog/blocks/blog-post-sidebar.html +2 -2
- package/plugins/blog/blocks/blog-post-split.html +2 -2
- package/plugins/blog/docs/guide.md +205 -0
- package/plugins/blog/lib/layouts.js +3 -3
- package/plugins/blog/lib/page.js +2 -1
- package/plugins/blog/plugin.js +3 -3
- package/plugins/blog/plugin.json +4 -4
- package/plugins/blog/tests/layouts.test.js +6 -0
- package/plugins/feedback/CLAUDE.md +22 -3
- package/plugins/feedback/admin/lib/kit.js +6 -7
- package/plugins/feedback/admin/views/feedback.js +79 -10
- package/plugins/feedback/admin/views/send.js +28 -6
- package/plugins/feedback/docs/guide.md +95 -0
- package/plugins/feedback/lib/receiver.js +9 -2
- package/plugins/feedback/lib/sender.js +3 -2
- package/plugins/feedback/plugin.js +54 -6
- package/plugins/feedback/plugin.json +4 -4
- package/plugins/feedback/tests/api.test.js +74 -2
- package/plugins/free-tier.lock.json +49 -44
- package/plugins/mail-reader/CLAUDE.md +33 -18
- package/plugins/mail-reader/docs/guide.md +147 -0
- package/plugins/mail-reader/plugin.json +1 -1
- package/plugins/security/CLAUDE.md +4 -1
- package/plugins/security/admin/views/security.js +5 -5
- package/plugins/security/docs/guide.md +170 -0
- package/plugins/security/plugin.js +2 -1
- package/plugins/security/plugin.json +2 -1
- package/plugins/shopping-cart/CLAUDE.md +7 -1
- package/plugins/shopping-cart/admin/lib/kit.js +5 -2
- package/plugins/shopping-cart/admin/views/orders.js +4 -4
- package/plugins/shopping-cart/admin/views/overview.js +2 -2
- package/plugins/shopping-cart/docs/guide.md +191 -0
- package/plugins/shopping-cart/lib/render.js +2 -1
- package/plugins/shopping-cart/plugin.json +3 -3
- package/public/js/collection-browser.js +2 -2
- package/public/js/site.js +1 -1
- package/scripts/gen-instance-secret.js +3 -1
- package/scripts/setup.js +3 -1
- package/server/middleware/auth.js +2 -1
- package/server/routes/api/actions.js +47 -27
- package/server/routes/api/blocks.js +2 -1
- package/server/routes/api/collections.js +16 -52
- package/server/routes/api/contacts.js +66 -3
- package/server/routes/api/documentation.js +42 -0
- package/server/routes/api/notifications.js +3 -2
- package/server/routes/api/users.js +10 -6
- package/server/server.js +16 -1
- package/server/services/actions.js +110 -34
- package/server/services/adapterRegistry.js +169 -16
- package/server/services/adapters/FileAdapter.js +25 -0
- package/server/services/adapters/MongoAdapter.js +23 -0
- package/server/services/collections.js +104 -1
- package/server/services/connectionManager.js +12 -0
- package/server/services/dates.js +81 -0
- package/server/services/docs.js +13 -2
- package/server/services/markdown.js +75 -26
- package/server/services/notification-sources.js +6 -5
- package/server/services/passwordReset.js +2 -1
- package/server/services/permissionRegistry.js +3 -2
- package/server/services/pluginGuides.js +255 -0
- package/server/services/pluginInstaller.js +54 -13
- package/server/services/plugins.js +29 -1
- package/server/services/presetCollections.js +31 -5
- package/server/services/renderer.js +2 -2
- package/server/services/sidebarBadges.js +3 -1
- package/server/services/tools.js +4 -2
- package/server/templates/page.html +2 -2
|
@@ -8,40 +8,51 @@
|
|
|
8
8
|
<div class="docs-body">
|
|
9
9
|
|
|
10
10
|
<p>Plugins live in the <code>plugins/</code> directory. Each plugin is a self-contained folder with
|
|
11
|
-
three required files and optional <code>admin/</code> and <code>
|
|
11
|
+
three required files and optional <code>admin/</code>, <code>public/</code> and <code>docs/</code>
|
|
12
|
+
subdirectories.</p>
|
|
13
|
+
<p>The quickest start is <strong>New plugin</strong> in the banner of <a href="#/plugins">System >
|
|
14
|
+
Plugins</a> (the Marketplace screen). It copies the built-in template into <code>plugins/<slug>/</code>
|
|
15
|
+
with a server entry, config defaults, a wired admin view and a <code>CLAUDE.md</code>, switched on. Restart
|
|
16
|
+
the server to activate it. The rest of this page explains what those files do.</p>
|
|
12
17
|
|
|
13
18
|
<h3>Directory structure</h3>
|
|
14
19
|
<pre class="code-block"><code>plugins/
|
|
15
20
|
my-plugin/
|
|
16
|
-
plugin.json
|
|
17
|
-
plugin.js
|
|
18
|
-
config.js
|
|
21
|
+
plugin.json - manifest (required)
|
|
22
|
+
plugin.js - Fastify plugin (required)
|
|
23
|
+
config.js - settings defaults (required)
|
|
19
24
|
admin/
|
|
20
25
|
views/
|
|
21
|
-
|
|
26
|
+
index.js - admin view (optional)
|
|
22
27
|
templates/
|
|
23
|
-
|
|
28
|
+
index.html - view template (optional)
|
|
29
|
+
css/
|
|
30
|
+
index.css - admin styles (optional)
|
|
24
31
|
public/
|
|
25
|
-
inject-head.html
|
|
26
|
-
inject-body.html
|
|
27
|
-
|
|
32
|
+
inject-head.html - injected into <head> on every page (optional)
|
|
33
|
+
inject-body.html - injected before </body> on every page (optional)
|
|
34
|
+
docs/
|
|
35
|
+
guide.md - user guide under Documentation > Plugins (optional)
|
|
36
|
+
data/ - plugin data store (optional, never served)</code></pre>
|
|
37
|
+
<p>Only <code>admin/</code> and <code>public/</code> are served over HTTP; <code>plugin.js</code>,
|
|
38
|
+
<code>config.js</code> and <code>data/</code> never are.</p>
|
|
28
39
|
|
|
29
40
|
<hr>
|
|
30
41
|
|
|
31
42
|
<h3>1. plugin.json - the manifest</h3>
|
|
32
43
|
<p>All fields below are <strong>required</strong>. Missing any will cause the plugin to be skipped on
|
|
33
|
-
startup with a warning in the server log.</p>
|
|
44
|
+
startup with a warning in the server log. <code>name</code> must match the folder name.</p>
|
|
34
45
|
<pre class="code-block"><code>{
|
|
35
46
|
"name": "my-plugin",
|
|
36
47
|
"displayName": "My Plugin",
|
|
37
48
|
"version": "1.0.0",
|
|
38
|
-
"description": "A short description shown
|
|
49
|
+
"description": "A short description shown in the Marketplace.",
|
|
39
50
|
"author": "Your Name",
|
|
40
51
|
"date": "2026-03-01",
|
|
41
52
|
"icon": "star"
|
|
42
53
|
}</code></pre>
|
|
43
54
|
|
|
44
|
-
<p>
|
|
55
|
+
<p>Common optional fields:</p>
|
|
45
56
|
<table class="table table-sm">
|
|
46
57
|
<thead>
|
|
47
58
|
<tr>
|
|
@@ -56,6 +67,12 @@
|
|
|
56
67
|
<td>string</td>
|
|
57
68
|
<td>Path (relative to plugin root) to an HTML snippet injected into <code><head></code>.</td>
|
|
58
69
|
</tr>
|
|
70
|
+
<tr>
|
|
71
|
+
<td><code>inject.headLate</code></td>
|
|
72
|
+
<td>string</td>
|
|
73
|
+
<td>A snippet injected at the end of <code><head></code>, after the site stylesheets - for CSS
|
|
74
|
+
that must win.</td>
|
|
75
|
+
</tr>
|
|
59
76
|
<tr>
|
|
60
77
|
<td><code>inject.bodyEnd</code></td>
|
|
61
78
|
<td>string</td>
|
|
@@ -76,6 +93,44 @@
|
|
|
76
93
|
<td>object</td>
|
|
77
94
|
<td>View modules to dynamically import into the admin SPA.</td>
|
|
78
95
|
</tr>
|
|
96
|
+
<tr>
|
|
97
|
+
<td><code>admin.css</code></td>
|
|
98
|
+
<td>array</td>
|
|
99
|
+
<td>Admin stylesheets loaded with the plugin's views.</td>
|
|
100
|
+
</tr>
|
|
101
|
+
<tr>
|
|
102
|
+
<td><code>permissions</code></td>
|
|
103
|
+
<td>array</td>
|
|
104
|
+
<td>Permission resources the plugin guards its routes with. They appear in the role editor;
|
|
105
|
+
<code>grant</code> gives existing roles them once.</td>
|
|
106
|
+
</tr>
|
|
107
|
+
<tr>
|
|
108
|
+
<td><code>requires</code></td>
|
|
109
|
+
<td>array</td>
|
|
110
|
+
<td>Built-in Tools (<code>contacts</code>, <code>notes</code>, <code>todo</code>, <code>analytics</code>,
|
|
111
|
+
<code>seo</code>) or plugins this one cannot work without. It is not loaded while one is off.</td>
|
|
112
|
+
</tr>
|
|
113
|
+
<tr>
|
|
114
|
+
<td><code>uses</code></td>
|
|
115
|
+
<td>object</td>
|
|
116
|
+
<td><code>{"contacts": "Invite a contact group"}</code> - Tools or plugins it works without but uses
|
|
117
|
+
when they are on. The text is what the admin is told they lose.</td>
|
|
118
|
+
</tr>
|
|
119
|
+
<tr>
|
|
120
|
+
<td><code>supersedes</code></td>
|
|
121
|
+
<td>array</td>
|
|
122
|
+
<td>Plugins (or built-in Tools) this one replaces outright, such as a Pro edition of a free plugin.</td>
|
|
123
|
+
</tr>
|
|
124
|
+
<tr>
|
|
125
|
+
<td><code>settingsSchema</code></td>
|
|
126
|
+
<td>array</td>
|
|
127
|
+
<td>Labels, types and help for the Marketplace <strong>Configure</strong> form.</td>
|
|
128
|
+
</tr>
|
|
129
|
+
<tr>
|
|
130
|
+
<td><code>minCmsVersion</code></td>
|
|
131
|
+
<td>string</td>
|
|
132
|
+
<td>The oldest Domma CMS the plugin works on. Install and update are refused on an older CMS.</td>
|
|
133
|
+
</tr>
|
|
79
134
|
</tbody>
|
|
80
135
|
</table>
|
|
81
136
|
|
|
@@ -84,38 +139,48 @@
|
|
|
84
139
|
<h3>2. plugin.js - the Fastify plugin</h3>
|
|
85
140
|
<p>This is the server-side entry point. It must export a default <strong>async function</strong> that
|
|
86
141
|
Fastify will call with <code>(fastify, options)</code>.</p>
|
|
87
|
-
<p>The CMS
|
|
88
|
-
|
|
142
|
+
<p>The CMS hands you auth middleware in <code>options.auth</code> (<code>authenticate</code>,
|
|
143
|
+
<code>requireAdmin</code>, <code>requirePermission</code>, <code>requireRole</code>,
|
|
144
|
+
<code>requireVisibility</code>), extension points in <code>options.hooks</code> (shortcodes, transforms,
|
|
145
|
+
sidebar items, notifications, <code>registerComponent</code> and more) and the merged settings in
|
|
146
|
+
<code>options.settings</code>. Always take them from there rather than importing the middleware directly.</p>
|
|
89
147
|
|
|
90
148
|
<pre class="code-block"><code>import { getPluginSettings, savePluginState } from '../../server/services/plugins.js';
|
|
91
|
-
|
|
92
149
|
export default async function myPlugin(fastify, options) {
|
|
93
150
|
const { authenticate, requireAdmin } = options.auth;
|
|
94
|
-
|
|
95
151
|
// Public endpoint - no auth needed
|
|
96
152
|
fastify.get('/hello', async () => {
|
|
97
153
|
return { message: 'Hello from my plugin!' };
|
|
98
154
|
});
|
|
99
|
-
|
|
100
|
-
// Admin-only endpoint
|
|
155
|
+
// Admin-only endpoint (role levels 0 and 1)
|
|
101
156
|
fastify.get('/settings', { preHandler: [authenticate, requireAdmin] }, async () => {
|
|
102
157
|
return getPluginSettings('my-plugin');
|
|
103
158
|
});
|
|
104
|
-
|
|
105
159
|
fastify.put('/settings', { preHandler: [authenticate, requireAdmin] }, async (request) => {
|
|
106
160
|
savePluginState('my-plugin', { settings: request.body });
|
|
107
161
|
return { ok: true };
|
|
108
162
|
});
|
|
163
|
+
}
|
|
164
|
+
// Optional: runs once each time the plugin is switched on, not at install.
|
|
165
|
+
export async function onEnable({ fastify, services, hooks }) {
|
|
166
|
+
// Create collections, pages or forms here if needed.
|
|
167
|
+
}
|
|
168
|
+
// Optional: runs when it is switched off, and before an uninstall (uninstall: true).
|
|
169
|
+
export async function onDisable({ fastify, services, hooks, uninstall }) {
|
|
109
170
|
}</code></pre>
|
|
110
171
|
|
|
111
|
-
<p>Routes are registered under the prefix <code>/api/plugins/{name}</code> automatically
|
|
112
|
-
|
|
172
|
+
<p>Routes are registered under the prefix <code>/api/plugins/{name}</code> automatically - write
|
|
173
|
+
<code>'/hello'</code>, not the full path. The prefix is always locked to your plugin's directory name.</p>
|
|
174
|
+
<p>For your own screens, prefer <code>requirePermission('my-plugin', 'read')</code> with a permission
|
|
175
|
+
declared in <code>plugin.json</code> over <code>requireAdmin</code>: the site owner can then give it to any
|
|
176
|
+
role in <a href="#/roles">System > Roles</a>.</p>
|
|
113
177
|
|
|
114
178
|
<hr>
|
|
115
179
|
|
|
116
180
|
<h3>3. config.js - settings defaults</h3>
|
|
117
|
-
<p>Export a plain object of default settings. These are merged with any
|
|
118
|
-
<code>config/plugins.json</code> when <code>getPluginSettings()</code> is called
|
|
181
|
+
<p>Export a plain object of default settings. These are merged with any overrides stored in
|
|
182
|
+
<code>config/plugins.json</code> when <code>getPluginSettings()</code> is called. The site owner changes
|
|
183
|
+
them with <strong>Configure</strong> on the plugin in the Marketplace.</p>
|
|
119
184
|
|
|
120
185
|
<pre class="code-block"><code>export default {
|
|
121
186
|
greeting: 'Hello, world!',
|
|
@@ -150,58 +215,55 @@ export default async function myPlugin(fastify, options) {
|
|
|
150
215
|
],
|
|
151
216
|
"views": {
|
|
152
217
|
"plugin-my-plugin": {
|
|
153
|
-
"entry": "my-plugin/admin/views/
|
|
218
|
+
"entry": "my-plugin/admin/views/index.js",
|
|
154
219
|
"exportName": "myPluginView"
|
|
155
220
|
}
|
|
156
221
|
}
|
|
157
222
|
}</code></pre>
|
|
158
223
|
|
|
159
224
|
<p>The view file follows the standard Domma view pattern - a <code>templateUrl</code> and an
|
|
160
|
-
<code>onMount($container)</code> function
|
|
225
|
+
<code>onMount($container)</code> function. Call your API with <code>H.get</code> / <code>H.post</code>,
|
|
226
|
+
which add the sign-in header and refresh the token for you - do not hand-build a Bearer header with
|
|
227
|
+
<code>fetch()</code>:</p>
|
|
161
228
|
|
|
162
|
-
<pre class="code-block"><code>// admin/views/
|
|
229
|
+
<pre class="code-block"><code>// admin/views/index.js
|
|
163
230
|
export const myPluginView = {
|
|
164
|
-
templateUrl: '/plugins/my-plugin/admin/templates/
|
|
165
|
-
|
|
231
|
+
templateUrl: '/plugins/my-plugin/admin/templates/index.html',
|
|
166
232
|
async onMount($container) {
|
|
167
|
-
const res = await
|
|
168
|
-
|
|
169
|
-
});
|
|
170
|
-
const settings = await res.json();
|
|
171
|
-
|
|
233
|
+
const res = await H.get('/api/plugins/my-plugin/settings');
|
|
234
|
+
const settings = res.data ?? res;
|
|
172
235
|
$container.find('#greeting').text(settings.greeting);
|
|
173
|
-
|
|
236
|
+
I.scan();
|
|
174
237
|
}
|
|
175
238
|
};</code></pre>
|
|
176
239
|
|
|
177
|
-
<p>The template is a plain HTML fragment (no <code><html></code> wrapper)
|
|
178
|
-
form
|
|
179
|
-
|
|
180
|
-
<pre class="code-block"><code><!-- admin/templates/my-view.html -->
|
|
181
|
-
<div class="view-header">
|
|
182
|
-
<h1><span data-icon="star"></span> My Plugin</h1>
|
|
183
|
-
</div>
|
|
240
|
+
<p>The template is a plain HTML fragment (no <code><html></code> wrapper) using Domma classes
|
|
241
|
+
(<code>card</code>, <code>btn</code>, <code>form-input</code>). The admin already wraps every plugin view in
|
|
242
|
+
a banner with the plugin's name, version and licence, so do not draw your own page heading:</p>
|
|
184
243
|
|
|
244
|
+
<pre class="code-block"><code><!-- admin/templates/index.html -->
|
|
185
245
|
<div class="card">
|
|
186
246
|
<div class="card-body">
|
|
187
|
-
<p id="greeting">Loading
|
|
247
|
+
<p id="greeting">Loading...</p>
|
|
188
248
|
</div>
|
|
189
249
|
</div></code></pre>
|
|
250
|
+
<p>Files under <code>admin/</code> are fetched by the browser, so a hard refresh of the admin picks up a
|
|
251
|
+
change without a restart.</p>
|
|
190
252
|
|
|
191
253
|
<hr>
|
|
192
254
|
|
|
193
255
|
<h3>5. Injection snippets (optional)</h3>
|
|
194
|
-
<p>HTML snippets declared in <code>inject.head</code
|
|
195
|
-
the plugin's
|
|
196
|
-
analytics scripts, stylesheets, or widgets.</p>
|
|
256
|
+
<p>HTML snippets declared in <code>inject.head</code>, <code>inject.headLate</code> and
|
|
257
|
+
<code>inject.bodyEnd</code> are read from the plugin's folder and inserted into every public page. Use
|
|
258
|
+
this for analytics scripts, stylesheets, or widgets. They are read once at start-up.</p>
|
|
197
259
|
|
|
198
260
|
<pre class="code-block"><code><!-- public/inject-body.html -->
|
|
199
261
|
<script>
|
|
200
262
|
(function () {
|
|
201
263
|
// This runs on every public page
|
|
202
264
|
fetch('/api/plugins/my-plugin/hello')
|
|
203
|
-
.then(r
|
|
204
|
-
.then(d
|
|
265
|
+
.then(r => r.json())
|
|
266
|
+
.then(d => console.log(d.message));
|
|
205
267
|
})();
|
|
206
268
|
</script></code></pre>
|
|
207
269
|
|
|
@@ -210,23 +272,37 @@ export const myPluginView = {
|
|
|
210
272
|
|
|
211
273
|
<hr>
|
|
212
274
|
|
|
213
|
-
<h3>6.
|
|
275
|
+
<h3>6. User guide (optional)</h3>
|
|
276
|
+
<p>Markdown files in the plugin's <code>docs/</code> folder become pages under <strong>Documentation >
|
|
277
|
+
Plugins</strong> while the plugin is loaded, shown only to users who can use the plugin.
|
|
278
|
+
<code>guide.md</code> is the first page; further files become tabs. Start with <code>##</code> sections,
|
|
279
|
+
not a top-level heading, and write for site owners.</p>
|
|
280
|
+
|
|
281
|
+
<hr>
|
|
282
|
+
|
|
283
|
+
<h3>7. Switching it on and testing</h3>
|
|
214
284
|
<ol>
|
|
215
|
-
<li>Create the <code>plugins/my-plugin/</code> directory with all three required files
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<li>
|
|
285
|
+
<li>Create the <code>plugins/my-plugin/</code> directory with all three required files (or use
|
|
286
|
+
<strong>New plugin</strong>, which does this and switches it on).</li>
|
|
287
|
+
<li>Open <a href="#/plugins">System > Plugins</a>. A plugin you made by hand is listed, switched off.
|
|
288
|
+
Right-click it (or use its menu) and choose <strong>Switch on</strong>. Its <code>onEnable</code> runs
|
|
289
|
+
now.</li>
|
|
290
|
+
<li>The server restarts by itself to load the plugin's routes and screens, where a supervisor (the
|
|
291
|
+
fleet manager or pm2) will bring it back and <code>restartOnPluginToggle</code> is not
|
|
292
|
+
<code>false</code> in <code>config/server.json</code>. Otherwise restart it yourself. The log then
|
|
293
|
+
shows <code>[plugins] Loaded N plugins: ..., my-plugin</code>.</li>
|
|
221
294
|
<li>Verify your endpoint: <code>GET /api/plugins/my-plugin/hello</code></li>
|
|
222
295
|
</ol>
|
|
223
296
|
|
|
224
297
|
<p class="text-muted" style="font-size:.9rem">Tip: use <code>npm run dev</code> during development -
|
|
225
|
-
the server restarts automatically on file changes
|
|
298
|
+
the server restarts automatically on file changes. Server-side files (<code>plugin.js</code>,
|
|
299
|
+
<code>config.js</code>, <code>plugin.json</code>, snippets) only apply after a restart; the in-admin code
|
|
300
|
+
editor (<strong>View source</strong> on the plugin) has a Restart server button. For the full reference see
|
|
301
|
+
<code>docs/plugin-development.md</code> in the Domma CMS package.</p>
|
|
226
302
|
|
|
227
303
|
<hr>
|
|
228
304
|
<p class="text-muted" style="font-size:.9rem;">
|
|
229
|
-
Next: <a href="#/tutorials/forms">Form Follow-Up
|
|
305
|
+
Next: <a href="#/tutorials/forms">Form Follow-Up</a>
|
|
230
306
|
</p>
|
|
231
307
|
|
|
232
308
|
</div>
|
|
@@ -9,24 +9,27 @@
|
|
|
9
9
|
|
|
10
10
|
<p>Actions are admin-designed sequential workflow operations triggered against individual collection
|
|
11
11
|
entries. When an action is assigned to a collection, a trigger button appears per row in the
|
|
12
|
-
entry list. Actions
|
|
12
|
+
entry list. Actions need a MongoDB connection, which is a Pro feature: without one, Data > Actions
|
|
13
|
+
says so and nothing can be run.</p>
|
|
13
14
|
|
|
14
15
|
<h3>Creating an Action</h3>
|
|
15
16
|
<ol>
|
|
16
17
|
<li>Navigate to <strong>Data → Actions</strong> and click <strong>New Action</strong>.</li>
|
|
17
18
|
<li><strong>General tab</strong> - enter a title and select the target collection.</li>
|
|
18
|
-
<li><strong>Trigger tab</strong> -
|
|
19
|
-
|
|
19
|
+
<li><strong>Trigger tab</strong> - the button label, icon and optional confirmation message.
|
|
20
|
+
Manual (a button) is the only trigger. An optional <strong>transition</strong> makes the
|
|
21
|
+
action a workflow step - see below.
|
|
20
22
|
</li>
|
|
21
23
|
<li><strong>Steps tab</strong> - add steps in order. Each step runs sequentially; if one fails the
|
|
22
24
|
action stops.
|
|
23
25
|
</li>
|
|
24
|
-
<li><strong>Access tab</strong> -
|
|
26
|
+
<li><strong>Access tab</strong> - which roles can run the action, and an optional row-level
|
|
27
|
+
rule. See "Who can run an action" below.</li>
|
|
25
28
|
<li>Click <strong>Save Action</strong>. The button will appear on the collection's entry list.
|
|
26
29
|
</li>
|
|
27
30
|
</ol>
|
|
28
31
|
|
|
29
|
-
<h3>
|
|
32
|
+
<h3>Step Types</h3>
|
|
30
33
|
<table class="table table-sm">
|
|
31
34
|
<thead>
|
|
32
35
|
<tr>
|
|
@@ -51,6 +54,11 @@
|
|
|
51
54
|
<td>Create the entry in a target collection then delete it from the source</td>
|
|
52
55
|
<td><code>targetCollection</code></td>
|
|
53
56
|
</tr>
|
|
57
|
+
<tr>
|
|
58
|
+
<td><code>createInCollection</code></td>
|
|
59
|
+
<td>Create a new entry in another collection, leaving the source entry as it is (a log line, a related record)</td>
|
|
60
|
+
<td><code>targetCollection</code>, <code>data</code> (JSON object of field values), <code>createdBy</code> (optional; defaults to the person running the action)</td>
|
|
61
|
+
</tr>
|
|
54
62
|
<tr>
|
|
55
63
|
<td><code>webhook</code></td>
|
|
56
64
|
<td>HTTP request to an external URL</td>
|
|
@@ -70,7 +78,7 @@
|
|
|
70
78
|
</table>
|
|
71
79
|
|
|
72
80
|
<h3>Template Variables</h3>
|
|
73
|
-
<p>Step config fields support <code
|
|
81
|
+
<p>Step config fields support <code>{{variable}}</code> interpolation:</p>
|
|
74
82
|
<table class="table table-sm">
|
|
75
83
|
<thead>
|
|
76
84
|
<tr>
|
|
@@ -80,43 +88,81 @@
|
|
|
80
88
|
</thead>
|
|
81
89
|
<tbody>
|
|
82
90
|
<tr>
|
|
83
|
-
<td><code
|
|
91
|
+
<td><code>{{entry.data.fieldName}}</code></td>
|
|
84
92
|
<td>A field value from the current entry</td>
|
|
85
93
|
</tr>
|
|
86
94
|
<tr>
|
|
87
|
-
<td><code
|
|
95
|
+
<td><code>{{entry.id}}</code></td>
|
|
88
96
|
<td>The entry's ID</td>
|
|
89
97
|
</tr>
|
|
90
98
|
<tr>
|
|
91
|
-
<td><code
|
|
99
|
+
<td><code>{{now}}</code></td>
|
|
92
100
|
<td>Current timestamp (ISO 8601)</td>
|
|
93
101
|
</tr>
|
|
94
102
|
<tr>
|
|
95
|
-
<td><code
|
|
103
|
+
<td><code>{{user.id}}</code></td>
|
|
104
|
+
<td>ID of the user who triggered the action</td>
|
|
105
|
+
</tr>
|
|
106
|
+
<tr>
|
|
107
|
+
<td><code>{{user.name}}</code></td>
|
|
96
108
|
<td>Name of the user who triggered the action</td>
|
|
97
109
|
</tr>
|
|
98
110
|
<tr>
|
|
99
|
-
<td><code
|
|
111
|
+
<td><code>{{user.email}}</code></td>
|
|
100
112
|
<td>Email of the triggering user</td>
|
|
101
113
|
</tr>
|
|
102
114
|
<tr>
|
|
103
|
-
<td><code
|
|
115
|
+
<td><code>{{user.role}}</code></td>
|
|
116
|
+
<td>Primary role of the triggering user</td>
|
|
117
|
+
</tr>
|
|
118
|
+
<tr>
|
|
119
|
+
<td><code>{{env.CMS_PUBLIC_*}}</code></td>
|
|
104
120
|
<td>Environment variables prefixed <code>CMS_PUBLIC_</code> only</td>
|
|
105
121
|
</tr>
|
|
106
122
|
</tbody>
|
|
107
123
|
</table>
|
|
108
124
|
<p><strong>Example</strong> - approve an application and notify by email:</p>
|
|
109
125
|
<pre class="code-block"><code>Step 1: updateField field=status value=approved
|
|
110
|
-
Step 2: updateField field=approvedAt value
|
|
111
|
-
Step 3: email to
|
|
126
|
+
Step 2: updateField field=approvedAt value={{now}}
|
|
127
|
+
Step 3: email to={{entry.data.email}}
|
|
112
128
|
subject=Your application has been approved
|
|
113
|
-
template=Congratulations
|
|
129
|
+
template=Congratulations {{entry.data.name}}, your application is approved.</code></pre>
|
|
130
|
+
|
|
131
|
+
<h3>Who can run an action</h3>
|
|
132
|
+
<ul>
|
|
133
|
+
<li><strong>Any listed role is enough.</strong> Ticking <code>admin</code> and <code>hr</code>
|
|
134
|
+
lets holders of either run it, and everyone more senior than either - the same ladder as
|
|
135
|
+
page visibility. All the roles a user holds count, not just their primary one.</li>
|
|
136
|
+
<li><strong>No roles ticked</strong> - admins only (role levels 0 and 1).</li>
|
|
137
|
+
<li><strong>A role the site does not have</strong> (deleted or renamed) admits only the level-0
|
|
138
|
+
role. The editor keeps it and flags it so you can fix it.</li>
|
|
139
|
+
<li><strong>Row-level rule</strong> - Owner (entries the user created) or Field match (entries
|
|
140
|
+
that name them in a field), checked on the server every time the action runs. The level-0
|
|
141
|
+
role is never limited by it.</li>
|
|
142
|
+
</ul>
|
|
143
|
+
|
|
144
|
+
<h3>Workflows: transitions</h3>
|
|
145
|
+
<p>Give an action a transition - a state field, the <strong>From</strong> values it may start
|
|
146
|
+
from, and the <strong>To</strong> value - and it becomes a step in a workflow. It is offered
|
|
147
|
+
only while the entry's field holds one of the From values; your steps (usually an Update a
|
|
148
|
+
field step) do the actual change.</p>
|
|
149
|
+
<p>On the public site, an interactive <code>[collection]</code> with the <code>transitions</code>
|
|
150
|
+
attribute shows each row the buttons that apply to it right now, for the person looking -
|
|
151
|
+
roles and the row-level rule included. Add <code>scope="mine"</code> for a "my entries" page
|
|
152
|
+
where people move their own entries on:</p>
|
|
153
|
+
<pre class="code-block"><code>[collection slug="applications" scope="mine" display="cards" title-field="jobTitle" paginate transitions /]</code></pre>
|
|
154
|
+
<p>Running an action whose transition no longer applies (the entry has moved on) is refused with
|
|
155
|
+
409 and changes nothing. From a <code>scope="mine"</code> block, a row the person did not
|
|
156
|
+
create is refused with 403. For an action meant only for people's own entries, also give it
|
|
157
|
+
an Owner row-level rule, so it is refused however it is called.</p>
|
|
114
158
|
|
|
115
159
|
<h3>Partial Execution</h3>
|
|
116
160
|
<p>Actions are <strong>not transactional</strong>. If a step fails, the action stops and returns the
|
|
117
161
|
number of steps completed so far (<code>stepsCompleted</code>). Steps that already ran are
|
|
118
162
|
<strong>not rolled back</strong>. Design step order with this in mind - put irreversible steps
|
|
119
163
|
(delete, email) last.</p>
|
|
164
|
+
<p>See also <a href="#/docs/usage/cta-shortcode">CTA Shortcode</a> for action buttons on pages, and
|
|
165
|
+
the <a href="#/docs/api/actions">Actions API</a>.</p>
|
|
120
166
|
|
|
121
167
|
<h3>After a deleteEntry step</h3>
|
|
122
168
|
<p>If an action contains a <code>deleteEntry</code> step, subsequent steps will fail because the entry
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<div class="view-header">
|
|
2
|
+
<h1><span data-icon="book"></span> Collections & Forms</h1>
|
|
3
|
+
<a href="#/documentation" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> All usage topics</a>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<div class="row">
|
|
7
|
+
<div class="col-12">
|
|
8
|
+
<div class="docs-body">
|
|
9
|
+
|
|
10
|
+
<p>A collection is a set of entries that share the same fields - products, bookings, team members,
|
|
11
|
+
enquiries. Collections are at <strong>Data > Collections</strong>. Their entries are edited in
|
|
12
|
+
the admin, shown on pages with <code>[collection]</code>, filled in by visitors through forms,
|
|
13
|
+
and can be read by other systems through the API if you allow it.</p>
|
|
14
|
+
|
|
15
|
+
<h3>Creating a collection</h3>
|
|
16
|
+
<p>Choose <strong>New collection</strong>. The editor has four tabs:</p>
|
|
17
|
+
<ul>
|
|
18
|
+
<li><strong>Settings</strong> - title, slug (its id, fixed once created), project and the
|
|
19
|
+
layout of the entry form (stacked, or a grid where each field can span columns).</li>
|
|
20
|
+
<li><strong>Fields</strong> - what every entry holds. The editor offers text, email, phone,
|
|
21
|
+
number, textarea, dropdown, radio buttons, single checkbox, checkbox group, date, time,
|
|
22
|
+
URL and hidden fields. Reference (a link to an entry in another collection), file and
|
|
23
|
+
multi-select fields exist too, but are added by editing the collection's
|
|
24
|
+
<code>schema.json</code>.</li>
|
|
25
|
+
<li><strong>API & Export</strong> - what the public API may do (read, create, update,
|
|
26
|
+
delete) and who may do it, which fields a read returns, and whether visitors may export
|
|
27
|
+
what a <code>[collection]</code> display shows.</li>
|
|
28
|
+
<li><strong>Storage</strong> - files on this server (the default) or MongoDB (Pro, when the
|
|
29
|
+
site has a connection).</li>
|
|
30
|
+
</ul>
|
|
31
|
+
<p>Every collection made in the admin gets a matching form, so visitors can add entries with
|
|
32
|
+
<code>[form name="the-slug" /]</code>.</p>
|
|
33
|
+
|
|
34
|
+
<h3>Working with entries</h3>
|
|
35
|
+
<p>Click a collection to see its entries. <strong>Add entry</strong>, <strong>Import</strong> and
|
|
36
|
+
<strong>Export</strong> are in the banner. Click an entry to edit it; right-click it to see
|
|
37
|
+
all of it, duplicate it, copy its id, run one of the collection's Actions or delete it, and
|
|
38
|
+
right-click a value to filter by it. Dates show as dd/mm/yyyy.</p>
|
|
39
|
+
<ul>
|
|
40
|
+
<li><strong>Export</strong> downloads the entries as JSON or CSV.</li>
|
|
41
|
+
<li><strong>Import</strong> takes a JSON array of entries and adds them (nothing is removed).
|
|
42
|
+
Each entry is checked like any other save: one that fails the field rules or points a
|
|
43
|
+
reference at an entry that does not exist is skipped and reported. Fields the collection
|
|
44
|
+
does not have are stored as given. For backups, restores and
|
|
45
|
+
clones of whole collections, see the Data Transfer plugin in the Marketplace.</li>
|
|
46
|
+
</ul>
|
|
47
|
+
|
|
48
|
+
<h3>Moving between files and MongoDB</h3>
|
|
49
|
+
<p>Changing a collection's storage on the Storage tab asks whether to move its entries when you
|
|
50
|
+
save. A move copies each entry exactly as it is - same id, data, created and updated dates and
|
|
51
|
+
owner - so references, links and "my entries" pages keep working. It refuses, changing
|
|
52
|
+
nothing, if the target already holds an entry with one of the same ids, or if MongoDB is not
|
|
53
|
+
reachable. The old copy is set aside, never deleted (<code>data.json</code> becomes
|
|
54
|
+
<code>data.json.bak</code>; a MongoDB collection is renamed). System collections (roles, user
|
|
55
|
+
profiles, projects, notifications, API tokens and endpoints) always stay on files.</p>
|
|
56
|
+
|
|
57
|
+
<h3>Showing entries on a page</h3>
|
|
58
|
+
<pre class="code-block"><code>[collection slug="team" display="cards" columns="3" title-field="name" /]
|
|
59
|
+
[collection slug="jobs" display="cards" searchable filterable="location,type" sortable where_status="open" /]
|
|
60
|
+
[collection slug="applications" scope="mine" display="cards" paginate transitions /]</code></pre>
|
|
61
|
+
<ul>
|
|
62
|
+
<li>Eight displays: table, cards, list, accordion, timeline, carousel, listgroup and block
|
|
63
|
+
(your own block template per entry).</li>
|
|
64
|
+
<li><code>searchable</code>, <code>sortable</code>, <code>filterable</code> or
|
|
65
|
+
<code>paginate</code> turn a display into the interactive <strong>Collection
|
|
66
|
+
Browser</strong>: search, sort, a filter rail and pages.</li>
|
|
67
|
+
<li><code>scope="mine"</code> shows signed-in visitors only the entries they created - a "my
|
|
68
|
+
applications" or "my bookings" page. It needs no public API access.</li>
|
|
69
|
+
<li><code>transitions</code> on an interactive block gives each row the workflow buttons that
|
|
70
|
+
apply to it now, and works with <code>scope="mine"</code>. See
|
|
71
|
+
<a href="#/docs/usage/actions">Actions</a>.</li>
|
|
72
|
+
</ul>
|
|
73
|
+
<p>Visitors can right-click any collection display to filter, sort, group, copy, print or export
|
|
74
|
+
it. The full attribute list is in <a href="#/docs/usage/shortcodes">Shortcodes</a>; for
|
|
75
|
+
joins, totals and per-user results, build a <a href="#/docs/usage/views">View</a>.</p>
|
|
76
|
+
|
|
77
|
+
<h3>Who can see entries</h3>
|
|
78
|
+
<ul>
|
|
79
|
+
<li>In the admin, the <code>collections</code> permission decides who can view, add, edit and
|
|
80
|
+
delete entries.</li>
|
|
81
|
+
<li>On pages, a <code>[collection]</code> shows its entries to anyone who can open the page -
|
|
82
|
+
gate the page, or use <code>scope="mine"</code> or a View with a row-level rule.</li>
|
|
83
|
+
<li>Through the API, the API & Export tab decides: Public, Token (an API token), or signed-in
|
|
84
|
+
users with a role and above. A role the site does not have admits only the level-0
|
|
85
|
+
role.</li>
|
|
86
|
+
</ul>
|
|
87
|
+
|
|
88
|
+
<h3>Forms</h3>
|
|
89
|
+
<p>Forms are at <strong>Data > Forms</strong>. A form stores what visitors send in a
|
|
90
|
+
collection, and can then email someone, call a webhook, run an Action and show a message or
|
|
91
|
+
redirect. Fields can show, hide or require other fields, and triggers can react to answers
|
|
92
|
+
(a banner, blocking the submit, jumping a step). Submissions are listed under the form.
|
|
93
|
+
Visitors' submissions are kept out of the site's Git repository. See
|
|
94
|
+
<a href="#/tutorials/forms">Form Follow-Up</a> and the <a href="#/docs/api/forms">Forms
|
|
95
|
+
API</a>.</p>
|
|
96
|
+
|
|
97
|
+
<h3>See also</h3>
|
|
98
|
+
<ul>
|
|
99
|
+
<li><a href="#/docs/api/collections">Collections API</a>,
|
|
100
|
+
<a href="#/docs/api/external">External API & tokens</a> and
|
|
101
|
+
<a href="#/docs/api/builder">API Builder</a></li>
|
|
102
|
+
<li><a href="#/tutorials/crud">Building a CRUD App</a> - collections, forms, actions and pages
|
|
103
|
+
together</li>
|
|
104
|
+
</ul>
|
|
105
|
+
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
<p>The <code>[cta]</code> shortcode places an action-trigger button in any public page. Clicking it
|
|
11
11
|
calls
|
|
12
12
|
<code>POST /api/actions/:slug/public</code> with the entry ID, using the logged-in user's JWT.
|
|
13
|
-
If the user is not logged in, a warning toast is shown instead
|
|
13
|
+
If the user is not logged in, a warning toast is shown instead. The action decides who may run
|
|
14
|
+
it (its roles and row-level rule, checked on the server), and Actions need a MongoDB
|
|
15
|
+
connection - see <a href="#/docs/usage/actions">Actions</a>.</p>
|
|
14
16
|
|
|
15
17
|
<h3>Syntax & Attributes</h3>
|
|
16
18
|
<p><strong>Wrapping form:</strong></p>
|
|
@@ -116,8 +118,17 @@
|
|
|
116
118
|
</tr>
|
|
117
119
|
</tbody>
|
|
118
120
|
</table>
|
|
119
|
-
<p>
|
|
120
|
-
<strong>
|
|
121
|
+
<p>CTA buttons appear on the <strong>cards</strong> (card footer), <strong>list</strong> (inline),
|
|
122
|
+
<strong>table</strong> (a column of its own) and <strong>block</strong> displays, and on each
|
|
123
|
+
row of an interactive Collection Browser. On a <code>scope="mine"</code> block the server
|
|
124
|
+
refuses a row the visitor did not create.</p>
|
|
125
|
+
<p>A saved View takes the same buttons with <code>action="slug"</code> and the same
|
|
126
|
+
<code>cta-label</code>, <code>cta-style</code>, <code>cta-icon</code> and
|
|
127
|
+
<code>cta-confirm</code> attributes:</p>
|
|
128
|
+
<pre class="code-block"><code>[view slug="pending-applications" display="cards" action="approve-application" cta-label="Approve" /]</code></pre>
|
|
129
|
+
<p>For buttons that change with each row's state (Submit, Withdraw, Approve), use
|
|
130
|
+
<code>transitions</code> on an interactive <code>[collection]</code> instead - see
|
|
131
|
+
<a href="#/docs/usage/actions">Actions</a>.</p>
|
|
121
132
|
|
|
122
133
|
</div>
|
|
123
134
|
</div>
|
|
@@ -85,7 +85,6 @@
|
|
|
85
85
|
<pre class="code-block"><code>[card id="my-panel" title="Hidden Details"]
|
|
86
86
|
Content goes here.
|
|
87
87
|
[/card]
|
|
88
|
-
|
|
89
88
|
[dconfig]
|
|
90
89
|
{
|
|
91
90
|
"#my-btn": {
|
|
@@ -107,9 +106,7 @@ Content goes here.
|
|
|
107
106
|
}
|
|
108
107
|
}
|
|
109
108
|
[/dconfig]
|
|
110
|
-
|
|
111
109
|
<button id="my-btn" class="btn btn-primary">Toggle</button>
|
|
112
|
-
|
|
113
110
|
<div id="my-panel" class="card hidden">
|
|
114
111
|
<div class="card-body">Hidden content.</div>
|
|
115
112
|
</div></code></pre>
|