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
|
@@ -1,259 +1,78 @@
|
|
|
1
1
|
<div class="view-header">
|
|
2
2
|
<h1><span data-icon="zap"></span> Pro Documentation</h1>
|
|
3
|
+
<a href="#/documentation" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> All usage topics</a>
|
|
3
4
|
</div>
|
|
4
5
|
|
|
5
6
|
<div class="tabs" id="pro-docs-tabs">
|
|
6
7
|
<div class="tab-list">
|
|
7
|
-
<button class="tab-item active">
|
|
8
|
+
<button class="tab-item active">What needs Pro</button>
|
|
9
|
+
<button class="tab-item">Views</button>
|
|
8
10
|
<button class="tab-item">Actions</button>
|
|
9
|
-
<button class="tab-item">CTA Shortcode</button>
|
|
10
11
|
</div>
|
|
12
|
+
|
|
11
13
|
<div class="tab-content">
|
|
12
14
|
|
|
13
|
-
<!--
|
|
15
|
+
<!-- What needs Pro -->
|
|
14
16
|
<div class="tab-panel active">
|
|
15
|
-
|
|
16
|
-
<div class="card card-collapsible mb-4">
|
|
17
|
-
<div class="card-header" role="button" tabindex="0">
|
|
18
|
-
<div class="card-header-content"><h2><span data-icon="eye"></span> What is a View?</h2></div>
|
|
19
|
-
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
20
|
-
</div>
|
|
21
|
-
<div class="card-body docs-body">
|
|
22
|
-
<p>A <strong>View</strong> is a saved MongoDB aggregation pipeline that runs against a collection and returns shaped results. Views are created under <strong>Data → Views</strong> and exposed via a REST endpoint that respects role-level and row-level access.</p>
|
|
23
|
-
<p>Views require a MongoDB connection configured in <code>config/connections.json</code>.</p>
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|
|
26
|
-
|
|
27
|
-
<div class="card card-collapsible mb-4">
|
|
28
|
-
<div class="card-header" role="button" tabindex="0">
|
|
29
|
-
<div class="card-header-content"><h2><span data-icon="layers"></span> Allowed Pipeline Stages</h2></div>
|
|
30
|
-
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
31
|
-
</div>
|
|
17
|
+
<div class="card mb-4">
|
|
32
18
|
<div class="card-body docs-body">
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<p>Collections are prefixed <code>cms_</code> in MongoDB - use this prefix in <code>$lookup</code> <code>from</code> values.</p>
|
|
19
|
+
<p>Pro, for the data features, means one thing: a working <strong>MongoDB connection</strong>
|
|
20
|
+
(the <strong>Database connections</strong> tab on Data > Collections, stored in <code>config/connections.json</code>). With one, a site gets:</p>
|
|
21
|
+
<ul>
|
|
22
|
+
<li><strong>Actions</strong> - workflow buttons, transitions and <code>[cta]</code>. Without MongoDB,
|
|
23
|
+
Data > Actions says so and nothing can be run.</li>
|
|
24
|
+
<li><strong>MongoDB storage</strong> for any collection, chosen on its Storage tab. Moving a collection
|
|
25
|
+
either way keeps every entry's id.</li>
|
|
26
|
+
<li><strong>Notes and Todo on MongoDB</strong>, as their collections declare. A site whose files
|
|
27
|
+
still hold notes or to-dos stays on files until the Storage move settles it.</li>
|
|
28
|
+
</ul>
|
|
29
|
+
<p>Everything else works without it: collections on files, <strong>Views</strong> (a built-in
|
|
30
|
+
evaluator runs the same pipeline stages), forms, <code>[collection]</code> and <code>[view]</code>
|
|
31
|
+
displays, the Collection Browser, the API and API Builder.</p>
|
|
32
|
+
<p>Pro plugins (SEO Pro, Analytics Pro, Security Pro, Blog Pro and others) are a separate matter: they come
|
|
33
|
+
from the Marketplace with a licence. See <a href="#/docs/usage/editions">Editions & Licences</a> and
|
|
34
|
+
<a href="#/docs/plugins">Plugin guides</a>.</p>
|
|
50
35
|
</div>
|
|
51
36
|
</div>
|
|
37
|
+
</div>
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
<div class="card-header" role="button" tabindex="0">
|
|
55
|
-
<div class="card-header-content"><h2><span data-icon="code"></span> Example Pipeline</h2></div>
|
|
56
|
-
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
57
|
-
</div>
|
|
58
|
-
<div class="card-body docs-body">
|
|
59
|
-
<p>Match active entries and count related submissions:</p>
|
|
60
|
-
<pre class="code-block"><code>[
|
|
61
|
-
{ "$match": { "data.isActive": true } },
|
|
62
|
-
{
|
|
63
|
-
"$lookup": {
|
|
64
|
-
"from": "cms_feedback",
|
|
65
|
-
"localField": "_id",
|
|
66
|
-
"foreignField": "data.userId",
|
|
67
|
-
"as": "submissions"
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
{ "$addFields": { "submissionCount": { "$size": "$submissions" } } },
|
|
71
|
-
{ "$project": { "data.name": 1, "data.email": 1, "submissionCount": 1 } },
|
|
72
|
-
{ "$sort": { "submissionCount": -1 } }
|
|
73
|
-
]</code></pre>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
|
|
77
|
-
<div class="card card-collapsible mb-4">
|
|
78
|
-
<div class="card-header" role="button" tabindex="0">
|
|
79
|
-
<div class="card-header-content"><h2><span data-icon="lock"></span> Access Control</h2></div>
|
|
80
|
-
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
81
|
-
</div>
|
|
82
|
-
<div class="card-body docs-body">
|
|
83
|
-
<table class="table table-sm">
|
|
84
|
-
<thead><tr><th>Setting</th><th>Description</th></tr></thead>
|
|
85
|
-
<tbody>
|
|
86
|
-
<tr><td>Allowed Roles</td><td>Which roles can call <code>GET /api/views/:slug/public</code></td></tr>
|
|
87
|
-
<tr><td>Public</td><td>No authentication required when checked</td></tr>
|
|
88
|
-
<tr><td>Row-Level - Owner</td><td>User sees only entries they created</td></tr>
|
|
89
|
-
<tr><td>Row-Level - Field Match</td><td>A specified entry field must match a user property</td></tr>
|
|
90
|
-
</tbody>
|
|
91
|
-
</table>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
|
|
95
|
-
</div><!-- /Views tab -->
|
|
96
|
-
|
|
97
|
-
<!-- ── Actions tab ───────────────────────────────────── -->
|
|
39
|
+
<!-- Views -->
|
|
98
40
|
<div class="tab-panel">
|
|
99
|
-
|
|
100
|
-
<div class="card card-collapsible mb-4">
|
|
101
|
-
<div class="card-header" role="button" tabindex="0">
|
|
102
|
-
<div class="card-header-content"><h2><span data-icon="zap"></span> What is an Action?</h2></div>
|
|
103
|
-
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
104
|
-
</div>
|
|
105
|
-
<div class="card-body docs-body">
|
|
106
|
-
<p>An <strong>Action</strong> is a named sequence of steps that runs against a single collection entry. Actions are defined under <strong>Data → Actions</strong> and can be triggered from the entry list or via the <code>[cta]</code> shortcode on any public page.</p>
|
|
107
|
-
<p>Actions require a MongoDB connection configured in <code>config/connections.json</code>.</p>
|
|
108
|
-
<p>Steps run in order. If a step fails, execution stops and a partial result is returned with a <code>stepsCompleted</code> count.</p>
|
|
109
|
-
</div>
|
|
110
|
-
</div>
|
|
111
|
-
|
|
112
|
-
<div class="card card-collapsible mb-4">
|
|
113
|
-
<div class="card-header" role="button" tabindex="0">
|
|
114
|
-
<div class="card-header-content"><h2><span data-icon="list"></span> Step Types</h2></div>
|
|
115
|
-
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
116
|
-
</div>
|
|
41
|
+
<div class="card mb-4">
|
|
117
42
|
<div class="card-body docs-body">
|
|
118
|
-
<
|
|
119
|
-
<
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
</table>
|
|
43
|
+
<p>A <strong>View</strong> is a saved pipeline over a collection (<code>$match</code>,
|
|
44
|
+
<code>$lookup</code>, <code>$sort</code>, <code>$project</code>, <code>$unwind</code>,
|
|
45
|
+
<code>$addFields</code>, <code>$group</code>, <code>$count</code>, <code>$skip</code>,
|
|
46
|
+
<code>$limit</code>). Views do <strong>not</strong> need MongoDB: a MongoDB collection runs a native
|
|
47
|
+
aggregation, any other runs the built-in evaluator.</p>
|
|
48
|
+
<p>Who may read a view is set on its Access tab: public, any listed role and everyone more senior, or -
|
|
49
|
+
with no roles - the admin tier. A row-level rule (Owner or Field match) shows each user only their
|
|
50
|
+
results.</p>
|
|
51
|
+
<p>Full guide: <a href="#/docs/usage/views">Views</a> and the <a href="#/docs/api/views">Views API</a>.</p>
|
|
128
52
|
</div>
|
|
129
53
|
</div>
|
|
54
|
+
</div>
|
|
130
55
|
|
|
131
|
-
|
|
132
|
-
<div class="card-header" role="button" tabindex="0">
|
|
133
|
-
<div class="card-header-content"><h2><span data-icon="tag"></span> Template Variables</h2></div>
|
|
134
|
-
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
135
|
-
</div>
|
|
136
|
-
<div class="card-body docs-body">
|
|
137
|
-
<p>All step field values support <code>{{...}}</code> interpolation:</p>
|
|
138
|
-
<table class="table table-sm">
|
|
139
|
-
<thead><tr><th>Variable</th><th>Value</th></tr></thead>
|
|
140
|
-
<tbody>
|
|
141
|
-
<tr><td><code>{{entry.data.fieldName}}</code></td><td>Any field from the target entry</td></tr>
|
|
142
|
-
<tr><td><code>{{entry.id}}</code></td><td>The entry's UUID</td></tr>
|
|
143
|
-
<tr><td><code>{{now}}</code></td><td>Current ISO 8601 timestamp</td></tr>
|
|
144
|
-
<tr><td><code>{{user.name}}</code></td><td>Name of the triggering user</td></tr>
|
|
145
|
-
<tr><td><code>{{user.email}}</code></td><td>Email of the triggering user</td></tr>
|
|
146
|
-
<tr><td><code>{{user.id}}</code></td><td>UUID of the triggering user</td></tr>
|
|
147
|
-
<tr><td><code>{{env.CMS_PUBLIC_*}}</code></td><td>Public environment variables</td></tr>
|
|
148
|
-
</tbody>
|
|
149
|
-
</table>
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
<div class="card card-collapsible mb-4">
|
|
154
|
-
<div class="card-header" role="button" tabindex="0">
|
|
155
|
-
<div class="card-header-content"><h2><span data-icon="code"></span> Step Examples</h2></div>
|
|
156
|
-
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
157
|
-
</div>
|
|
158
|
-
<div class="card-body docs-body">
|
|
159
|
-
<p><strong>updateField</strong> - mark an entry as approved:</p>
|
|
160
|
-
<pre class="code-block"><code>{ "field": "status", "value": "approved" }</code></pre>
|
|
161
|
-
<p><strong>webhook</strong> - notify an external service:</p>
|
|
162
|
-
<pre class="code-block"><code>{
|
|
163
|
-
"url": "https://hooks.example.com/notify",
|
|
164
|
-
"headers": { "X-Secret": "{{env.CMS_PUBLIC_HOOK_SECRET}}" }
|
|
165
|
-
}</code></pre>
|
|
166
|
-
<p><strong>email</strong> - send confirmation to applicant:</p>
|
|
167
|
-
<pre class="code-block"><code>{
|
|
168
|
-
"to": "{{entry.data.email}}",
|
|
169
|
-
"subject": "Your application was approved",
|
|
170
|
-
"body": "Hi {{entry.data.name}}, your application has been approved."
|
|
171
|
-
}</code></pre>
|
|
172
|
-
</div>
|
|
173
|
-
</div>
|
|
174
|
-
|
|
175
|
-
</div><!-- /Actions tab -->
|
|
176
|
-
|
|
177
|
-
<!-- ── CTA Shortcode tab ──────────────────────────────── -->
|
|
56
|
+
<!-- Actions -->
|
|
178
57
|
<div class="tab-panel">
|
|
179
|
-
|
|
180
|
-
<div class="card card-collapsible mb-4">
|
|
181
|
-
<div class="card-header" role="button" tabindex="0">
|
|
182
|
-
<div class="card-header-content"><h2><span data-icon="mouse-pointer"></span> What is the CTA shortcode?</h2></div>
|
|
183
|
-
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
184
|
-
</div>
|
|
185
|
-
<div class="card-body docs-body">
|
|
186
|
-
<p>The <code>[cta]</code> shortcode places an action-trigger button in any public page. Clicking it calls <code>POST /api/actions/:slug/public</code> with the entry ID, using the logged-in user's JWT.</p>
|
|
187
|
-
<p>If the user is not logged in, a warning toast is shown instead.</p>
|
|
188
|
-
</div>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
<div class="card card-collapsible mb-4">
|
|
192
|
-
<div class="card-header" role="button" tabindex="0">
|
|
193
|
-
<div class="card-header-content"><h2><span data-icon="code"></span> Syntax & Attributes</h2></div>
|
|
194
|
-
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
195
|
-
</div>
|
|
196
|
-
<div class="card-body docs-body">
|
|
197
|
-
<p><strong>Wrapping form:</strong></p>
|
|
198
|
-
<pre class="code-block"><code>[cta action="slug" entry="entry-id" icon="check" confirm="Are you sure?"]Button label[/cta]</code></pre>
|
|
199
|
-
<p><strong>Self-closing form:</strong></p>
|
|
200
|
-
<pre class="code-block"><code>[cta action="slug" entry="entry-id" label="Button label" /]</code></pre>
|
|
201
|
-
<table class="table table-sm" style="margin-top:1rem;">
|
|
202
|
-
<thead><tr><th>Attribute</th><th>Required</th><th>Default</th><th>Description</th></tr></thead>
|
|
203
|
-
<tbody>
|
|
204
|
-
<tr><td><code>action</code></td><td>Yes</td><td>-</td><td>Action slug</td></tr>
|
|
205
|
-
<tr><td><code>entry</code></td><td>Yes</td><td>-</td><td>Entry UUID to act on</td></tr>
|
|
206
|
-
<tr><td><code>label</code></td><td>No</td><td><code>"Run"</code></td><td>Button text (self-closing only)</td></tr>
|
|
207
|
-
<tr><td><code>style</code></td><td>No</td><td><code>"primary"</code></td><td>primary · secondary · ghost · danger</td></tr>
|
|
208
|
-
<tr><td><code>icon</code></td><td>No</td><td>-</td><td>Domma icon name</td></tr>
|
|
209
|
-
<tr><td><code>size</code></td><td>No</td><td>-</td><td>sm · md · lg</td></tr>
|
|
210
|
-
<tr><td><code>confirm</code></td><td>No</td><td>-</td><td>Confirmation prompt before executing</td></tr>
|
|
211
|
-
</tbody>
|
|
212
|
-
</table>
|
|
213
|
-
</div>
|
|
214
|
-
</div>
|
|
215
|
-
|
|
216
|
-
<div class="card card-collapsible mb-4">
|
|
217
|
-
<div class="card-header" role="button" tabindex="0">
|
|
218
|
-
<div class="card-header-content"><h2><span data-icon="grid"></span> Collection Integration</h2></div>
|
|
219
|
-
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
220
|
-
</div>
|
|
221
|
-
<div class="card-body docs-body">
|
|
222
|
-
<p>Add per-entry buttons to any <code>[collection]</code> shortcode:</p>
|
|
223
|
-
<pre class="code-block"><code>[collection slug="applications" display="cards" title-field="name"
|
|
224
|
-
cta="approve-application"
|
|
225
|
-
cta-label="Approve"
|
|
226
|
-
cta-icon="check"
|
|
227
|
-
cta-style="primary"
|
|
228
|
-
cta-confirm="Approve this application?" /]</code></pre>
|
|
229
|
-
<table class="table table-sm" style="margin-top:1rem;">
|
|
230
|
-
<thead><tr><th>Attribute</th><th>Default</th><th>Description</th></tr></thead>
|
|
231
|
-
<tbody>
|
|
232
|
-
<tr><td><code>cta</code></td><td>-</td><td>Action slug - enables per-entry buttons</td></tr>
|
|
233
|
-
<tr><td><code>cta-label</code></td><td><code>"Run"</code></td><td>Button label</td></tr>
|
|
234
|
-
<tr><td><code>cta-icon</code></td><td>-</td><td>Domma icon name</td></tr>
|
|
235
|
-
<tr><td><code>cta-style</code></td><td><code>"primary"</code></td><td>Button variant</td></tr>
|
|
236
|
-
<tr><td><code>cta-confirm</code></td><td>-</td><td>Confirmation prompt</td></tr>
|
|
237
|
-
</tbody>
|
|
238
|
-
</table>
|
|
239
|
-
<p>All three display modes support CTA buttons: <strong>cards</strong> (card footer), <strong>list</strong> (inline), <strong>table</strong> (dedicated column).</p>
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
242
|
-
|
|
243
|
-
<div class="card card-collapsible mb-4">
|
|
244
|
-
<div class="card-header" role="button" tabindex="0">
|
|
245
|
-
<div class="card-header-content"><h2><span data-icon="book-open"></span> Full Reference</h2></div>
|
|
246
|
-
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
247
|
-
</div>
|
|
58
|
+
<div class="card mb-4">
|
|
248
59
|
<div class="card-body docs-body">
|
|
249
|
-
<p>
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
60
|
+
<p>An <strong>Action</strong> is a named sequence of steps (update a field, delete, move to or create in
|
|
61
|
+
another collection, webhook, email, notify) run against one entry. Actions need MongoDB.</p>
|
|
62
|
+
<ul>
|
|
63
|
+
<li>Any role the action lists is enough to run it, and everyone more senior. No roles means admins
|
|
64
|
+
only; a role the site does not have admits only the level-0 role.</li>
|
|
65
|
+
<li>A transition makes the action a workflow step, offered only in the states it starts from. An
|
|
66
|
+
interactive <code>[collection ... transitions]</code> shows the buttons that apply to each row,
|
|
67
|
+
including on a <code>scope="mine"</code> block. A move that no longer applies is refused with 409.</li>
|
|
68
|
+
<li>Steps are not rolled back if a later one fails - put deletes and emails last.</li>
|
|
69
|
+
</ul>
|
|
70
|
+
<p>Full guides: <a href="#/docs/usage/actions">Actions</a>,
|
|
71
|
+
<a href="#/docs/usage/cta-shortcode">CTA Shortcode</a> and the
|
|
72
|
+
<a href="#/docs/api/actions">Actions API</a>.</p>
|
|
253
73
|
</div>
|
|
254
74
|
</div>
|
|
75
|
+
</div>
|
|
255
76
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
</div><!-- .tab-content -->
|
|
259
|
-
</div><!-- .tabs -->
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
<div class="card-body">
|
|
29
29
|
<h3 style="margin-top:0;"><span data-icon="package"></span> Writing a Plugin</h3>
|
|
30
30
|
<p class="text-muted" style="margin-bottom:0;">
|
|
31
|
-
Extend the CMS with your own server routes, admin pages and page-injection snippets
|
|
32
|
-
|
|
31
|
+
Extend the CMS with your own server routes, admin pages and page-injection snippets:
|
|
32
|
+
the manifest, the Fastify entry point, switching it on and testing it.
|
|
33
33
|
</p>
|
|
34
34
|
</div>
|
|
35
35
|
</a>
|
|
@@ -50,8 +50,9 @@
|
|
|
50
50
|
|
|
51
51
|
<p class="text-muted" style="font-size:.9rem;">
|
|
52
52
|
Looking for reusable custom elements? See <a href="#/docs/components">Documentation → Components</a>.
|
|
53
|
-
For task-by-task usage, see <a href="#/documentation">
|
|
54
|
-
<a href="#/api-reference">API Reference</a
|
|
53
|
+
For task-by-task usage, see <a href="#/documentation">Documentation</a>; for the REST surface, see
|
|
54
|
+
<a href="#/api-reference">API Reference</a>; for installed Marketplace plugins, see
|
|
55
|
+
<a href="#/docs/plugins">Plugin guides</a>.
|
|
55
56
|
</p>
|
|
56
57
|
|
|
57
58
|
</div>
|
|
@@ -11,7 +11,7 @@ import{apiRequest as $}from"../api.js";import{addBannerAction as ge}from"../lib/
|
|
|
11
11
|
<span class="acx-chip" title="Project" data-bind-hidden="!project">{{project}}</span>
|
|
12
12
|
</span>
|
|
13
13
|
<button type="button" class="acx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
|
|
14
|
-
</div>`,C=u=>String(u??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function Ce(u){const l=document.createElement("template");return l.innerHTML=u.trim(),l.content.firstElementChild}const o=be(ee);export const actionsListView={templateUrl:"/admin/js/templates/actions-list.html",async onMount(u){o.reset();const l=u.get(0).querySelector(".acx")||u.get(0),f=()=>ee.test(location.hash)&&l.isConnected,v=we(),
|
|
14
|
+
</div>`,C=u=>String(u??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function Ce(u){const l=document.createElement("template");return l.innerHTML=u.trim(),l.content.firstElementChild}const o=be(ee);export const actionsListView={templateUrl:"/admin/js/templates/actions-list.html",async onMount(u){o.reset();const l=u.get(0).querySelector(".acx")||u.get(0),f=()=>ee.test(location.hash)&&l.isConnected,v=we(),c=M.observable([]),q=M.observable(0),P=M.observable(!0),A=M.observable(""),h=M.observable(!1),U=M.observable(""),b=M.observable(""),y=M.observable([]);o.add(M.effect(()=>{y.value=Ee(c.value,{query:U.value,collection:b.value}).map(xe)}));const H=M.computed(()=>Me(c.value)),ae=M.computed(()=>{const e=y.value.length,t=c.value.length;return e===t?`${t} action${t===1?"":"s"}`:`${e} of ${t}`}),ne=M.computed(()=>c.value.length?"No actions match.":"No actions yet - New action in the banner makes one."),se=M.computed(()=>$e(q.value,c.value.length,v)),w=e=>c.peek().find(t=>t.slug===e);async function L(){try{const e=await $("/actions/_count",{method:"GET"}).catch(()=>null);if(!f())return;if(e?.available===!1){h.value=!0;return}const t=await $("/actions",{method:"GET"});if(!f())return;const a=Array.isArray(t)?t:[];q.value=a.length,c.value=v?ye(a,v):a,h.value=!1,A.value=""}catch(e){if(!f())return;Se(e)?h.value=!0:A.value=`Could not load the actions: ${e.message||e}`}finally{P.value=!1}}const D=e=>{location.hash=`#/actions/edit/${encodeURIComponent(e)}`},oe=()=>{location.hash=v?`#/actions/new?project=${encodeURIComponent(v)}`:"#/actions/new"};async function le(e){if(await E.confirm(`Delete the action "${C(e.title||e.slug)}"? Its button disappears from the "${C(e.collection)}" entries. This cannot be undone.`))try{await $(`/actions/${encodeURIComponent(e.slug)}`,{method:"DELETE"}),E.toast(`Deleted "${e.title||e.slug}".`,{type:"success"}),Z(te),await L()}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}async function ce(e){try{await navigator.clipboard.writeText(e.slug),E.toast(`Copied "${e.slug}".`,{type:"success"})}catch{E.toast("The browser would not copy it - the slug is in the row.",{type:"warning"})}}let d=null;const O=new Set;function B(e,t=null){const a=w(e);if(!a){E.toast(`There is no action "${e}" here.`,{type:"warning"});return}const R={title:t?.title??`${a.title||a.slug} copy`,slug:t?.slug??""},g={title:M.observable(R.title),slug:M.observable(R.slug)},_=()=>({title:g.title.peek(),slug:g.slug.peek()}),de=JSON.stringify(R),J=()=>JSON.stringify(_())!==de||!!t,k=M.observable(!1),p=M.computed(()=>ke(a,{title:g.title.value,slug:g.slug.value},c.value.map(n=>n.slug))),W={...g,saving:k,titleErr:M.computed(()=>p.value.errors.title||""),slugErr:M.computed(()=>p.value.errors.slug||""),slugHint:M.computed(()=>p.value.errors.slug?"":`Will be created as "${p.value.payload.slug}".`),cannot:M.computed(()=>k.value||Object.keys(p.value.errors).length>0)},s=E.slideover({title:`Duplicate ${C(a.title||a.slug)}`,size:"sm",position:"right"}),m=Ce(`
|
|
15
15
|
<div class="acx-form">
|
|
16
16
|
<p class="acx-note">The copy runs on the same collection, with the same button, steps, roles and
|
|
17
17
|
transition. Edit it afterwards to make it different.</p>
|
|
@@ -22,7 +22,7 @@ import{apiRequest as $}from"../api.js";import{addBannerAction as ge}from"../lib/
|
|
|
22
22
|
</div>
|
|
23
23
|
<div>
|
|
24
24
|
<label class="form-label" for="acx-f-slug">Slug
|
|
25
|
-
<span data-help="The action's id - in its address and in
|
|
25
|
+
<span data-help="The action's id - in its address and in API calls. Leave it empty to make one from the title."></span></label>
|
|
26
26
|
<input id="acx-f-slug" type="text" class="form-input font-mono" maxlength="100" placeholder="from the title" data-model="slug.value">
|
|
27
27
|
<small class="acx-err" data-bind-hidden="!slugErr.value" data-bind-text="slugErr.value"></small>
|
|
28
28
|
<small class="acx-hint" data-bind-hidden="!slugHint.value" data-bind-text="slugHint.value"></small>
|
|
@@ -34,4 +34,4 @@ import{apiRequest as $}from"../api.js";import{addBannerAction as ge}from"../lib/
|
|
|
34
34
|
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
|
|
35
35
|
<span data-icon="copy" data-icon-size="13"></span> Duplicate</button>
|
|
36
36
|
</div>
|
|
37
|
-
</div>`);let j=!1;async function G(){if(!W.cannot.peek()){k.value=!0;try{const n=await $("/actions",{method:"POST",body:JSON.stringify(p.peek().payload)});E.toast(`Made "${n?.title||p.peek().payload.title}".`,{type:"success"}),j=!0,s.close(),Z(te),f()&&await
|
|
37
|
+
</div>`);let j=!1;async function G(){if(!W.cannot.peek()){k.value=!0;try{const n=await $("/actions",{method:"POST",body:JSON.stringify(p.peek().payload)});E.toast(`Made "${n?.title||p.peek().payload.title}".`,{type:"success"}),j=!0,s.close(),Z(te),f()&&await L()}catch(n){E.toast(`Not duplicated: ${n.message||n}`,{type:"error"})}finally{k.value=!1}}}s.element.appendChild(m);const pe=Q(m),fe=M.applyBindings(W,m,{methods:{save:G,cancel:()=>s.close()}});m.addEventListener("keydown",n=>{(n.ctrlKey||n.metaKey)&&n.key==="Enter"&&(n.preventDefault(),G())});const V={source:a.slug,dirty:J,values:_};d=V,O.add(s),he(s,()=>{fe.dispose(),pe(),O.delete(s),d===V&&(d=null)});const me=s.close.bind(s);let N=!1;s.close=async n=>{if(!j&&n!==!0&&J()){if(N)return;N=!0;const ve=await E.confirm("Discard the changes you have not saved?");if(N=!1,!ve)return}return j=!0,me()},I.scan(m),s.open();const Y=m.querySelector("#acx-f-title");Y?.focus(),Y?.select()}function ie(e){const t=w(e);return t?[{header:t.title||t.slug},{label:"Edit",icon:"edit",action:()=>D(t.slug)},{label:"Duplicate\u2026",icon:"copy",action:()=>B(t.slug)},{type:"divider"},{label:`Open ${t.collection||"collection"} entries`,icon:"database",disabled:!t.collection,action:()=>{location.hash=`#/collections/${encodeURIComponent(t.collection)}/entries`}},{label:"Copy slug",icon:"clipboard",action:()=>ce(t)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>le(t)}]:[]}const i=l.querySelector(".acx-list");i.innerHTML=Te;const re=M.applyBindings({loading:P,loadError:A,needsMongo:h,query:U,collection:b,collections:H,rows:y,countText:ae,emptyText:ne,notice:se},l);o.add(()=>re.dispose()),o.add(Q(l,{position:"bottom"}));const x=l.querySelector(".acx-filter");o.add(M.effect(()=>{const e=H.value,t=b.peek();x.replaceChildren(new Option("All collections",""),...e.map(a=>new Option(a,a))),x.value=e.includes(t)?t:"",x.value!==t&&(b.value=x.value)})),o.add(M.effect(()=>{y.value,requestAnimationFrame(()=>{i.isConnected&&I.scan(i)})}));const ue=E.contextMenu(i,{cursor:"glyph",match:".acx-row",inherit:!1,density:"compact",items:e=>ie(e.dataset.slug)});o.add(()=>ue?.destroy?.());const F=e=>{const t=e.target.closest(".acx-row");if(!t)return;const a=e.target.closest(".acx-kebab");if(a){X(a);return}D(t.dataset.slug)},K=e=>{const t=e.target.closest(".acx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),X(t)):e.key==="Enter"&&e.target===t&&D(t.dataset.slug))};i.addEventListener("click",F),i.addEventListener("keydown",K),o.add(()=>{i.removeEventListener("click",F),i.removeEventListener("keydown",K)}),ge(u,{id:"actions-new",icon:"plus",title:"New action",onClick:oe});const z=e=>{d?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",z),o.add(()=>window.removeEventListener("beforeunload",z)),o.add(()=>{if(d?.dirty())try{S.set(T,{source:d.source,values:d.values(),at:new Date().toISOString()})}catch{}[...O].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(l),await L(),!f())return;let r=null;try{r=S.get(T)}catch{}if(r?.values&&r.source){try{S.remove?S.remove(T):S.set(T,null)}catch{}w(r.source)&&await E.confirm(`You left a copy of "${C(w(r.source).title||r.source)}" unfinished. Pick up where you left off?`)&&f()&&B(r.source,r.values)}}};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{apiRequest as v,getToken as
|
|
1
|
+
import{apiRequest as v,getToken as K,refreshAccessToken as Z}from"../api.js";import{addBannerAction as Q}from"../lib/plugin-chrome.js";import{helpPopovers as V}from"../lib/help-popover.js";import{onceClosed as X,openMenuAt as ee,settingsStore as te,sidebarCount as ae,viewLifetime as se}from"../lib/tool-kit.js";import{DEFAULT_SETTINGS as ne,journeyRows as oe,kpis as le,LIMITS as ie,normaliseSettings as O,RANGE_LABEL as D,RANGES as re,seriesRange as ce,topRows as de}from"../lib/analytics-shape.js";import{fmtDate as pe}from"../lib/dates.js";const ue="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js",me=3e4,B=te("analytics.settings",O),ve=`
|
|
2
2
|
<div class="an-row" data-url="{{url}}">
|
|
3
3
|
<span class="an-bar-fill" style="width: {{bar}}"></span>
|
|
4
4
|
<span class="an-rank">{{rank}}</span>
|
|
@@ -18,12 +18,12 @@ import{apiRequest as v,getToken as Z,refreshAccessToken as Q}from"../api.js";imp
|
|
|
18
18
|
<span class="an-num" title="Views per hour today, against the usual {{baseline}}">{{hits}}/h</span>
|
|
19
19
|
<span class="an-share">\xD7{{ratio}}</span>
|
|
20
20
|
<button type="button" class="an-kebab" aria-label="Page actions" title="Actions">\u22EE</button>
|
|
21
|
-
</div>`;function he(){return window.Chart?Promise.resolve():new Promise((r,s)=>{const t=document.createElement("script");t.src=ue,t.async=!0,t.onload=()=>r(),t.onerror=()=>s(new Error("chart.js load failed")),document.head.appendChild(t)})}function fe(r){const s=document.createElement("template");return s.innerHTML=r.trim(),s.content.firstElementChild}async function ye(r){const s=b=>fetch("/api/analytics/export.csv?range="+encodeURIComponent(r),{headers:b?{Authorization:"Bearer "+b}:{}});let t=await s(
|
|
21
|
+
</div>`;function he(){return window.Chart?Promise.resolve():new Promise((r,s)=>{const t=document.createElement("script");t.src=ue,t.async=!0,t.onload=()=>r(),t.onerror=()=>s(new Error("chart.js load failed")),document.head.appendChild(t)})}function fe(r){const s=document.createElement("template");return s.innerHTML=r.trim(),s.content.firstElementChild}async function ye(r){const s=b=>fetch("/api/analytics/export.csv?range="+encodeURIComponent(r),{headers:b?{Authorization:"Bearer "+b}:{}});let t=await s(K());if(t.status===401&&(t=await s(await Z())),!t.ok)throw new Error("export failed");const c=await t.blob(),d=(t.headers.get("Content-Disposition")||"").match(/filename="?([^"]+)"?/),u=URL.createObjectURL(c),i=document.createElement("a");i.href=u,i.download=d?d[1]:"analytics.csv",document.body.appendChild(i),i.click(),i.remove(),URL.revokeObjectURL(u)}const l=se(/^#\/analytics\b/);export const analyticsView={templateUrl:"/admin/js/templates/analytics.html",async onMount(r){l.reset();const s=r.get(0),t=M.observable(B.read()),c=M.observable(t.peek().range),d=M.observable("pages"),u=M.observable(""),i=M.observable(null),b=M.observable([]),y=M.observable(null),S=M.observable([]),g=M.observable(null),x=M.observable([]),j=M.observable("");l.add(M.effect(()=>{const e=i.value,{rows:a,matched:n}=de(e?.top,{filter:u.value,limit:t.value.limit,total:e?.total});x.value=a,j.value=e?e.top.length?n?n>a.length?`Showing ${a.length} of ${n} pages - the cog changes how many.`:"":"No pages match that filter.":"No page views recorded in this range yet.":"Loading\u2026"}));const P=M.observable([]),U=M.observable([]),$=M.observable([]);l.add(M.effect(()=>{const e=oe(y.value);P.value=e.entry,U.value=e.exit,$.value=e.paths}));const q={range:c,section:d,filter:u,spikes:S,pageRows:x,pagesNote:j,entryRows:P,exitRows:U,pathRows:$,kpi:M.computed(()=>le(i.value,y.value)),isAllTime:M.computed(()=>c.value==="all"),sessionsCount:M.computed(()=>y.value?.totalSessions??0),liveText:M.computed(()=>g.value===null?"Active now: -":`${g.value} active now`),liveClass:M.computed(()=>g.value?"is-on":""),liveTitle:M.computed(()=>t.value.live?"Refreshes every 30 seconds":"Live refresh is off - see settings")};l.add(M.effect(()=>B.write(t.value)));const G=(e,a)=>{t.value=O({...t.peek(),[e]:a})};let k=0;async function w(){const e=c.peek(),a=++k;try{const[n,p,m,f]=await Promise.all([v("/analytics/stats?range="+encodeURIComponent(e)),v("/analytics/series?range="+encodeURIComponent(ce(e))),v("/analytics/journeys?range="+encodeURIComponent(e)),v("/analytics/spikes")]);if(a!==k)return;i.value=n,b.value=p?.series??[],y.value=m,S.value=(f?.items??[]).map(o=>({...o,key:o.url}))}catch{a===k&&E.toast("Could not load analytics.",{type:"error"})}}async function C(){try{g.value=(await v("/analytics/realtime"))?.activeSessions??0,ae("#/analytics")}catch{}}let _=!0;l.add(M.effect(()=>{if(c.value,_){_=!1;return}w()}));let L=null;l.add(M.effect(()=>{clearInterval(L),L=t.value.live?setInterval(()=>{document.hidden||C()},me):null})),l.add(()=>clearInterval(L));const H=s.querySelector(".an-chart");let R=null;l.add(()=>{R?.destroy(),R=null}),l.add(M.effect(()=>{const e=b.value,a=t.value.chart;if(!window.Chart||!H.isConnected)return;const n=getComputedStyle(document.documentElement),p=n.getPropertyValue("--dm-primary").trim()||"rgb(99, 102, 241)",m=n.getPropertyValue("--dm-text-muted").trim()||"#888",f=`color-mix(in srgb, ${n.getPropertyValue("--dm-border").trim()||"#888"} 45%, transparent)`;R?.destroy(),R=new window.Chart(H,{type:a,data:{labels:e.map(o=>pe(o.date).slice(0,5)),datasets:[{label:"Page views",data:e.map(o=>o.hits),borderColor:p,backgroundColor:a==="bar"?p:`color-mix(in srgb, ${p} 18%, transparent)`,fill:a==="line",tension:.3,borderWidth:2,pointRadius:e.length>40?0:2,borderRadius:3}]},options:{responsive:!0,maintainAspectRatio:!1,animation:!1,plugins:{legend:{display:!1}},scales:{y:{beginAtZero:!0,ticks:{precision:0,color:m},grid:{color:f}},x:{ticks:{maxRotation:0,autoSkip:!0,maxTicksLimit:10,color:m},grid:{display:!1}}}}})}));function W(){const e=E.slideover({title:"Analytics settings",size:"sm",position:"right"}),a=fe(`
|
|
22
22
|
<div class="an-panel-form">
|
|
23
23
|
<div>
|
|
24
24
|
<label class="form-label" for="an-set-range">Open on</label>
|
|
25
25
|
<select id="an-set-range" class="form-input" data-model="range.value">
|
|
26
|
-
${
|
|
26
|
+
${re.map(o=>`<option value="${o}">${D[o]}</option>`).join("")}
|
|
27
27
|
</select>
|
|
28
28
|
</div>
|
|
29
29
|
<div>
|
|
@@ -31,7 +31,7 @@ import{apiRequest as v,getToken as Z,refreshAccessToken as Q}from"../api.js";imp
|
|
|
31
31
|
<span data-help="How many pages the Top pages list shows. The filter still searches all of them, and the CSV export always has every page."></span>
|
|
32
32
|
</label>
|
|
33
33
|
<select id="an-set-limit" class="form-input" data-model="limit.value">
|
|
34
|
-
${
|
|
34
|
+
${ie.map(o=>`<option value="${o}">${o||"All"}</option>`).join("")}
|
|
35
35
|
</select>
|
|
36
36
|
</div>
|
|
37
37
|
<div>
|
|
@@ -59,4 +59,4 @@ import{apiRequest as v,getToken as Z,refreshAccessToken as Q}from"../api.js";imp
|
|
|
59
59
|
<span class="spacer"></span>
|
|
60
60
|
<button type="button" class="btn btn-sm btn-primary" data-on-click="done">Done</button>
|
|
61
61
|
</div>
|
|
62
|
-
</div>`),n=(o,
|
|
62
|
+
</div>`),n=(o,F=T=>T)=>M.computed({read:()=>t.value[o],write:T=>G(o,F(T))}),p={range:n("range"),limit:n("limit",Number),chart:n("chart"),live:n("live"),confirmReset:n("confirmReset")};e.element.appendChild(a);const m=V(a),f=M.applyBindings(p,a,{methods:{done:()=>e.close(),reset:()=>{t.value={...ne}}}});X(e,()=>{f.dispose(),m()}),e.open()}Q(r,{id:"analytics-settings",icon:"settings",title:"Analytics settings",onClick:W});const h=[...s.querySelectorAll(".an-list")];h[0].innerHTML=ve,h[1].innerHTML=A,h[2].innerHTML=A,h[3].innerHTML=A,h[4].innerHTML=be,l.add(V(s,{position:"bottom"}));const J=M.applyBindings(q,s,{methods:{refresh:()=>{w(),C()},showPages:()=>{d.value="pages"},showJourneys:()=>{d.value="journeys"},showSpikes:()=>{d.value="spikes"}}});l.add(()=>J.dispose());const N=e=>new URL(e,location.origin).href,z=E.contextMenu(s,{cursor:"glyph",match:".an-row",inherit:!1,density:"compact",items:e=>{const a=e.dataset.url;return[{label:"Open on the site",icon:"external-link",action:()=>window.open(N(a),"_blank","noopener")},{label:"Copy address",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(N(a)),E.toast("Address copied",{type:"success"})}catch{E.toast("The browser would not allow copying",{type:"error"})}}},{label:"Show only this page",icon:"filter",action:()=>{d.value="pages",u.value=a}}]}});l.add(()=>z?.destroy?.());const Y=E.contextMenu(s.querySelector(".an-bar-end"),{cursor:"glyph",match:".an-more",inherit:!1,density:"compact",items:()=>[{label:`Export ${D[c.peek()].toLowerCase()} as CSV`,icon:"download",action:async()=>{try{await ye(c.peek())}catch{E.toast("Export failed.",{type:"error"})}}},{type:"divider"},{label:"Reset all statistics\u2026",icon:"trash",danger:!0,action:async()=>{if(!(t.peek().confirmReset&&!await E.confirm("Delete every analytics counter for this site? This cannot be undone.")))try{await v("/analytics/stats",{method:"DELETE"}),E.toast("Analytics reset.",{type:"success"}),w()}catch{E.toast("Reset failed.",{type:"error"})}}}]});l.add(()=>Y?.destroy?.()),s.addEventListener("click",e=>{const a=e.target.closest(".an-kebab, .an-more");a&&ee(a)}),I.scan(s);try{await he()}catch{E.toast("The chart library could not be loaded; figures still work.",{type:"warning"})}await Promise.all([w(),C()])}};
|