domma-cms 0.8.7 → 0.9.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/README.md +31 -9
- package/admin/js/templates/action-editor.html +5 -0
- package/admin/js/templates/block-editor.html +5 -0
- package/admin/js/templates/collection-editor.html +7 -0
- package/admin/js/templates/effects.html +147 -72
- package/admin/js/templates/form-editor.html +7 -0
- package/admin/js/templates/page-editor.html +5 -0
- package/admin/js/templates/view-editor.html +5 -0
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/block-editor.js +4 -4
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/effects.js +1 -1
- package/admin/js/views/form-editor.js +1 -1
- package/admin/js/views/navigation.js +13 -12
- package/admin/js/views/page-editor.js +11 -11
- package/admin/js/views/pages.js +2 -2
- package/admin/js/views/view-editor.js +1 -1
- package/package.json +1 -1
- package/plugins/contacts/collections/user-contact-groups/schema.json +35 -0
- package/plugins/contacts/collections/user-contacts/schema.json +71 -0
- package/plugins/contacts/plugin.js +1 -55
- package/plugins/garage/collections/garage-vehicles/schema.json +101 -0
- package/plugins/garage/plugin.js +0 -40
- package/plugins/notes/collections/user-notes/schema.json +53 -0
- package/plugins/notes/plugin.js +1 -47
- package/plugins/todo/collections/todos/schema.json +59 -0
- package/plugins/todo/plugin.js +1 -48
- package/server/routes/api/blocks.js +19 -43
- package/server/routes/api/forms.js +8 -1
- package/server/services/blocks.js +124 -5
- package/server/services/collections.js +17 -3
- package/server/services/forms.js +78 -0
- package/server/services/plugins.js +197 -2
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@ by [Fastify](https://fastify.dev) on the backend and [Domma](https://npmjs.com/p
|
|
|
17
17
|
- [Configuration](#configuration)
|
|
18
18
|
- [Content](#content)
|
|
19
19
|
- [Admin Panel](#admin-panel)
|
|
20
|
+
- [Built-in Features](#built-in-features)
|
|
20
21
|
- [Plugins](#plugins)
|
|
21
22
|
- [Bundled Plugins](#bundled-plugins)
|
|
22
23
|
- [Building a Plugin](#building-a-plugin)
|
|
@@ -125,9 +126,10 @@ Controls the public-facing site identity.
|
|
|
125
126
|
}
|
|
126
127
|
```
|
|
127
128
|
|
|
128
|
-
**Available themes:** `charcoal-dark`, `charcoal-light`, `
|
|
129
|
-
`
|
|
130
|
-
`
|
|
129
|
+
**Available themes:** `charcoal-dark`, `charcoal-light`, `christmas-dark`, `christmas-light`, `dreamy-dark`,
|
|
130
|
+
`dreamy-light`, `forest-dark`, `forest-light`, `grayve-dark`, `grayve-light`, `lemon-dark`, `lemon-light`, `mint-dark`,
|
|
131
|
+
`mint-light`, `ocean-dark`, `ocean-light`, `royal-dark`, `royal-light`, `silver-dark`, `silver-light`, `sunset-dark`,
|
|
132
|
+
`sunset-light`, `unicorn-dark`, `unicorn-light`, `wedding-dark`, `wedding-light`
|
|
131
133
|
|
|
132
134
|
### `config/navigation.json`
|
|
133
135
|
|
|
@@ -270,18 +272,38 @@ The sidebar groups content by role:
|
|
|
270
272
|
|
|
271
273
|
---
|
|
272
274
|
|
|
275
|
+
## Built-in Features
|
|
276
|
+
|
|
277
|
+
These features are part of the CMS core — no plugins required. They are configured via the admin panel under
|
|
278
|
+
**Site Settings** or through `config/site.json`.
|
|
279
|
+
|
|
280
|
+
| Feature | Description |
|
|
281
|
+
|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
282
|
+
| **Effects** | Scroll-reveal animations on page elements (`[data-fx]`) and row shortcodes (`[data-reveal]`) via IntersectionObserver |
|
|
283
|
+
| **Celebrations** | Seasonal particle effects that activate automatically by date — Christmas, Halloween, Valentine's, Guy Fawkes, St Patrick's, St Andrew's, St David's, St George's |
|
|
284
|
+
| **Back to Top** | Configurable scroll-to-top button with position, offset, and label options |
|
|
285
|
+
| **Cookie Consent** | GDPR cookie consent banner with per-category toggles (necessary, functional, analytics, marketing) |
|
|
286
|
+
| **Auto Day/Night** | Automatic theme switching between light and dark variants based on time of day |
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
273
290
|
## Plugins
|
|
274
291
|
|
|
275
292
|
Plugins extend Domma CMS with backend routes, public page injections, and admin panel views.
|
|
276
293
|
|
|
277
294
|
### Bundled Plugins
|
|
278
295
|
|
|
279
|
-
| Plugin | Description
|
|
280
|
-
|
|
281
|
-
| **
|
|
282
|
-
| **
|
|
283
|
-
| **
|
|
284
|
-
| **
|
|
296
|
+
| Plugin | Description |
|
|
297
|
+
|--------------------|----------------------------------------------------------------------------------|
|
|
298
|
+
| **Analytics** | Basic page view analytics — tracks hits per page using a simple JSON store |
|
|
299
|
+
| **Contacts** | Contact manager with groups, favourites, search, and import/export |
|
|
300
|
+
| **Demo Viewer** | Embeds interactive Domma JS demos via iframe shortcode |
|
|
301
|
+
| **Docs** | Document editor with folders, version history, templates, and find & replace |
|
|
302
|
+
| **Garage** | UK vehicle management with DVLA API lookup, save vehicles, and search history |
|
|
303
|
+
| **Notes** | Rich note-taking with categories, search, and markdown content |
|
|
304
|
+
| **Site Search** | Full-text search for the public site — search icon in navbar, Cmd+K shortcut |
|
|
305
|
+
| **Theme Switcher** | Floating disc icon with colour theme dots for switching between all Domma themes |
|
|
306
|
+
| **Todo** | Personal task manager with priorities and status tracking |
|
|
285
307
|
|
|
286
308
|
Enable or disable any plugin in the admin panel under **Plugins**, or directly in `config/plugins.json`.
|
|
287
309
|
|
|
@@ -43,6 +43,11 @@
|
|
|
43
43
|
</select>
|
|
44
44
|
<small class="text-muted">The collection this action operates on.</small>
|
|
45
45
|
</div>
|
|
46
|
+
<div>
|
|
47
|
+
<label class="form-check-label" title="Included in fresh installs via the seed script">
|
|
48
|
+
<input id="action-bundled" type="checkbox" class="form-check"> Bundled
|
|
49
|
+
</label>
|
|
50
|
+
</div>
|
|
46
51
|
</div>
|
|
47
52
|
</div>
|
|
48
53
|
</div>
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
<label class="form-label">Name <span style="color:var(--dm-danger,#f87171);">*</span></label>
|
|
21
21
|
<input id="block-name" type="text" class="form-input" placeholder="e.g. feedback-card">
|
|
22
22
|
<small class="text-muted">Lowercase letters, digits, and hyphens only. Cannot be changed after creation.</small>
|
|
23
|
+
<div class="mt-3">
|
|
24
|
+
<label class="form-check-label" title="Included in fresh installs via the seed script">
|
|
25
|
+
<input id="block-bundled" type="checkbox" class="form-check"> Bundled
|
|
26
|
+
</label>
|
|
27
|
+
</div>
|
|
23
28
|
</div>
|
|
24
29
|
</div>
|
|
25
30
|
|
|
@@ -53,6 +53,13 @@
|
|
|
53
53
|
<input id="collection-columns" type="number" class="form-input" min="1" max="6" value="2">
|
|
54
54
|
</div>
|
|
55
55
|
</div>
|
|
56
|
+
<div class="row mt-3">
|
|
57
|
+
<div class="col-auto">
|
|
58
|
+
<label class="form-check-label" title="Included in fresh installs via the seed script">
|
|
59
|
+
<input id="collection-bundled" type="checkbox" class="form-check"> Bundled
|
|
60
|
+
</label>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
56
63
|
</div>
|
|
57
64
|
</div>
|
|
58
65
|
</div>
|
|
@@ -7,64 +7,71 @@
|
|
|
7
7
|
</div>
|
|
8
8
|
</div>
|
|
9
9
|
|
|
10
|
-
<!--
|
|
11
|
-
<div
|
|
12
|
-
<div class="
|
|
13
|
-
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
10
|
+
<!-- Top-level tabs -->
|
|
11
|
+
<div id="effects-tabs" class="tabs">
|
|
12
|
+
<div class="tab-list">
|
|
13
|
+
<button class="tab-item active">Settings</button>
|
|
14
|
+
<button class="tab-item">Shortcode Reference</button>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="tab-content">
|
|
17
|
+
|
|
18
|
+
<!-- Settings tab panel -->
|
|
19
|
+
<div class="tab-panel active">
|
|
20
|
+
<div class="card mb-4">
|
|
21
|
+
<div class="card-body">
|
|
22
|
+
<div class="row mb-3">
|
|
23
|
+
<div class="col">
|
|
24
|
+
<label class="form-check-label">
|
|
25
|
+
<input id="field-respect-motion" type="checkbox">
|
|
26
|
+
Respect <code>prefers-reduced-motion</code>
|
|
27
|
+
</label>
|
|
28
|
+
<span class="form-hint">When enabled, JS effects are skipped for users who prefer reduced motion. Content remains visible.</span>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="row mb-3">
|
|
32
|
+
<div class="col-6">
|
|
33
|
+
<label class="form-label">Default animation duration (ms)</label>
|
|
34
|
+
<input id="field-default-duration" type="number" class="form-input" min="0" max="10000"
|
|
35
|
+
placeholder="600">
|
|
36
|
+
<span class="form-hint">Used by reveal when no <code>duration</code> attribute is specified.</span>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="col-6">
|
|
39
|
+
<label class="form-label">Default reveal animation</label>
|
|
40
|
+
<select id="field-default-animation" class="form-select">
|
|
41
|
+
<option value="fade">Fade</option>
|
|
42
|
+
<option value="slide-up">Slide up</option>
|
|
43
|
+
<option value="slide-down">Slide down</option>
|
|
44
|
+
<option value="zoom">Zoom</option>
|
|
45
|
+
<option value="flip">Flip</option>
|
|
46
|
+
</select>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="row">
|
|
50
|
+
<div class="col-6">
|
|
51
|
+
<label class="form-label">Default scroll threshold</label>
|
|
52
|
+
<input id="field-default-threshold" type="number" class="form-input" min="0" max="1"
|
|
53
|
+
step="0.05"
|
|
54
|
+
placeholder="0.1">
|
|
55
|
+
<span class="form-hint">Fraction of element visible before reveal fires (0.0–1.0).</span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
47
59
|
</div>
|
|
48
60
|
</div>
|
|
49
|
-
</div>
|
|
50
|
-
</div>
|
|
51
61
|
|
|
52
|
-
<!-- Shortcode Reference
|
|
53
|
-
<div class="
|
|
54
|
-
<div class="card-header"><h2>Shortcode Reference</h2></div>
|
|
55
|
-
<div class="card-body">
|
|
62
|
+
<!-- Shortcode Reference tab panel -->
|
|
63
|
+
<div class="tab-panel">
|
|
56
64
|
<p class="text-muted mb-3">Use these shortcodes in any page's Markdown content. The <strong>Effects</strong>
|
|
57
|
-
toolbar
|
|
58
|
-
button in the editor inserts them automatically.</p>
|
|
65
|
+
toolbar button in the editor inserts them automatically.</p>
|
|
59
66
|
|
|
60
|
-
|
|
67
|
+
<!-- Sub-tabs -->
|
|
61
68
|
<div class="mb-3" style="display:flex;gap:6px;flex-wrap:wrap;">
|
|
62
|
-
<button class="btn btn-sm effects-tab-btn active" data-tab="entrance">Entrance</button>
|
|
63
|
-
<button class="btn btn-sm effects-tab-btn" data-tab="animation">Animation</button>
|
|
64
|
-
<button class="btn btn-sm effects-tab-btn" data-tab="text">Text</button>
|
|
65
|
-
<button class="btn btn-sm effects-tab-btn" data-tab="visual">Visual</button>
|
|
66
|
-
<button class="btn btn-sm effects-tab-btn" data-tab="examples">Examples</button>
|
|
67
|
-
<button class="btn btn-sm effects-tab-btn" data-tab="celebrations">Celebrations</button>
|
|
69
|
+
<button class="btn btn-sm effects-tab-btn active" data-fx-tab="entrance">Entrance</button>
|
|
70
|
+
<button class="btn btn-sm effects-tab-btn" data-fx-tab="animation">Animation</button>
|
|
71
|
+
<button class="btn btn-sm effects-tab-btn" data-fx-tab="text">Text</button>
|
|
72
|
+
<button class="btn btn-sm effects-tab-btn" data-fx-tab="visual">Visual</button>
|
|
73
|
+
<button class="btn btn-sm effects-tab-btn" data-fx-tab="examples">Examples</button>
|
|
74
|
+
<button class="btn btn-sm effects-tab-btn" data-fx-tab="celebrations">Celebrations</button>
|
|
68
75
|
</div>
|
|
69
76
|
|
|
70
77
|
<!-- Entrance tab -->
|
|
@@ -114,9 +121,80 @@ Markdown **works** inside.
|
|
|
114
121
|
</tr>
|
|
115
122
|
</tbody>
|
|
116
123
|
</table>
|
|
117
|
-
<p class="text-muted" style="font-size:.85rem;"><strong>Tip:</strong> Use <code>delay</code> to stagger
|
|
124
|
+
<p class="text-muted mb-3" style="font-size:.85rem;"><strong>Tip:</strong> Use <code>delay</code> to stagger
|
|
118
125
|
multiple
|
|
119
|
-
reveal blocks for a cascade effect.</p>
|
|
126
|
+
reveal blocks for a cascade effect, or use <code>[row reveal]</code> below for automatic staggering.</p>
|
|
127
|
+
|
|
128
|
+
<h3 class="mb-2">Row Reveal</h3>
|
|
129
|
+
<p class="text-muted mb-2">Add <code>reveal</code> to a <code>[row]</code> shortcode to automatically
|
|
130
|
+
animate
|
|
131
|
+
child columns into view one by one as the row scrolls into the viewport. No need to wrap each column in
|
|
132
|
+
a
|
|
133
|
+
separate <code>[reveal]</code> block.</p>
|
|
134
|
+
<pre class="code-block mb-2">[row gap="4" reveal reveal-mode="stagger" reveal-animation="slide-up"]
|
|
135
|
+
[col]First to appear[/col]
|
|
136
|
+
[col]Second to appear[/col]
|
|
137
|
+
[col]Third to appear[/col]
|
|
138
|
+
[/row]</pre>
|
|
139
|
+
<table class="table mb-3">
|
|
140
|
+
<thead>
|
|
141
|
+
<tr>
|
|
142
|
+
<th>Attribute</th>
|
|
143
|
+
<th>Default</th>
|
|
144
|
+
<th>Description</th>
|
|
145
|
+
</tr>
|
|
146
|
+
</thead>
|
|
147
|
+
<tbody>
|
|
148
|
+
<tr>
|
|
149
|
+
<td><code>reveal</code></td>
|
|
150
|
+
<td>—</td>
|
|
151
|
+
<td>Flag — enables scroll-triggered reveal on child columns</td>
|
|
152
|
+
</tr>
|
|
153
|
+
<tr>
|
|
154
|
+
<td><code>reveal-animation</code></td>
|
|
155
|
+
<td>slide-up</td>
|
|
156
|
+
<td>slide-up, slide-down, slide-left, slide-right, fade, zoom, flip</td>
|
|
157
|
+
</tr>
|
|
158
|
+
<tr>
|
|
159
|
+
<td><code>reveal-mode</code></td>
|
|
160
|
+
<td>stagger</td>
|
|
161
|
+
<td>stagger (overlapping) or sequence (one after another)</td>
|
|
162
|
+
</tr>
|
|
163
|
+
<tr>
|
|
164
|
+
<td><code>reveal-duration</code></td>
|
|
165
|
+
<td>400</td>
|
|
166
|
+
<td>Animation duration in milliseconds</td>
|
|
167
|
+
</tr>
|
|
168
|
+
<tr>
|
|
169
|
+
<td><code>reveal-stagger</code></td>
|
|
170
|
+
<td>60</td>
|
|
171
|
+
<td>Delay between each child column (ms)</td>
|
|
172
|
+
</tr>
|
|
173
|
+
<tr>
|
|
174
|
+
<td><code>reveal-delay</code></td>
|
|
175
|
+
<td>0</td>
|
|
176
|
+
<td>Initial delay before first animation (ms)</td>
|
|
177
|
+
</tr>
|
|
178
|
+
<tr>
|
|
179
|
+
<td><code>reveal-direction</code></td>
|
|
180
|
+
<td>ltr</td>
|
|
181
|
+
<td>ltr (left to right) or rtl (right to left)</td>
|
|
182
|
+
</tr>
|
|
183
|
+
</tbody>
|
|
184
|
+
</table>
|
|
185
|
+
|
|
186
|
+
<h4 class="mb-1" style="font-size:.9rem;">Fade with right-to-left direction</h4>
|
|
187
|
+
<pre class="code-block mb-2">[row gap="3" reveal reveal-animation="fade" reveal-direction="rtl"]
|
|
188
|
+
[col]Appears third[/col]
|
|
189
|
+
[col]Appears second[/col]
|
|
190
|
+
[col]Appears first[/col]
|
|
191
|
+
[/row]</pre>
|
|
192
|
+
|
|
193
|
+
<h4 class="mb-1" style="font-size:.9rem;">Zoom with initial delay</h4>
|
|
194
|
+
<pre class="code-block mb-2">[row gap="4" reveal reveal-animation="zoom" reveal-delay="200" reveal-stagger="100"]
|
|
195
|
+
[col]Zooms in after 200ms[/col]
|
|
196
|
+
[col]Zooms in after 300ms[/col]
|
|
197
|
+
[/row]</pre>
|
|
120
198
|
</div>
|
|
121
199
|
|
|
122
200
|
<!-- Animation tab -->
|
|
@@ -142,9 +220,7 @@ This will shake.
|
|
|
142
220
|
[/shake]</pre>
|
|
143
221
|
|
|
144
222
|
<h3 class="mb-2">CSS Animate</h3>
|
|
145
|
-
<p class="text-muted mb-2">Applies Domma CSS animation utility classes — no JavaScript required
|
|
146
|
-
without the
|
|
147
|
-
plugin enabled.</p>
|
|
223
|
+
<p class="text-muted mb-2">Applies Domma CSS animation utility classes — no JavaScript required.</p>
|
|
148
224
|
<pre class="code-block mb-2">[animate type="fade-in-up" duration="normal" delay="200" repeat="once"]
|
|
149
225
|
Content here.
|
|
150
226
|
[/animate]</pre>
|
|
@@ -358,8 +434,7 @@ Content beneath the particles.
|
|
|
358
434
|
[/twinkle]</pre>
|
|
359
435
|
|
|
360
436
|
<h3 class="mb-2">Ambient Background</h3>
|
|
361
|
-
<p class="text-muted mb-2">Applies animated CSS background classes — no JavaScript needed
|
|
362
|
-
plugin.</p>
|
|
437
|
+
<p class="text-muted mb-2">Applies animated CSS background classes — no JavaScript needed.</p>
|
|
363
438
|
<pre class="code-block mb-2">[ambient type="float-blobs" speed="slow" intensity="subtle"]
|
|
364
439
|
Content on animated background.
|
|
365
440
|
[/ambient]</pre>
|
|
@@ -420,17 +495,17 @@ Satisfaction
|
|
|
420
495
|
[/reveal]</pre>
|
|
421
496
|
|
|
422
497
|
<h3 class="mb-2">Staggered card reveal</h3>
|
|
423
|
-
<pre class="code-block mb-3">[reveal animation="
|
|
498
|
+
<pre class="code-block mb-3">[row gap="4" reveal reveal-animation="slide-up" reveal-stagger="100"]
|
|
499
|
+
[col]
|
|
424
500
|
[card title="Feature One"]First card content.[/card]
|
|
425
|
-
[/
|
|
426
|
-
|
|
427
|
-
[reveal animation="fade-in-up" delay="150"]
|
|
501
|
+
[/col]
|
|
502
|
+
[col]
|
|
428
503
|
[card title="Feature Two"]Second card content.[/card]
|
|
429
|
-
[/
|
|
430
|
-
|
|
431
|
-
[reveal animation="fade-in-up" delay="300"]
|
|
504
|
+
[/col]
|
|
505
|
+
[col]
|
|
432
506
|
[card title="Feature Three"]Third card content.[/card]
|
|
433
|
-
[/
|
|
507
|
+
[/col]
|
|
508
|
+
[/row]</pre>
|
|
434
509
|
|
|
435
510
|
<h3 class="mb-2">Hero with ambient background</h3>
|
|
436
511
|
<pre class="code-block mb-3">[ambient type="aurora" speed="slow" intensity="subtle"]
|
|
@@ -509,10 +584,9 @@ Click me for a burst!
|
|
|
509
584
|
[firework type="trail" colour="warning" /]
|
|
510
585
|
[/fireworks]</pre>
|
|
511
586
|
|
|
512
|
-
<h3 class="mb-2">Celebrate <span class="badge badge-
|
|
587
|
+
<h3 class="mb-2">Celebrate <span class="badge badge-info">JS canvas</span></h3>
|
|
513
588
|
<p class="text-muted mb-2">Canvas-based seasonal particle system. Auto-detects the active celebration based
|
|
514
|
-
on
|
|
515
|
-
today's date, or specify a theme manually. Skipped automatically when
|
|
589
|
+
on today's date, or specify a theme manually. Skipped automatically when
|
|
516
590
|
<code>prefers-reduced-motion</code> is active.</p>
|
|
517
591
|
<pre class="code-block mb-2">[celebrate theme="auto" intensity="medium" /]</pre>
|
|
518
592
|
<pre class="code-block mb-2">[celebrate theme="christmas" intensity="heavy" /]</pre>
|
|
@@ -590,5 +664,6 @@ Click me for a burst!
|
|
|
590
664
|
Canvas celebrations (<code>[celebrate]</code>) load the JS module on demand and degrade silently if
|
|
591
665
|
unavailable.</p>
|
|
592
666
|
</div>
|
|
593
|
-
|
|
594
|
-
</div
|
|
667
|
+
</div><!-- /tab-panel shortcodes -->
|
|
668
|
+
</div><!-- /tab-content -->
|
|
669
|
+
</div><!-- /effects-tabs -->
|
|
@@ -95,6 +95,13 @@
|
|
|
95
95
|
placeholder="Optional form description..."></textarea>
|
|
96
96
|
</div>
|
|
97
97
|
</div>
|
|
98
|
+
<div class="row mt-3">
|
|
99
|
+
<div class="col-auto">
|
|
100
|
+
<label class="form-check-label" title="Included in fresh installs via the seed script">
|
|
101
|
+
<input id="field-bundled" type="checkbox" class="form-check"> Bundled
|
|
102
|
+
</label>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
98
105
|
</div>
|
|
99
106
|
</div>
|
|
100
107
|
</div>
|
|
@@ -127,6 +127,11 @@
|
|
|
127
127
|
<input id="field-show-breadcrumbs" type="checkbox" class="form-check" checked> Show Breadcrumbs
|
|
128
128
|
</label>
|
|
129
129
|
</div>
|
|
130
|
+
<div class="col-auto">
|
|
131
|
+
<label class="form-check-label" title="Included in fresh installs via the seed script">
|
|
132
|
+
<input id="field-bundled" type="checkbox" class="form-check"> Bundled
|
|
133
|
+
</label>
|
|
134
|
+
</div>
|
|
130
135
|
</div>
|
|
131
136
|
</div>
|
|
132
137
|
|
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
<small class="text-muted">Changing the collection will refresh field lists in Pipeline and
|
|
45
45
|
Display tabs.</small>
|
|
46
46
|
</div>
|
|
47
|
+
<div>
|
|
48
|
+
<label class="form-check-label" title="Included in fresh installs via the seed script">
|
|
49
|
+
<input id="view-bundled" type="checkbox" class="form-check"> Bundled
|
|
50
|
+
</label>
|
|
51
|
+
</div>
|
|
47
52
|
<div>
|
|
48
53
|
<label class="form-label">Connection</label>
|
|
49
54
|
<select id="view-connection" class="form-input">
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as S}from"../api.js";let _=null;const z={deleteEntry:[],moveToCollection:[{name:"targetCollection",label:"Target Collection Slug",placeholder:"e.g. archived-applications"}],webhook:[{name:"url",label:"URL",placeholder:"https://hooks.example.com/notify"},{name:"method",label:"Method",placeholder:"POST"},{name:"body",label:"Body (JSON)",placeholder:'{"email": "{{entry.data.email}}"}',multiline:!0}],email:[{name:"to",label:"To",placeholder:"{{entry.data.email}}"},{name:"subject",label:"Subject",placeholder:"Your application update"},{name:"template",label:"Body",placeholder:"Your application has been approved.",multiline:!0}]};export const actionEditorView={templateUrl:"/admin/js/templates/action-editor.html",async onMount(e){_=null;const o=window.location.hash.match(/\/actions\/edit\/([^/?#]+)/);o&&(_=o[1]),E.tabs(e.find("#action-editor-tabs").get(0)),await D(e),await J(e),_&&(e.find("#action-editor-title").text("Edit Action"),await P(e,_)),e.find("#add-step-btn").off("click").on("click",()=>{const n=e.find("#add-step-type").val()||"updateField";B(e,{type:n,config:{}})}),e.find("#save-action-btn").off("click").on("click",async()=>{await K(e)}),U(e),Domma.icons.scan()}};async function D(e){const t=e.find("#action-collection").get(0);if(t)try{(await S.collections.list()).forEach(n=>{const i=document.createElement("option");i.value=n.slug,i.textContent=`${n.title} (${n.slug})`,t.appendChild(i)})}catch{}}async function J(e){const t=e.find("#action-roles-checkboxes").get(0);if(!t)return;["admin","manager","editor","subscriber"].forEach(n=>{const i=document.createElement("label");i.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const l=document.createElement("input");l.type="checkbox",l.value=n,l.dataset.role=n,l.className="action-role-cb",l.checked=n==="admin",i.appendChild(l),i.appendChild(document.createTextNode(n)),t.appendChild(i)})}async function P(e,t){try{const o=await S.actions.get(t);if(!o){E.toast("Action not found.",{type:"error"}),R.navigate("/actions");return}j(e,o)}catch(o){E.toast(o.message||"Failed to load action.",{type:"error"}),R.navigate("/actions")}}function U(e){const t=e.find("#action-rowlevel-enabled").get(0),o=e.find("#action-rowlevel-config").get(0),n=e.find("#action-rowlevel-mode").get(0),i=e.find("#action-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{o&&(o.style.display=t.checked?"flex":"none")}),n&&n.addEventListener("change",()=>{i&&(i.style.display=n.value==="field"?"":"none")}))}function j(e,t){e.find("#action-title").val(t.title||""),e.find("#action-slug").val(t.slug||""),e.find("#action-description").val(t.description||""),e.find("#action-collection").val(t.collection||""),e.find("#action-trigger-type").val(t.trigger?.type||"manual"),e.find("#action-trigger-label").val(t.trigger?.label||"Run"),e.find("#action-trigger-icon").val(t.trigger?.icon||"zap"),e.find("#action-trigger-confirm").val(t.trigger?.confirmMessage||"");const o=t.access?.roles||["admin"];e.find(".action-role-cb").each(function(){this.checked=o.includes(this.value)});const n=t.access?.rowLevel;n&&(e.find("#action-rowlevel-enabled").prop("checked",!0),e.find("#action-rowlevel-config").css("display","flex"),e.find("#action-rowlevel-mode").val(n.mode||"owner"),e.find("#action-rowlevel-userkey").val(n.userKey||"id"),n.mode==="field"&&(e.find("#action-rowlevel-field-group").css("display",""),e.find("#action-rowlevel-field").val(n.field||"")));const i=e.find("#action-steps-list").get(0);if(i){const l=i.querySelector(".steps-empty-placeholder");for(l&&l.remove();i.firstChild;)i.removeChild(i.firstChild);const a=document.createElement("p");a.className="text-muted steps-empty-placeholder",a.textContent="No steps yet. Add a step to define what this action does.",a.style.cssText="text-align:center;padding:2rem 0;",i.appendChild(a)}(t.steps||[]).forEach(l=>B(e,l))}function B(e,t){const o=e.find("#action-steps-list").get(0);if(!o)return;const n=o.querySelector(".steps-empty-placeholder");n&&n.remove();const i=z[t.type]||[],l=document.createElement("div");l.className="card mb-2 step-card",l.dataset.stepType=t.type;const a=document.createElement("div");a.className="card-header",a.style.cssText="display:flex;align-items:center;gap:.5rem;";const s=document.createElement("code");s.textContent=t.type,s.style.cssText="flex:1;font-size:.85rem;";const p=document.createElement("button");p.type="button",p.className="btn btn-sm btn-danger";const g=document.createElement("span");g.setAttribute("data-icon","trash-2"),p.appendChild(g),p.addEventListener("click",()=>{if(l.remove(),!o.querySelector(".step-card")){const c=document.createElement("p");c.className="text-muted steps-empty-placeholder",c.textContent="No steps yet. Add a step to define what this action does.",c.style.cssText="text-align:center;padding:2rem 0;",o.appendChild(c)}}),a.appendChild(s),a.appendChild(p);const u=document.createElement("div");if(u.className="card-body",i.length===0){const c=document.createElement("p");c.className="text-muted",c.textContent=t.type==="deleteEntry"?"This step deletes the entry. No configuration required.":"No additional configuration required.",c.style.margin="0",u.appendChild(c)}t.type==="updateField"?G(u,t,e):i.forEach(c=>{const h=document.createElement("div");h.style.cssText="margin-bottom:.75rem;";const b=document.createElement("label");b.className="form-label",b.textContent=c.label,h.appendChild(b);let m;c.multiline?(m=document.createElement("textarea"),m.rows=3,m.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",m.value=typeof t.config?.[c.name]=="object"?JSON.stringify(t.config[c.name],null,2):t.config?.[c.name]??""):(m=document.createElement("input"),m.type="text",m.value=t.config?.[c.name]??""),m.className=`form-input step-field-${c.name}`,m.placeholder=c.placeholder||"",m.dataset.field=c.name,h.appendChild(m),u.appendChild(h)}),l.appendChild(a),l.appendChild(u),o.appendChild(l),Domma.icons.scan(l)}async function G(e,t,o){const n=o.find("#action-collection").val(),i=document.createElement("div");i.style.cssText="margin-bottom:.75rem;";const l=document.createElement("label");l.className="form-label",l.textContent="Field",i.appendChild(l);const a=document.createElement("select");a.className="form-input step-field-field",a.dataset.field="field";const s=document.createElement("input");s.type="text",s.className="form-input mt-2 step-field-field-custom",s.placeholder="Field name, e.g. status",s.style.display="none";const p=document.createElement("div");p.style.cssText="margin-bottom:.75rem;";const g=document.createElement("label");g.className="form-label",g.textContent="New Value",p.appendChild(g);const u=document.createElement("div");p.appendChild(u);const c=document.createElement("button");c.type="button",c.className="btn btn-ghost btn-sm mt-2",c.style.cssText="font-size:.75rem;padding:.2rem .5rem;",c.textContent="Template Variables";const h=document.createElement("div");h.style.cssText="display:none;background:var(--dm-surface-2,#1e1e2e);border-radius:.4rem;padding:.75rem;margin-top:.5rem;font-size:.8rem;line-height:1.7;";const b=document.createElement("strong");b.textContent="Available template variables:",h.appendChild(b);const m=document.createElement("table");m.style.cssText="width:100%;border-collapse:collapse;margin-top:.4rem;",[["{{now}}","Current ISO timestamp"],["{{user.id}}","Executing user's ID"],["{{user.name}}","Executing user's name"],["{{user.email}}","Executing user's email"],["{{entry.id}}","Entry's unique ID"],["{{entry.data.fieldName}}","Any field value from the entry"],["{{env.CMS_PUBLIC_*}}","Public environment variables"]].forEach(([r,f])=>{const x=document.createElement("tr"),w=document.createElement("td");w.style.cssText="padding:.2rem .5rem .2rem 0;opacity:.7;white-space:nowrap;";const T=document.createElement("code");T.textContent=r,w.appendChild(T);const d=document.createElement("td");d.textContent=f,x.appendChild(w),x.appendChild(d),m.appendChild(x)}),h.appendChild(m),c.addEventListener("click",()=>{const r=h.style.display!=="none";h.style.display=r?"none":"",c.textContent=r?"Template Variables":"Hide Variables"}),p.appendChild(c),p.appendChild(h),e.appendChild(i),e.appendChild(p);let k=[];if(n)try{k=(await S.collections.get(n)).fields||[]}catch{}const F=document.createElement("option");F.value="",F.textContent=k.length?"\u2014 select a field \u2014":"\u2014 no fields available \u2014",a.appendChild(F),k.forEach(r=>{const f=document.createElement("option");f.value=r.name,f.textContent=`${r.label} (${r.name})`,f.dataset.fieldType=r.type,f.dataset.fieldOptions=r.type==="select"?JSON.stringify(r.options||[]):"",a.appendChild(f)});const L=document.createElement("option");L.value="__custom__",L.textContent="\u2014 enter manually \u2014",a.appendChild(L);const C=t.config?.field||"",I=C&&[...a.options].find(r=>r.value===C);I?a.value=C:C&&(a.value="__custom__",s.value=C,s.style.display=""),i.appendChild(a),i.appendChild(s);function V(r,f){u.textContent="";const x=r?[...a.options].find(d=>d.value===r):null,w=x?.dataset.fieldType==="select",T=w?JSON.parse(x.dataset.fieldOptions||"[]"):[];if(w&&T.length){const d=document.createElement("select");d.className="form-input step-field-value",d.dataset.field="value";const O=document.createElement("option");O.value="",O.textContent="\u2014 select a value \u2014",d.appendChild(O),T.forEach(v=>{const N=typeof v=="string"?v:v.value??"",M=typeof v=="string"?v:v.label||v.value||N;if(!N||N==="undefined")return;const A=document.createElement("option");A.value=N,A.textContent=M,d.appendChild(A)});const q=document.createElement("option");q.value="__custom__",q.textContent="\u2014 enter manually \u2014",d.appendChild(q);const y=document.createElement("input");y.type="text",y.className="form-input mt-2",y.placeholder="e.g. approved or {{now}}",y.style.display="none",f&&[...d.options].find(v=>v.value===f&&v.value!=="__custom__")?d.value=f:f&&(d.value="__custom__",y.value=f,y.style.display=""),d.addEventListener("change",()=>{const v=d.value==="__custom__";y.style.display=v?"":"none",v||(y.value="")}),u.appendChild(d),u.appendChild(y)}else{const d=document.createElement("input");d.type="text",d.className="form-input step-field-value",d.dataset.field="value",d.placeholder="e.g. approved or {{now}}",d.value=f||"",u.appendChild(d)}}V(I?C:null,t.config?.value||""),a.addEventListener("change",()=>{const r=a.value==="__custom__";s.style.display=r?"":"none",r||(s.value=""),V(r?null:a.value,"")})}function H(e){const t=[];return e.find(".step-card").each(function(){const o=this.dataset.stepType,n={};if(o==="updateField"){let l=this.querySelector(".step-field-field")?.value?.trim()||"";l==="__custom__"&&(l=this.querySelector(".step-field-field-custom")?.value?.trim()||""),n.field=l;const a=this.querySelector(".step-field-value");let s=a?.value?.trim()||"";s==="__custom__"&&(s=a?.nextElementSibling?.value?.trim()||""),n.value=s}else(z[o]||[]).forEach(l=>{const a=this.querySelector(`.step-field-${l.name}`);if(!a)return;const s=a.value.trim();if(l.multiline&&s)try{n[l.name]=JSON.parse(s)}catch{n[l.name]=s}else n[l.name]=s});t.push({type:o,config:n})}),t}async function K(e){const t=e.find("#action-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const o=e.find("#action-collection").val();if(!o){E.toast("Target collection is required (General tab).",{type:"warning"});return}const n=[];e.find(".action-role-cb:checked").each(function(){n.push(this.value)});const i=e.find("#action-rowlevel-enabled").is(":checked");let l=null;if(i){const p=e.find("#action-rowlevel-mode").val()||"owner",g=e.find("#action-rowlevel-userkey").val()||"id";if(l={mode:p,userKey:g},p==="field"){const u=e.find("#action-rowlevel-field").val().trim();if(!u){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}l.field=u}}const a={title:t,slug:e.find("#action-slug").val().trim()||void 0,description:e.find("#action-description").val().trim(),collection:o,trigger:{type:e.find("#action-trigger-type").val()||"manual",label:e.find("#action-trigger-label").val().trim()||"Run",icon:e.find("#action-trigger-icon").val().trim()||"zap",confirmMessage:e.find("#action-trigger-confirm").val().trim()||null},steps:H(e),access:{roles:n,rowLevel:l}},s=e.find("#save-action-btn").get(0);s&&(s.disabled=!0);try{if(_)await S.actions.update(_,a),E.toast("Action updated.",{type:"success"});else{const p=await S.actions.create(a);E.toast("Action created.",{type:"success"}),R.navigate(`/actions/edit/${p.slug}`)}}catch(p){E.toast(p.message||"Failed to save action.",{type:"error"})}finally{s&&(s.disabled=!1)}}
|
|
1
|
+
import{api as S}from"../api.js";let _=null;const z={deleteEntry:[],moveToCollection:[{name:"targetCollection",label:"Target Collection Slug",placeholder:"e.g. archived-applications"}],webhook:[{name:"url",label:"URL",placeholder:"https://hooks.example.com/notify"},{name:"method",label:"Method",placeholder:"POST"},{name:"body",label:"Body (JSON)",placeholder:'{"email": "{{entry.data.email}}"}',multiline:!0}],email:[{name:"to",label:"To",placeholder:"{{entry.data.email}}"},{name:"subject",label:"Subject",placeholder:"Your application update"},{name:"template",label:"Body",placeholder:"Your application has been approved.",multiline:!0}]};export const actionEditorView={templateUrl:"/admin/js/templates/action-editor.html",async onMount(e){_=null;const o=window.location.hash.match(/\/actions\/edit\/([^/?#]+)/);o&&(_=o[1]),E.tabs(e.find("#action-editor-tabs").get(0)),await D(e),await J(e),_&&(e.find("#action-editor-title").text("Edit Action"),await P(e,_)),e.find("#add-step-btn").off("click").on("click",()=>{const n=e.find("#add-step-type").val()||"updateField";B(e,{type:n,config:{}})}),e.find("#save-action-btn").off("click").on("click",async()=>{await K(e)}),U(e),Domma.icons.scan()}};async function D(e){const t=e.find("#action-collection").get(0);if(t)try{(await S.collections.list()).forEach(n=>{const i=document.createElement("option");i.value=n.slug,i.textContent=`${n.title} (${n.slug})`,t.appendChild(i)})}catch{}}async function J(e){const t=e.find("#action-roles-checkboxes").get(0);if(!t)return;["admin","manager","editor","subscriber"].forEach(n=>{const i=document.createElement("label");i.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const l=document.createElement("input");l.type="checkbox",l.value=n,l.dataset.role=n,l.className="action-role-cb",l.checked=n==="admin",i.appendChild(l),i.appendChild(document.createTextNode(n)),t.appendChild(i)})}async function P(e,t){try{const o=await S.actions.get(t);if(!o){E.toast("Action not found.",{type:"error"}),R.navigate("/actions");return}j(e,o)}catch(o){E.toast(o.message||"Failed to load action.",{type:"error"}),R.navigate("/actions")}}function U(e){const t=e.find("#action-rowlevel-enabled").get(0),o=e.find("#action-rowlevel-config").get(0),n=e.find("#action-rowlevel-mode").get(0),i=e.find("#action-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{o&&(o.style.display=t.checked?"flex":"none")}),n&&n.addEventListener("change",()=>{i&&(i.style.display=n.value==="field"?"":"none")}))}function j(e,t){e.find("#action-title").val(t.title||""),e.find("#action-slug").val(t.slug||""),e.find("#action-description").val(t.description||""),e.find("#action-bundled").prop("checked",!!t.bundled),e.find("#action-collection").val(t.collection||""),e.find("#action-trigger-type").val(t.trigger?.type||"manual"),e.find("#action-trigger-label").val(t.trigger?.label||"Run"),e.find("#action-trigger-icon").val(t.trigger?.icon||"zap"),e.find("#action-trigger-confirm").val(t.trigger?.confirmMessage||"");const o=t.access?.roles||["admin"];e.find(".action-role-cb").each(function(){this.checked=o.includes(this.value)});const n=t.access?.rowLevel;n&&(e.find("#action-rowlevel-enabled").prop("checked",!0),e.find("#action-rowlevel-config").css("display","flex"),e.find("#action-rowlevel-mode").val(n.mode||"owner"),e.find("#action-rowlevel-userkey").val(n.userKey||"id"),n.mode==="field"&&(e.find("#action-rowlevel-field-group").css("display",""),e.find("#action-rowlevel-field").val(n.field||"")));const i=e.find("#action-steps-list").get(0);if(i){const l=i.querySelector(".steps-empty-placeholder");for(l&&l.remove();i.firstChild;)i.removeChild(i.firstChild);const a=document.createElement("p");a.className="text-muted steps-empty-placeholder",a.textContent="No steps yet. Add a step to define what this action does.",a.style.cssText="text-align:center;padding:2rem 0;",i.appendChild(a)}(t.steps||[]).forEach(l=>B(e,l))}function B(e,t){const o=e.find("#action-steps-list").get(0);if(!o)return;const n=o.querySelector(".steps-empty-placeholder");n&&n.remove();const i=z[t.type]||[],l=document.createElement("div");l.className="card mb-2 step-card",l.dataset.stepType=t.type;const a=document.createElement("div");a.className="card-header",a.style.cssText="display:flex;align-items:center;gap:.5rem;";const s=document.createElement("code");s.textContent=t.type,s.style.cssText="flex:1;font-size:.85rem;";const p=document.createElement("button");p.type="button",p.className="btn btn-sm btn-danger";const g=document.createElement("span");g.setAttribute("data-icon","trash-2"),p.appendChild(g),p.addEventListener("click",()=>{if(l.remove(),!o.querySelector(".step-card")){const d=document.createElement("p");d.className="text-muted steps-empty-placeholder",d.textContent="No steps yet. Add a step to define what this action does.",d.style.cssText="text-align:center;padding:2rem 0;",o.appendChild(d)}}),a.appendChild(s),a.appendChild(p);const u=document.createElement("div");if(u.className="card-body",i.length===0){const d=document.createElement("p");d.className="text-muted",d.textContent=t.type==="deleteEntry"?"This step deletes the entry. No configuration required.":"No additional configuration required.",d.style.margin="0",u.appendChild(d)}t.type==="updateField"?G(u,t,e):i.forEach(d=>{const h=document.createElement("div");h.style.cssText="margin-bottom:.75rem;";const b=document.createElement("label");b.className="form-label",b.textContent=d.label,h.appendChild(b);let m;d.multiline?(m=document.createElement("textarea"),m.rows=3,m.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",m.value=typeof t.config?.[d.name]=="object"?JSON.stringify(t.config[d.name],null,2):t.config?.[d.name]??""):(m=document.createElement("input"),m.type="text",m.value=t.config?.[d.name]??""),m.className=`form-input step-field-${d.name}`,m.placeholder=d.placeholder||"",m.dataset.field=d.name,h.appendChild(m),u.appendChild(h)}),l.appendChild(a),l.appendChild(u),o.appendChild(l),Domma.icons.scan(l)}async function G(e,t,o){const n=o.find("#action-collection").val(),i=document.createElement("div");i.style.cssText="margin-bottom:.75rem;";const l=document.createElement("label");l.className="form-label",l.textContent="Field",i.appendChild(l);const a=document.createElement("select");a.className="form-input step-field-field",a.dataset.field="field";const s=document.createElement("input");s.type="text",s.className="form-input mt-2 step-field-field-custom",s.placeholder="Field name, e.g. status",s.style.display="none";const p=document.createElement("div");p.style.cssText="margin-bottom:.75rem;";const g=document.createElement("label");g.className="form-label",g.textContent="New Value",p.appendChild(g);const u=document.createElement("div");p.appendChild(u);const d=document.createElement("button");d.type="button",d.className="btn btn-ghost btn-sm mt-2",d.style.cssText="font-size:.75rem;padding:.2rem .5rem;",d.textContent="Template Variables";const h=document.createElement("div");h.style.cssText="display:none;background:var(--dm-surface-2,#1e1e2e);border-radius:.4rem;padding:.75rem;margin-top:.5rem;font-size:.8rem;line-height:1.7;";const b=document.createElement("strong");b.textContent="Available template variables:",h.appendChild(b);const m=document.createElement("table");m.style.cssText="width:100%;border-collapse:collapse;margin-top:.4rem;",[["{{now}}","Current ISO timestamp"],["{{user.id}}","Executing user's ID"],["{{user.name}}","Executing user's name"],["{{user.email}}","Executing user's email"],["{{entry.id}}","Entry's unique ID"],["{{entry.data.fieldName}}","Any field value from the entry"],["{{env.CMS_PUBLIC_*}}","Public environment variables"]].forEach(([r,f])=>{const x=document.createElement("tr"),w=document.createElement("td");w.style.cssText="padding:.2rem .5rem .2rem 0;opacity:.7;white-space:nowrap;";const T=document.createElement("code");T.textContent=r,w.appendChild(T);const c=document.createElement("td");c.textContent=f,x.appendChild(w),x.appendChild(c),m.appendChild(x)}),h.appendChild(m),d.addEventListener("click",()=>{const r=h.style.display!=="none";h.style.display=r?"none":"",d.textContent=r?"Template Variables":"Hide Variables"}),p.appendChild(d),p.appendChild(h),e.appendChild(i),e.appendChild(p);let k=[];if(n)try{k=(await S.collections.get(n)).fields||[]}catch{}const F=document.createElement("option");F.value="",F.textContent=k.length?"\u2014 select a field \u2014":"\u2014 no fields available \u2014",a.appendChild(F),k.forEach(r=>{const f=document.createElement("option");f.value=r.name,f.textContent=`${r.label} (${r.name})`,f.dataset.fieldType=r.type,f.dataset.fieldOptions=r.type==="select"?JSON.stringify(r.options||[]):"",a.appendChild(f)});const L=document.createElement("option");L.value="__custom__",L.textContent="\u2014 enter manually \u2014",a.appendChild(L);const C=t.config?.field||"",I=C&&[...a.options].find(r=>r.value===C);I?a.value=C:C&&(a.value="__custom__",s.value=C,s.style.display=""),i.appendChild(a),i.appendChild(s);function V(r,f){u.textContent="";const x=r?[...a.options].find(c=>c.value===r):null,w=x?.dataset.fieldType==="select",T=w?JSON.parse(x.dataset.fieldOptions||"[]"):[];if(w&&T.length){const c=document.createElement("select");c.className="form-input step-field-value",c.dataset.field="value";const O=document.createElement("option");O.value="",O.textContent="\u2014 select a value \u2014",c.appendChild(O),T.forEach(v=>{const N=typeof v=="string"?v:v.value??"",M=typeof v=="string"?v:v.label||v.value||N;if(!N||N==="undefined")return;const A=document.createElement("option");A.value=N,A.textContent=M,c.appendChild(A)});const q=document.createElement("option");q.value="__custom__",q.textContent="\u2014 enter manually \u2014",c.appendChild(q);const y=document.createElement("input");y.type="text",y.className="form-input mt-2",y.placeholder="e.g. approved or {{now}}",y.style.display="none",f&&[...c.options].find(v=>v.value===f&&v.value!=="__custom__")?c.value=f:f&&(c.value="__custom__",y.value=f,y.style.display=""),c.addEventListener("change",()=>{const v=c.value==="__custom__";y.style.display=v?"":"none",v||(y.value="")}),u.appendChild(c),u.appendChild(y)}else{const c=document.createElement("input");c.type="text",c.className="form-input step-field-value",c.dataset.field="value",c.placeholder="e.g. approved or {{now}}",c.value=f||"",u.appendChild(c)}}V(I?C:null,t.config?.value||""),a.addEventListener("change",()=>{const r=a.value==="__custom__";s.style.display=r?"":"none",r||(s.value=""),V(r?null:a.value,"")})}function H(e){const t=[];return e.find(".step-card").each(function(){const o=this.dataset.stepType,n={};if(o==="updateField"){let l=this.querySelector(".step-field-field")?.value?.trim()||"";l==="__custom__"&&(l=this.querySelector(".step-field-field-custom")?.value?.trim()||""),n.field=l;const a=this.querySelector(".step-field-value");let s=a?.value?.trim()||"";s==="__custom__"&&(s=a?.nextElementSibling?.value?.trim()||""),n.value=s}else(z[o]||[]).forEach(l=>{const a=this.querySelector(`.step-field-${l.name}`);if(!a)return;const s=a.value.trim();if(l.multiline&&s)try{n[l.name]=JSON.parse(s)}catch{n[l.name]=s}else n[l.name]=s});t.push({type:o,config:n})}),t}async function K(e){const t=e.find("#action-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const o=e.find("#action-collection").val();if(!o){E.toast("Target collection is required (General tab).",{type:"warning"});return}const n=[];e.find(".action-role-cb:checked").each(function(){n.push(this.value)});const i=e.find("#action-rowlevel-enabled").is(":checked");let l=null;if(i){const p=e.find("#action-rowlevel-mode").val()||"owner",g=e.find("#action-rowlevel-userkey").val()||"id";if(l={mode:p,userKey:g},p==="field"){const u=e.find("#action-rowlevel-field").val().trim();if(!u){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}l.field=u}}const a={title:t,slug:e.find("#action-slug").val().trim()||void 0,description:e.find("#action-description").val().trim(),...e.find("#action-bundled").is(":checked")?{bundled:!0}:{},collection:o,trigger:{type:e.find("#action-trigger-type").val()||"manual",label:e.find("#action-trigger-label").val().trim()||"Run",icon:e.find("#action-trigger-icon").val().trim()||"zap",confirmMessage:e.find("#action-trigger-confirm").val().trim()||null},steps:H(e),access:{roles:n,rowLevel:l}},s=e.find("#save-action-btn").get(0);s&&(s.disabled=!0);try{if(_)await S.actions.update(_,a),E.toast("Action updated.",{type:"success"});else{const p=await S.actions.create(a);E.toast("Action created.",{type:"success"}),R.navigate(`/actions/edit/${p.slug}`)}}catch(p){E.toast(p.message||"Failed to save action.",{type:"error"})}finally{s&&(s.disabled=!1)}}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as m}from"../api.js";let u=null;export const blockEditorView={templateUrl:"/admin/js/templates/block-editor.html",async onMount(e){u=null;const s=window.location.hash.match(/\/blocks\/edit\/([^/?#]+)/);s&&(u=decodeURIComponent(s[1]));const i=e.find("#block-name").get(0),n=e.find("#block-content").get(0);if(u){e.find("#block-editor-title").text("Edit Block"),i&&(i.value=u,i.disabled=!0);try{const c=await m.blocks.get(u);n&&(n.value=c.content||""),e.find("#block-bundled").prop("checked",!!c.bundled)}catch(c){E.toast(c.message||"Block not found.",{type:"error"}),R.navigate("/blocks");return}}n&&(v(n,e),w(n,e)),e.find("#save-block-btn").off("click").on("click",async()=>{await L(e)}),Domma.icons.scan()}};function v(e,t){const s=t.find("#block-line-numbers").get(0),i=t.find("#block-cursor-pos").get(0);function n(){const l=e.value.split(`
|
|
2
2
|
`).length;s.textContent=Array.from({length:l},(o,r)=>r+1).join(`
|
|
3
3
|
`),s.scrollTop=e.scrollTop}n(),e.addEventListener("input",n),e.addEventListener("scroll",()=>{s.scrollTop=e.scrollTop});function c(){if(!i)return;const o=e.value.slice(0,e.selectionStart).split(`
|
|
4
4
|
`);i.textContent=`Ln ${o.length}, Col ${o[o.length-1].length+1}`}e.addEventListener("keyup",c),e.addEventListener("click",c),e.addEventListener("keydown",l=>{if(l.key==="Tab"){l.preventDefault();const o=e.selectionStart,r=e.selectionEnd;if(!l.shiftKey)e.value=e.value.slice(0,o)+" "+e.value.slice(r),e.selectionStart=e.selectionEnd=o+2;else{const a=e.value.lastIndexOf(`
|
|
5
|
-
`,o-1)+1,
|
|
5
|
+
`,o-1)+1,p=e.value.slice(a,a+2),f=p===" "?2:p[0]===" "?1:0;f>0&&(e.value=e.value.slice(0,a)+e.value.slice(a+f),e.selectionStart=e.selectionEnd=Math.max(a,o-f))}e.dispatchEvent(new Event("input"))}if(l.key==="Enter"){l.preventDefault();const o=e.selectionStart,r=e.value.lastIndexOf(`
|
|
6
6
|
`,o-1)+1,a=e.value.slice(r,o).match(/^(\s*)/)[1];e.value=e.value.slice(0,o)+`
|
|
7
|
-
`+a+e.value.slice(e.selectionEnd),e.selectionStart=e.selectionEnd=o+1+a.length,e.dispatchEvent(new Event("input"))}});const
|
|
8
|
-
`),
|
|
7
|
+
`+a+e.value.slice(e.selectionEnd),e.selectionStart=e.selectionEnd=o+1+a.length,e.dispatchEvent(new Event("input"))}});const d=t.find("#block-editor-toolbar").get(0);d&&d.addEventListener("click",l=>{const o=l.target.closest("[data-action]");o&&(b(o.dataset.action,e),e.focus())})}function b(e,t){switch(e){case"undo":document.execCommand("undo");break;case"redo":document.execCommand("redo");break;case"cut":t.selectionStart!==t.selectionEnd&&(navigator.clipboard.writeText(t.value.slice(t.selectionStart,t.selectionEnd)).catch(()=>{}),document.execCommand("cut"));break;case"copy":t.selectionStart!==t.selectionEnd&&navigator.clipboard.writeText(t.value.slice(t.selectionStart,t.selectionEnd)).catch(()=>{});break;case"paste":navigator.clipboard.readText().then(s=>{const i=t.selectionStart;t.value=t.value.slice(0,i)+s+t.value.slice(t.selectionEnd),t.selectionStart=t.selectionEnd=i+s.length,t.dispatchEvent(new Event("input"))}).catch(()=>E.toast("Use Ctrl+V to paste.",{type:"info"}));break;case"select-all":t.select();break;case"format":g(t);break}}function g(e){const t=e.value.match(/(<[^>]+>|[^<]+)/g)||[],s=[];let i=0;const n=" ";for(const l of t){const o=l.trim();if(o)if(o.startsWith("</"))i=Math.max(0,i-1),s.push(n.repeat(i)+o);else if(o.startsWith("<")&&!o.startsWith("<!")&&!o.endsWith("/>")){s.push(n.repeat(i)+o);const r=(o.match(/^<(\w+)/)||[])[1]||"";k.has(r.toLowerCase())||i++}else s.push(n.repeat(i)+o)}const c=s.join(`
|
|
8
|
+
`),d=e.selectionStart;e.value=c,e.selectionStart=e.selectionEnd=Math.min(d,c.length),e.dispatchEvent(new Event("input"))}const k=new Set(["area","base","br","col","embed","hr","img","input","link","meta","param","source","track","wbr"]);function w(e,t){const s=t.find("#block-editor-body").get(0),i=t.find("#block-sample-data").get(0),n=t.find("#block-preview-output");t.find("[data-mode]").each(function(){this.addEventListener("click",()=>{t.find("[data-mode]").each(function(){this.classList.remove("active")}),this.classList.add("active"),s.className=`editor-body editor-mode-${this.dataset.mode}`,this.dataset.mode!=="write"&&c()})}),e.addEventListener("input",()=>{h(i,e),c()}),e.value.trim()&&h(i,e);function c(){if(!!s.classList.contains("editor-mode-write"))return;const l=e.value.replace(/\{\{([\w_]+)\}\}/g,(o,r)=>{const a=i.querySelector(`[data-placeholder="${CSS.escape(r)}"]`);return C(a?.value??`[${r}]`)});n.html(l,{safe:!1}),Domma.icons.scan(n.get(0))}i._renderPreview=c}function h(e,t){const s=y(t.value),i=new Set([...e.querySelectorAll("[data-placeholder]")].map(n=>n.dataset.placeholder));if(s.size>0&&e.querySelector("p.text-muted")?.remove(),s.forEach(n=>{i.has(n)||S(e,n)}),e.querySelectorAll("[data-placeholder]").forEach(n=>{s.has(n.dataset.placeholder)||n.closest(".block-sample-row")?.remove()}),s.size===0&&!e.querySelector("p.text-muted")){const n=document.createElement("p");n.className="text-muted",n.style.cssText="font-size:.8rem;margin:0;",n.textContent="No {{placeholders}} detected in template.",e.appendChild(n)}}function S(e,t){const s=document.createElement("div");s.className="block-sample-row",s.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.35rem;";const i=document.createElement("label");i.style.cssText="font-size:.75rem;color:var(--dm-text-muted,#888);white-space:nowrap;min-width:90px;text-align:right;flex-shrink:0;",i.textContent=`{{${t}}}`;const n=document.createElement("input");n.type="text",n.className="form-input form-input--sm",n.style.flex="1",n.dataset.placeholder=t,n.placeholder=`Sample ${t}`,n.value=x(t),n.addEventListener("input",()=>{e._renderPreview&&e._renderPreview()}),s.appendChild(i),s.appendChild(n),e.appendChild(s)}function x(e){const t=e.toLowerCase();return t==="_id"?"a1b2c3d4-e5f6-7890-abcd-ef1234567890":t==="_createdat"?new Date().toISOString():t==="_updatedat"?new Date().toISOString():t.includes("email")?"user@example.com":t.includes("phone")?"+44 7700 900000":t.includes("name")?"Jane Smith":t.includes("title")?"Sample Title":t.includes("message")||t.includes("content")||t.includes("description")?"This is a sample value for preview purposes.":t.includes("rating")?"excellent":t.includes("status")?"active":t.includes("priority")?"high":t.includes("date")?new Date().toLocaleDateString():t.includes("tag")?"tag1, tag2":t.includes("subject")?"Sample Subject":t.includes("category")?"general":`Sample ${e}`}function y(e){const t=new Set;for(const[,s]of e.matchAll(/\{\{([\w_]+)\}\}/g))t.add(s);return t}function C(e){return String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}async function L(e){const t=e.find("#block-name").get(0),s=e.find("#block-content").get(0),i=(t?.value||"").trim(),n=s?.value??"";if(!i){E.toast("Block name is required.",{type:"warning"});return}if(!/^[a-z0-9][a-z0-9-]*$/.test(i)){E.toast("Name must start with a letter or digit and contain only lowercase letters, digits, and hyphens.",{type:"warning"});return}const c=e.find("#save-block-btn").get(0);c&&(c.disabled=!0);try{const d=!!e.find("#block-bundled").is(":checked");await m.blocks.put(i,{content:n,bundled:d}),E.toast(u?"Block updated.":"Block created.",{type:"success"}),u||(u=i,R.navigate(`/blocks/edit/${encodeURIComponent(i)}`))}catch(d){E.toast(d.message||"Failed to save block.",{type:"error"})}finally{c&&(c.disabled=!1)}}
|