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.
@@ -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
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "blog",
3
3
  "displayName": "Blog",
4
- "version": "1.9.4",
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.1",
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@c9103e9",
3
+ "source": "dcms-marketplace@8b9c2f1",
4
4
  "plugins": {
5
5
  "blog": {
6
- "version": "1.9.4",
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": "7bfba248651527bb98fa388a66234a53e9808c6a28e878504a1fa9fae216b658",
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.1",
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": "ce7b3e0154550b8c25e447571dc0deb329f4adff13408d569573ea592d91073c",
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.4",
141
+ "version": "1.2.6",
142
142
  "files": {
143
- "CLAUDE.md": "29a46b6113dafb37c5e4c728a0d1f7286e805301e7d3d2bbae0c72eb43d5f456",
143
+ "CLAUDE.md": "40b25e33b9f14636bf766b68d1af5be24dcf69ccf6d770b9d080c1ed81664abb",
144
144
  "admin/views/mail.js": "c5c6483f9618c75cac8f4be32d3aea99511db76d4b9d38b8ef43bdc1d35db3c5",
145
145
  "config.js": "6e65803ad822a9acda40c203631b8eb32a87cf9d9e5f1c7cb59d01aa406075c6",
146
- "docs/guide.md": "14f8c45663c1325f681cce418d59d4093cc858d605a98630cce0030fff5e479d",
146
+ "docs/guide.md": "a8febdc46bf60af35c6d980ac37629baa93da94e0970473f6fbe2d3f242ceb63",
147
147
  "plugin.js": "7f602a60c4eca7d3637659d03d5de9e242584666846b16c9e5a1b758e4856c9f",
148
- "plugin.json": "0d928f4a6dc5bcca9e23e35280d026eb133a8bb1120daea54cbe754c2b4e7773"
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.0",
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": "eccf7d9bf2f7fdde26d78c82b944897031387b42ab26daa04c59d7a5cac3ea5f",
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.0",
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": "e6efaf06f7f2c99b196909e56f0a0b6d65b438ffd6d616aa4207769861f9bd66",
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",
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.0",
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.0",
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 the admin on my board" - for a user the admin does not confine.
254
- if (!adminScope && isDashboardPro() && (await readPrefs(request.user)).home) adminHome = '/my-dashboard';
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
- return {...menu, items: filterItemsForUser(menu.items || [], request.user, {keepHidden: true})};
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
- // Dashboard Pro: My Dashboard wears the name this person (or the site) gave it.
41
- const boardLabel = isDashboardPro()
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 === '#/my-dashboard'
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: 'My Dashboard', locked: false};
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 "My Dashboard". */
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
- return {name: cleanName(s.name) || DEFAULT_SITE.name, locked: s.locked === true};
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
- return cleanName(prefs?.name) || normaliseSite(site).name;
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, registerSidebarItem} from './hooks.js';
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
- // The board's own address in the sidebar (Overview), once: `#/` can belong to
243
- // another Tool (the fleet manager's Dashboard), and My Dashboard still reaches it.
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
- const BOARD_ID_RE = /^[a-z0-9][a-z0-9-]{0,39}$/;
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,3 +0,0 @@
1
- <!-- My Dashboard - the Dashboard Pro board at its own address (admin/js/views/my-dashboard.js), for a site
2
- whose `#/` belongs to another Tool (the fleet manager's Dashboard, a chosen admin home). -->
3
- <div class="dbx"></div>
@@ -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})}};