domma-cms 0.43.1 → 0.45.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 +53 -53
- package/README.md +27 -27
- package/admin/css/admin.css +1 -1
- package/admin/dist/domma/domma-tools.css +3 -3
- package/admin/dist/domma/domma-tools.min.js +3 -3
- package/admin/index.html +2 -2
- package/admin/js/app.js +1 -1
- package/admin/js/lib/card-builder.js +2 -2
- package/admin/js/lib/crud-tutorial.js +1 -1
- package/admin/js/lib/effect-defs.js +1 -1
- package/admin/js/lib/effects-builder.js +1 -1
- package/admin/js/lib/image-editor.js +1 -1
- package/admin/js/lib/markdown-toolbar.js +2 -2
- package/admin/js/lib/project-context.js +1 -1
- package/admin/js/lib/project-quick-create.js +1 -1
- package/admin/js/lib/scribe-composer.js +1 -1
- package/admin/js/lib/server-restart.js +1 -1
- package/admin/js/lib/shortcode-context-menu.js +2 -2
- package/admin/js/lib/simple-editor.js +1 -1
- package/admin/js/lib/themes.js +1 -1
- package/admin/js/lib/timeline-builder.js +1 -1
- package/admin/js/templates/action-editor.html +8 -8
- package/admin/js/templates/api-endpoint-editor.html +7 -7
- package/admin/js/templates/api-endpoints.html +1 -1
- package/admin/js/templates/api-reference.html +1 -1
- package/admin/js/templates/block-editor.html +6 -6
- package/admin/js/templates/collection-editor.html +1 -1
- package/admin/js/templates/component-editor.html +2 -2
- package/admin/js/templates/dashboard/cache.html +3 -3
- package/admin/js/templates/dashboard/journeys.html +3 -3
- package/admin/js/templates/dashboard/kpi-strip.html +3 -3
- package/admin/js/templates/dashboard/top-pages.html +1 -1
- package/admin/js/templates/dashboard/traffic-chart.html +1 -1
- package/admin/js/templates/docs/api-actions.html +1 -1
- package/admin/js/templates/docs/api-collections.html +4 -4
- package/admin/js/templates/docs/api-pages.html +2 -2
- package/admin/js/templates/docs/api-plugins.html +1 -1
- package/admin/js/templates/docs/api-settings.html +2 -2
- package/admin/js/templates/docs/api-users.html +1 -1
- package/admin/js/templates/docs/components-howto.html +4 -4
- package/admin/js/templates/docs/components-reference.html +21 -21
- package/admin/js/templates/docs/components-rules.html +14 -14
- package/admin/js/templates/docs/components-walkthrough.html +13 -13
- package/admin/js/templates/docs/tutorial-crud.html +49 -49
- package/admin/js/templates/docs/tutorial-forms.html +4 -4
- package/admin/js/templates/docs/tutorial-plugin.html +10 -10
- package/admin/js/templates/docs/usage-actions.html +6 -6
- package/admin/js/templates/docs/usage-cta-shortcode.html +9 -9
- package/admin/js/templates/docs/usage-dconfig.html +4 -4
- package/admin/js/templates/docs/usage-navigation.html +1 -1
- package/admin/js/templates/docs/usage-pages.html +1 -1
- package/admin/js/templates/docs/usage-shortcodes.html +24 -24
- package/admin/js/templates/docs/usage-users-roles.html +3 -3
- package/admin/js/templates/docs/usage-views.html +4 -4
- package/admin/js/templates/documentation.html +1 -1
- package/admin/js/templates/effects.html +25 -25
- package/admin/js/templates/form-editor.html +5 -5
- package/admin/js/templates/login.html +5 -5
- package/admin/js/templates/menu-editor.html +10 -10
- package/admin/js/templates/navigation.html +11 -11
- package/admin/js/templates/notifications.html +1 -1
- package/admin/js/templates/page-editor.html +4 -4
- package/admin/js/templates/pro-docs.html +14 -14
- package/admin/js/templates/role-editor.html +1 -1
- package/admin/js/templates/settings.html +10 -10
- package/admin/js/templates/tutorials.html +3 -3
- package/admin/js/templates/view-editor.html +15 -15
- package/admin/js/views/action-editor.js +1 -1
- package/admin/js/views/actions-list.js +1 -1
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/api-endpoints.js +2 -2
- package/admin/js/views/api-tokens.js +3 -3
- package/admin/js/views/blocks.js +2 -2
- package/admin/js/views/collection-editor.js +1 -1
- package/admin/js/views/collection-entries.js +1 -1
- package/admin/js/views/collections.js +1 -1
- package/admin/js/views/component-editor.js +1 -1
- package/admin/js/views/components.js +4 -4
- package/admin/js/views/dashboard/widgets/cache.js +1 -1
- package/admin/js/views/dashboard/widgets/journeys.js +1 -1
- package/admin/js/views/dashboard/widgets/kpi-strip.js +1 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +1 -1
- package/admin/js/views/forms.js +1 -1
- package/admin/js/views/layouts.js +1 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/menu-editor.js +3 -3
- package/admin/js/views/menu-locations.js +1 -1
- package/admin/js/views/menus.js +3 -3
- package/admin/js/views/navigation.js +8 -8
- package/admin/js/views/page-editor.js +40 -40
- package/admin/js/views/pages.js +3 -3
- package/admin/js/views/plugin-code.js +2 -2
- package/admin/js/views/plugins.js +1 -1
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/projects.js +1 -1
- package/admin/js/views/roles.js +1 -1
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +3 -3
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/view-preview.js +1 -1
- package/admin/js/views/views-list.js +1 -1
- package/bin/cli.js +8 -8
- package/bin/lib/config-merge.js +5 -5
- package/bin/update.js +16 -16
- package/config/site.json +86 -86
- package/package.json +2 -2
- package/plugins/_template/admin/views/index.js +1 -1
- package/plugins/_template/config.js +1 -1
- package/plugins/_template/plugin.js +1 -1
- package/plugins/analytics/admin/templates/analytics.html +3 -3
- package/plugins/analytics/admin/views/analytics.js +1 -1
- package/plugins/analytics/config.js +1 -1
- package/plugins/analytics/plugin.js +4 -4
- package/plugins/analytics/plugin.json +1 -1
- package/plugins/analytics/public/inject-body.html +2 -2
- package/plugins/analytics/public/inject-head.html +1 -1
- package/plugins/blog/admin/templates/blog.html +4 -4
- package/plugins/blog/admin/views/blog.js +3 -3
- package/plugins/blog/admin/views/categories.js +2 -2
- package/plugins/blog/admin/views/comments.js +4 -4
- package/plugins/blog/admin/views/post-editor.js +1 -1
- package/plugins/blog/plugin.js +2 -2
- package/plugins/blog/plugin.json +4 -4
- package/plugins/contacts/admin/views/contacts.js +8 -8
- package/plugins/contacts/plugin.js +4 -4
- package/plugins/contacts/plugin.json +1 -1
- package/plugins/invoice/admin/templates/editor.html +6 -6
- package/plugins/invoice/admin/templates/index.html +4 -4
- package/plugins/invoice/admin/views/editor.js +3 -3
- package/plugins/invoice/admin/views/index.js +4 -4
- package/plugins/invoice/admin/views/issuers.js +1 -1
- package/plugins/invoice/admin/views/party-view.js +3 -3
- package/plugins/invoice/admin/views/receivers.js +1 -1
- package/plugins/invoice/config.js +1 -1
- package/plugins/invoice/plugin.js +1 -1
- package/plugins/invoice/plugin.json +4 -4
- package/plugins/notes/admin/views/notes.js +4 -4
- package/plugins/notes/plugin.js +1 -1
- package/plugins/notes/plugin.json +1 -1
- package/plugins/site-search/admin/templates/site-search.html +3 -3
- package/plugins/site-search/admin/views/site-search.js +1 -1
- package/plugins/site-search/config.js +1 -1
- package/plugins/site-search/plugin.js +4 -4
- package/plugins/site-search/plugin.json +1 -1
- package/plugins/surveys/admin/templates/results.html +1 -1
- package/plugins/surveys/admin/templates/survey-editor.html +4 -4
- package/plugins/surveys/admin/views/audience.js +6 -6
- package/plugins/surveys/admin/views/results.js +3 -3
- package/plugins/surveys/admin/views/survey-editor.js +1 -1
- package/plugins/surveys/admin/views/surveys.js +2 -2
- package/plugins/surveys/plugin.js +1 -1
- package/plugins/surveys/plugin.json +4 -4
- package/plugins/theme-switcher/admin/views/theme-switcher.js +1 -1
- package/plugins/theme-switcher/plugin.json +3 -3
- package/plugins/theme-switcher/public/inject-body.html +28 -28
- package/plugins/theme-switcher/public/inject-head.html +1 -1
- package/plugins/todo/plugin.js +1 -1
- package/public/css/forms.css +1 -1
- package/public/js/collection-browser.js +1 -1
- package/public/js/collection-context.js +1 -1
- package/public/js/form-logic-engine.js +1 -1
- package/public/js/forms.js +2 -1
- package/public/js/site.js +1 -1
- package/scripts/build.js +100 -6
- package/scripts/create-plugin.js +2 -2
- package/scripts/fresh.js +2 -2
- package/scripts/gen-instance-secret.js +1 -1
- package/scripts/pro.js +12 -12
- package/scripts/reset.js +2 -2
- package/scripts/setup.js +15 -15
- package/scripts/users.js +5 -5
- package/scripts/verify-assets.mjs +6 -6
- package/server/config.js +1 -1
- package/server/middleware/auth.js +11 -11
- package/server/middleware/managerAuth.js +3 -3
- package/server/routes/api/actions.js +4 -4
- package/server/routes/api/api-endpoints.js +7 -7
- package/server/routes/api/api-tokens.js +5 -5
- package/server/routes/api/auth.js +10 -10
- package/server/routes/api/blocks.js +3 -3
- package/server/routes/api/collections.js +18 -18
- package/server/routes/api/dashboard.js +5 -5
- package/server/routes/api/effects.js +2 -2
- package/server/routes/api/endpoints-public.js +5 -5
- package/server/routes/api/forms.js +47 -47
- package/server/routes/api/menu-locations.js +3 -3
- package/server/routes/api/menus.js +7 -7
- package/server/routes/api/notifications.js +2 -2
- package/server/routes/api/pages.js +14 -2
- package/server/routes/api/plugin-marketplace.js +3 -3
- package/server/routes/api/plugins.js +6 -6
- package/server/routes/api/projects.js +9 -9
- package/server/routes/api/scaffold.js +7 -7
- package/server/routes/api/settings.js +6 -6
- package/server/routes/api/sidebar.js +1 -1
- package/server/routes/api/users.js +5 -5
- package/server/routes/api/versions.js +1 -1
- package/server/routes/api/views.js +1 -1
- package/server/routes/docs-public.js +3 -3
- package/server/routes/public.js +14 -14
- package/server/server.js +34 -16
- package/server/services/actions.js +19 -19
- package/server/services/adapters/FileAdapter.js +2 -2
- package/server/services/adapters/MongoAdapter.js +5 -5
- package/server/services/apiEndpoints.js +15 -15
- package/server/services/apiTokens.js +7 -7
- package/server/services/blocks.js +13 -13
- package/server/services/cache/drivers/MemoryDriver.js +3 -3
- package/server/services/cache/drivers/NoneDriver.js +1 -1
- package/server/services/cache/index.js +5 -5
- package/server/services/collections.js +14 -14
- package/server/services/components.js +9 -9
- package/server/services/connectionManager.js +4 -4
- package/server/services/content.js +25 -2
- package/server/services/docs.js +14 -14
- package/server/services/email.js +1 -1
- package/server/services/filterEngine.js +6 -6
- package/server/services/forms.js +18 -10
- package/server/services/health.js +1 -1
- package/server/services/hooks.js +7 -7
- package/server/services/images.js +4 -4
- package/server/services/managerClient.js +1 -1
- package/server/services/markdown.js +79 -79
- package/server/services/menuRender.js +5 -5
- package/server/services/menus-migration.js +3 -3
- package/server/services/menus.js +31 -31
- package/server/services/permissionRegistry.js +1 -1
- package/server/services/pluginFiles.js +5 -5
- package/server/services/pluginInstaller.js +17 -17
- package/server/services/pluginScaffold.js +1 -1
- package/server/services/plugins.js +17 -17
- package/server/services/presetCollections.js +2 -2
- package/server/services/projects.js +35 -26
- package/server/services/recipes/contact-list.json +1 -1
- package/server/services/recipes/onboarding.json +8 -8
- package/server/services/references.js +8 -8
- package/server/services/renderer.js +9 -9
- package/server/services/roles.js +9 -9
- package/server/services/rowAccess.js +10 -10
- package/server/services/scaffolder.js +30 -30
- package/server/services/sidebar-migration.js +10 -10
- package/server/services/sitemap.js +1 -1
- package/server/services/userProfiles.js +4 -4
- package/server/services/userRoles.js +7 -7
- package/server/services/users.js +5 -5
- package/server/services/versions.js +3 -3
- package/server/services/viewPipeline.js +6 -6
- package/server/services/views.js +11 -11
- package/server/templates/page.html +5 -5
- package/config/navigation.json.bak +0 -31
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
<h1>Domma CMS</h1>
|
|
7
7
|
</div>
|
|
8
8
|
|
|
9
|
-
<!-- ── Step 1: Setup (first-time
|
|
9
|
+
<!-- ── Step 1: Setup (first-time - no users exist) ── -->
|
|
10
10
|
<div id="setup-panel" style="display:none">
|
|
11
|
-
<h2 class="login-heading">Welcome
|
|
11
|
+
<h2 class="login-heading">Welcome - create your admin account</h2>
|
|
12
12
|
<p class="text-muted mb-4">This is a first-time setup. Set up your administrator account to get started.</p>
|
|
13
13
|
<div id="setup-form-container"></div>
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
-
<!-- ── Step 2: Onboarding
|
|
16
|
+
<!-- ── Step 2: Onboarding - Site Identity ── -->
|
|
17
17
|
<div id="onboarding-site-panel" style="display:none">
|
|
18
18
|
<h2 class="login-heading">Name your site</h2>
|
|
19
19
|
<p class="text-muted mb-4">You can change these any time from Settings.</p>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<a href="#" id="ob-site-skip" class="btn-skip">Skip this step</a>
|
|
31
31
|
</div>
|
|
32
32
|
|
|
33
|
-
<!-- ── Step 3: Onboarding
|
|
33
|
+
<!-- ── Step 3: Onboarding - Theme Picker ── -->
|
|
34
34
|
<div id="onboarding-theme-panel" style="display:none">
|
|
35
35
|
<h2 class="login-heading">Choose a theme</h2>
|
|
36
36
|
<p class="text-muted mb-3">Pick the colour scheme for your site. You can change it later.</p>
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<a href="#" id="ob-theme-skip" class="btn-skip">Skip this step</a>
|
|
41
41
|
</div>
|
|
42
42
|
|
|
43
|
-
<!-- ── Step 4: Onboarding
|
|
43
|
+
<!-- ── Step 4: Onboarding - Done ── -->
|
|
44
44
|
<div id="onboarding-done-panel" style="display:none">
|
|
45
45
|
<div class="ob-done-icon"><span data-icon="check-circle"></span></div>
|
|
46
46
|
<h2 class="login-heading">You're all set!</h2>
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
|
|
27
27
|
<div class="tab-panel" id="me-behaviour" data-panel="me-behaviour">
|
|
28
28
|
<p class="text-muted mb-3" id="me-behaviour-note">
|
|
29
|
-
Variant + style (font / icon size) apply both to the public <strong>navbar</strong> and the <strong>admin sidebar</strong>. The <strong>position</strong> field only affects the public navbar
|
|
29
|
+
Variant + style (font / icon size) apply both to the public <strong>navbar</strong> and the <strong>admin sidebar</strong>. The <strong>position</strong> field only affects the public navbar - the admin sidebar's position is fixed by the admin shell. Footer columns ignore everything here.
|
|
30
30
|
</p>
|
|
31
31
|
<div class="grid grid-cols-2 dm-pe-grid">
|
|
32
32
|
<div class="form-group">
|
|
33
33
|
<label for="me-variant">Variant</label>
|
|
34
34
|
<select id="me-variant" name="variant" class="form-input">
|
|
35
|
-
<option value=""
|
|
35
|
+
<option value="">- default -</option>
|
|
36
36
|
<option value="transparent">Transparent</option>
|
|
37
37
|
<option value="dark">Dark</option>
|
|
38
38
|
<option value="light">Light</option>
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
<div class="form-group">
|
|
43
43
|
<label for="me-position">Position</label>
|
|
44
44
|
<select id="me-position" name="position" class="form-input">
|
|
45
|
-
<option value=""
|
|
45
|
+
<option value="">- default -</option>
|
|
46
46
|
<option value="sticky">Sticky</option>
|
|
47
47
|
<option value="fixed">Fixed</option>
|
|
48
48
|
<option value="static">Static</option>
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
<div class="form-group">
|
|
53
53
|
<label for="me-orientation">Orientation</label>
|
|
54
54
|
<select id="me-orientation" name="orientation" class="form-input">
|
|
55
|
-
<option value=""
|
|
55
|
+
<option value="">- default (horizontal) -</option>
|
|
56
56
|
<option value="horizontal">Horizontal</option>
|
|
57
57
|
<option value="vertical">Vertical</option>
|
|
58
58
|
</select>
|
|
59
|
-
<small class="form-hint">Vertical stacks the items into a column
|
|
59
|
+
<small class="form-hint">Vertical stacks the items into a column - a docked side rail in the navbar slot, an indented list for <code>[menu]</code> and floating panels. Below 993px the navbar always falls back to the mobile drawer.</small>
|
|
60
60
|
</div>
|
|
61
61
|
<div class="form-group dm-vertical-field" style="display:none">
|
|
62
62
|
<label for="me-side">Rail side</label>
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
<div class="form-group dm-float-field" style="display:none">
|
|
97
97
|
<label for="me-float-shadow">Shadow</label>
|
|
98
98
|
<select id="me-float-shadow" name="floatShadow" class="form-input">
|
|
99
|
-
<option value=""
|
|
99
|
+
<option value="">- theme default -</option>
|
|
100
100
|
<option value="none">None</option>
|
|
101
101
|
<option value="sm">Small</option>
|
|
102
102
|
<option value="md">Medium</option>
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
<label for="me-float-accent">Accent colour</label>
|
|
109
109
|
<div class="me-colour-control">
|
|
110
110
|
<select id="me-float-accent" name="floatAccent" class="form-input">
|
|
111
|
-
<option value=""
|
|
111
|
+
<option value="">- none -</option>
|
|
112
112
|
<option value="primary">primary</option>
|
|
113
113
|
<option value="success">success</option>
|
|
114
114
|
<option value="danger">danger</option>
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
<div class="tab-panel" id="me-binding" data-panel="me-binding">
|
|
160
160
|
<p class="text-muted mb-3">
|
|
161
161
|
A binding lets this menu <strong>take over a slot for part of the site</strong> instead of
|
|
162
|
-
being mapped to it everywhere. Pick the slot, then say where it applies
|
|
162
|
+
being mapped to it everywhere. Pick the slot, then say where it applies - a project, a list of
|
|
163
163
|
page URLs, or both. On matching pages this menu replaces whatever
|
|
164
164
|
<a href="#/menu-locations">Menu locations</a> maps to that slot; everywhere else the mapped
|
|
165
165
|
menu still renders. The most specific match wins: an exact page URL beats a wildcard, which
|
|
@@ -169,7 +169,7 @@
|
|
|
169
169
|
<div class="form-group">
|
|
170
170
|
<label for="me-bind-slot">Slot</label>
|
|
171
171
|
<select id="me-bind-slot" name="bindSlot" class="form-input"></select>
|
|
172
|
-
<small class="form-hint">Leave unset for no binding
|
|
172
|
+
<small class="form-hint">Leave unset for no binding - this menu then only renders where it is mapped or where a <code>[menu]</code> shortcode places it.</small>
|
|
173
173
|
</div>
|
|
174
174
|
<div class="form-group">
|
|
175
175
|
<label for="me-bind-projects">Projects</label>
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
<div class="form-group">
|
|
197
197
|
<label for="me-slug">Slug <span class="required">*</span></label>
|
|
198
198
|
<input type="text" id="me-slug" name="slug" class="form-input" required>
|
|
199
|
-
<small class="form-hint">Lowercase alphanumeric + hyphen
|
|
199
|
+
<small class="form-hint">Lowercase alphanumeric + hyphen - affects file name</small>
|
|
200
200
|
</div>
|
|
201
201
|
<div class="form-group">
|
|
202
202
|
<label for="me-name">Name <span class="required">*</span></label>
|
|
@@ -141,14 +141,14 @@
|
|
|
141
141
|
<label class="form-label">Font Size</label>
|
|
142
142
|
<select id="field-nav-font-size" class="form-select">
|
|
143
143
|
<option value="">Default</option>
|
|
144
|
-
<option value="12px">12px
|
|
145
|
-
<option value="13px">13px
|
|
146
|
-
<option value="14px">14px
|
|
144
|
+
<option value="12px">12px - XS</option>
|
|
145
|
+
<option value="13px">13px - Small</option>
|
|
146
|
+
<option value="14px">14px - Compact</option>
|
|
147
147
|
<option value="15px">15px</option>
|
|
148
|
-
<option value="16px">16px
|
|
148
|
+
<option value="16px">16px - Base</option>
|
|
149
149
|
<option value="17px">17px</option>
|
|
150
|
-
<option value="18px">18px
|
|
151
|
-
<option value="20px">20px
|
|
150
|
+
<option value="18px">18px - Large</option>
|
|
151
|
+
<option value="20px">20px - XL</option>
|
|
152
152
|
</select>
|
|
153
153
|
</div>
|
|
154
154
|
<div class="col-2">
|
|
@@ -180,14 +180,14 @@
|
|
|
180
180
|
<label class="form-label">Icon Size</label>
|
|
181
181
|
<select id="field-nav-icon-size" class="form-select">
|
|
182
182
|
<option value="">Default</option>
|
|
183
|
-
<option value="12px">12px
|
|
184
|
-
<option value="14px">14px
|
|
185
|
-
<option value="16px">16px
|
|
183
|
+
<option value="12px">12px - XS</option>
|
|
184
|
+
<option value="14px">14px - Small</option>
|
|
185
|
+
<option value="16px">16px - Base</option>
|
|
186
186
|
<option value="18px">18px</option>
|
|
187
187
|
<option value="20px">20px</option>
|
|
188
188
|
<option value="22px">22px</option>
|
|
189
|
-
<option value="24px">24px
|
|
190
|
-
<option value="28px">28px
|
|
189
|
+
<option value="24px">24px - Large</option>
|
|
190
|
+
<option value="28px">28px - XL</option>
|
|
191
191
|
</select>
|
|
192
192
|
</div>
|
|
193
193
|
</div>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<div id="notif-empty" hidden class="card">
|
|
9
9
|
<div class="card-body" style="text-align:center;padding:3rem;color:var(--dm-text-muted)">
|
|
10
10
|
<span data-icon="check-circle" style="font-size:2rem;display:block;margin-bottom:.5rem"></span>
|
|
11
|
-
<p>No notifications
|
|
11
|
+
<p>No notifications - you're all caught up.</p>
|
|
12
12
|
</div>
|
|
13
13
|
</div>
|
|
14
14
|
</div>
|
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
<div class="col-4">
|
|
90
90
|
<label class="form-label">Visibility</label>
|
|
91
91
|
<select id="field-visibility" class="form-select">
|
|
92
|
-
<option value="public">Public
|
|
92
|
+
<option value="public">Public - everyone</option>
|
|
93
93
|
<option value="subscriber">Subscribers & above</option>
|
|
94
94
|
<option value="editor">Editors & above</option>
|
|
95
95
|
<option value="manager">Managers & above</option>
|
|
@@ -99,9 +99,9 @@
|
|
|
99
99
|
<div class="col-4">
|
|
100
100
|
<label class="form-label">Project</label>
|
|
101
101
|
<select id="field-project" class="form-select">
|
|
102
|
-
<option value="">
|
|
102
|
+
<option value="">Inherit from URL</option>
|
|
103
103
|
</select>
|
|
104
|
-
<small class="form-hint">Pre-filled from URL inheritance; choose explicitly to override.</small>
|
|
104
|
+
<small class="form-hint">Pre-filled from the project you came from, or from URL inheritance; choose explicitly to override.</small>
|
|
105
105
|
</div>
|
|
106
106
|
</div>
|
|
107
107
|
<div class="row mb-3">
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
<textarea id="field-dconfig" class="form-input dconfig-textarea"
|
|
165
165
|
placeholder='{ "#my-btn": { "events": { "click": { "target": "#panel", "toggleClass": "hidden" } } } }'></textarea>
|
|
166
166
|
<small class="form-hint">Declarative click handlers and class toggles applied on page load. Merged with any <code>[dconfig]</code>
|
|
167
|
-
shortcodes in the content body
|
|
167
|
+
shortcodes in the content body - inline shortcodes win on selector conflict. Leave blank if unused.</small>
|
|
168
168
|
</div>
|
|
169
169
|
|
|
170
170
|
<!-- Live Preview -->
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
</tbody>
|
|
47
47
|
</table>
|
|
48
48
|
<p class="text-muted" style="margin-top:.75rem;">Blocked: <code>$out</code>, <code>$merge</code>, <code>$function</code>, <code>$accumulator</code>, <code>$graphLookup</code></p>
|
|
49
|
-
<p>Collections are prefixed <code>cms_</code> in MongoDB
|
|
49
|
+
<p>Collections are prefixed <code>cms_</code> in MongoDB - use this prefix in <code>$lookup</code> <code>from</code> values.</p>
|
|
50
50
|
</div>
|
|
51
51
|
</div>
|
|
52
52
|
|
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
<tbody>
|
|
86
86
|
<tr><td>Allowed Roles</td><td>Which roles can call <code>GET /api/views/:slug/public</code></td></tr>
|
|
87
87
|
<tr><td>Public</td><td>No authentication required when checked</td></tr>
|
|
88
|
-
<tr><td>Row-Level
|
|
89
|
-
<tr><td>Row-Level
|
|
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
90
|
</tbody>
|
|
91
91
|
</table>
|
|
92
92
|
</div>
|
|
@@ -156,14 +156,14 @@
|
|
|
156
156
|
<span class="card-collapse-icon" data-icon="chevron-down"></span>
|
|
157
157
|
</div>
|
|
158
158
|
<div class="card-body docs-body">
|
|
159
|
-
<p><strong>updateField</strong>
|
|
159
|
+
<p><strong>updateField</strong> - mark an entry as approved:</p>
|
|
160
160
|
<pre class="code-block"><code>{ "field": "status", "value": "approved" }</code></pre>
|
|
161
|
-
<p><strong>webhook</strong>
|
|
161
|
+
<p><strong>webhook</strong> - notify an external service:</p>
|
|
162
162
|
<pre class="code-block"><code>{
|
|
163
163
|
"url": "https://hooks.example.com/notify",
|
|
164
164
|
"headers": { "X-Secret": "{{env.CMS_PUBLIC_HOOK_SECRET}}" }
|
|
165
165
|
}</code></pre>
|
|
166
|
-
<p><strong>email</strong>
|
|
166
|
+
<p><strong>email</strong> - send confirmation to applicant:</p>
|
|
167
167
|
<pre class="code-block"><code>{
|
|
168
168
|
"to": "{{entry.data.email}}",
|
|
169
169
|
"subject": "Your application was approved",
|
|
@@ -201,13 +201,13 @@
|
|
|
201
201
|
<table class="table table-sm" style="margin-top:1rem;">
|
|
202
202
|
<thead><tr><th>Attribute</th><th>Required</th><th>Default</th><th>Description</th></tr></thead>
|
|
203
203
|
<tbody>
|
|
204
|
-
<tr><td><code>action</code></td><td>Yes</td><td
|
|
205
|
-
<tr><td><code>entry</code></td><td>Yes</td><td
|
|
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
206
|
<tr><td><code>label</code></td><td>No</td><td><code>"Run"</code></td><td>Button text (self-closing only)</td></tr>
|
|
207
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
|
|
209
|
-
<tr><td><code>size</code></td><td>No</td><td
|
|
210
|
-
<tr><td><code>confirm</code></td><td>No</td><td
|
|
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
211
|
</tbody>
|
|
212
212
|
</table>
|
|
213
213
|
</div>
|
|
@@ -229,11 +229,11 @@
|
|
|
229
229
|
<table class="table table-sm" style="margin-top:1rem;">
|
|
230
230
|
<thead><tr><th>Attribute</th><th>Default</th><th>Description</th></tr></thead>
|
|
231
231
|
<tbody>
|
|
232
|
-
<tr><td><code>cta</code></td><td
|
|
232
|
+
<tr><td><code>cta</code></td><td>-</td><td>Action slug - enables per-entry buttons</td></tr>
|
|
233
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
|
|
234
|
+
<tr><td><code>cta-icon</code></td><td>-</td><td>Domma icon name</td></tr>
|
|
235
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
|
|
236
|
+
<tr><td><code>cta-confirm</code></td><td>-</td><td>Confirmation prompt</td></tr>
|
|
237
237
|
</tbody>
|
|
238
238
|
</table>
|
|
239
239
|
<p>All three display modes support CTA buttons: <strong>cards</strong> (card footer), <strong>list</strong> (inline), <strong>table</strong> (dedicated column).</p>
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
<div class="mb-3">
|
|
53
53
|
<label class="form-label">Project</label>
|
|
54
54
|
<select id="role-project" class="form-input">
|
|
55
|
-
<option value=""
|
|
55
|
+
<option value="">- none -</option>
|
|
56
56
|
</select>
|
|
57
57
|
<p class="form-hint">Tag this role to a project for grouping.</p>
|
|
58
58
|
</div>
|
|
@@ -68,13 +68,13 @@
|
|
|
68
68
|
<div class="col-3">
|
|
69
69
|
<label class="form-label">Font Size</label>
|
|
70
70
|
<select id="field-font-size" class="form-select">
|
|
71
|
-
<option value="13">13px
|
|
72
|
-
<option value="14">14px
|
|
73
|
-
<option value="15">15px
|
|
74
|
-
<option value="16">16px
|
|
75
|
-
<option value="17">17px
|
|
76
|
-
<option value="18">18px
|
|
77
|
-
<option value="20">20px
|
|
71
|
+
<option value="13">13px - Compact</option>
|
|
72
|
+
<option value="14">14px - Small</option>
|
|
73
|
+
<option value="15">15px - Slightly small</option>
|
|
74
|
+
<option value="16">16px - Default</option>
|
|
75
|
+
<option value="17">17px - Slightly large</option>
|
|
76
|
+
<option value="18">18px - Large</option>
|
|
77
|
+
<option value="20">20px - Extra large</option>
|
|
78
78
|
</select>
|
|
79
79
|
<p class="form-hint text-muted" style="margin-top:0.4rem;font-size:0.8rem;">Root font size. All rem/em values
|
|
80
80
|
scale with this.</p>
|
|
@@ -124,7 +124,7 @@
|
|
|
124
124
|
</div>
|
|
125
125
|
<div class="col-12 mt-2">
|
|
126
126
|
<p class="form-hint text-muted" style="font-size:.8rem;">Themes switch based on each visitor's local
|
|
127
|
-
time. Any theme can be used
|
|
127
|
+
time. Any theme can be used - they need not be light/dark pairs.</p>
|
|
128
128
|
</div>
|
|
129
129
|
</div>
|
|
130
130
|
<div class="row mt-3">
|
|
@@ -529,7 +529,7 @@
|
|
|
529
529
|
Status: <strong id="cache-status">checking…</strong>
|
|
530
530
|
</p>
|
|
531
531
|
<p class="text-muted" style="font-size:.875rem;margin-bottom:1rem;">
|
|
532
|
-
Public page renders are cached by URL and role. Writes (page, collection, form, block, view, navigation, site settings) auto-invalidate the relevant tags. Use the button below to flush every cached entry
|
|
532
|
+
Public page renders are cached by URL and role. Writes (page, collection, form, block, view, navigation, site settings) auto-invalidate the relevant tags. Use the button below to flush every cached entry - the next visitor to each page will trigger a fresh render.
|
|
533
533
|
</p>
|
|
534
534
|
<button id="btn-clear-cache" class="btn btn-secondary" type="button"><span data-icon="trash"></span> Clear cache</button>
|
|
535
535
|
<button id="btn-refresh-cache-keys" class="btn btn-ghost" type="button" style="margin-left:8px;"><span data-icon="refresh"></span> Refresh list</button>
|
|
@@ -539,7 +539,7 @@
|
|
|
539
539
|
<div class="col">
|
|
540
540
|
<h4 style="margin-top:1.5rem;">Cached entries <span id="cache-key-count" class="text-muted" style="font-weight:normal;font-size:.875rem;"></span></h4>
|
|
541
541
|
<p class="text-muted" style="font-size:.85rem;margin-bottom:.5rem;">
|
|
542
|
-
Snapshot of what's currently keyed. Newest entries first. Values are not shown
|
|
542
|
+
Snapshot of what's currently keyed. Newest entries first. Values are not shown - they can be large HTML blobs.
|
|
543
543
|
</p>
|
|
544
544
|
<div id="cache-keys-table"></div>
|
|
545
545
|
</div>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<div class="docs-body">
|
|
8
8
|
|
|
9
9
|
<p class="lead">
|
|
10
|
-
Step-by-step, end-to-end guides for building real things with Domma CMS. Pick a track below
|
|
10
|
+
Step-by-step, end-to-end guides for building real things with Domma CMS. Pick a track below -
|
|
11
11
|
each opens as its own page, and you can jump straight to any tutorial from the
|
|
12
12
|
<strong>Documentation → Tutorials</strong> menu in the sidebar.
|
|
13
13
|
</p>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<h3 style="margin-top:0;"><span data-icon="database"></span> Building a CRUD App</h3>
|
|
20
20
|
<p class="text-muted" style="margin-bottom:0;">
|
|
21
21
|
The headline tutorial. Compose Collections, Forms, Actions and Pages into a complete
|
|
22
|
-
data-driven app
|
|
22
|
+
data-driven app - with workflows, row-level access and a live scaffolder to try at the end.
|
|
23
23
|
</p>
|
|
24
24
|
</div>
|
|
25
25
|
</a>
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
<h3 style="margin-top:0;"><span data-icon="layout"></span> Form Follow-Up</h3>
|
|
40
40
|
<p class="text-muted" style="margin-bottom:0;">
|
|
41
41
|
What happens after a form is submitted: email notifications, webhooks, CMS Actions, and
|
|
42
|
-
success message vs. redirect
|
|
42
|
+
success message vs. redirect - plus the fixed execution order of the pipeline.
|
|
43
43
|
</p>
|
|
44
44
|
</div>
|
|
45
45
|
</a>
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
<div>
|
|
40
40
|
<label class="form-label">Source Collection <span style="color:var(--dm-danger,#f87171);">*</span></label>
|
|
41
41
|
<select id="view-source" class="form-input">
|
|
42
|
-
<option value=""
|
|
42
|
+
<option value="">- select collection -</option>
|
|
43
43
|
</select>
|
|
44
44
|
<small class="text-muted">Changing the collection will refresh field lists in Pipeline and
|
|
45
45
|
Display tabs.</small>
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
<div>
|
|
48
48
|
<label class="form-label">Project</label>
|
|
49
49
|
<select id="view-project" class="form-input">
|
|
50
|
-
<option value=""
|
|
50
|
+
<option value="">- none -</option>
|
|
51
51
|
</select>
|
|
52
52
|
<small class="text-muted">Tag this view to a project for grouping.</small>
|
|
53
53
|
</div>
|
|
@@ -74,13 +74,13 @@
|
|
|
74
74
|
<h2>Aggregation Stages</h2>
|
|
75
75
|
<div style="display:flex;gap:.5rem;">
|
|
76
76
|
<select id="add-stage-type" class="form-input form-input--sm">
|
|
77
|
-
<option value="$match">$match
|
|
78
|
-
<option value="$sort">$sort
|
|
79
|
-
<option value="$lookup">$lookup
|
|
80
|
-
<option value="$project">$project
|
|
81
|
-
<option value="$unwind">$unwind
|
|
82
|
-
<option value="$addFields">$addFields
|
|
83
|
-
<option value="$group">$group
|
|
77
|
+
<option value="$match">$match - filter rows</option>
|
|
78
|
+
<option value="$sort">$sort - order rows</option>
|
|
79
|
+
<option value="$lookup">$lookup - join collection</option>
|
|
80
|
+
<option value="$project">$project - reshape fields</option>
|
|
81
|
+
<option value="$unwind">$unwind - flatten array</option>
|
|
82
|
+
<option value="$addFields">$addFields - compute fields</option>
|
|
83
|
+
<option value="$group">$group - aggregate</option>
|
|
84
84
|
</select>
|
|
85
85
|
<button id="add-stage-btn" class="btn btn-ghost btn-sm">
|
|
86
86
|
<span data-icon="plus"></span> Add Stage
|
|
@@ -98,9 +98,9 @@
|
|
|
98
98
|
<div class="card">
|
|
99
99
|
<div class="card-body" style="font-size:.8rem;color:var(--dm-text-muted,#888);">
|
|
100
100
|
<strong>Guided stages</strong> ($match, $sort) show a visual condition builder.
|
|
101
|
-
Other stages use a raw JSON textarea
|
|
102
|
-
<code>{ from, localField, foreignField, as }</code>
|
|
103
|
-
Field dropdowns are populated from the source collection
|
|
101
|
+
Other stages use a raw JSON textarea - enter only the <em>inner</em> config (e.g. for $lookup, enter
|
|
102
|
+
<code>{ from, localField, foreignField, as }</code> - do not wrap in <code>{ "$lookup": ... }</code>).
|
|
103
|
+
Field dropdowns are populated from the source collection - select a collection on the Source tab
|
|
104
104
|
first.
|
|
105
105
|
</div>
|
|
106
106
|
</div>
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
<div id="view-block-section" style="display:none;">
|
|
122
122
|
<label class="form-label">Block Template</label>
|
|
123
123
|
<select id="view-block-name" class="form-input">
|
|
124
|
-
<option value=""
|
|
124
|
+
<option value="">- select block -</option>
|
|
125
125
|
</select>
|
|
126
126
|
<small class="text-muted">
|
|
127
127
|
Choose a reusable HTML block template.
|
|
@@ -183,8 +183,8 @@
|
|
|
183
183
|
<div>
|
|
184
184
|
<label class="form-label">Mode</label>
|
|
185
185
|
<select id="view-rowlevel-mode" class="form-input">
|
|
186
|
-
<option value="owner">Owner
|
|
187
|
-
<option value="field">Field Match
|
|
186
|
+
<option value="owner">Owner - entry was created by the current user</option>
|
|
187
|
+
<option value="field">Field Match - a field on the entry matches the user</option>
|
|
188
188
|
</select>
|
|
189
189
|
</div>
|
|
190
190
|
<div id="view-rowlevel-field-group" style="display:none;">
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as _}from"../api.js";let N=null,L={};async function J(e){const t=e.find("#action-project").get(0);if(t)try{(await _.projects.list()).forEach(l=>{const n=document.createElement("option");n.value=l.slug,n.textContent=l.name||l.slug,t.appendChild(n)})}catch{}}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){N=null,L={};const t=window.location.hash.match(/\/actions\/edit\/([^/?#]+)/);t&&(N=t[1]),E.tabs(e.find("#action-editor-tabs").get(0)),await P(e),await U(e),await J(e),N&&(e.find("#action-editor-title").text("Edit Action"),await B(e,N)),e.find("#add-step-btn").off("click").on("click",()=>{const a=e.find("#add-step-type").val()||"updateField";M(e,{type:a,config:{}})}),e.find("#save-action-btn").off("click").on("click",async()=>{await Q(e)}),K(e),Domma.icons.scan()}};async function P(e){const t=e.find("#action-collection").get(0);if(t)try{(await _.collections.list()).forEach(a=>{const l=document.createElement("option");l.value=a.slug,l.textContent=`${a.title} (${a.slug})`,t.appendChild(l)})}catch{}}async function U(e){const t=e.find("#action-roles-checkboxes").get(0);t&&["admin","manager","editor","subscriber"].forEach(a=>{const l=document.createElement("label");l.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const n=document.createElement("input");n.type="checkbox",n.value=a,n.dataset.role=a,n.className="action-role-cb",n.checked=a==="admin",l.appendChild(n),l.appendChild(document.createTextNode(a)),t.appendChild(l)})}async function B(e,t){try{const a=await _.actions.get(t);if(!a){E.toast("Action not found.",{type:"error"}),R.navigate("/actions");return}Y(e,a)}catch(a){E.toast(a.message||"Failed to load action.",{type:"error"}),R.navigate("/actions")}}function K(e){const t=e.find("#action-rowlevel-enabled").get(0),a=e.find("#action-rowlevel-config").get(0),l=e.find("#action-rowlevel-mode").get(0),n=e.find("#action-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{a&&(a.style.display=t.checked?"flex":"none")}),l&&l.addEventListener("change",()=>{n&&(n.style.display=l.value==="field"?"":"none")}))}function Y(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||""),L=t.meta||{},e.find("#action-project").val(t.meta?.project||""),e.find("#action-bundled").prop("checked",!!t.bundled),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 a=t.access?.roles||["admin"];e.find(".action-role-cb").each(function(){this.checked=a.includes(this.value)});const l=t.access?.rowLevel;l&&(e.find("#action-rowlevel-enabled").prop("checked",!0),e.find("#action-rowlevel-config").css("display","flex"),e.find("#action-rowlevel-mode").val(l.mode||"owner"),e.find("#action-rowlevel-userkey").val(l.userKey||"id"),l.mode==="field"&&(e.find("#action-rowlevel-field-group").css("display",""),e.find("#action-rowlevel-field").val(l.field||"")));const n=e.find("#action-steps-list").get(0);if(n){const s=n.querySelector(".steps-empty-placeholder");for(s&&s.remove();n.firstChild;)n.removeChild(n.firstChild);const i=document.createElement("p");i.className="text-muted steps-empty-placeholder",i.textContent="No steps yet. Add a step to define what this action does.",i.style.cssText="text-align:center;padding:2rem 0;",n.appendChild(i)}(t.steps||[]).forEach(s=>M(e,s))}function M(e,t){const a=e.find("#action-steps-list").get(0);if(!a)return;const l=a.querySelector(".steps-empty-placeholder");l&&l.remove();const n=z[t.type]||[],s=document.createElement("div");s.className="card mb-2 step-card",s.dataset.stepType=t.type;const i=document.createElement("div");i.className="card-header",i.style.cssText="display:flex;align-items:center;gap:.5rem;";const p=document.createElement("code");p.textContent=t.type,p.style.cssText="flex:1;font-size:.85rem;";const r=document.createElement("button");r.type="button",r.className="btn btn-sm btn-ghost";const f=document.createElement("span");f.setAttribute("data-icon","trash"),r.appendChild(f),r.addEventListener("click",()=>{if(s.remove(),!a.querySelector(".step-card")){const o=document.createElement("p");o.className="text-muted steps-empty-placeholder",o.textContent="No steps yet. Add a step to define what this action does.",o.style.cssText="text-align:center;padding:2rem 0;",a.appendChild(o)}}),i.appendChild(p),i.appendChild(r);const h=document.createElement("div");if(h.className="card-body",n.length===0){const o=document.createElement("p");o.className="text-muted",o.textContent=t.type==="deleteEntry"?"This step deletes the entry. No configuration required.":"No additional configuration required.",o.style.margin="0",h.appendChild(o)}t.type==="updateField"?G(h,t,e):n.forEach(o=>{const g=document.createElement("div");g.style.cssText="margin-bottom:.75rem;";const b=document.createElement("label");b.className="form-label",b.textContent=o.label,g.appendChild(b);let m;o.multiline?(m=document.createElement("textarea"),m.rows=3,m.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",m.value=typeof t.config?.[o.name]=="object"?JSON.stringify(t.config[o.name],null,2):t.config?.[o.name]??""):(m=document.createElement("input"),m.type="text",m.value=t.config?.[o.name]??""),m.className=`form-input step-field-${o.name}`,m.placeholder=o.placeholder||"",m.dataset.field=o.name,g.appendChild(m),h.appendChild(g)}),s.appendChild(i),s.appendChild(h),a.appendChild(s),Domma.icons.scan(s)}async function G(e,t,a){const l=a.find("#action-collection").val(),n=document.createElement("div");n.style.cssText="margin-bottom:.75rem;";const s=document.createElement("label");s.className="form-label",s.textContent="Field",n.appendChild(s);const i=document.createElement("select");i.className="form-input step-field-field",i.dataset.field="field";const p=document.createElement("input");p.type="text",p.className="form-input mt-2 step-field-field-custom",p.placeholder="Field name, e.g. status",p.style.display="none";const r=document.createElement("div");r.style.cssText="margin-bottom:.75rem;";const f=document.createElement("label");f.className="form-label",f.textContent="New Value",r.appendChild(f);const h=document.createElement("div");r.appendChild(h);const o=document.createElement("button");o.type="button",o.className="btn btn-ghost btn-sm mt-2",o.style.cssText="font-size:.75rem;padding:.2rem .5rem;",o.textContent="Template Variables";const g=document.createElement("div");g.style.cssText="display:none;background:var(--dm-surface-raised,#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:",g.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(([d,u])=>{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=d,w.appendChild(T);const c=document.createElement("td");c.textContent=u,x.appendChild(w),x.appendChild(c),m.appendChild(x)}),g.appendChild(m),o.addEventListener("click",()=>{const d=g.style.display!=="none";g.style.display=d?"none":"",o.textContent=d?"Template Variables":"Hide Variables"}),r.appendChild(o),r.appendChild(g),e.appendChild(n),e.appendChild(r);let S=[];if(l)try{S=(await _.collections.get(l)).fields||[]}catch{}const F=document.createElement("option");F.value="",F.textContent=S.length?"\u2014 select a field \u2014":"\u2014 no fields available \u2014",i.appendChild(F),S.forEach(d=>{const u=document.createElement("option");u.value=d.name,u.textContent=`${d.label} (${d.name})`,u.dataset.fieldType=d.type,u.dataset.fieldOptions=d.type==="select"?JSON.stringify(d.options||[]):"",i.appendChild(u)});const j=document.createElement("option");j.value="__custom__",j.textContent="\u2014 enter manually \u2014",i.appendChild(j);const C=t.config?.field||"",V=C&&[...i.options].find(d=>d.value===C);V?i.value=C:C&&(i.value="__custom__",p.value=C,p.style.display=""),n.appendChild(i),n.appendChild(p);function I(d,u){h.textContent="";const x=d?[...i.options].find(c=>c.value===d):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(y=>{const k=typeof y=="string"?y:y.value??"",D=typeof y=="string"?y:y.label||y.value||k;if(!k||k==="undefined")return;const q=document.createElement("option");q.value=k,q.textContent=D,c.appendChild(q)});const A=document.createElement("option");A.value="__custom__",A.textContent="\u2014 enter manually \u2014",c.appendChild(A);const v=document.createElement("input");v.type="text",v.className="form-input mt-2",v.placeholder="e.g. approved or {{now}}",v.style.display="none",u&&[...c.options].find(y=>y.value===u&&y.value!=="__custom__")?c.value=u:u&&(c.value="__custom__",v.value=u,v.style.display=""),c.addEventListener("change",()=>{const y=c.value==="__custom__";v.style.display=y?"":"none",y||(v.value="")}),h.appendChild(c),h.appendChild(v)}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=u||"",h.appendChild(c)}}I(V?C:null,t.config?.value||""),i.addEventListener("change",()=>{const d=i.value==="__custom__";p.style.display=d?"":"none",d||(p.value=""),I(d?null:i.value,"")})}function H(e){const t=[];return e.find(".step-card").each(function(){const a=this.dataset.stepType,l={};if(a==="updateField"){let n=this.querySelector(".step-field-field")?.value?.trim()||"";n==="__custom__"&&(n=this.querySelector(".step-field-field-custom")?.value?.trim()||""),l.field=n;const s=this.querySelector(".step-field-value");let i=s?.value?.trim()||"";i==="__custom__"&&(i=s?.nextElementSibling?.value?.trim()||""),l.value=i}else(z[a]||[]).forEach(n=>{const s=this.querySelector(`.step-field-${n.name}`);if(!s)return;const i=s.value.trim();if(n.multiline&&i)try{l[n.name]=JSON.parse(i)}catch{l[n.name]=i}else l[n.name]=i});t.push({type:a,config:l})}),t}async function Q(e){const t=e.find("#action-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const a=e.find("#action-collection").val();if(!a){E.toast("Target collection is required (General tab).",{type:"warning"});return}const l=[];e.find(".action-role-cb:checked").each(function(){l.push(this.value)});const n=e.find("#action-rowlevel-enabled").is(":checked");let s=null;if(n){const f=e.find("#action-rowlevel-mode").val()||"owner",h=e.find("#action-rowlevel-userkey").val()||"id";if(s={mode:f,userKey:h},f==="field"){const o=e.find("#action-rowlevel-field").val().trim();if(!o){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}s.field=o}}const i=e.find("#action-project").val()||"",p={title:t,slug:e.find("#action-slug").val().trim()||void 0,description:e.find("#action-description").val().trim(),collection:a,...e.find("#action-bundled").is(":checked")?{bundled:!0}:{},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:l,rowLevel:s},meta:{...L||{},project:i||null}},r=e.find("#save-action-btn").get(0);r&&(r.disabled=!0);try{if(N)await _.actions.update(N,p),E.toast("Action updated.",{type:"success"});else{const f=await _.actions.create(p);E.toast("Action created.",{type:"success"}),R.navigate(`/actions/edit/${f.slug}`)}}catch(f){E.toast(f.message||"Failed to save action.",{type:"error"})}finally{r&&(r.disabled=!1)}}
|
|
1
|
+
import{api as _}from"../api.js";let N=null,L={};async function J(e){const t=e.find("#action-project").get(0);if(t)try{(await _.projects.list()).forEach(l=>{const n=document.createElement("option");n.value=l.slug,n.textContent=l.name||l.slug,t.appendChild(n)})}catch{}}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){N=null,L={};const t=window.location.hash.match(/\/actions\/edit\/([^/?#]+)/);t&&(N=t[1]),E.tabs(e.find("#action-editor-tabs").get(0)),await P(e),await U(e),await J(e),N&&(e.find("#action-editor-title").text("Edit Action"),await B(e,N)),e.find("#add-step-btn").off("click").on("click",()=>{const a=e.find("#add-step-type").val()||"updateField";M(e,{type:a,config:{}})}),e.find("#save-action-btn").off("click").on("click",async()=>{await Q(e)}),K(e),Domma.icons.scan()}};async function P(e){const t=e.find("#action-collection").get(0);if(t)try{(await _.collections.list()).forEach(a=>{const l=document.createElement("option");l.value=a.slug,l.textContent=`${a.title} (${a.slug})`,t.appendChild(l)})}catch{}}async function U(e){const t=e.find("#action-roles-checkboxes").get(0);t&&["admin","manager","editor","subscriber"].forEach(a=>{const l=document.createElement("label");l.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const n=document.createElement("input");n.type="checkbox",n.value=a,n.dataset.role=a,n.className="action-role-cb",n.checked=a==="admin",l.appendChild(n),l.appendChild(document.createTextNode(a)),t.appendChild(l)})}async function B(e,t){try{const a=await _.actions.get(t);if(!a){E.toast("Action not found.",{type:"error"}),R.navigate("/actions");return}Y(e,a)}catch(a){E.toast(a.message||"Failed to load action.",{type:"error"}),R.navigate("/actions")}}function K(e){const t=e.find("#action-rowlevel-enabled").get(0),a=e.find("#action-rowlevel-config").get(0),l=e.find("#action-rowlevel-mode").get(0),n=e.find("#action-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{a&&(a.style.display=t.checked?"flex":"none")}),l&&l.addEventListener("change",()=>{n&&(n.style.display=l.value==="field"?"":"none")}))}function Y(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||""),L=t.meta||{},e.find("#action-project").val(t.meta?.project||""),e.find("#action-bundled").prop("checked",!!t.bundled),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 a=t.access?.roles||["admin"];e.find(".action-role-cb").each(function(){this.checked=a.includes(this.value)});const l=t.access?.rowLevel;l&&(e.find("#action-rowlevel-enabled").prop("checked",!0),e.find("#action-rowlevel-config").css("display","flex"),e.find("#action-rowlevel-mode").val(l.mode||"owner"),e.find("#action-rowlevel-userkey").val(l.userKey||"id"),l.mode==="field"&&(e.find("#action-rowlevel-field-group").css("display",""),e.find("#action-rowlevel-field").val(l.field||"")));const n=e.find("#action-steps-list").get(0);if(n){const s=n.querySelector(".steps-empty-placeholder");for(s&&s.remove();n.firstChild;)n.removeChild(n.firstChild);const i=document.createElement("p");i.className="text-muted steps-empty-placeholder",i.textContent="No steps yet. Add a step to define what this action does.",i.style.cssText="text-align:center;padding:2rem 0;",n.appendChild(i)}(t.steps||[]).forEach(s=>M(e,s))}function M(e,t){const a=e.find("#action-steps-list").get(0);if(!a)return;const l=a.querySelector(".steps-empty-placeholder");l&&l.remove();const n=z[t.type]||[],s=document.createElement("div");s.className="card mb-2 step-card",s.dataset.stepType=t.type;const i=document.createElement("div");i.className="card-header",i.style.cssText="display:flex;align-items:center;gap:.5rem;";const p=document.createElement("code");p.textContent=t.type,p.style.cssText="flex:1;font-size:.85rem;";const r=document.createElement("button");r.type="button",r.className="btn btn-sm btn-ghost";const f=document.createElement("span");f.setAttribute("data-icon","trash"),r.appendChild(f),r.addEventListener("click",()=>{if(s.remove(),!a.querySelector(".step-card")){const o=document.createElement("p");o.className="text-muted steps-empty-placeholder",o.textContent="No steps yet. Add a step to define what this action does.",o.style.cssText="text-align:center;padding:2rem 0;",a.appendChild(o)}}),i.appendChild(p),i.appendChild(r);const h=document.createElement("div");if(h.className="card-body",n.length===0){const o=document.createElement("p");o.className="text-muted",o.textContent=t.type==="deleteEntry"?"This step deletes the entry. No configuration required.":"No additional configuration required.",o.style.margin="0",h.appendChild(o)}t.type==="updateField"?G(h,t,e):n.forEach(o=>{const g=document.createElement("div");g.style.cssText="margin-bottom:.75rem;";const b=document.createElement("label");b.className="form-label",b.textContent=o.label,g.appendChild(b);let m;o.multiline?(m=document.createElement("textarea"),m.rows=3,m.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",m.value=typeof t.config?.[o.name]=="object"?JSON.stringify(t.config[o.name],null,2):t.config?.[o.name]??""):(m=document.createElement("input"),m.type="text",m.value=t.config?.[o.name]??""),m.className=`form-input step-field-${o.name}`,m.placeholder=o.placeholder||"",m.dataset.field=o.name,g.appendChild(m),h.appendChild(g)}),s.appendChild(i),s.appendChild(h),a.appendChild(s),Domma.icons.scan(s)}async function G(e,t,a){const l=a.find("#action-collection").val(),n=document.createElement("div");n.style.cssText="margin-bottom:.75rem;";const s=document.createElement("label");s.className="form-label",s.textContent="Field",n.appendChild(s);const i=document.createElement("select");i.className="form-input step-field-field",i.dataset.field="field";const p=document.createElement("input");p.type="text",p.className="form-input mt-2 step-field-field-custom",p.placeholder="Field name, e.g. status",p.style.display="none";const r=document.createElement("div");r.style.cssText="margin-bottom:.75rem;";const f=document.createElement("label");f.className="form-label",f.textContent="New Value",r.appendChild(f);const h=document.createElement("div");r.appendChild(h);const o=document.createElement("button");o.type="button",o.className="btn btn-ghost btn-sm mt-2",o.style.cssText="font-size:.75rem;padding:.2rem .5rem;",o.textContent="Template Variables";const g=document.createElement("div");g.style.cssText="display:none;background:var(--dm-surface-raised,#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:",g.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(([d,u])=>{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=d,w.appendChild(T);const c=document.createElement("td");c.textContent=u,x.appendChild(w),x.appendChild(c),m.appendChild(x)}),g.appendChild(m),o.addEventListener("click",()=>{const d=g.style.display!=="none";g.style.display=d?"none":"",o.textContent=d?"Template Variables":"Hide Variables"}),r.appendChild(o),r.appendChild(g),e.appendChild(n),e.appendChild(r);let S=[];if(l)try{S=(await _.collections.get(l)).fields||[]}catch{}const F=document.createElement("option");F.value="",F.textContent=S.length?"- select a field -":"- no fields available -",i.appendChild(F),S.forEach(d=>{const u=document.createElement("option");u.value=d.name,u.textContent=`${d.label} (${d.name})`,u.dataset.fieldType=d.type,u.dataset.fieldOptions=d.type==="select"?JSON.stringify(d.options||[]):"",i.appendChild(u)});const j=document.createElement("option");j.value="__custom__",j.textContent="- enter manually -",i.appendChild(j);const C=t.config?.field||"",V=C&&[...i.options].find(d=>d.value===C);V?i.value=C:C&&(i.value="__custom__",p.value=C,p.style.display=""),n.appendChild(i),n.appendChild(p);function I(d,u){h.textContent="";const x=d?[...i.options].find(c=>c.value===d):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="- select a value -",c.appendChild(O),T.forEach(y=>{const k=typeof y=="string"?y:y.value??"",D=typeof y=="string"?y:y.label||y.value||k;if(!k||k==="undefined")return;const q=document.createElement("option");q.value=k,q.textContent=D,c.appendChild(q)});const A=document.createElement("option");A.value="__custom__",A.textContent="- enter manually -",c.appendChild(A);const v=document.createElement("input");v.type="text",v.className="form-input mt-2",v.placeholder="e.g. approved or {{now}}",v.style.display="none",u&&[...c.options].find(y=>y.value===u&&y.value!=="__custom__")?c.value=u:u&&(c.value="__custom__",v.value=u,v.style.display=""),c.addEventListener("change",()=>{const y=c.value==="__custom__";v.style.display=y?"":"none",y||(v.value="")}),h.appendChild(c),h.appendChild(v)}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=u||"",h.appendChild(c)}}I(V?C:null,t.config?.value||""),i.addEventListener("change",()=>{const d=i.value==="__custom__";p.style.display=d?"":"none",d||(p.value=""),I(d?null:i.value,"")})}function H(e){const t=[];return e.find(".step-card").each(function(){const a=this.dataset.stepType,l={};if(a==="updateField"){let n=this.querySelector(".step-field-field")?.value?.trim()||"";n==="__custom__"&&(n=this.querySelector(".step-field-field-custom")?.value?.trim()||""),l.field=n;const s=this.querySelector(".step-field-value");let i=s?.value?.trim()||"";i==="__custom__"&&(i=s?.nextElementSibling?.value?.trim()||""),l.value=i}else(z[a]||[]).forEach(n=>{const s=this.querySelector(`.step-field-${n.name}`);if(!s)return;const i=s.value.trim();if(n.multiline&&i)try{l[n.name]=JSON.parse(i)}catch{l[n.name]=i}else l[n.name]=i});t.push({type:a,config:l})}),t}async function Q(e){const t=e.find("#action-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const a=e.find("#action-collection").val();if(!a){E.toast("Target collection is required (General tab).",{type:"warning"});return}const l=[];e.find(".action-role-cb:checked").each(function(){l.push(this.value)});const n=e.find("#action-rowlevel-enabled").is(":checked");let s=null;if(n){const f=e.find("#action-rowlevel-mode").val()||"owner",h=e.find("#action-rowlevel-userkey").val()||"id";if(s={mode:f,userKey:h},f==="field"){const o=e.find("#action-rowlevel-field").val().trim();if(!o){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}s.field=o}}const i=e.find("#action-project").val()||"",p={title:t,slug:e.find("#action-slug").val().trim()||void 0,description:e.find("#action-description").val().trim(),collection:a,...e.find("#action-bundled").is(":checked")?{bundled:!0}:{},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:l,rowLevel:s},meta:{...L||{},project:i||null}},r=e.find("#save-action-btn").get(0);r&&(r.disabled=!0);try{if(N)await _.actions.update(N,p),E.toast("Action updated.",{type:"success"});else{const f=await _.actions.create(p);E.toast("Action created.",{type:"success"}),R.navigate(`/actions/edit/${f.slug}`)}}catch(f){E.toast(f.message||"Failed to save action.",{type:"error"})}finally{r&&(r.disabled=!1)}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as r}from"../api.js";import{filterByProject as p,getProjectFromHash as m}from"../lib/project-context.js";import{keepTableIconsAlive as
|
|
1
|
+
import{api as r}from"../api.js";import{filterByProject as p,getProjectFromHash as m,renderProjectFilterNotice as u}from"../lib/project-context.js";import{keepTableIconsAlive as g}from"../lib/table-icons.js";function i(e){return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}export const actionsListView={templateUrl:"/admin/js/templates/actions-list.html",async onMount(e){await d(e),g(e.get(0)),e.off("click",".js-delete-action").on("click",".js-delete-action",async function(){const n=this.dataset.slug;if(await E.confirm(`Delete action "${n}"? This cannot be undone.`))try{await r.actions.delete(n),E.toast("Action deleted.",{type:"success"}),await d(e)}catch{E.toast("Failed to delete action.",{type:"error"})}}),e.find("#create-action-btn").off("click").on("click",()=>{R.navigate("/actions/new")}),Domma.icons.scan()}};async function d(e){let n=[];const l=E.loader(e.get(0),{type:"dots"});try{n=await r.actions.list(),e.find("#actions-pro-notice").hide()}catch(t){t.message?.includes("MongoDB")||t.message?.includes("pro mode")||t.message?.includes("connection")?e.find("#actions-pro-notice").show():E.toast("Could not load actions.",{type:"error"})}finally{l.destroy()}const c=m();if(c){const t=n.length;n=p(n,c),u(e,{total:t,shown:n.length,slug:c,allHref:"#/actions",noun:"actions"})}T.create("#actions-table",{pageSize:25,data:n,columns:[{key:"title",title:"Title",render:(t,a)=>{const o=document.createElement("a");return o.href=`#/actions/edit/${i(a.slug)}`,o.textContent=t,o.style.fontWeight="600",o.outerHTML}},{key:"slug",title:"Slug",render:t=>`<code>${i(t)}</code>`},{key:"collection",title:"Collection",render:t=>`<code>${i(t||"-")}</code>`},{key:"trigger",title:"Trigger",render:t=>i(t?.label||t?.type||"manual")},{key:"steps",title:"Steps",render:t=>String(Array.isArray(t)?t.length:0)},{key:"access",title:"Roles",render:t=>(t?.roles||[]).map(a=>`<span class="badge badge-secondary">${i(a)}</span>`).join(" ")},{key:"slug",title:"Actions",render:t=>{const a=document.createElement("div");a.style.cssText="display:flex;gap:.4rem;justify-content:flex-end;";const o=document.createElement("a");o.href=`#/actions/edit/${i(t)}`,o.className="btn btn-sm btn-ghost",o.setAttribute("data-tooltip","Edit"),o.innerHTML='<span data-icon="edit" data-icon-size="14"></span>';const s=document.createElement("button");return s.className="btn btn-sm btn-ghost js-delete-action",s.dataset.slug=t,s.setAttribute("data-tooltip","Delete"),s.innerHTML='<span data-icon="trash" data-icon-size="14"></span>',a.appendChild(o),a.appendChild(s),a.outerHTML}}],emptyMessage:'No actions yet. Click "New Action" to create your first action.'}),Domma.icons.scan(),document.querySelectorAll("#actions-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})})}
|