domma-cms 0.73.0 → 0.75.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 +3 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/actions-arrange.js +1 -0
- package/admin/js/lib/api-endpoints-arrange.js +1 -0
- package/admin/js/lib/blocks-arrange.js +1 -0
- package/admin/js/lib/collections-arrange.js +1 -0
- package/admin/js/lib/components-arrange.js +1 -0
- package/admin/js/lib/context-menus-arrange.js +1 -0
- package/admin/js/lib/forms-arrange.js +1 -0
- package/admin/js/lib/media-arrange.js +1 -0
- package/admin/js/lib/menus-arrange.js +1 -0
- package/admin/js/lib/pages-arrange.js +1 -0
- package/admin/js/lib/projects-arrange.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/lib/theme-settings-arrange.js +1 -0
- package/admin/js/lib/views-arrange.js +1 -0
- package/admin/js/templates/actions-list.html +73 -14
- package/admin/js/templates/api-endpoints.html +63 -10
- package/admin/js/templates/blocks.html +76 -15
- package/admin/js/templates/collections.html +108 -52
- package/admin/js/templates/components.html +82 -15
- package/admin/js/templates/context-menus.html +55 -13
- package/admin/js/templates/forms.html +67 -14
- package/admin/js/templates/media.html +99 -13
- package/admin/js/templates/menus.html +57 -11
- package/admin/js/templates/pages.html +78 -23
- package/admin/js/templates/projects.html +69 -10
- package/admin/js/templates/theme.html +164 -132
- package/admin/js/templates/views-list.html +63 -9
- package/admin/js/views/actions-list.js +37 -1
- package/admin/js/views/api-endpoints.js +11 -7
- package/admin/js/views/blocks.js +34 -5
- package/admin/js/views/collections.js +36 -1
- package/admin/js/views/components.js +27 -5
- package/admin/js/views/context-menus.js +10 -5
- package/admin/js/views/forms.js +57 -1
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/media.js +40 -1
- package/admin/js/views/menus.js +11 -5
- package/admin/js/views/pages.js +28 -8
- package/admin/js/views/projects.js +58 -3
- package/admin/js/views/theme.js +55 -1
- package/admin/js/views/views-list.js +11 -1
- package/package.json +1 -1
- package/server/routes/api/actions.js +64 -2
- package/server/routes/api/api-endpoints.js +41 -0
- package/server/routes/api/blocks.js +170 -8
- package/server/routes/api/collections.js +53 -1
- package/server/routes/api/components.js +46 -0
- package/server/routes/api/context-menus.js +8 -0
- package/server/routes/api/forms.js +123 -10
- package/server/routes/api/media.js +56 -1
- package/server/routes/api/menus.js +53 -6
- package/server/routes/api/pages.js +46 -0
- package/server/routes/api/projects.js +16 -2
- package/server/routes/api/theme.js +55 -0
- package/server/routes/api/views.js +27 -3
- package/server/server.js +19 -0
- package/server/services/contextMenus.js +41 -0
- package/server/services/projects.js +86 -26
- package/server/services/views.js +38 -0
- package/admin/js/templates/project-editor.html +0 -45
- package/admin/js/views/project-editor.js +0 -1
|
@@ -1,94 +1,90 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
<!-- Content › Theme. Bound with M.applyBindings - see admin/js/views/theme.js, which also inserts the
|
|
2
|
+
override row template (the router renders a templateUrl through _.render, eating any {{…}}).
|
|
3
|
+
No page heading: the Tool banner above is the heading; Save and View site are banner actions. -->
|
|
4
|
+
<div class="thx">
|
|
5
|
+
<p class="thx-alert is-bad" data-bind-hidden="!problemsText.value">
|
|
6
|
+
<span data-icon="alert-triangle" data-icon-size="16"></span>
|
|
7
|
+
<span data-bind-text="problemsText.value"></span></p>
|
|
8
|
+
<div class="thx-alert is-bad" data-bind-hidden="!errorsText.value">
|
|
9
|
+
<span data-icon="x-circle" data-icon-size="16"></span>
|
|
10
|
+
<span><strong>Not saved.</strong> <span data-bind-text="errorsText.value"></span></span></div>
|
|
11
|
+
<p class="thx-alert is-dirty" data-bind-hidden="!dirty.value">
|
|
12
|
+
<span data-icon="edit-3" data-icon-size="16"></span>
|
|
13
|
+
<span>Unsaved changes - Save (Ctrl+S) to apply them. Leave and they are kept as a draft for next time.</span>
|
|
14
|
+
<span class="thx-spacer"></span>
|
|
15
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="discard">Discard</button>
|
|
16
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="saving.value">Save</button></p>
|
|
10
17
|
|
|
11
|
-
<div
|
|
18
|
+
<div class="tabs thx-tabs">
|
|
19
|
+
<div class="tab-list">
|
|
20
|
+
<button type="button" class="tab-item active">Themes</button>
|
|
21
|
+
<button type="button" class="tab-item">Element overrides <span class="thx-count" data-bind-text="overrides.value.length"></span></button>
|
|
22
|
+
</div>
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
<!-- Themes -->
|
|
25
|
+
<div class="tab-panel active">
|
|
26
|
+
<div class="thx-card">
|
|
27
|
+
<div class="thx-card-title">Live now
|
|
28
|
+
<span data-help-title="Live now" data-help="What the public site and this admin show at the moment - the saved settings, not what you are editing. With day/night switching on, the site changes theme at the times below, on each visitor's own clock."></span></div>
|
|
29
|
+
<div class="thx-chips">
|
|
30
|
+
<span class="thx-chip is-ok" data-bind-text="liveSite.value"></span>
|
|
31
|
+
<span class="thx-chip" data-bind-text="liveAdmin.value"></span>
|
|
32
|
+
<span class="thx-chip" data-bind-hidden="!liveOverrides.value" data-bind-text="liveOverrides.value"></span>
|
|
33
|
+
<span class="thx-chip" data-bind-hidden="!liveSwitcher.value" data-bind-text="liveSwitcher.value"></span>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
18
36
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
<div class="col-6">
|
|
28
|
-
<label class="form-label">Front-end Theme</label>
|
|
29
|
-
<select id="theme-frontend" class="form-select"></select>
|
|
30
|
-
<p class="form-hint text-muted" style="margin-top:.4rem;font-size:.8rem;">
|
|
31
|
-
Applied to every public page.</p>
|
|
37
|
+
<div class="thx-card">
|
|
38
|
+
<div class="thx-card-title">Site & admin</div>
|
|
39
|
+
<div class="thx-grid">
|
|
40
|
+
<div>
|
|
41
|
+
<label class="form-label" for="thx-site">Site theme
|
|
42
|
+
<span data-help="Every public page. With day/night switching on, this is replaced by the day and night themes below."></span></label>
|
|
43
|
+
<select id="thx-site" class="form-select" data-model="theme.value" data-bind-disabled="autoEnabled.value"></select>
|
|
44
|
+
<small class="thx-hint" data-bind-hidden="!autoEnabled.value">Switching is on - the day and night themes below apply.</small>
|
|
32
45
|
</div>
|
|
33
|
-
<div
|
|
34
|
-
<label class="form-label">Admin
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
Applied to this admin panel. Takes effect as soon as you save.</p>
|
|
46
|
+
<div>
|
|
47
|
+
<label class="form-label" for="thx-admin">Admin theme
|
|
48
|
+
<span data-help="This admin panel, for everyone who signs in. Applied as soon as you save. Admin-only palettes (Sharp, Smooth) are offered here and nowhere else."></span></label>
|
|
49
|
+
<select id="thx-admin" class="form-select" data-model="adminTheme.value"></select>
|
|
38
50
|
</div>
|
|
39
51
|
</div>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<input id="theme-auto-enabled" type="checkbox" class="form-check">
|
|
45
|
-
Switch automatically between a day and a night theme
|
|
46
|
-
</label>
|
|
47
|
-
</div>
|
|
52
|
+
<div class="thx-setting">
|
|
53
|
+
<label class="form-label" for="thx-auto">Switch between a day and a night theme
|
|
54
|
+
<span data-help-title="Day and night" data-help="The site shows the day theme from the day start until the night start, then the night theme - by each visitor's local time. They need not be a light/dark pair. The admin theme is not affected."></span></label>
|
|
55
|
+
<input id="thx-auto" type="checkbox" class="form-switch" data-model="autoEnabled.value">
|
|
48
56
|
</div>
|
|
49
|
-
<div
|
|
50
|
-
<div
|
|
51
|
-
<label class="form-label">Day
|
|
52
|
-
<select id="
|
|
57
|
+
<div class="thx-grid" data-bind-hidden="!autoEnabled.value">
|
|
58
|
+
<div>
|
|
59
|
+
<label class="form-label" for="thx-day">Day theme</label>
|
|
60
|
+
<select id="thx-day" class="form-select" data-model="dayTheme.value"></select>
|
|
53
61
|
</div>
|
|
54
|
-
<div
|
|
55
|
-
<label class="form-label">Night
|
|
56
|
-
<select id="
|
|
62
|
+
<div>
|
|
63
|
+
<label class="form-label" for="thx-night">Night theme</label>
|
|
64
|
+
<select id="thx-night" class="form-select" data-model="nightTheme.value"></select>
|
|
57
65
|
</div>
|
|
58
|
-
<div
|
|
59
|
-
<label class="form-label">Day
|
|
60
|
-
<input id="
|
|
66
|
+
<div>
|
|
67
|
+
<label class="form-label" for="thx-day-start">Day starts at</label>
|
|
68
|
+
<input id="thx-day-start" type="time" class="form-input" data-model="dayStart.value">
|
|
61
69
|
</div>
|
|
62
|
-
<div
|
|
63
|
-
<label class="form-label">Night
|
|
64
|
-
<input id="
|
|
65
|
-
</div>
|
|
66
|
-
<div class="col-12 mt-2">
|
|
67
|
-
<p class="form-hint text-muted" style="font-size:.8rem;">Themes switch on each visitor's local
|
|
68
|
-
time. They need not be a light/dark pair.</p>
|
|
70
|
+
<div>
|
|
71
|
+
<label class="form-label" for="thx-night-start">Night starts at</label>
|
|
72
|
+
<input id="thx-night-start" type="time" class="form-input" data-model="nightStart.value">
|
|
69
73
|
</div>
|
|
70
74
|
</div>
|
|
71
75
|
</div>
|
|
72
|
-
</div>
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<input id="theme-switcher-enabled" type="checkbox" class="form-check">
|
|
81
|
-
Show a theme switcher on the public site
|
|
82
|
-
</label>
|
|
83
|
-
<p class="form-hint text-muted" style="margin-top:.35rem;font-size:.8rem;">
|
|
84
|
-
A floating disc of theme dots that lets a visitor pick their own theme.
|
|
85
|
-
It offers every theme the stylesheet ships.</p>
|
|
86
|
-
</div>
|
|
77
|
+
<div class="thx-card">
|
|
78
|
+
<div class="thx-card-title">Theme switcher</div>
|
|
79
|
+
<div class="thx-setting">
|
|
80
|
+
<label class="form-label" for="thx-sw">Show a theme switcher on the public site
|
|
81
|
+
<span data-help-title="Theme switcher" data-help="A floating disc of theme dots that lets a visitor pick their own theme, remembered in their browser. It offers the built-in themes only - custom themes cannot be switched to from the page."></span></label>
|
|
82
|
+
<input id="thx-sw" type="checkbox" class="form-switch" data-model="swEnabled.value">
|
|
87
83
|
</div>
|
|
88
|
-
<div
|
|
89
|
-
<div
|
|
90
|
-
<label class="form-label">Position</label>
|
|
91
|
-
<select id="
|
|
84
|
+
<div class="thx-grid thx-grid-3" data-bind-hidden="!swEnabled.value">
|
|
85
|
+
<div>
|
|
86
|
+
<label class="form-label" for="thx-sw-pos">Position</label>
|
|
87
|
+
<select id="thx-sw-pos" class="form-select" data-model="swPosition.value">
|
|
92
88
|
<option value="right-center">Right, centred</option>
|
|
93
89
|
<option value="left-center">Left, centred</option>
|
|
94
90
|
<option value="bottom-right">Bottom right</option>
|
|
@@ -97,25 +93,24 @@
|
|
|
97
93
|
<option value="top-left">Top left</option>
|
|
98
94
|
</select>
|
|
99
95
|
</div>
|
|
100
|
-
<div
|
|
101
|
-
<label class="form-label">Offset X (px)</label>
|
|
102
|
-
<input id="
|
|
96
|
+
<div>
|
|
97
|
+
<label class="form-label" for="thx-sw-x">Offset X (px)</label>
|
|
98
|
+
<input id="thx-sw-x" type="number" class="form-input" min="0" max="400" data-model="swOffsetX.value">
|
|
103
99
|
</div>
|
|
104
|
-
<div
|
|
105
|
-
<label class="form-label">Offset Y (px)</label>
|
|
106
|
-
<input id="
|
|
100
|
+
<div>
|
|
101
|
+
<label class="form-label" for="thx-sw-y">Offset Y (px)</label>
|
|
102
|
+
<input id="thx-sw-y" type="number" class="form-input" min="0" max="400" data-model="swOffsetY.value">
|
|
107
103
|
</div>
|
|
108
104
|
</div>
|
|
109
105
|
</div>
|
|
110
|
-
</div>
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
<select id="
|
|
107
|
+
<div class="thx-card">
|
|
108
|
+
<div class="thx-card-title">Typography</div>
|
|
109
|
+
<div class="thx-grid">
|
|
110
|
+
<div>
|
|
111
|
+
<label class="form-label" for="thx-font">Font family
|
|
112
|
+
<span data-help="Public site body text. The dyslexia-friendly group uses wider letterforms and spacing."></span></label>
|
|
113
|
+
<select id="thx-font" class="form-select" data-model="fontFamily.value">
|
|
119
114
|
<optgroup label="Sans-serif">
|
|
120
115
|
<option value="Roboto">Roboto (default)</option>
|
|
121
116
|
<option value="Inter">Inter</option>
|
|
@@ -144,12 +139,11 @@
|
|
|
144
139
|
<option value="system-ui">System UI (no web font)</option>
|
|
145
140
|
</optgroup>
|
|
146
141
|
</select>
|
|
147
|
-
<p class="form-hint text-muted" style="margin-top:.4rem;font-size:.8rem;">Public site body text.
|
|
148
|
-
The dyslexia-friendly group uses wider letterforms and spacing.</p>
|
|
149
142
|
</div>
|
|
150
|
-
<div
|
|
151
|
-
<label class="form-label">Font
|
|
152
|
-
|
|
143
|
+
<div>
|
|
144
|
+
<label class="form-label" for="thx-size">Font size
|
|
145
|
+
<span data-help="The root font size. Every rem value on the site scales with it."></span></label>
|
|
146
|
+
<select id="thx-size" class="form-select" data-model="fontSize.value">
|
|
153
147
|
<option value="13">13px - Compact</option>
|
|
154
148
|
<option value="14">14px - Small</option>
|
|
155
149
|
<option value="15">15px - Slightly small</option>
|
|
@@ -158,51 +152,89 @@
|
|
|
158
152
|
<option value="18">18px - Large</option>
|
|
159
153
|
<option value="20">20px - Extra large</option>
|
|
160
154
|
</select>
|
|
161
|
-
<p class="form-hint text-muted" style="margin-top:.4rem;font-size:.8rem;">Root font size. Every
|
|
162
|
-
rem value scales with it.</p>
|
|
163
155
|
</div>
|
|
164
156
|
</div>
|
|
165
157
|
</div>
|
|
166
158
|
</div>
|
|
167
|
-
</div>
|
|
168
159
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
</p>
|
|
181
|
-
<button id="theme-add-override" class="btn btn-ghost btn-sm" type="button">
|
|
182
|
-
<span data-icon="plus"></span> Add override
|
|
183
|
-
</button>
|
|
160
|
+
<!-- Element overrides -->
|
|
161
|
+
<div class="tab-panel">
|
|
162
|
+
<div class="thx-card">
|
|
163
|
+
<div class="thx-card-title">Element overrides
|
|
164
|
+
<span data-help-title="Element overrides" data-help="An override re-themes one region of the page: give it a CSS selector, then adopt another theme there wholesale, or set individual --dm-* tokens. Everything inside the region follows, including components drawn later, because a Domma theme is nothing but a block of --dm-* values. Click an override to edit it; right-click it (or ⋮, or Shift+F10) to switch it off, duplicate, move or remove it. Order matters: later overrides win where they overlap."></span>
|
|
165
|
+
<span class="thx-spacer"></span>
|
|
166
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="addOverride">
|
|
167
|
+
<span data-icon="plus" data-icon-size="14"></span> Add override</button>
|
|
168
|
+
</div>
|
|
169
|
+
<p class="thx-note" data-bind-hidden="overrides.value.length">No overrides yet - the whole site uses the site theme.</p>
|
|
170
|
+
<div class="thx-list" data-each="rows.value key=id"></div>
|
|
184
171
|
</div>
|
|
185
|
-
</div>
|
|
186
172
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
<
|
|
195
|
-
<button id="theme-refresh-css" class="btn btn-ghost btn-sm" type="button">
|
|
196
|
-
<span data-icon="refresh-cw"></span> Refresh
|
|
197
|
-
</button>
|
|
198
|
-
</div>
|
|
199
|
-
<div class="card-body">
|
|
200
|
-
<p class="text-muted" style="margin-top:0;font-size:.85rem;">
|
|
201
|
-
Exactly what is injected into every public page, ahead of your custom CSS.
|
|
202
|
-
</p>
|
|
203
|
-
<pre id="theme-css-preview" class="p-3"
|
|
204
|
-
style="max-height:320px;overflow:auto;background:var(--dm-surface-raised);border-radius:var(--dm-radius-md);font-size:.8rem;margin:0;">(nothing yet)</pre>
|
|
173
|
+
<div class="thx-card">
|
|
174
|
+
<div class="thx-card-title">Generated CSS
|
|
175
|
+
<span data-help="Exactly what is injected into every public page for the overrides, ahead of your custom CSS - so hand-written CSS still wins. It follows your edits, before you save."></span>
|
|
176
|
+
<span class="thx-spacer"></span>
|
|
177
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="refreshCss" title="Compile again">
|
|
178
|
+
<span data-icon="refresh-cw" data-icon-size="14"></span> Refresh</button>
|
|
179
|
+
</div>
|
|
180
|
+
<pre class="thx-css" data-bind-class="cssError.value && 'is-bad'" data-bind-text="cssText.value">(nothing yet)</pre>
|
|
205
181
|
</div>
|
|
206
182
|
</div>
|
|
207
183
|
</div>
|
|
208
184
|
</div>
|
|
185
|
+
|
|
186
|
+
<style>
|
|
187
|
+
.thx { display: flex; flex-direction: column; gap: .75rem; }
|
|
188
|
+
.thx [hidden], .thx-form [hidden] { display: none !important; }
|
|
189
|
+
.thx-alert { margin: 0; display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; padding: .6rem .8rem; border-radius: var(--dm-radius-md);
|
|
190
|
+
background: color-mix(in srgb, var(--dm-warning) 16%, transparent); color: var(--dm-text); }
|
|
191
|
+
.thx-alert.is-bad { background: color-mix(in srgb, var(--dm-danger) 14%, transparent); }
|
|
192
|
+
.thx-alert.is-dirty { background: color-mix(in srgb, var(--dm-primary) 12%, transparent); }
|
|
193
|
+
.thx-count { opacity: .65; font-size: var(--dm-text-xs); margin-left: .2rem; }
|
|
194
|
+
.thx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .7rem; margin-top: .75rem; }
|
|
195
|
+
.thx-card-title { display: flex; align-items: center; gap: .35rem; font-weight: 600; }
|
|
196
|
+
.thx-card-title .btn { display: inline-flex; align-items: center; gap: .3rem; }
|
|
197
|
+
.thx-spacer { flex: 1; }
|
|
198
|
+
.thx-grid { display: grid; grid-template-columns: 1fr 1fr; gap: .75rem 1rem; }
|
|
199
|
+
.thx-grid-3 { grid-template-columns: 2fr 1fr 1fr; }
|
|
200
|
+
.thx .form-label, .thx-form .form-label { display: flex; align-items: center; gap: .3rem; }
|
|
201
|
+
.thx-setting { display: flex; align-items: center; justify-content: space-between; gap: .75rem; }
|
|
202
|
+
.thx-setting .form-label { margin: 0; }
|
|
203
|
+
.thx-hint { display: block; color: var(--dm-text-muted); font-size: var(--dm-text-xs); margin-top: .3rem; }
|
|
204
|
+
.thx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
|
|
205
|
+
.thx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
|
|
206
|
+
.thx-chips { display: flex; flex-wrap: wrap; gap: .4rem; }
|
|
207
|
+
.thx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .15rem .55rem; border-radius: 999px; white-space: nowrap;
|
|
208
|
+
background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); }
|
|
209
|
+
.thx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
|
|
210
|
+
.thx-chip.is-warn { background: color-mix(in srgb, var(--dm-warning) 18%, transparent); color: var(--dm-warning); }
|
|
211
|
+
.thx-chip.is-danger { background: color-mix(in srgb, var(--dm-danger) 16%, transparent); color: var(--dm-danger); }
|
|
212
|
+
.thx-list { display: flex; flex-direction: column; }
|
|
213
|
+
.thx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
|
|
214
|
+
.thx-row:last-child { border-bottom: 0; }
|
|
215
|
+
.thx-row:hover, .thx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
|
|
216
|
+
.thx-row.is-off .thx-main strong { color: var(--dm-text-muted); }
|
|
217
|
+
.thx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
218
|
+
.thx-main strong, .thx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
219
|
+
.thx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
|
|
220
|
+
.thx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
|
|
221
|
+
.thx-kebab:hover { color: var(--dm-primary); }
|
|
222
|
+
.thx-css { margin: 0; max-height: 320px; overflow: auto; padding: .75rem; background: var(--dm-surface-raised); border-radius: var(--dm-radius-md);
|
|
223
|
+
font-size: var(--dm-text-xs); color: var(--dm-text); white-space: pre-wrap; }
|
|
224
|
+
.thx-css.is-bad { color: var(--dm-danger); }
|
|
225
|
+
/* The override editor (slideover) */
|
|
226
|
+
.thx-form { display: flex; flex-direction: column; gap: .85rem; }
|
|
227
|
+
.thx-tok { display: flex; gap: .4rem; align-items: center; margin-bottom: .4rem; flex-wrap: wrap; }
|
|
228
|
+
.thx-tok .thx-tok-name { flex: 1 1 12rem; }
|
|
229
|
+
.thx-tok .thx-tok-value { flex: 0 1 9rem; }
|
|
230
|
+
.thx-tok input[type="color"] { flex: 0 0 38px; height: 34px; padding: 0; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); background: none; cursor: pointer; }
|
|
231
|
+
.thx-tok .thx-err { flex-basis: 100%; margin: 0; }
|
|
232
|
+
.thx-diff { padding: .5rem .6rem; border-radius: var(--dm-radius-md); background: var(--dm-surface-raised); font-size: var(--dm-text-xs); display: flex; flex-direction: column; gap: .35rem; }
|
|
233
|
+
.thx-diff p { margin: 0; }
|
|
234
|
+
.thx-diff .is-warn { color: var(--dm-warning); }
|
|
235
|
+
.thx-diff-list { display: flex; flex-wrap: wrap; gap: .15rem .75rem; color: var(--dm-text-muted); }
|
|
236
|
+
.thx-sw { display: inline-block; width: .7rem; height: .7rem; border-radius: 2px; border: 1px solid var(--dm-border); vertical-align: middle; margin-right: .25rem; }
|
|
237
|
+
.thx-actions { display: flex; align-items: center; gap: .4rem; padding-top: .6rem; border-top: 1px solid var(--dm-border); }
|
|
238
|
+
.thx-actions .btn { display: inline-flex; align-items: center; gap: .3rem; }
|
|
239
|
+
@media (max-width: 720px) { .thx-grid, .thx-grid-3 { grid-template-columns: 1fr; } }
|
|
240
|
+
</style>
|
|
@@ -1,12 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
</
|
|
1
|
+
<!-- Data › Views. Bound with M.applyBindings - see admin/views/views-list.js, which also inserts the row
|
|
2
|
+
template (the router renders a templateUrl through _.render, eating any {{…}}). No page heading: the
|
|
3
|
+
Tool banner above is the heading, and New view is a banner action. -->
|
|
4
|
+
<div class="vwx">
|
|
5
|
+
<p class="vwx-note">Saved queries over a collection - filter, sort, group, join - shown with
|
|
6
|
+
<code>[view slug="…" /]</code> on any page, or read through the API.
|
|
7
|
+
<span data-help-title="Views" data-help="A view reads one collection through a pipeline of stages and shows the result in a display mode (table, cards, list…). MongoDB collections run the pipeline as a native aggregation; file collections run the same stages in the CMS. Access decides who may see it: public, or only the roles listed."></span></p>
|
|
8
|
+
|
|
9
|
+
<p class="vwx-scope" data-bind-hidden="!project">
|
|
10
|
+
<span data-bind-text="scopeText.value"></span>
|
|
11
|
+
<a href="#/views">Show all views</a>
|
|
12
|
+
</p>
|
|
7
13
|
|
|
8
|
-
<div class="card">
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
<div class="vwx-card">
|
|
15
|
+
<div class="vwx-bar">
|
|
16
|
+
<div class="vwx-seg" role="group" aria-label="Show">
|
|
17
|
+
<button type="button" data-on-click="showAll" data-bind-class="filter.value === 'all' && 'is-on'">All <b data-bind-text="counts.value.all"></b></button>
|
|
18
|
+
<button type="button" data-on-click="showPublic" data-bind-class="filter.value === 'public' && 'is-on'">Public <b data-bind-text="counts.value.public"></b></button>
|
|
19
|
+
<button type="button" data-on-click="showRestricted" data-bind-class="filter.value === 'restricted' && 'is-on'">Restricted <b data-bind-text="counts.value.restricted"></b></button>
|
|
20
|
+
<button type="button" data-on-click="showBroken" data-bind-class="filter.value === 'broken' && 'is-on'" data-bind-hidden="!counts.value.broken">Missing source <b data-bind-text="counts.value.broken"></b></button>
|
|
21
|
+
</div>
|
|
22
|
+
<input type="search" class="form-input vwx-search" placeholder="Search views…" aria-label="Search views" data-model="query.value">
|
|
23
|
+
<span data-help-title="The list" data-help="Click a view to edit it. Right-click it (or ⋮, or Shift+F10) to preview its results, copy its shortcode, duplicate it or delete it. A view whose collection has been deleted is listed first, in red - it errors wherever it is shown until it reads from another collection."></span>
|
|
24
|
+
</div>
|
|
25
|
+
<p class="vwx-note" data-bind-hidden="!loading.value">Loading…</p>
|
|
26
|
+
<p class="vwx-note is-bad" data-bind-hidden="!loadError.value" data-bind-text="loadError.value"></p>
|
|
27
|
+
<p class="vwx-note" data-bind-hidden="loading.value || loadError.value || rows.value.length" data-bind-text="emptyText.value"></p>
|
|
28
|
+
<div class="vwx-list" data-each="rows.value key=slug"></div>
|
|
11
29
|
</div>
|
|
12
30
|
</div>
|
|
31
|
+
|
|
32
|
+
<style>
|
|
33
|
+
.vwx { display: flex; flex-direction: column; gap: .75rem; }
|
|
34
|
+
.vwx [hidden] { display: none !important; }
|
|
35
|
+
.vwx-note { margin: 0; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
|
|
36
|
+
.vwx-note.is-bad { color: var(--dm-danger); }
|
|
37
|
+
.vwx-scope { margin: 0; display: flex; gap: .5rem; align-items: center; padding: .5rem .75rem; border-radius: var(--dm-radius-md);
|
|
38
|
+
background: color-mix(in srgb, var(--dm-primary) 10%, transparent); color: var(--dm-text); font-size: var(--dm-text-sm); }
|
|
39
|
+
.vwx-card { border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); padding: .75rem .9rem; display: flex; flex-direction: column; gap: .6rem; }
|
|
40
|
+
.vwx-bar { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
|
|
41
|
+
.vwx-search { flex: 1; min-width: 10rem; max-width: 22rem; }
|
|
42
|
+
.vwx-seg { display: inline-flex; border: 1px solid var(--dm-border); border-radius: var(--dm-radius-md); overflow: hidden; }
|
|
43
|
+
.vwx-seg button { border: 0; background: none; color: var(--dm-text); padding: .3rem .7rem; cursor: pointer; font: inherit; font-size: var(--dm-text-sm); }
|
|
44
|
+
.vwx-seg button + button { border-left: 1px solid var(--dm-border); }
|
|
45
|
+
.vwx-seg button b { font-weight: 500; opacity: .65; margin-left: .2rem; }
|
|
46
|
+
.vwx-seg button:hover { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); }
|
|
47
|
+
.vwx-seg button.is-on { background: var(--dm-primary); color: var(--dm-primary-text); }
|
|
48
|
+
.vwx-list { display: flex; flex-direction: column; }
|
|
49
|
+
.vwx-row { display: flex; align-items: center; gap: .75rem; padding: .55rem .35rem; border-bottom: 1px solid var(--dm-border); cursor: pointer; }
|
|
50
|
+
.vwx-row:last-child { border-bottom: 0; }
|
|
51
|
+
.vwx-row:hover, .vwx-row:focus-visible { background: var(--dm-table-hover-bg, color-mix(in srgb, var(--dm-text) 6%, transparent)); outline: none; }
|
|
52
|
+
.vwx-row.is-broken { box-shadow: inset 3px 0 0 var(--dm-danger); }
|
|
53
|
+
.vwx-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
|
54
|
+
.vwx-main strong, .vwx-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
55
|
+
.vwx-main small { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
|
|
56
|
+
.vwx-main small.is-bad { color: var(--dm-danger); }
|
|
57
|
+
.vwx-chips { display: inline-flex; gap: .3rem; flex-shrink: 0; flex-wrap: wrap; justify-content: flex-end; }
|
|
58
|
+
.vwx-chip { display: inline-flex; align-items: center; font-size: var(--dm-text-xs); font-weight: 600; padding: .1rem .5rem; border-radius: 999px; white-space: nowrap;
|
|
59
|
+
background: color-mix(in srgb, var(--dm-text) 10%, transparent); color: var(--dm-text-muted); max-width: 14rem; overflow: hidden; text-overflow: ellipsis; }
|
|
60
|
+
.vwx-chip.is-ok { background: color-mix(in srgb, var(--dm-success) 16%, transparent); color: var(--dm-success); }
|
|
61
|
+
.vwx-chip.is-danger { background: color-mix(in srgb, var(--dm-danger) 16%, transparent); color: var(--dm-danger); }
|
|
62
|
+
.vwx-chip.is-info { background: color-mix(in srgb, var(--dm-info, var(--dm-primary)) 14%, transparent); color: var(--dm-info, var(--dm-primary)); }
|
|
63
|
+
.vwx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
|
|
64
|
+
.vwx-kebab:hover { color: var(--dm-primary); }
|
|
65
|
+
@media (max-width: 720px) { .vwx-chips .vwx-chip.is-info { display: none; } }
|
|
66
|
+
</style>
|
|
@@ -1 +1,37 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{apiRequest as $}from"../api.js";import{addBannerAction as ge}from"../lib/plugin-chrome.js";import{helpPopovers as Q}from"../lib/help-popover.js";import{onceClosed as he,openMenuAt as X,sidebarCount as Z,viewLifetime as be}from"../lib/tool-kit.js";import{filterByProject as ye,getProjectFromHash as we}from"../lib/project-context.js";import{actionRow as xe,arrangeActions as Ee,collectionsOf as Me,duplicateAction as ke,isMongoMissing as Se,projectNotice as $e}from"../lib/actions-arrange.js";const ee=/^#\/(actions|projects\/[^/?#]+\/actions)\/?(\?.*)?$/,te="#/actions",T="actions.duplicate.draft",Te=`
|
|
2
|
+
<div class="acx-row" data-slug="{{slug}}" tabindex="0">
|
|
3
|
+
<span class="acx-icon" aria-hidden="true"><span data-icon="zap" data-icon-size="16"></span></span>
|
|
4
|
+
<span class="acx-main"><strong>{{title}}</strong><small>{{meta}}</small></span>
|
|
5
|
+
<span class="acx-chips">
|
|
6
|
+
<span class="acx-chip is-col" title="Runs on this collection's entries" data-bind-hidden="!collection">{{collection}}</span>
|
|
7
|
+
<span class="acx-chip is-trig" title="Its button">{{trigger}}</span>
|
|
8
|
+
<span class="acx-chip is-flow" title="State transition" data-bind-hidden="!transition">{{transition}}</span>
|
|
9
|
+
<span class="acx-chip" title="{{stepsTitle}}" data-bind-class="noSteps ? 'is-warn' : ''">{{steps}}</span>
|
|
10
|
+
<span class="acx-chip" title="Who may run it">{{roles}}</span>
|
|
11
|
+
<span class="acx-chip" title="Project" data-bind-hidden="!project">{{project}}</span>
|
|
12
|
+
</span>
|
|
13
|
+
<button type="button" class="acx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
|
|
14
|
+
</div>`,C=u=>String(u??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function Ce(u){const l=document.createElement("template");return l.innerHTML=u.trim(),l.content.firstElementChild}const o=be(ee);export const actionsListView={templateUrl:"/admin/js/templates/actions-list.html",async onMount(u){o.reset();const l=u.get(0).querySelector(".acx")||u.get(0),f=()=>ee.test(location.hash)&&l.isConnected,v=we(),i=M.observable([]),q=M.observable(0),U=M.observable(!0),L=M.observable(""),h=M.observable(!1),H=M.observable(""),b=M.observable(""),y=M.observable([]);o.add(M.effect(()=>{y.value=Ee(i.value,{query:H.value,collection:b.value}).map(xe)}));const P=M.computed(()=>Me(i.value)),ae=M.computed(()=>{const e=y.value.length,t=i.value.length;return e===t?`${t} action${t===1?"":"s"}`:`${e} of ${t}`}),ne=M.computed(()=>i.value.length?"No actions match.":"No actions yet - New action in the banner makes one."),se=M.computed(()=>$e(q.value,i.value.length,v)),w=e=>i.peek().find(t=>t.slug===e);async function A(){try{const e=await $("/actions/_count",{method:"GET"}).catch(()=>null);if(!f())return;if(e?.available===!1){h.value=!0;return}const t=await $("/actions",{method:"GET"});if(!f())return;const a=Array.isArray(t)?t:[];q.value=a.length,i.value=v?ye(a,v):a,h.value=!1,L.value=""}catch(e){if(!f())return;Se(e)?h.value=!0:L.value=`Could not load the actions: ${e.message||e}`}finally{U.value=!1}}const D=e=>{location.hash=`#/actions/edit/${encodeURIComponent(e)}`},oe=()=>{location.hash=v?`#/actions/new?project=${encodeURIComponent(v)}`:"#/actions/new"};async function le(e){if(await E.confirm(`Delete the action "${C(e.title||e.slug)}"? Its button disappears from the "${C(e.collection)}" entries. This cannot be undone.`))try{await $(`/actions/${encodeURIComponent(e.slug)}`,{method:"DELETE"}),E.toast(`Deleted "${e.title||e.slug}".`,{type:"success"}),Z(te),await A()}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}async function ie(e){try{await navigator.clipboard.writeText(e.slug),E.toast(`Copied "${e.slug}".`,{type:"success"})}catch{E.toast("The browser would not copy it - the slug is in the row.",{type:"warning"})}}let d=null;const O=new Set;function B(e,t=null){const a=w(e);if(!a){E.toast(`There is no action "${e}" here.`,{type:"warning"});return}const R={title:t?.title??`${a.title||a.slug} copy`,slug:t?.slug??""},g={title:M.observable(R.title),slug:M.observable(R.slug)},_=()=>({title:g.title.peek(),slug:g.slug.peek()}),de=JSON.stringify(R),J=()=>JSON.stringify(_())!==de||!!t,k=M.observable(!1),p=M.computed(()=>ke(a,{title:g.title.value,slug:g.slug.value},i.value.map(n=>n.slug))),W={...g,saving:k,titleErr:M.computed(()=>p.value.errors.title||""),slugErr:M.computed(()=>p.value.errors.slug||""),slugHint:M.computed(()=>p.value.errors.slug?"":`Will be created as "${p.value.payload.slug}".`),cannot:M.computed(()=>k.value||Object.keys(p.value.errors).length>0)},s=E.slideover({title:`Duplicate ${C(a.title||a.slug)}`,size:"sm",position:"right"}),m=Ce(`
|
|
15
|
+
<div class="acx-form">
|
|
16
|
+
<p class="acx-note">The copy runs on the same collection, with the same button, steps, roles and
|
|
17
|
+
transition. Edit it afterwards to make it different.</p>
|
|
18
|
+
<div>
|
|
19
|
+
<label class="form-label" for="acx-f-title">Title <span class="text-danger">*</span></label>
|
|
20
|
+
<input id="acx-f-title" type="text" class="form-input" maxlength="120" data-model="title.value">
|
|
21
|
+
<small class="acx-err" data-bind-hidden="!titleErr.value" data-bind-text="titleErr.value"></small>
|
|
22
|
+
</div>
|
|
23
|
+
<div>
|
|
24
|
+
<label class="form-label" for="acx-f-slug">Slug
|
|
25
|
+
<span data-help="The action's id - in its address and in the [transitions] shortcode. Leave it empty to make one from the title."></span></label>
|
|
26
|
+
<input id="acx-f-slug" type="text" class="form-input font-mono" maxlength="100" placeholder="from the title" data-model="slug.value">
|
|
27
|
+
<small class="acx-err" data-bind-hidden="!slugErr.value" data-bind-text="slugErr.value"></small>
|
|
28
|
+
<small class="acx-hint" data-bind-hidden="!slugHint.value" data-bind-text="slugHint.value"></small>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="acx-actions">
|
|
31
|
+
<small class="acx-hint">Ctrl+Enter saves.</small>
|
|
32
|
+
<span class="acx-spacer"></span>
|
|
33
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
34
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
|
|
35
|
+
<span data-icon="copy" data-icon-size="13"></span> Duplicate</button>
|
|
36
|
+
</div>
|
|
37
|
+
</div>`);let j=!1;async function G(){if(!W.cannot.peek()){k.value=!0;try{const n=await $("/actions",{method:"POST",body:JSON.stringify(p.peek().payload)});E.toast(`Made "${n?.title||p.peek().payload.title}".`,{type:"success"}),j=!0,s.close(),Z(te),f()&&await A()}catch(n){E.toast(`Not duplicated: ${n.message||n}`,{type:"error"})}finally{k.value=!1}}}s.element.appendChild(m);const pe=Q(m),fe=M.applyBindings(W,m,{methods:{save:G,cancel:()=>s.close()}});m.addEventListener("keydown",n=>{(n.ctrlKey||n.metaKey)&&n.key==="Enter"&&(n.preventDefault(),G())});const V={source:a.slug,dirty:J,values:_};d=V,O.add(s),he(s,()=>{fe.dispose(),pe(),O.delete(s),d===V&&(d=null)});const me=s.close.bind(s);let N=!1;s.close=async n=>{if(!j&&n!==!0&&J()){if(N)return;N=!0;const ve=await E.confirm("Discard the changes you have not saved?");if(N=!1,!ve)return}return j=!0,me()},I.scan(m),s.open();const Y=m.querySelector("#acx-f-title");Y?.focus(),Y?.select()}function ce(e){const t=w(e);return t?[{header:t.title||t.slug},{label:"Edit",icon:"edit",action:()=>D(t.slug)},{label:"Duplicate\u2026",icon:"copy",action:()=>B(t.slug)},{type:"divider"},{label:`Open ${t.collection||"collection"} entries`,icon:"database",disabled:!t.collection,action:()=>{location.hash=`#/collections/${encodeURIComponent(t.collection)}/entries`}},{label:"Copy slug",icon:"clipboard",action:()=>ie(t)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>le(t)}]:[]}const c=l.querySelector(".acx-list");c.innerHTML=Te;const re=M.applyBindings({loading:U,loadError:L,needsMongo:h,query:H,collection:b,collections:P,rows:y,countText:ae,emptyText:ne,notice:se},l);o.add(()=>re.dispose()),o.add(Q(l,{position:"bottom"}));const x=l.querySelector(".acx-filter");o.add(M.effect(()=>{const e=P.value,t=b.peek();x.replaceChildren(new Option("All collections",""),...e.map(a=>new Option(a,a))),x.value=e.includes(t)?t:"",x.value!==t&&(b.value=x.value)})),o.add(M.effect(()=>{y.value,requestAnimationFrame(()=>{c.isConnected&&I.scan(c)})}));const ue=E.contextMenu(c,{cursor:"glyph",match:".acx-row",inherit:!1,density:"compact",items:e=>ce(e.dataset.slug)});o.add(()=>ue?.destroy?.());const F=e=>{const t=e.target.closest(".acx-row");if(!t)return;const a=e.target.closest(".acx-kebab");if(a){X(a);return}D(t.dataset.slug)},K=e=>{const t=e.target.closest(".acx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),X(t)):e.key==="Enter"&&e.target===t&&D(t.dataset.slug))};c.addEventListener("click",F),c.addEventListener("keydown",K),o.add(()=>{c.removeEventListener("click",F),c.removeEventListener("keydown",K)}),ge(u,{id:"actions-new",icon:"plus",title:"New action",onClick:oe});const z=e=>{d?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",z),o.add(()=>window.removeEventListener("beforeunload",z)),o.add(()=>{if(d?.dirty())try{S.set(T,{source:d.source,values:d.values(),at:new Date().toISOString()})}catch{}[...O].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(l),await A(),!f())return;let r=null;try{r=S.get(T)}catch{}if(r?.values&&r.source){try{S.remove?S.remove(T):S.set(T,null)}catch{}w(r.source)&&await E.confirm(`You left a copy of "${C(w(r.source).title||r.source)}" unfinished. Pick up where you left off?`)&&f()&&B(r.source,r.values)}}};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import{apiRequest as y}from"../api.js";import{addBannerAction as z}from"../lib/plugin-chrome.js";import{helpPopovers as Q}from"../lib/help-popover.js";import{openMenuAt as D,sidebarCount as P,viewLifetime as X}from"../lib/tool-kit.js";import{getProjectFromHash as Z}from"../lib/project-context.js";import{FILTERS as ee,arrangeEndpoints as te,countEndpoints as ne,endpointRow as g,endpointUrl as q,pathParams as oe,projectsOf as ae}from"../lib/api-endpoints-arrange.js";const O=/^#\/(api-endpoints|projects\/[^/?#]+\/apis)\/?(\?.*)?$/,N="#/api-endpoints",w="api-endpoints.filter",se=`
|
|
2
|
+
<div class="apx-row" data-id="{{id}}" tabindex="0" data-bind-class="rowCls">
|
|
3
|
+
<span class="apx-verb" title="Endpoints answer GET requests">GET</span>
|
|
4
|
+
<span class="apx-main"><strong>{{name}}</strong><code title="{{url}}">{{url}}</code><small>{{meta}}</small></span>
|
|
5
|
+
<span class="apx-chips">
|
|
6
|
+
<span class="apx-chip" title="Project - the first part of its URL">{{project}}</span>
|
|
7
|
+
<span class="apx-chip" data-bind-class="authCls" title="Who may call it">{{auth}}</span>
|
|
8
|
+
<span class="apx-chip is-off" data-bind-hidden="!off" title="Answers 404 until enabled">Disabled</span>
|
|
9
|
+
</span>
|
|
10
|
+
<button type="button" class="apx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
|
|
11
|
+
</div>`,f=l=>String(l??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),c=X(O);async function ie(l){try{return await navigator.clipboard.writeText(l),!0}catch{const o=document.createElement("textarea");o.value=l,o.setAttribute("readonly",""),o.style.cssText="position:fixed;left:-9999px;top:0;",document.body.appendChild(o),o.select();let m=!1;try{m=document.execCommand("copy")}catch{}return o.remove(),m}}export const apiEndpointsView={templateUrl:"/admin/js/templates/api-endpoints.html",async onMount(l){c.reset();const o=l.get(0).querySelector(".apx")||l.get(0),m=()=>O.test(location.hash)&&o.isConnected,s=/^#\/projects\//.test(location.hash)?Z():null,a=M.observable([]),x=M.observable(!0),v=M.observable("");let C="all";try{C=ee.includes(S.get(w))?S.get(w):"all"}catch{}const r=M.observable(C),b=M.observable(s||""),k=M.observable("");c.add(M.effect(()=>{const e=r.value;try{S.set(w,e)}catch{}}));const d=M.computed(()=>s?a.value.filter(e=>e.project===s):a.value),$=M.observable([]);c.add(M.effect(()=>{$.value=te(d.value,{filter:r.value,project:b.value,query:k.value}).map(g)}));const B=M.computed(()=>ne(d.value)),F=M.computed(()=>d.value.length?"No endpoints match.":s?"This project has no endpoints yet - New endpoint in the banner makes one.":"No endpoints yet - New endpoint in the banner builds one."),H=M.computed(()=>s?`Showing the ${d.value.length} endpoint${d.value.length===1?"":"s"} of the "${s}" project (of ${a.value.length}).`:""),j=e=>a.peek().find(t=>t.id===e);async function _(){try{const e=await y("/api-endpoints",{method:"GET"});if(!m())return;a.value=Array.isArray(e)?e:[],v.value=""}catch(e){v.value=`Could not load the endpoints: ${e.message||e}`}finally{x.value=!1}}const T=e=>`#/api-endpoints/edit/${encodeURIComponent(e)}`,G=()=>s?`#/api-endpoints/new?project=${encodeURIComponent(s)}`:"#/api-endpoints/new";async function L(e,t){if(!(!t&&!await E.confirm(`Disable "${f(e.name)}"? Callers of ${f(g(e).url)} get 404s until it is enabled again.`)))try{const n=await y(`/api-endpoints/${encodeURIComponent(e.id)}`,{method:"PUT",body:JSON.stringify({enabled:t})});a.value=a.peek().map(i=>i.id===e.id?{...i,...n||{},enabled:t}:i),E.toast(`"${e.name}" ${t?"enabled":"disabled"}.`,{type:"success"}),P(N)}catch(n){E.toast(`Not changed: ${n.message||n}`,{type:"error"})}}async function K(e){if(await E.confirm(`Delete the endpoint "${f(e.name)}"? Callers of ${f(g(e).url)} get 404s at once. This cannot be undone.`))try{await y(`/api-endpoints/${encodeURIComponent(e.id)}`,{method:"DELETE"}),a.value=a.peek().filter(t=>t.id!==e.id),E.toast(`Deleted "${e.name}".`,{type:"success"}),P(N)}catch(t){E.toast(`Delete failed: ${t.message||t}`,{type:"error"})}}async function W(e){const t=q(e,location.origin);E.toast(await ie(t)?`Copied ${t}`:`Copy this URL: ${t}`,{type:"success"})}function J(e){const t=j(e);if(!t)return[];const n=t.enabled===!1,i=oe(t.path).length>0,u=!n&&t.auth==="public"&&!i;return[{header:t.name||t.path},{label:"Open in the builder",icon:"edit",action:()=>{location.hash=T(t.id)}},{label:"Copy URL",icon:"copy",action:()=>W(t)},{label:i?"Open in a new tab (needs params)":t.auth==="public"?"Open in a new tab":"Open in a new tab (not public)",icon:"external-link",disabled:!u,action:()=>window.open(q(t,location.origin),"_blank","noopener")},{type:"divider"},n?{label:"Enable",icon:"play",action:()=>L(t,!0)}:{label:"Disable\u2026",icon:"pause",action:()=>L(t,!1)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>K(t)}]}const p=o.querySelector(".apx-list");p.innerHTML=se;const h=o.querySelector(".apx-project");c.add(M.effect(()=>{const e=ae(a.value),t=b.peek();h.replaceChildren();const n=document.createElement("option");n.value="",n.textContent="All projects",h.appendChild(n);for(const i of e){const u=document.createElement("option");u.value=i,u.textContent=i,h.appendChild(u)}h.value=e.includes(t)?t:"",!s&&t&&!e.includes(t)&&(b.value="")}));const V=M.applyBindings({endpoints:a,loading:x,loadError:v,filter:r,project:b,query:k,rows:$,counts:B,emptyText:F,scopeText:H,projectLocked:M.observable(!!s)},o,{methods:{showAll:()=>{r.value="all"},showEnabled:()=>{r.value="enabled"},showDisabled:()=>{r.value="disabled"},showPublic:()=>{r.value="public"}}});c.add(()=>V.dispose()),c.add(Q(o,{position:"bottom"}));const Y=E.contextMenu(p,{cursor:"glyph",match:".apx-row",inherit:!1,density:"compact",items:e=>J(e.dataset.id)});c.add(()=>Y?.destroy?.());const R=e=>{j(e)&&(location.hash=T(e))},U=e=>{const t=e.target.closest(".apx-row");if(!t)return;const n=e.target.closest(".apx-kebab");if(n){D(n);return}R(t.dataset.id)},A=e=>{const t=e.target.closest(".apx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),D(t)):e.key==="Enter"&&e.target===t&&R(t.dataset.id))};p.addEventListener("click",U),p.addEventListener("keydown",A),c.add(()=>{p.removeEventListener("click",U),p.removeEventListener("keydown",A)}),z(l,{id:"api-endpoints-new",icon:"plus",title:"New endpoint",onClick:()=>{location.hash=G()}}),I.scan(o),await _()}};
|