domma-cms 1.4.0 → 1.6.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/admin/css/admin.css +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/dashboards-arrange.js +3 -0
- package/admin/js/lib/dashboards-strip.js +1 -0
- package/admin/js/lib/hotkeys-ui.js +23 -0
- package/admin/js/lib/hotkeys.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +6 -6
- package/admin/js/templates/dashboards.html +2 -0
- package/admin/js/templates/docs/usage-dashboard.html +16 -12
- package/admin/js/templates/docs/usage-tools.html +23 -0
- package/admin/js/views/dashboard-board.js +18 -19
- package/admin/js/views/dashboard.js +2 -2
- package/admin/js/views/dashboards.js +1 -0
- package/admin/js/views/index.js +1 -1
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +23 -2
- package/plugins/_lib/admin/mail/keyboard.js +16 -1
- package/plugins/_lib/admin/mail/scheduling.js +22 -0
- package/plugins/blog/plugin.json +2 -1
- package/plugins/booking/plugin.json +2 -1
- package/plugins/free-tier.lock.json +13 -13
- package/plugins/mail-reader/CLAUDE.md +6 -0
- package/plugins/mail-reader/docs/guide.md +2 -2
- package/plugins/mail-reader/plugin.json +2 -1
- package/plugins/shopping-cart/plugin.json +2 -1
- package/plugins/support/plugin.json +2 -1
- package/server/routes/api/auth.js +2 -2
- package/server/routes/api/sidebar.js +17 -6
- package/server/services/dashboardSettings.js +8 -4
- package/server/services/widgets.js +5 -11
- package/admin/js/templates/my-dashboard.html +0 -3
- package/admin/js/views/my-dashboard.js +0 -1
|
@@ -69,7 +69,8 @@ function formatSize(bytes) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
import {applyIdentity, findIdentity, identityFrom, swapSignature} from './identity.js';
|
|
72
|
-
import {describeWhen, quickTimes} from './scheduling.js';
|
|
72
|
+
import {describeWhen, quickTimes, sendQuestion} from './scheduling.js';
|
|
73
|
+
import {isSendShortcut} from './keyboard.js';
|
|
73
74
|
import {fmtDateTime} from '../ui/dates.js';
|
|
74
75
|
import {applyTemplate, expandTemplate} from './templates.js';
|
|
75
76
|
import {makeResizable} from './resizable.js';
|
|
@@ -300,7 +301,7 @@ const FORM = `
|
|
|
300
301
|
<button type="button" class="btn btn-sm btn-ghost" data-on-click="later" title="Send this at a time you choose">Later…</button>
|
|
301
302
|
<button type="button" class="btn btn-sm btn-primary" data-on-click="send"
|
|
302
303
|
data-bind-disabled="cannotSend.value" data-bind-text="sendLabel.value" data-bind-title="sendTitle.value"></button>
|
|
303
|
-
<span data-help-title="Sending" data-help="Later… sends at a time you choose, and the button then says when. With an undo delay set in Mail Settings, Send holds the message for those seconds so you can take it back; it has not gone until they pass."></span>
|
|
304
|
+
<span data-help-title="Sending" data-help="Ctrl+Enter (Cmd+Enter on a Mac) sends too, after asking first. Later… sends at a time you choose, and the button then says when. With an undo delay set in Mail Settings, Send holds the message for those seconds so you can take it back; it has not gone until they pass."></span>
|
|
304
305
|
</div>
|
|
305
306
|
</div>`;
|
|
306
307
|
|
|
@@ -848,6 +849,25 @@ export function openCompose({
|
|
|
848
849
|
attachList.addEventListener('click', onAttachClick);
|
|
849
850
|
attachList.addEventListener('keydown', onAttachKey);
|
|
850
851
|
|
|
852
|
+
// Ctrl+Enter (Cmd+Enter): send, from anywhere in the window - the editor included - but only
|
|
853
|
+
// after "Are you sure?", because the combination is easy to hit while typing.
|
|
854
|
+
let askingSend = false;
|
|
855
|
+
const onSendKey = async (e) => {
|
|
856
|
+
if (!isSendShortcut(e)) return;
|
|
857
|
+
e.preventDefault();
|
|
858
|
+
e.stopPropagation();
|
|
859
|
+
if (askingSend || closed || cannotSend.peek()) return;
|
|
860
|
+
askingSend = true;
|
|
861
|
+
try {
|
|
862
|
+
const q = sendQuestion({...collect(), sendAt: scheduledAt.peek(), undoSeconds});
|
|
863
|
+
const ok = await E.confirm(q.text, {title: q.title, confirmText: q.confirmText, cancelText: 'Keep writing'});
|
|
864
|
+
if (ok && !closed && !cannotSend.peek()) await send();
|
|
865
|
+
} finally {
|
|
866
|
+
askingSend = false;
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
slideover.element.addEventListener('keydown', onSendKey, true);
|
|
870
|
+
|
|
851
871
|
slideover.element.appendChild(form);
|
|
852
872
|
const stopResizing = makeResizable(slideover, {
|
|
853
873
|
storageKey: 'domma-mail.compose-width',
|
|
@@ -911,6 +931,7 @@ export function openCompose({
|
|
|
911
931
|
attachMenu?.destroy?.();
|
|
912
932
|
attachList.removeEventListener('click', onAttachClick);
|
|
913
933
|
attachList.removeEventListener('keydown', onAttachKey);
|
|
934
|
+
slideover.element?.removeEventListener('keydown', onSendKey, true);
|
|
914
935
|
try { stopHelp?.(); } catch { /* gone */ }
|
|
915
936
|
closedListeners.splice(0).forEach(fn => { try { fn(); } catch { /* ignore */ } });
|
|
916
937
|
// Domma hides a closed slideover rather than removing it: every message
|
|
@@ -126,11 +126,26 @@ export const SHORTCUTS = [
|
|
|
126
126
|
['r / Shift-R', 'Reply / reply all'],
|
|
127
127
|
['f', 'Forward'],
|
|
128
128
|
['c', 'New message'],
|
|
129
|
+
['Ctrl-Enter', 'Send the message you are writing (asks first; Cmd-Enter on a Mac)'],
|
|
129
130
|
['/', 'Search'],
|
|
130
131
|
['x', 'Select or deselect'],
|
|
131
132
|
['Shift-A', 'Select all on this page'],
|
|
132
133
|
['!', 'Mark as spam, or not spam'],
|
|
133
134
|
['# or Delete', 'Delete'],
|
|
134
135
|
['Esc', 'Clear search or selection'],
|
|
135
|
-
['?', 'This list']
|
|
136
|
+
['?', 'This list'],
|
|
137
|
+
['G then a letter', 'Go to another Tool (G then D for Dashboards); Ctrl-K finds any Tool by name, and ? outside Email lists every hotkey']
|
|
136
138
|
];
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Ctrl+Enter (Cmd+Enter on a Mac) - "send" in the compose window, as in most
|
|
142
|
+
* mail clients. The one combination this file claims, and only in compose,
|
|
143
|
+
* where it asks before anything goes (compose-window.js).
|
|
144
|
+
*
|
|
145
|
+
* @param {KeyboardEvent} event
|
|
146
|
+
* @returns {boolean}
|
|
147
|
+
*/
|
|
148
|
+
export function isSendShortcut(event) {
|
|
149
|
+
if (!event || event.key !== 'Enter' || event.altKey || event.shiftKey || event.isComposing) return false;
|
|
150
|
+
return Boolean(event.ctrlKey || event.metaKey) && !(event.ctrlKey && event.metaKey);
|
|
151
|
+
}
|
|
@@ -203,3 +203,25 @@ export function dueNow(items, now = Date.now()) {
|
|
|
203
203
|
return Number.isFinite(when) && when <= at;
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The "Are you sure?" for Ctrl+Enter: who it goes to, what, and when - a
|
|
209
|
+
* keypress is easy to make by accident, so the question says what would happen.
|
|
210
|
+
*
|
|
211
|
+
* @param {{to?: Array<{name?: string, address?: string}|string>, subject?: string, sendAt?: string|null, undoSeconds?: number}} msg
|
|
212
|
+
* @param {Date} [now]
|
|
213
|
+
* @returns {{title: string, text: string, confirmText: string}}
|
|
214
|
+
*/
|
|
215
|
+
export function sendQuestion({to = [], subject = '', sendAt = null, undoSeconds = 0} = {}, now = new Date()) {
|
|
216
|
+
const names = (Array.isArray(to) ? to : []).map(r => (typeof r === 'string' ? r : (r?.name || r?.address || ''))).filter(Boolean);
|
|
217
|
+
const who = !names.length ? 'nobody yet'
|
|
218
|
+
: names.length <= 3 ? names.join(', ')
|
|
219
|
+
: `${names.slice(0, 2).join(', ')} and ${names.length - 2} more`;
|
|
220
|
+
const what = String(subject || '').trim() ? `"${String(subject).trim().slice(0, 80)}"` : 'this message (no subject)';
|
|
221
|
+
if (sendAt) {
|
|
222
|
+
return {title: 'Schedule this message?', confirmText: 'Schedule',
|
|
223
|
+
text: `Send ${what} to ${who} ${describeWhen(sendAt, now)}?`};
|
|
224
|
+
}
|
|
225
|
+
return {title: 'Send this message?', confirmText: 'Send',
|
|
226
|
+
text: `Send ${what} to ${who} now?${undoSeconds > 0 ? ` You will have ${undoSeconds} seconds to take it back.` : ''}`};
|
|
227
|
+
}
|
package/plugins/blog/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blog",
|
|
3
3
|
"displayName": "Blog",
|
|
4
|
-
"version": "1.9.
|
|
4
|
+
"version": "1.9.5",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Public-facing blog with posts, categories and comments - layouts and listings built from editable blocks.",
|
|
7
7
|
"author": "Domma CMS",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"text": "Blog",
|
|
18
18
|
"icon": "file-text",
|
|
19
19
|
"url": "#/plugins/blog",
|
|
20
|
+
"hotkey": "b",
|
|
20
21
|
"section": "#/plugins/blog",
|
|
21
22
|
"countUrl": "/api/plugins/blog/comments/pending-count",
|
|
22
23
|
"children": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "booking",
|
|
3
3
|
"displayName": "Booking",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.2",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Let people book appointments with you or your team, and rooms or equipment, from a booking page on your site. Opening hours, days off and buffers; confirmation and reminder emails with a link to change or cancel; a diary in the admin. Book straight from a contact or an email.",
|
|
7
7
|
"author": "Domma CMS",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"text": "Booking",
|
|
49
49
|
"icon": "clipboard-check",
|
|
50
50
|
"url": "#/plugins/booking",
|
|
51
|
+
"hotkey": "k",
|
|
51
52
|
"section": "#/plugins/booking",
|
|
52
53
|
"permission": "booking",
|
|
53
54
|
"countUrl": "/api/plugins/booking/count"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"note": "Written by `make sync-free-tier` in dcms-marketplace. Do not edit these plugins here - edit them there and sync.",
|
|
3
|
-
"source": "dcms-marketplace@
|
|
3
|
+
"source": "dcms-marketplace@8b9c2f1",
|
|
4
4
|
"plugins": {
|
|
5
5
|
"blog": {
|
|
6
|
-
"version": "1.9.
|
|
6
|
+
"version": "1.9.5",
|
|
7
7
|
"files": {
|
|
8
8
|
"CLAUDE.md": "926730d510cc38f6764ffcb9afbed1c3ae3a547117c6c0243b14f899271350fb",
|
|
9
9
|
"admin/css/index.css": "6142a6b756e1218157842528830b1e84f592f0b84b7d5ce6449db421b6f4b039",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"lib/render.js": "0e97c6a950bc962c8d8ea4b2f21d06cb73fbcdc738d8942d6935d01309a29b1d",
|
|
51
51
|
"lib/samples.js": "e8cdb77ce22e43937707f6854d3a5cb722437dc584e22ac7b24480bef686cf85",
|
|
52
52
|
"plugin.js": "0aa46a01b5298065497f39729b88e34a9f56ac684b0a14f8d94ab6007f0221d0",
|
|
53
|
-
"plugin.json": "
|
|
53
|
+
"plugin.json": "b4d7637ecb053df7ab182aaed7962c52e47fbee0eedcd041964585fbc85ea7da",
|
|
54
54
|
"plugin.public.js": "67e109da613237d9ec52e7cc2f3442bb61ca5a51cb184448b9f0d993e50f7ae3",
|
|
55
55
|
"public/blog.css": "fd733e6b624190da1e70dc1b6613e2376efd7f7570a28fab311d81006d03d960",
|
|
56
56
|
"public/blog.js": "d01c72a9834dcc7c5241923371f2b164ac30d20ac9f1a5c5dc5b71fa3cb09f08",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
"booking": {
|
|
76
|
-
"version": "1.6.
|
|
76
|
+
"version": "1.6.2",
|
|
77
77
|
"files": {
|
|
78
78
|
"CLAUDE.md": "9a862d4f9ee4676c67af9699820015d7e0ab98b14b62deee6d51cefe0cc28471",
|
|
79
79
|
"admin/css/index.css": "4f7731921c0c5038cb1566a6ab2d43ffc35bf9b161e58f87a0cff423e81b47fa",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"lib/mail.js": "cad8f409df62d6e42f0f4024ec364bbe018a62b842fa3f7c38786fd4e7f92408",
|
|
101
101
|
"lib/store.js": "7aa7967e4694e5a3b85378f60302e5a47394b1d90c7dd86e09578156f1c13e03",
|
|
102
102
|
"plugin.js": "948cf76ee8495d9a6bce85c4f5549107385c8a947bc7690ff08d6d2b870f458d",
|
|
103
|
-
"plugin.json": "
|
|
103
|
+
"plugin.json": "6032a3e333e3ae492a2484fbb0ab93cfffe20c9d01e85f67ef7369c55e5c6f10",
|
|
104
104
|
"plugin.public.js": "4d5b4e8ad6943182ee3b6c6e6f6762a2d3f676d8d7ee363fcb07bd47d3d6532e",
|
|
105
105
|
"public/booking.css": "8b88989fdd04e50efc1688a2f32fe2e422299d7d13b81bb8fce0cc162fa99df6",
|
|
106
106
|
"public/booking.js": "7dfc3d205696967013886567f67a867443c2e370bac29019d69911d695fdbade",
|
|
@@ -138,14 +138,14 @@
|
|
|
138
138
|
}
|
|
139
139
|
},
|
|
140
140
|
"mail-reader": {
|
|
141
|
-
"version": "1.2.
|
|
141
|
+
"version": "1.2.6",
|
|
142
142
|
"files": {
|
|
143
|
-
"CLAUDE.md": "
|
|
143
|
+
"CLAUDE.md": "40b25e33b9f14636bf766b68d1af5be24dcf69ccf6d770b9d080c1ed81664abb",
|
|
144
144
|
"admin/views/mail.js": "c5c6483f9618c75cac8f4be32d3aea99511db76d4b9d38b8ef43bdc1d35db3c5",
|
|
145
145
|
"config.js": "6e65803ad822a9acda40c203631b8eb32a87cf9d9e5f1c7cb59d01aa406075c6",
|
|
146
|
-
"docs/guide.md": "
|
|
146
|
+
"docs/guide.md": "a8febdc46bf60af35c6d980ac37629baa93da94e0970473f6fbe2d3f242ceb63",
|
|
147
147
|
"plugin.js": "7f602a60c4eca7d3637659d03d5de9e242584666846b16c9e5a1b758e4856c9f",
|
|
148
|
-
"plugin.json": "
|
|
148
|
+
"plugin.json": "af84350ede3edcb8b07ca752f8799245519a475c2242e89919a7e3e9b74cc8ff"
|
|
149
149
|
}
|
|
150
150
|
},
|
|
151
151
|
"security": {
|
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
}
|
|
176
176
|
},
|
|
177
177
|
"shopping-cart": {
|
|
178
|
-
"version": "1.1.
|
|
178
|
+
"version": "1.1.1",
|
|
179
179
|
"files": {
|
|
180
180
|
"CLAUDE.md": "70765cf061e97d0a57890d7731e0624b224e36267df42741e763e3efcd076d05",
|
|
181
181
|
"admin/css/index.css": "a6bab1d0072e364ee1d94c2d7d7b99c3660ba91c5eba537e269a579f5b17f924",
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
"lib/shop.js": "4399931dd46ab4dc9bd28e8613c992420ab822bd694b1495ef2b4ad6483b5153",
|
|
200
200
|
"lib/summary.js": "5697ffa45c9cc5338bbcb6eada08671c3e8376c49c629d2ae99b022397523f71",
|
|
201
201
|
"plugin.js": "1f8477188b4054d172b37d953a439b4d5d4a532c6d26daaf9808346f332ddfcf",
|
|
202
|
-
"plugin.json": "
|
|
202
|
+
"plugin.json": "e379f3d5a713684d3f1d32dfa2ae1ec26f8a79a346e2591b1b2ea9b1263d6f54",
|
|
203
203
|
"plugin.public.js": "98fa32b6c69db4a967577f3536fc521c926053319dd20d2bae8bc7e94830e3e8",
|
|
204
204
|
"public/cart.js": "fcc706773a36e4bfd02ea235bcdad629137e996218e0a0e23892e33c689f2a6e",
|
|
205
205
|
"public/lib/money.js": "d22b2b6f80dde6f7887b69e24188c05ea93c02217aba639b5331aa3bd616f4fc",
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
}
|
|
225
225
|
},
|
|
226
226
|
"support": {
|
|
227
|
-
"version": "1.1.
|
|
227
|
+
"version": "1.1.1",
|
|
228
228
|
"files": {
|
|
229
229
|
"CLAUDE.md": "ff42515f210dfdbeb1b14992012177ad8513323a340a0d4fe69a6ec13773db5a",
|
|
230
230
|
"admin/css/index.css": "bad02a487ebb131b64ee55e2a4533c8b79d6ada80fffd34b0952bf3b68204a36",
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
"lib/sender.js": "b20aaee34ade89814a897f68467f9902693843fa6b5c8eb0090c4059193c8227",
|
|
241
241
|
"lib/shape.js": "04d840b589d20700ccb2cfa4c72072f8d9f8e03f0573e2ea1177a7a83a383bea",
|
|
242
242
|
"plugin.js": "e3c50a05fa99cb24375782bbb3ca8c58cf5ddc3036937e6115f4d963715c120e",
|
|
243
|
-
"plugin.json": "
|
|
243
|
+
"plugin.json": "85ea5009e63d12ddbcf2be9d0df330827e3bf3babd35deddd47fce1da807049e",
|
|
244
244
|
"tests/api.test.js": "1fa87f6ae12c48fc34a5fb2b5bbeed6cfaa36329610674d4fae05d64734dbb15",
|
|
245
245
|
"tests/shape.test.js": "364c6b7680699f59fcd449cd448184486488838300f155135daea4c05387cf74"
|
|
246
246
|
}
|
|
@@ -102,3 +102,9 @@ With the Tool switched off its API 404s and the action is simply absent.
|
|
|
102
102
|
|
|
103
103
|
Right-click on the open message is engine code (`_lib/admin/mail/body-menu.js`, the `MENU_BRIDGE` in
|
|
104
104
|
`_lib/mail/render.js`); this version only documents it in docs/guide.md. Ship after the core release.
|
|
105
|
+
|
|
106
|
+
## Messages per page (guide only, 1.2.5; core domma-cms PR #98, 0.116.8)
|
|
107
|
+
|
|
108
|
+
The page-size picker under the list is engine code (`listPageSize()`/`pageAfterResize()` in
|
|
109
|
+
`_lib/admin/mail/settings-section.js`, sent as `limit` by `reader-view.js`, capped by `maxListLimit`);
|
|
110
|
+
this version only documents it in docs/guide.md. 0.116.8 is already released, so it can ship now.
|
|
@@ -41,7 +41,7 @@ You need your mailbox's IMAP details from your email provider: the server name,
|
|
|
41
41
|
|
|
42
42
|
### Message list
|
|
43
43
|
|
|
44
|
-
- Newest first, 50 messages a page. Use **Newer** and **Older** to page.
|
|
44
|
+
- Newest first, 50 messages a page. Use **Newer** and **Older** to page. The picker between them shows 25, 50, 100 or 200 a page instead; this browser remembers your choice, and changing it keeps you at the same messages.
|
|
45
45
|
- Icons on a row show high priority, messages you have replied to, and attachments.
|
|
46
46
|
- **Refresh** (the circular arrow) fetches the list again.
|
|
47
47
|
- Right-click a message for **Open**, **Copy sender's address**, **Copy subject** and **Add sender to contacts**. The last one is shown only when the Contacts Tool is switched on, and is greyed out when the sender is already in your contacts.
|
|
@@ -96,7 +96,7 @@ Whoever runs the site can change these in the plugin's configuration file (`conf
|
|
|
96
96
|
|
|
97
97
|
| Option | Default | Meaning |
|
|
98
98
|
|---|---|---|
|
|
99
|
-
| `listLimit` | 50 | Messages per page |
|
|
99
|
+
| `listLimit` | 50 | Messages per page until you pick another size under the list |
|
|
100
100
|
| `allowRemoteImages` | false | Load remote images without asking |
|
|
101
101
|
| `allowInsecureTLS` | false | Accept mail server certificates that do not verify (see Tips) |
|
|
102
102
|
| `connectionTimeoutMs` | 20000 | How long to wait for a mail server, in milliseconds |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mail-reader",
|
|
3
3
|
"displayName": "Mail Reader",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.6",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Read-only IMAP mailbox reader. Connects to a standard IMAP server and reads mail without ever writing to it.",
|
|
7
7
|
"author": "Darryl Waterhouse",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"text": "Mail Reader",
|
|
18
18
|
"icon": "mail",
|
|
19
19
|
"url": "#/plugins/mail-reader",
|
|
20
|
+
"hotkey": "r",
|
|
20
21
|
"section": "#/plugins/mail-reader",
|
|
21
22
|
"countUrl": "/api/plugins/mail-reader/unread",
|
|
22
23
|
"permission": "email"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shopping-cart",
|
|
3
3
|
"displayName": "Shopping Cart",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Sell from your site: products with options, stock and pictures, a storefront and a cart that follows shoppers from page to page, and checkout with Stripe, PayPal or payment offline. Orders to fulfil in the sidebar, refunds from the order.",
|
|
7
7
|
"author": "Domma CMS",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"text": "Shop",
|
|
33
33
|
"icon": "shopping-bag",
|
|
34
34
|
"url": "#/plugins/shopping-cart",
|
|
35
|
+
"hotkey": "h",
|
|
35
36
|
"section": "#/plugins/shopping-cart",
|
|
36
37
|
"permission": "shop",
|
|
37
38
|
"countUrl": "/api/plugins/shopping-cart/count"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "support",
|
|
3
3
|
"displayName": "Support",
|
|
4
|
-
"version": "1.1.
|
|
4
|
+
"version": "1.1.1",
|
|
5
5
|
"tier": "free",
|
|
6
6
|
"description": "Get help from Domma straight from your admin: our email, phone and hours always to hand, and a conversation with the Domma team for anything you need - urgent requests first. Right-click a request to archive, resolve or remove it.",
|
|
7
7
|
"author": "Domma CMS",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"text": "Support",
|
|
42
42
|
"icon": "headphones",
|
|
43
43
|
"url": "#/plugins/support",
|
|
44
|
+
"hotkey": "s",
|
|
44
45
|
"section": "#/plugins/support",
|
|
45
46
|
"permission": "support",
|
|
46
47
|
"countUrl": "/api/plugins/support/count"
|
|
@@ -250,8 +250,8 @@ export async function authRoutes(fastify) {
|
|
|
250
250
|
menuItems: async () => (await getMenu('admin-sidebar').catch(() => null))?.items || [],
|
|
251
251
|
canSee: (visibility) => checkVisibility(request.user, visibility)
|
|
252
252
|
});
|
|
253
|
-
// Dashboard Pro's "Open
|
|
254
|
-
if (!adminScope && isDashboardPro() && (await readPrefs(request.user)).home) adminHome = '/
|
|
253
|
+
// Dashboard Pro's "Open on my boards" - for a user the admin does not confine.
|
|
254
|
+
if (!adminScope && isDashboardPro() && (await readPrefs(request.user)).home) adminHome = '/dashboards';
|
|
255
255
|
return {permissions, adminScope, adminHome};
|
|
256
256
|
});
|
|
257
257
|
|
|
@@ -26,6 +26,17 @@ export async function sidebarRoutes(fastify, opts = {}) {
|
|
|
26
26
|
const authenticate = opts.authenticate || defaultAuthenticate;
|
|
27
27
|
const signedIn = {preHandler: [authenticate]};
|
|
28
28
|
|
|
29
|
+
/** Dashboard Pro: the Dashboard entry (`#/`) is Dashboards, wearing the name this person (or the site) gave it. */
|
|
30
|
+
const dashboardsLabel = async (request) => (isDashboardPro()
|
|
31
|
+
? boardName(await readPrefs(request.user), await readSite())
|
|
32
|
+
: null);
|
|
33
|
+
/** Relabel the entry for `#/` - only while it still says "Dashboard", so a name an admin gave it stays. */
|
|
34
|
+
const relabel = (items, label) => (items || []).map((it) => {
|
|
35
|
+
if (!it || typeof it !== 'object') return it;
|
|
36
|
+
const next = it.url === '#/' && String(it.text || '').trim() === 'Dashboard' ? {...it, text: label} : it;
|
|
37
|
+
return Array.isArray(next.items) ? {...next, items: relabel(next.items, label)} : next;
|
|
38
|
+
});
|
|
39
|
+
|
|
29
40
|
fastify.get('/sidebar/menu', signedIn, async (request) => {
|
|
30
41
|
const menu = await getMenu(ADMIN_SIDEBAR_SLUG);
|
|
31
42
|
if (!menu) return {items: []};
|
|
@@ -33,16 +44,16 @@ export async function sidebarRoutes(fastify, opts = {}) {
|
|
|
33
44
|
// renderer needs them to place Tools before it drops them. An item
|
|
34
45
|
// this user may not see (`visibility`) arrives as a label-less hidden
|
|
35
46
|
// stub for the same reason - see restrictedStub() in menus.js.
|
|
36
|
-
|
|
47
|
+
const items = filterItemsForUser(menu.items || [], request.user, {keepHidden: true});
|
|
48
|
+
const label = await dashboardsLabel(request);
|
|
49
|
+
return {...menu, items: label ? relabel(items, label) : items};
|
|
37
50
|
});
|
|
38
51
|
|
|
39
52
|
fastify.get('/sidebar/registered-items', signedIn, async (request) => {
|
|
40
|
-
//
|
|
41
|
-
const boardLabel =
|
|
42
|
-
? boardName(await readPrefs(request.user), await readSite())
|
|
43
|
-
: null;
|
|
53
|
+
// A Tool that owns `#/` (the fleet manager's Dashboard) registers the entry itself.
|
|
54
|
+
const boardLabel = await dashboardsLabel(request);
|
|
44
55
|
return getRegisteredSidebarItems()
|
|
45
|
-
.map(entry => (boardLabel && entry?.item?.url === '#/
|
|
56
|
+
.map(entry => (boardLabel && entry?.item?.url === '#/' && String(entry.item.text || '').trim() === 'Dashboard'
|
|
46
57
|
? {...entry, item: {...entry.item, text: boardLabel}}
|
|
47
58
|
: entry))
|
|
48
59
|
.filter(entry => !entry?.item?.hidden && checkVisibility(request.user, entry?.item?.visibility))
|
|
@@ -24,7 +24,9 @@ export const UPDATES = ['live', 'slower', 'paused'];
|
|
|
24
24
|
export const NAME_MAX = 40;
|
|
25
25
|
|
|
26
26
|
export const DEFAULT_PREFS = {name: '', greeting: true, columns: 4, density: 'normal', updates: 'live', home: false};
|
|
27
|
-
export const DEFAULT_SITE = {name: '
|
|
27
|
+
export const DEFAULT_SITE = {name: 'Dashboards', locked: false};
|
|
28
|
+
/** The default before 1.5, when it named the board's own sidebar entry: read as no name at all. */
|
|
29
|
+
const OLD_DEFAULT = 'My Dashboard';
|
|
28
30
|
|
|
29
31
|
const cleanName = (v) => String(v ?? '').replace(/[\u0000-\u001f<>]/g, '').replace(/\s+/g, ' ').trim().slice(0, NAME_MAX);
|
|
30
32
|
|
|
@@ -41,10 +43,11 @@ export function normalisePrefs(input) {
|
|
|
41
43
|
};
|
|
42
44
|
}
|
|
43
45
|
|
|
44
|
-
/** The site's settings, cleaned. A blank name falls back to "
|
|
46
|
+
/** The site's settings, cleaned. A blank name (or the old default) falls back to "Dashboards". */
|
|
45
47
|
export function normaliseSite(input) {
|
|
46
48
|
const s = input && typeof input === 'object' ? input : {};
|
|
47
|
-
|
|
49
|
+
const name = cleanName(s.name);
|
|
50
|
+
return {name: name && name !== OLD_DEFAULT ? name : DEFAULT_SITE.name, locked: s.locked === true};
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
/** What is wrong with submitted settings, as messages ([] when they are fine). */
|
|
@@ -66,7 +69,8 @@ export function siteProblems(input) {
|
|
|
66
69
|
|
|
67
70
|
/** What the board is called for this person: their own name, else the site's. */
|
|
68
71
|
export function boardName(prefs, site) {
|
|
69
|
-
|
|
72
|
+
const mine = cleanName(prefs?.name);
|
|
73
|
+
return mine && mine !== OLD_DEFAULT ? mine : normaliseSite(site).name;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
// ---- Files -------------------------------------------------------------------
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* @module services/widgets
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
-
import {getRegisteredSidebarItems
|
|
35
|
+
import {getRegisteredSidebarItems} from './hooks.js';
|
|
36
36
|
|
|
37
37
|
/** Widths on the board's four-column grid. */
|
|
38
38
|
export const WIDGET_SIZES = ['s', 'm', 'l', 'xl'];
|
|
@@ -226,7 +226,6 @@ export function layoutProblems(input) {
|
|
|
226
226
|
// ---------------------------------------------------------------------------
|
|
227
227
|
|
|
228
228
|
const owners = new Set();
|
|
229
|
-
let sidebarRegistered = false;
|
|
230
229
|
|
|
231
230
|
/**
|
|
232
231
|
* A Pro Tool switches the board on (plugins.js hands it to each plugin as
|
|
@@ -239,14 +238,8 @@ let sidebarRegistered = false;
|
|
|
239
238
|
export function enableDashboardPro({owner, tier, closedSource} = {}) {
|
|
240
239
|
if (typeof owner !== 'string' || !owner.trim()) return false;
|
|
241
240
|
if (tier !== 'paid' || closedSource !== true) return false;
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
if (!sidebarRegistered) {
|
|
245
|
-
sidebarRegistered = true;
|
|
246
|
-
try {
|
|
247
|
-
registerSidebarItem({folder: 'overview', item: {source: 'Dashboard Pro', text: 'My Dashboard', url: '#/my-dashboard', icon: 'layout'}});
|
|
248
|
-
} catch { /* a sidebar without folders still has the route */ }
|
|
249
|
-
}
|
|
241
|
+
// No sidebar entry of its own: the Dashboard entry becomes Dashboards (routes/api/sidebar.js),
|
|
242
|
+
// whose first tab is the primary Dashboard and the rest the boards (admin lib/dashboards-arrange.js).
|
|
250
243
|
owners.add(owner.trim());
|
|
251
244
|
return true;
|
|
252
245
|
}
|
|
@@ -274,7 +267,8 @@ export function _resetWidgetsForTests() {
|
|
|
274
267
|
|
|
275
268
|
export const MAX_BOARDS = 12;
|
|
276
269
|
export const BOARD_NAME_MAX = 40;
|
|
277
|
-
|
|
270
|
+
// `overview` is the primary Dashboard's tab (admin lib/dashboards-arrange.js), never a board.
|
|
271
|
+
const BOARD_ID_RE = /^(?!overview$)[a-z0-9][a-z0-9-]{0,39}$/;
|
|
278
272
|
|
|
279
273
|
const boardName = (v, fallback) => String(v ?? '').replace(/[\u0000-\u001f<>]/g, '').replace(/\s+/g, ' ').trim().slice(0, BOARD_NAME_MAX) || fallback;
|
|
280
274
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{apiRequest as r}from"../api.js";import{viewLifetime as m}from"../lib/tool-kit.js";const o=/^#\/my-dashboard\/?(\?.*)?$/,a=m(o);export const myDashboardView={templateUrl:"/admin/js/templates/my-dashboard.html",async onMount(t){a.reset();const e=await r("/dashboard/pro",{method:"GET"}).catch(()=>null);if(!o.test(location.hash))return;const i=t.get(0).querySelector(".dbx")||t.get(0);if(!e?.enabled){i.innerHTML='<p class="dbp-empty">My Dashboard comes with Dashboard Pro, which is not switched on for this site. Find it in the Marketplace.</p>';return}const{mountBoard:s}=await import("./dashboard-board.js");await s(t,a,{isAdmin:e.isAdmin===!0})}};
|