domma-cms 0.74.0 → 0.76.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/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/api-tokens-arrange.js +3 -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/effects-arrange.js +3 -0
- package/admin/js/lib/forms-arrange.js +1 -0
- package/admin/js/lib/layouts-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/my-profile-arrange.js +1 -0
- package/admin/js/lib/notifications-arrange.js +1 -0
- package/admin/js/lib/pages-arrange.js +1 -0
- package/admin/js/lib/plugins-arrange.js +2 -0
- package/admin/js/lib/roles-arrange.js +1 -0
- package/admin/js/lib/search-settings-arrange.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +3 -3
- package/admin/js/lib/site-settings-arrange.js +1 -0
- package/admin/js/lib/theme-settings-arrange.js +1 -0
- package/admin/js/lib/users-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/api-tokens.html +83 -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/effects.html +155 -72
- package/admin/js/templates/forms.html +67 -14
- package/admin/js/templates/layouts.html +126 -43
- package/admin/js/templates/media.html +99 -13
- package/admin/js/templates/menus.html +57 -11
- package/admin/js/templates/my-profile.html +100 -12
- package/admin/js/templates/notifications.html +70 -10
- package/admin/js/templates/pages.html +78 -23
- package/admin/js/templates/plugins.html +100 -28
- package/admin/js/templates/roles.html +78 -7
- package/admin/js/templates/search.html +193 -216
- package/admin/js/templates/settings.html +404 -472
- package/admin/js/templates/theme.html +164 -132
- package/admin/js/templates/users.html +69 -9
- 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/api-tokens.js +62 -8
- 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/effects.js +31 -1
- package/admin/js/views/forms.js +57 -1
- package/admin/js/views/layouts.js +81 -1
- package/admin/js/views/media.js +40 -1
- package/admin/js/views/menus.js +11 -5
- package/admin/js/views/my-profile.js +1 -1
- package/admin/js/views/notifications.js +18 -1
- package/admin/js/views/pages.js +28 -8
- package/admin/js/views/plugins.js +97 -39
- package/admin/js/views/roles.js +49 -4
- package/admin/js/views/search.js +7 -1
- package/admin/js/views/settings.js +13 -3
- package/admin/js/views/theme.js +55 -1
- package/admin/js/views/users.js +13 -4
- package/admin/js/views/views-list.js +11 -1
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +97 -40
- package/plugins/_lib/admin/mail/contacts.js +4 -1
- package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
- package/plugins/_lib/admin/mail/folder-tree.js +1 -1
- package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
- package/plugins/_lib/admin/mail/mail.css +1 -1
- package/plugins/_lib/admin/mail/mail.html +1 -1
- package/plugins/_lib/admin/mail/reader-view.js +303 -44
- package/plugins/_lib/admin/mail/when-closed.js +26 -0
- package/plugins/_lib/mail/readRoutes.js +43 -1
- package/plugins/_lib/mail/unreadBadge.js +42 -0
- package/plugins/free-tier.lock.json +5 -5
- package/plugins/mail-reader/CLAUDE.md +1 -0
- package/plugins/mail-reader/plugin.js +3 -1
- package/plugins/mail-reader/plugin.json +5 -4
- package/server/routes/api/actions.js +64 -2
- package/server/routes/api/api-endpoints.js +41 -0
- package/server/routes/api/api-tokens.js +56 -0
- package/server/routes/api/auth.js +7 -1
- package/server/routes/api/blocks.js +170 -8
- package/server/routes/api/collections.js +66 -2
- package/server/routes/api/components.js +46 -0
- package/server/routes/api/context-menus.js +8 -0
- package/server/routes/api/effects.js +68 -13
- package/server/routes/api/forms.js +123 -10
- package/server/routes/api/layouts.js +93 -2
- package/server/routes/api/media.js +56 -1
- package/server/routes/api/menus.js +53 -6
- package/server/routes/api/notifications.js +140 -14
- package/server/routes/api/pages.js +46 -0
- package/server/routes/api/plugins.js +135 -4
- package/server/routes/api/roles.js +111 -0
- package/server/routes/api/search.js +38 -4
- package/server/routes/api/settings.js +52 -0
- package/server/routes/api/theme.js +55 -0
- package/server/routes/api/users.js +53 -1
- package/server/routes/api/views.js +27 -3
- package/server/server.js +28 -1
- package/server/services/contextMenus.js +41 -0
- package/server/services/plugins.js +7 -0
- package/server/services/roles.js +3 -0
- package/server/services/search.js +58 -0
- package/server/services/users.js +14 -0
- package/server/services/views.js +38 -0
- package/admin/js/templates/plugin-marketplace.html +0 -29
- package/admin/js/views/plugin-marketplace.js +0 -1
|
@@ -411,3 +411,61 @@ export function searchPages(pages, query, {maxResults = 10, onError = () => {}}
|
|
|
411
411
|
results.sort((a, b) => b.score - a.score);
|
|
412
412
|
return results.slice(0, maxResults);
|
|
413
413
|
}
|
|
414
|
+
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// The admin screen: coverage, and the sidebar badge
|
|
417
|
+
// ---------------------------------------------------------------------------
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Which pages search can return, and which it leaves out - the same rule
|
|
421
|
+
* searchPages() applies (drafts and private pages are never results).
|
|
422
|
+
*
|
|
423
|
+
* @param {object[]} pages
|
|
424
|
+
* @returns {{searchable: number, drafts: number, private: number, total: number}}
|
|
425
|
+
*/
|
|
426
|
+
export function searchCoverage(pages = []) {
|
|
427
|
+
let drafts = 0;
|
|
428
|
+
let priv = 0;
|
|
429
|
+
for (const p of pages) {
|
|
430
|
+
if (p?.status === 'draft') drafts++;
|
|
431
|
+
else if (p?.visibility === 'private') priv++;
|
|
432
|
+
}
|
|
433
|
+
return {searchable: pages.length - drafts - priv, drafts, private: priv, total: pages.length};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* The sidebar badge on System > Search (its countUrl, GET /api/search/_count):
|
|
438
|
+
* how many pages a visitor's search can find, with whether search is on at all
|
|
439
|
+
* and how it is reached in the popover. An inventory, not a signal - plain text
|
|
440
|
+
* throughout, the popover sets it with textContent.
|
|
441
|
+
*
|
|
442
|
+
* @param {object} settings - normalised search settings
|
|
443
|
+
* @param {object[]} pages
|
|
444
|
+
*/
|
|
445
|
+
export function searchBadge(settings = SEARCH_DEFAULTS, pages = []) {
|
|
446
|
+
const s = normaliseSearchSettings(settings);
|
|
447
|
+
const c = searchCoverage(pages);
|
|
448
|
+
const pl = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
449
|
+
const trigger = s.displayMode === 'floating'
|
|
450
|
+
? `Floating button, ${s.position.replace('-', ' ')}`
|
|
451
|
+
: 'Icon in the navigation bar';
|
|
452
|
+
return {
|
|
453
|
+
count: c.searchable,
|
|
454
|
+
label: s.enabled
|
|
455
|
+
? `Search is on - ${pl(c.searchable, 'page')} searchable`
|
|
456
|
+
: `Search is off - ${pl(c.searchable, 'page')} would be searchable`,
|
|
457
|
+
tone: 'info',
|
|
458
|
+
details: {
|
|
459
|
+
title: 'Site search',
|
|
460
|
+
rows: [
|
|
461
|
+
{label: 'Status', value: s.enabled ? 'On' : 'Off'},
|
|
462
|
+
{label: 'Trigger', value: s.enabled ? trigger : '-'},
|
|
463
|
+
{label: 'Keyboard shortcut', value: s.enabled && s.keyboardShortcut ? 'Ctrl+K / ⌘K' : 'Off'},
|
|
464
|
+
{label: 'Searchable pages', value: String(c.searchable)},
|
|
465
|
+
{label: 'Left out (drafts, private)', value: String(c.drafts + c.private)}
|
|
466
|
+
],
|
|
467
|
+
items: [],
|
|
468
|
+
empty: s.enabled ? 'Visitors can search every published, public page.' : 'Search is switched off - nothing is added to public pages.'
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
}
|
package/server/services/users.js
CHANGED
|
@@ -168,6 +168,20 @@ export async function updateUser(id, updates) {
|
|
|
168
168
|
const user = await readUserFile(id);
|
|
169
169
|
if (!user) throw new Error('User not found');
|
|
170
170
|
|
|
171
|
+
// An email is how an account signs in: two accounts must never share one.
|
|
172
|
+
// createUser always refused a taken address; an update did not check at all.
|
|
173
|
+
if (typeof updates.email === 'string' && updates.email.trim()) {
|
|
174
|
+
updates.email = updates.email.toLowerCase().trim();
|
|
175
|
+
if (updates.email !== String(user.email || '').toLowerCase()) {
|
|
176
|
+
const other = await getUserByEmail(updates.email);
|
|
177
|
+
if (other && other.id !== user.id) {
|
|
178
|
+
const err = new Error('A user with that email already exists');
|
|
179
|
+
err.code = 'EMAIL_TAKEN';
|
|
180
|
+
throw err;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
171
185
|
if (updates.password) {
|
|
172
186
|
updates.password = await bcrypt.hash(updates.password, config.auth.bcryptRounds);
|
|
173
187
|
}
|
package/server/services/views.js
CHANGED
|
@@ -475,3 +475,41 @@ export async function executeView(slug, {page = 1, limit = 25, user = null} = {}
|
|
|
475
475
|
? executeMongoView(view, { page, limit, user })
|
|
476
476
|
: executeFileView(view, { page, limit, user });
|
|
477
477
|
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* The sidebar badge on Views (its countUrl, GET /api/views/_count): how many
|
|
481
|
+
* views, red when one reads from a collection that no longer exists - such a
|
|
482
|
+
* view errors on every page that shows it. Plain text: the popover sets it
|
|
483
|
+
* with textContent.
|
|
484
|
+
*
|
|
485
|
+
* @param {object[]} views
|
|
486
|
+
* @param {Set<string>} collectionSlugs - the collections that exist
|
|
487
|
+
* @returns {{count: number, label: string, tone: string, details: object}}
|
|
488
|
+
*/
|
|
489
|
+
export function viewsBadge(views = [], collectionSlugs = new Set()) {
|
|
490
|
+
const broken = views.filter(v => v.pipeline?.source && !collectionSlugs.has(v.pipeline.source));
|
|
491
|
+
const pub = views.filter(v => v.access?.public).length;
|
|
492
|
+
const n = views.length;
|
|
493
|
+
const ordered = [...broken, ...views.filter(v => !broken.includes(v))];
|
|
494
|
+
return {
|
|
495
|
+
count: n,
|
|
496
|
+
label: `${n} view${n === 1 ? '' : 's'}${broken.length ? `, ${broken.length} reading a missing collection` : ''}`,
|
|
497
|
+
tone: broken.length ? 'danger' : 'info',
|
|
498
|
+
details: {
|
|
499
|
+
title: 'Views',
|
|
500
|
+
rows: [
|
|
501
|
+
{label: 'Views', value: String(n)},
|
|
502
|
+
{label: 'Public', value: String(pub)},
|
|
503
|
+
{label: 'Missing source', value: String(broken.length), ...(broken.length && {tone: 'danger'})}
|
|
504
|
+
],
|
|
505
|
+
items: ordered.slice(0, 8).map(v => ({
|
|
506
|
+
text: String(v.title || v.slug),
|
|
507
|
+
meta: broken.includes(v)
|
|
508
|
+
? `source "${v.pipeline.source}" is gone`
|
|
509
|
+
: [v.pipeline?.source, v.display?.mode || 'table', v.access?.public ? 'public' : ''].filter(Boolean).join(' · '),
|
|
510
|
+
...(broken.includes(v) && {tone: 'danger'})
|
|
511
|
+
})),
|
|
512
|
+
empty: 'No views yet.'
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
<div class="view-header">
|
|
2
|
-
<h2><span data-icon="store"></span> Browse</h2>
|
|
3
|
-
<p class="text-muted">Plugins available to this site.</p>
|
|
4
|
-
</div>
|
|
5
|
-
|
|
6
|
-
<!--
|
|
7
|
-
Shown when this site is not managed. Telling an unmanaged customer to set
|
|
8
|
-
MANAGER_URL is telling them to configure something they will never have: a
|
|
9
|
-
standalone purchase arrives as a file, and Install from File is the whole
|
|
10
|
-
procedure. The managed path is mentioned second because it is not theirs to
|
|
11
|
-
set up.
|
|
12
|
-
-->
|
|
13
|
-
<div id="marketplace-unavailable" style="display:none">
|
|
14
|
-
<div class="card">
|
|
15
|
-
<div class="card-body">
|
|
16
|
-
<p><strong>This site installs plugins from a file.</strong></p>
|
|
17
|
-
<p class="text-muted">Buy a plugin and you are sent a single <code>.dcmsplugin</code> file. Use
|
|
18
|
-
<strong>Install from File</strong> above to install it - no connection to anything is needed, and the
|
|
19
|
-
same button takes an update later.</p>
|
|
20
|
-
<p class="text-muted" style="margin-bottom:0">Sites managed for you browse and install here instead, over their
|
|
21
|
-
manager connection.</p>
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
</div>
|
|
25
|
-
|
|
26
|
-
<div id="marketplace-available" style="display:none">
|
|
27
|
-
<p id="marketplace-site" class="text-muted" style="display:none;font-size:.85rem;margin:0 0 .75rem"></p>
|
|
28
|
-
<div id="marketplace-grid" class="card-grid"></div>
|
|
29
|
-
</div>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{api as u}from"../api.js";import{pluginAccent as P,pluginWatermark as S}from"../lib/plugin-accent.js";export const pluginMarketplaceView={templateUrl:"/admin/js/templates/plugin-marketplace.html",async onMount(e){const a=E.loader(e.get(0),{type:"dots"});let t;try{t=await u.marketplace.catalogue()}catch(s){E.toast(`Failed to load marketplace: ${s.message}`,{type:"error"}),a.destroy();return}if(a.destroy(),!t.available){e.find("#marketplace-unavailable").show();return}e.find("#marketplace-available").show(),t.site&&e.find("#marketplace-site").text(`Licensed to ${t.site}.`).show(),U(e,t.catalogue),Domma.icons.scan()}};function U(e,a){const t=e.find("#marketplace-grid").get(0);if(t){for(;t.firstChild;)t.removeChild(t.firstChild);if(!a||a.length===0){const s=document.createElement("p");s.className="text-muted",s.textContent="No plugins available in the marketplace.",t.appendChild(s);return}a.forEach(s=>{const i=R(s,e);t.appendChild(i)})}}function F(e){if(e.tier!=="paid")return"Free";const a=e.terms?.managed?.monthly;return a==null||!Number.isFinite(Number(a))?"Paid":`${e.terms?.currency==="GBP"?"\xA3":(e.terms?.currency||"")+" "}${(Number(a)/100).toFixed(2)}/mo`}function R(e,a){const{slug:t,version:s,displayName:i,description:y,entitled:o,installed:l,installedVersion:p,updatable:b}=e,r=document.createElement("div");r.className="card plugin-card",r.dataset.slug=t,r.style.setProperty("--plugin-accent",P(t)),r.appendChild(S(e.icon));const h=document.createElement("div");h.className="card-body";const m=document.createElement("div");m.className="plugin-header";const N=document.createElement("div");N.className="plugin-icon";const I=document.createElement("span");I.dataset.icon=e.icon||"package",N.appendChild(I);const g=document.createElement("div");g.className="plugin-meta";const x=document.createElement("div");x.className="plugin-name",x.textContent=i||t;const k=document.createElement("div");k.className="plugin-version",k.textContent=l&&p&&p!==s?`v${p} \u2192 v${s}`:`v${s}`,g.appendChild(x),g.appendChild(k);const f=document.createElement("div");f.style.cssText="display:flex;gap:.35rem;align-items:center;";const w=document.createElement("span");if(w.className="badge badge-outline",w.textContent=F(e),f.appendChild(w),o&&e.tier==="paid"){const c=document.createElement("span");c.className="badge badge-success",c.textContent=e.licenceStatus==="grace"?"Renew":"Licensed",c.title=e.licenceStatus==="grace"?"The rental term has ended - still running during the grace period":"This site holds a live licence",f.appendChild(c)}m.appendChild(N),m.appendChild(g),m.appendChild(f);const $=document.createElement("p");$.className="plugin-desc",$.textContent=y||"",h.appendChild(m),h.appendChild($);const C=document.createElement("div");C.className="plugin-footer";const v=document.createElement("span");v.className="text-muted",v.style.fontSize=".8rem",v.textContent="Restart server after changes",C.appendChild(v);const d=document.createElement("div");d.className="plugin-footer-actions";const n=document.createElement("button");if(n.className="btn btn-sm",l&&b&&o){n.className+=" btn-primary",n.textContent="Update",n.onclick=()=>T(e,a,{update:!0}),d.appendChild(n);const c=document.createElement("button");c.className="btn btn-sm btn-ghost",c.textContent="Uninstall",c.onclick=()=>D(t,a),d.appendChild(c)}else l?(n.className+=" btn-ghost",n.textContent="Uninstall",n.onclick=()=>D(t,a),d.appendChild(n)):o?(n.className+=" btn-primary",n.textContent="Install",n.onclick=()=>T(e,a),d.appendChild(n)):(n.className+=" btn-ghost",n.textContent=e.licenceStatus==="expired"?"Licence expired":"Not licensed",n.disabled=!0,n.title="Contact us to add this to your site.",d.appendChild(n));return C.appendChild(d),r.appendChild(h),r.appendChild(C),r}async function T(e,a,{update:t=!1}={}){const{slug:s,version:i,installedVersion:y}=e,o=t?`<p>Update ${s} from ${y} to ${i}?</p><p class="text-muted">The plugin keeps its data, and the current version is put back if the update fails.</p>`:`<p>Install ${e.displayName||s} v${i}?</p><p class="text-muted">It arrives licensed to this site and takes effect after a restart.</p>`;if(await E.confirm(o,{title:t?"Update plugin":"Install plugin",confirmText:t?"Update":"Install",cancelText:"Cancel",html:!0}))try{const l=await u.marketplace.install(s,i,{update:t}),p=l?.action==="upgrade"?`${s} updated${l.from?` from ${l.from}`:""} to ${l.version}.`:`${s} ${l?.version??i} installed.`;E.toast(`${p} Restart the server to activate it.`,{type:"success"});const b=await u.marketplace.catalogue();U(a,b.catalogue),Domma.icons.scan()}catch(l){E.toast(l.message||"Install failed.",{type:"error"})}}async function D(e,a){if(await E.confirm(`Uninstall ${e}? This will remove its files.`))try{await u.marketplace.uninstall(e),E.toast(`${e} uninstalled. Restart to apply.`,{type:"success"});const t=await u.marketplace.catalogue();U(a,t.catalogue),Domma.icons.scan()}catch(t){E.toast(`Uninstall failed: ${t.message}`,{type:"error"})}}
|