domma-cms 0.79.1 → 0.80.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/js/app.js +2 -2
- package/admin/js/lib/desktop-notify.js +1 -0
- package/admin/js/lib/notifications-arrange.js +1 -1
- package/admin/js/lib/sidebar-renderer.js +1 -1
- package/admin/js/templates/notifications.html +21 -1
- package/admin/js/views/notifications.js +67 -4
- package/package.json +1 -1
- package/server/routes/api/api-tokens.js +5 -2
- package/server/routes/api/auth.js +15 -1
- package/server/routes/api/effects.js +36 -0
- package/server/routes/api/forms.js +5 -0
- package/server/routes/api/layouts.js +6 -2
- package/server/routes/api/notifications.js +97 -20
- package/server/routes/api/users.js +11 -0
- package/server/server.js +29 -5
- package/server/services/adapterRegistry.js +1 -0
- package/server/services/connectionManager.js +1 -0
- package/server/services/email.js +3 -0
- package/server/services/notification-sources.js +321 -0
- package/server/services/notify.js +420 -0
- package/server/services/plugins.js +6 -2
- package/server/services/sidebarBadges.js +33 -0
package/admin/js/app.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{views as O}from"./views/index.js";import{withPluginChrome as T}from"./lib/plugin-chrome.js";import{api as c,getUser as L,isAuthenticated as d,logout as H}from"./api.js";import{installHttpInterceptor as q}from"./http-interceptor.js";import{installResizableSlideovers as z}from"./lib/slideover-resizable.js";import{installActivityThrobber as K}from"./lib/activity-throbber.js";import{isPathAllowed as J,redirectFor as Y}from"./lib/admin-scope.js";$(()=>{q(),K(),z(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:e}=await import("./lib/admin-theme.js");d()?await e(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let l=null,r="/",u=!1,h=null;function C(t){const e=b[t]||Y(l,t);return e||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"?b[r]||r:null)}const b={};R.use(async(t,e,o)=>{if(t.path==="/login"||t.path==="/reset-password")return o();if(!d()){R.navigate("/login");return}const i=C(t.path);if(i){R.navigate(i);return}o()});async function w(){try{const t=await c.get("/auth/permissions");l=t.adminScope||null,r=t.adminHome||"/";const e=t.permissions||[];return u=J(l,"/settings")&&e.some(o=>o==="settings"||o.startsWith("settings.")),h=e.some(o=>o==="notifications"||o.startsWith("notifications.")),e}catch{return l=null,r="/",u=!1,h=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{w()});let D=null,y=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(D){y=!0;return}D=(async()=>{do{y=!1;try{await P(await w())}catch{}}while(y)})().finally(()=>{D=null})});async function P(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t,adminScope:l})}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&(p&&(clearInterval(p),p=null),l=null,r="/",u=!1,h=null)});const B=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Plugin - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:e})=>{if(!(t.path==="/login"||t.path==="/reset-password")){if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await I();const o=await w(),i=C(t.path);i&&R.navigate(i),P(o);try{const a=await c.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const f=g(s.data?.title||""),A=g((s.data?.body||"").slice(0,120));E.toast(`${f} - ${A}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}x()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const V=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function W(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const e=t?String(t.path).split("?")[0]:"";if(!t||!W(e))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(e))return;let a=0;const n=60,s=1500,f=()=>{a+=n;for(const A of V){const v=document.querySelector(A);if(!v)continue;if(Array.from(v.options||[]).find(F=>F.value===i)){v.value=i,v.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(f,n)};setTimeout(f,n)});const k="cms_card_states",N=S.get(k)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&N[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const e=$(this).closest(".card");e.toggleClass("card-collapsed");const o=$(this).find("h2, h3").first().text().trim();o&&(N[o]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(k,N))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login"||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const m={...O},_={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},analytics:{displayName:"Analytics",icon:"chart-bar"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},dashboard:{displayName:"Dashboard",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},blockEditor:{displayName:"Blocks",section:"Edit",icon:"box"},componentEditor:{displayName:"Components",section:"Edit",icon:"component"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,e]of Object.entries(_))m[t]&&(m[t]=T(m[t],{plugin:t,licence:"Built in",...e}));const j=[...B];async function I(){if(d())try{const t=await c.plugins.adminConfig();t.routes?.length&&j.push(...t.routes);for(const[e,o]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${o.entry}`);m[e]=T(i[o.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(b[`/${e}`]=o);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const i=document.createElement("link");i.id=e,i.rel="stylesheet",i.href=o,document.head.appendChild(i)}}catch{}}function x(){const t=L();if(!t||$("#topbar-user-name").length)return;const o={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role;$("#topbar-user").html(`
|
|
1
|
+
import{views as O}from"./views/index.js";import{withPluginChrome as T}from"./lib/plugin-chrome.js";import{api as l,getUser as L,isAuthenticated as d,logout as H}from"./api.js";import{installHttpInterceptor as q}from"./http-interceptor.js";import{installResizableSlideovers as z}from"./lib/slideover-resizable.js";import{installActivityThrobber as K}from"./lib/activity-throbber.js";import{isPathAllowed as J,redirectFor as Y}from"./lib/admin-scope.js";import{deliverDesktop as G,desktopActive as Q,notificationSettings as X}from"./lib/desktop-notify.js";$(()=>{q(),K(),z(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:e}=await import("./lib/admin-theme.js");d()?await e(l,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let c=null,r="/",u=!1,h=null;function C(t){const e=b[t]||Y(c,t);return e||((t.split("?")[0]||"/")==="/"&&r&&r!=="/"?b[r]||r:null)}const b={};R.use(async(t,e,o)=>{if(t.path==="/login"||t.path==="/reset-password")return o();if(!d()){R.navigate("/login");return}const i=C(t.path);if(i){R.navigate(i);return}o()});async function w(){try{const t=await l.get("/auth/permissions");c=t.adminScope||null,r=t.adminHome||"/";const e=t.permissions||[];return u=J(c,"/settings")&&e.some(o=>o==="settings"||o.startsWith("settings.")),h=e.some(o=>o==="notifications"||o.startsWith("notifications.")),e}catch{return c=null,r="/",u=!1,h=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{w()});let D=null,y=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(D){y=!0;return}D=(async()=>{do{y=!1;try{await P(await w())}catch{}}while(y)})().finally(()=>{D=null})});async function P(t){const{renderAdminSidebar:e}=await import("./lib/sidebar-renderer.js");await e({mount:"#admin-sidebar",permissions:t,adminScope:c})}M.subscribe("router:afterChange",({to:t})=>{(t.path==="/login"||t.path==="/reset-password")&&(p&&(clearInterval(p),p=null),c=null,r="/",u=!1,h=null)});const B=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/pages",view:"pages",title:"Pages - Domma CMS"},{path:"/pages/new",view:"pageEditor",title:"New Page - Domma CMS"},{path:"/pages/edit/*",view:"pageEditor",title:"Edit Page - Domma CMS"},{path:"/media",view:"media",title:"Media - Domma CMS"},{path:"/menus",view:"menus",title:"Menus - Domma CMS"},{path:"/context-menus",view:"contextMenus",title:"Context Menus - Domma CMS"},{path:"/context-menus/new",view:"contextMenuEditor",title:"New Context Menu - Domma CMS"},{path:"/context-menus/edit/:slug",view:"contextMenuEditor",title:"Edit Context Menu - Domma CMS"},{path:"/menus/new",view:"menuEditor",title:"New Menu - Domma CMS"},{path:"/menus/edit/:slug",view:"menuEditor",title:"Edit Menu - Domma CMS"},{path:"/menu-locations",view:"menuLocations",title:"Menu Locations - Domma CMS"},{path:"/navigation",view:"menusRedirect",title:"Menus - Domma CMS"},{path:"/layouts",view:"layouts",title:"Layouts - Domma CMS"},{path:"/settings",view:"settings",title:"Settings - Domma CMS"},{path:"/theme",view:"theme",title:"Theme - Domma CMS"},{path:"/users",view:"users",title:"Users - Domma CMS"},{path:"/users/new",view:"userEditor",title:"New User - Domma CMS"},{path:"/users/edit/:id",view:"userEditor",title:"Edit User - Domma CMS"},{path:"/plugins",view:"plugins",title:"Plugins - Domma CMS"},{path:"/plugins/code/:name",view:"pluginCode",title:"Plugin Code - Domma CMS"},{path:"/documentation",view:"documentation",title:"Usage - Domma CMS"},{path:"/tutorials",view:"tutorials",title:"Tutorials - Domma CMS"},{path:"/tutorials/crud",view:"tutorialCrud",title:"Building a CRUD App - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Plugin - Domma CMS"},{path:"/tutorials/forms",view:"tutorialForms",title:"Form Follow-Up - Domma CMS"},{path:"/docs/components",view:"componentsReference",title:"Components Reference - Domma CMS"},{path:"/docs/components-howto",view:"componentsHowto",title:"Components How-To - Domma CMS"},{path:"/docs/components-walkthrough",view:"componentsWalkthrough",title:"Components Walkthrough - Domma CMS"},{path:"/docs/components-rules",view:"componentsRules",title:"Components Rules - Domma CMS"},{path:"/docs/usage/pages",view:"usagePages",title:"Pages - Usage - Domma CMS"},{path:"/docs/usage/media",view:"usageMedia",title:"Media - Usage - Domma CMS"},{path:"/docs/usage/navigation",view:"usageNavigation",title:"Navigation - Usage - Domma CMS"},{path:"/docs/usage/dconfig",view:"usageDconfig",title:"DConfig - Usage - Domma CMS"},{path:"/docs/usage/shortcodes",view:"usageShortcodes",title:"Shortcodes - Usage - Domma CMS"},{path:"/docs/usage/site-settings",view:"usageSiteSettings",title:"Site Settings - Usage - Domma CMS"},{path:"/docs/usage/plugins",view:"usagePlugins",title:"Plugins - Usage - Domma CMS"},{path:"/docs/usage/users-roles",view:"usageUsersRoles",title:"Users & Roles - Usage - Domma CMS"},{path:"/docs/usage/views",view:"usageViews",title:"Views - Usage - Domma CMS"},{path:"/docs/usage/actions",view:"usageActions",title:"Actions - Usage - Domma CMS"},{path:"/docs/usage/cta-shortcode",view:"usageCtaShortcode",title:"CTA Shortcode - Usage - Domma CMS"},{path:"/docs/api/authentication",view:"apiRefAuthentication",title:"Authentication - API - Domma CMS"},{path:"/docs/api/pages",view:"apiRefPages",title:"Pages - API - Domma CMS"},{path:"/docs/api/settings",view:"apiRefSettings",title:"Settings - API - Domma CMS"},{path:"/docs/api/layouts",view:"apiRefLayouts",title:"Layouts - API - Domma CMS"},{path:"/docs/api/navigation",view:"apiRefNavigation",title:"Navigation - API - Domma CMS"},{path:"/docs/api/media",view:"apiRefMedia",title:"Media - API - Domma CMS"},{path:"/docs/api/users",view:"apiRefUsers",title:"Users - API - Domma CMS"},{path:"/docs/api/plugins",view:"apiRefPlugins",title:"Plugins - API - Domma CMS"},{path:"/docs/api/collections",view:"apiRefCollections",title:"Collections - API - Domma CMS"},{path:"/docs/api/views",view:"apiRefViews",title:"Views API - Domma CMS"},{path:"/docs/api/actions",view:"apiRefActions",title:"Actions API - Domma CMS"},{path:"/api-reference",view:"apiReference",title:"API Reference - Domma CMS"},{path:"/collections",view:"collections",title:"Collections - Domma CMS"},{path:"/collections/new",view:"collectionEditor",title:"New Collection - Domma CMS"},{path:"/collections/edit/:slug",view:"collectionEditor",title:"Edit Collection - Domma CMS"},{path:"/collections/:slug/entries",view:"collectionEntries",title:"Entries - Domma CMS"},{path:"/forms",view:"forms",title:"Forms - Domma CMS"},{path:"/forms/new",view:"formEditor",title:"New Form - Domma CMS"},{path:"/forms/edit/:slug",view:"formEditor",title:"Edit Form - Domma CMS"},{path:"/forms/:slug/submissions",view:"formSubmissions",title:"Submissions - Domma CMS"},{path:"/views",view:"viewsList",title:"Views - Domma CMS"},{path:"/views/new",view:"viewEditor",title:"New View - Domma CMS"},{path:"/views/edit/:slug",view:"viewEditor",title:"Edit View - Domma CMS"},{path:"/views/:slug/preview",view:"viewPreview",title:"View Preview - Domma CMS"},{path:"/actions",view:"actionsList",title:"Actions - Domma CMS"},{path:"/actions/new",view:"actionEditor",title:"New Action - Domma CMS"},{path:"/actions/edit/:slug",view:"actionEditor",title:"Edit Action - Domma CMS"},{path:"/pro/docs",view:"proDocs",title:"Pro Documentation - Domma CMS"},{path:"/blocks",view:"blocks",title:"Blocks - Domma CMS"},{path:"/blocks/new",view:"blockEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"blockEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"componentEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"componentEditor",title:"Edit Component - Domma CMS"},{path:"/my-profile",view:"myProfile",title:"My Profile - Domma CMS"},{path:"/roles",view:"roles",title:"Roles & Permissions - Domma CMS"},{path:"/roles/edit/:id",view:"roleEditor",title:"Edit Role - Domma CMS"},{path:"/effects",view:"effects",title:"Effects - Domma CMS"},{path:"/search",view:"search",title:"Search - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/system/notifications",view:"notifications",title:"Notifications - Domma CMS"},{path:"/api-tokens",view:"apiTokens",title:"API Tokens - Domma CMS"},{path:"/api-endpoints",view:"apiEndpoints",title:"API Builder - Domma CMS"},{path:"/api-endpoints/new",view:"apiEndpointEditor",title:"New API Endpoint - Domma CMS"},{path:"/api-endpoints/edit/:id",view:"apiEndpointEditor",title:"Edit API Endpoint - Domma CMS"},{path:"/projects",view:"projects",title:"Projects - Domma CMS"},{path:"/projects/new",view:"projects",title:"New Project - Domma CMS"},{path:"/projects/edit/:slug",view:"projects",title:"Edit Project - Domma CMS"},{path:"/projects/:slug/settings",view:"projectSettings",title:"Project settings - Domma CMS"},{path:"/projects/:slug/pages",view:"pages",title:"Project pages - Domma CMS"},{path:"/projects/:slug/collections",view:"collections",title:"Project collections - Domma CMS"},{path:"/projects/:slug/forms",view:"forms",title:"Project forms - Domma CMS"},{path:"/projects/:slug/actions",view:"actionsList",title:"Project actions - Domma CMS"},{path:"/projects/:slug/menus",view:"menus",title:"Project menus - Domma CMS"},{path:"/projects/:slug/blocks",view:"blocks",title:"Project blocks - Domma CMS"},{path:"/projects/:slug/components",view:"components",title:"Project components - Domma CMS"},{path:"/projects/:slug/views",view:"viewsList",title:"Project views - Domma CMS"},{path:"/projects/:slug/roles",view:"roles",title:"Project roles - Domma CMS"},{path:"/projects/:slug/users",view:"users",title:"Project users - Domma CMS"},{path:"/projects/:slug/apis",view:"apiEndpoints",title:"Project APIs - Domma CMS"},{path:"/projects/:slug",view:"projectDetail",title:"Project - Domma CMS"},{path:"/login",view:"login",title:"Sign in - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}},{path:"/reset-password",view:"login",title:"Reset Password - Domma CMS",onEnter:()=>{$("#admin-sidebar").hide(),$("#admin-topbar").hide()}}];M.subscribe("router:afterChange",async({to:t,from:e})=>{if(!(t.path==="/login"||t.path==="/reset-password")){if($("#admin-sidebar").show(),$("#admin-topbar").show(),e?.path==="/login"||e?.path==="/reset-password"){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(l)),await I();const o=await w(),i=C(t.path);if(i&&R.navigate(i),P(o),X.read().signInToasts)try{const a=await l.system.notifications.list().catch(()=>[]),n=(Array.isArray(a)?a:[]).filter(s=>s.unread&&["warning","critical"].includes(s.data?.severity)).slice(0,3);for(const s of n){const f=g(s.data?.title||""),A=g((s.data?.body||"").slice(0,120));E.toast(`${f} - ${A}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}x()}}),M.subscribe("router:afterChange",()=>{setTimeout(()=>{$(".btn-primary, .btn-danger").length&&Domma.effects.reveal(".btn-primary, .btn-danger",{animation:"fade",stagger:40,duration:300})},50)});const V=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function W(t){return/^\/(pages|menus|context-menus|collections|forms|actions|views|blocks|components|users|roles|api-endpoints)\/(new|edit\/)/.test(t)}M.subscribe("router:afterChange",({to:t})=>{const e=t?String(t.path).split("?")[0]:"";if(!t||!W(e))return;const i=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!i||!/\/new$/.test(e))return;let a=0;const n=60,s=1500,f=()=>{a+=n;for(const A of V){const v=document.querySelector(A);if(!v)continue;if(Array.from(v.options||[]).find(F=>F.value===i)){v.value=i,v.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(f,n)};setTimeout(f,n)});const k="cms_card_states",N=S.get(k)||{};M.subscribe("router:afterChange",()=>{setTimeout(()=>{$("#view-container .card-collapsible").each(function(){const t=$(this).find(".card-header h2, .card-header h3").first().text().trim();t&&N[t]==="collapsed"&&$(this).addClass("card-collapsed")})},200)}),$("#view-container").on("click",".card-collapsible .card-header",function(t){if($(t.target).closest("button, a").length)return;const e=$(this).closest(".card");e.toggleClass("card-collapsed");const o=$(this).find("h2, h3").first().text().trim();o&&(N[o]=e.hasClass("card-collapsed")?"collapsed":"open",S.set(k,N))}),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash==="#/login"||window.location.hash.startsWith("#/reset-password"))return;const e=document.querySelector("#view-container .view-header button.btn-primary");e&&(t.preventDefault(),e.click())});const m={...O},_={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},analytics:{displayName:"Analytics",icon:"chart-bar"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Menus",icon:"menu"},contextMenus:{displayName:"Context Menus",icon:"menu"},theme:{displayName:"Theme",icon:"droplet"},collections:{displayName:"Collections",icon:"database"},forms:{displayName:"Forms",icon:"layout"},viewsList:{displayName:"Views",icon:"eye"},actionsList:{displayName:"Actions",icon:"zap"},apiEndpoints:{displayName:"API Builder",icon:"code"},blocks:{displayName:"Blocks",icon:"box"},components:{displayName:"Components",icon:"component"},notifications:{displayName:"Notifications",icon:"bell"},roles:{displayName:"Roles",icon:"shield"},users:{displayName:"Users",icon:"users"},settings:{displayName:"Site Settings",icon:"settings"},effects:{displayName:"Effects",icon:"sparkles"},layouts:{displayName:"Layouts",icon:"layout"},plugins:{displayName:"Marketplace",icon:"store"},myProfile:{displayName:"My Profile",icon:"user"},apiTokens:{displayName:"API Tokens",icon:"key"},search:{displayName:"Search",icon:"search"},dashboard:{displayName:"Dashboard",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Menus",section:"Locations",icon:"map-pin"},contextMenuEditor:{displayName:"Context Menus",section:"Edit",icon:"menu"},collectionEditor:{displayName:"Collections",section:"Edit",icon:"database"},collectionEntries:{displayName:"Collections",section:"Entries",icon:"database"},formEditor:{displayName:"Forms",section:"Edit",icon:"layout"},formSubmissions:{displayName:"Forms",section:"Submissions",icon:"inbox"},viewEditor:{displayName:"Views",section:"Edit",icon:"eye"},viewPreview:{displayName:"Views",section:"Preview",icon:"eye"},actionEditor:{displayName:"Actions",section:"Edit",icon:"zap"},apiEndpointEditor:{displayName:"API Builder",section:"Edit",icon:"code"},blockEditor:{displayName:"Blocks",section:"Edit",icon:"box"},componentEditor:{displayName:"Components",section:"Edit",icon:"component"},roleEditor:{displayName:"Roles",section:"Edit",icon:"shield"},userEditor:{displayName:"Users",section:"Edit",icon:"users"},projectDetail:{displayName:"Projects",section:"Overview",icon:"folder"},projectSettings:{displayName:"Projects",section:"Settings",icon:"folder"},pluginCode:{displayName:"Marketplace",section:"Code",icon:"code"}};for(const[t,e]of Object.entries(_))m[t]&&(m[t]=T(m[t],{plugin:t,licence:"Built in",...e}));const j=[...B];async function I(){if(d())try{const t=await l.plugins.adminConfig();t.routes?.length&&j.push(...t.routes);for(const[e,o]of Object.entries(t.views||{}))try{const i=await import(`/plugins/${o.entry}`);m[e]=T(i[o.exportName],(t.meta||{})[e])}catch{}for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(b[`/${e}`]=o);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const i=document.createElement("link");i.id=e,i.rel="stylesheet",i.href=o,document.head.appendChild(i)}}catch{}}function x(){const t=L();if(!t||$("#topbar-user-name").length)return;const o={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role;$("#topbar-user").html(`
|
|
2
2
|
<span id="topbar-user-name" class="topbar-user-name">${g(t.name)}</span>
|
|
3
3
|
<span class="topbar-role-badge topbar-role-badge--${g(t.role)}">${o}</span>
|
|
4
4
|
`),$("#topbar-actions").html(`
|
|
@@ -18,4 +18,4 @@ import{views as O}from"./views/index.js";import{withPluginChrome as T}from"./lib
|
|
|
18
18
|
<span data-icon="log-out"></span>
|
|
19
19
|
<span>Sign out</span>
|
|
20
20
|
</a>
|
|
21
|
-
`);const i=$("#topbar-bell").get(0);i&&(i.hidden=!0);const a=$("#topbar-settings").get(0);a&&(a.hidden=!u),$("#topbar-logout-btn").on("click",n=>{n.preventDefault(),H()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),U(),p||(p=setInterval(U,6e4))}function g(t){return String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}let p=null;async function U(){if(!(!d()||h===!1))try{const{count:t}=await
|
|
21
|
+
`);const i=$("#topbar-bell").get(0);i&&(i.hidden=!0);const a=$("#topbar-settings").get(0);a&&(a.hidden=!u),$("#topbar-logout-btn").on("click",n=>{n.preventDefault(),H()}),Domma.icons.scan("#admin-topbar"),E.tooltip("#topbar-actions [data-tooltip]",{placement:"bottom"}),U(),p||(p=setInterval(U,6e4))}function g(t){return String(t).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}let p=null;async function U(){if(!(!d()||h===!1))try{const{count:t}=await l.system.notifications.unreadCount();$("#topbar-bell").removeAttr("hidden");const e=$("#topbar-bell-badge");t>0?e.text(t>99?"99+":String(t)).removeAttr("hidden"):e.attr("hidden","").text(""),t>0&&Q()&&G(await l.system.notifications.list().catch(()=>[]))}catch{}}(async()=>{if(!d()&&!window.location.hash.startsWith("#/reset-password"))window.location.hash="#/login";else if(await I(),L()){const a=await w(),n=(window.location.hash||"#/").slice(1)||"/",s=C(n);s&&(window.location.hash=`#${s}`),await P(a),x()}R.init({container:"#view-container",routes:j,views:m,default:"/",transitions:{enter:"fadeIn",leave:"fadeOut",duration:150}});const t=R._extractParams.bind(R);R._extractParams=function(i,a){const n=a.indexOf("?");if(n!==-1&&(a=a.slice(0,n)),i.endsWith("/*")){const s=i.slice(0,-2);return a.startsWith(s+"/")?{}:null}return t(i,a)};const e=(window.location.hash||"#/").slice(1)||"/";j.filter(i=>i.path.endsWith("/*")).some(i=>e.startsWith(i.path.slice(0,-2)+"/"))&&R._handleRouteChange()})()});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{settingsStore as f}from"./tool-kit.js";import{desktopPayload as p,desktopWorthy as d,normaliseSettings as l,rememberSeen as i}from"./notifications-arrange.js";export const notificationSettings=f("notifications.settings",l);const r="notifications.desktopSeen",s="#/system/notifications";export function desktopPermission(){return typeof window>"u"||!("Notification"in window)||!window.isSecureContext?"unsupported":Notification.permission}export async function requestDesktopPermission(){if(desktopPermission()==="unsupported")return"unsupported";try{return await Notification.requestPermission()}catch{return desktopPermission()}}function c(){try{const t=S.get(r);return Array.isArray(t)?t:null}catch{return null}}function a(t){try{S.set(r,t)}catch{}}export function primeSeen(t){a(i(c()||[],(t||[]).filter(e=>e?.unread).map(e=>e.id)))}function u({title:t,options:e}){try{const n=new Notification(t,e);return n.onclick=()=>{try{window.focus()}catch{}location.hash!==s&&(location.hash=s),n.close()},!0}catch{return!1}}export function desktopActive(){return notificationSettings.read().desktop&&desktopPermission()==="granted"}export function deliverDesktop(t){if(!desktopActive())return 0;const e=c();if(!e)return primeSeen(t),0;const n=d(t,{seen:e,level:notificationSettings.read().desktopLevel});return n.forEach(o=>u(p(o))),a(i(e,(t||[]).filter(o=>o?.unread).map(o=>o.id))),n.length}export function showTestNotification(){return desktopPermission()!=="granted"?!1:u({title:"Desktop notifications are on",options:{body:"New notifications for this site will appear like this.",tag:"dcms-notification-test"}})}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const SEVERITIES=["critical","warning","info","success"],FILTERS=["all","new",...SEVERITIES],SEVERITY_TONE={critical:"danger",warning:"warn",success:"ok",info:"info"};const s=(e,
|
|
1
|
+
export const SEVERITIES=["critical","warning","info","success"],FILTERS=["all","new",...SEVERITIES],SEVERITY_TONE={critical:"danger",warning:"warn",success:"ok",info:"info"};const s=(t,e,r=`${e}s`)=>`${t} ${t===1?e:r}`;export function severityOf(t){const e=t?.data?.severity;return SEVERITIES.includes(e)?e:"info"}export function ago(t,e=Date.now()){const r=new Date(t).getTime();if(!Number.isFinite(r))return"";const n=Math.max(0,Math.round((e-r)/1e3));return n<60?"just now":n<3600?`${Math.round(n/60)} min ago`:n<86400?`${s(Math.round(n/3600),"hour")} ago`:n<86400*14?`${s(Math.round(n/86400),"day")} ago`:new Date(r).toISOString().slice(0,10)}export function arrangeNotifications(t,{filter:e="all",query:r=""}={}){const n=String(r).trim().toLowerCase();return[...t||[]].filter(o=>e==="all"?!0:e==="new"?o.isNew:severityOf(o)===e).filter(o=>!n||[o.data?.title,o.data?.body,o.data?.link].some(i=>String(i||"").toLowerCase().includes(n))).sort((o,i)=>String(i.data?.createdAt||"").localeCompare(String(o.data?.createdAt||"")))}export function filterCounts(t){const e={all:0,new:0,critical:0,warning:0,info:0,success:0};for(const r of t||[])e.all++,r.isNew&&e.new++,e[severityOf(r)]++;return e}export function safeLink(t){const e=String(t||"").trim();return/^https?:\/\/\S+$/i.test(e)||/^#\/\S*$/.test(e)?e:""}export function sourceLabel(t={}){return t.source==="manager"?"from the manager":t.source==="sample"?"sample":t.sourceLabel?String(t.sourceLabel):t.source?String(t.source).startsWith("core:")?"System":String(t.source).split(":")[0]:""}export function notificationRow(t,{now:e=Date.now(),expanded:r=""}={}){const n=t?.data||{},o=severityOf(t),i=String(n.body||""),a=i.replace(/\s+/g," ").trim();return{id:String(t.id),title:String(n.title||"(no title)"),severity:o,severityLabel:o[0].toUpperCase()+o.slice(1),tone:SEVERITY_TONE[o],meta:[n.createdAt?ago(n.createdAt,e):"",sourceLabel(n),/^https?:\/\//i.test(String(n.link||""))?"has a link":"",n.expiresAt&&Date.parse(n.expiresAt)>e?`until ${String(n.expiresAt).slice(0,10)}`:""].filter(Boolean).join(" \xB7 "),preview:a.length>140?`${a.slice(0,139)}\u2026`:a,body:i,link:safeLink(n.link),linkTarget:/^#\//.test(safeLink(n.link))?"_self":"_blank",linkText:/^#\//.test(safeLink(n.link))?"Go there \u2192":"Open link \u2197",isNew:!!t.isNew,unread:!!t.unread,open:r===String(t.id),rowCls:[t.unread?"is-unread":"",r===String(t.id)?"is-open":"",`is-${o}`].filter(Boolean).join(" ")}}const l={boot:"at start-up","15m":"every 15 minutes",hourly:"hourly",daily:"daily at 08:00"};export function sourceRow(t,e=Date.now()){const r=t.every?`${l[t.every]||t.every}${t.atBoot&&t.every!=="boot"?" and at start-up":""}`:"as it happens";return{id:String(t.id),label:String(t.label||t.id),description:String(t.description||""),meta:[t.ownerLabel||t.owner,r,t.lastRun?`last run ${ago(t.lastRun,e)}`:""].filter(Boolean).join(" \xB7 "),enabled:!!t.enabled,audience:t.audience||"super-admin",every:t.every||"",lastError:t.lastError?`Last run failed: ${t.lastError}`:""}}export const DESKTOP_LEVELS=["all","warning","critical"],DEFAULT_SETTINGS=Object.freeze({desktop:!1,desktopLevel:"all",signInToasts:!0});export function normaliseSettings(t){const e=t&&typeof t=="object"?t:{},r=DEFAULT_SETTINGS;return{desktop:typeof e.desktop=="boolean"?e.desktop:r.desktop,desktopLevel:DESKTOP_LEVELS.includes(e.desktopLevel)?e.desktopLevel:r.desktopLevel,signInToasts:typeof e.signInToasts=="boolean"?e.signInToasts:r.signInToasts}}export function meetsLevel(t,e="all"){return e==="critical"?t==="critical":e==="warning"?t==="critical"||t==="warning":!0}export function desktopWorthy(t,{seen:e=[],level:r="all",max:n=3}={}){const o=new Set((e||[]).map(String));return[...t||[]].filter(i=>i&&i.unread&&!o.has(String(i.id))&&meetsLevel(severityOf(i),r)).sort((i,a)=>String(i.data?.createdAt||"").localeCompare(String(a.data?.createdAt||""))).slice(-n)}export function rememberSeen(t,e,r=200){const n=[...(t||[]).map(String)];for(const o of e||[])n.includes(String(o))||n.push(String(o));return n.slice(-r)}export function desktopPayload(t){const e=t?.data||{},r=severityOf(t),n=String(e.body||"").replace(/\s+/g," ").trim();return{title:String(e.title||"Notification").slice(0,120),options:{body:n.length>240?`${n.slice(0,239)}\u2026`:n,tag:`dcms-notification-${t?.id}`,requireInteraction:r==="critical"}}}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as p}from"../api.js";import{isPathAllowed as te}from"./admin-scope.js";import{colourToCss as X,applyFloat as ne,applyHighlight as se,itemHighlightAttrs as ie}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{placeTools as oe,ensureLockedTools as ae,managePluginsItem as re,stripItemByUrl as le,insertFoldersBeforeSystem as ce,pruneEmptyToolsFolder as de,MANAGE_PLUGINS_URL as K}from"./sidebar-grouping.js";function h(t){return String(t??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}const ue=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];function N(t,s){return s?!t||!t.length?!1:t.includes(s)?!0:["read","create","update","delete"].some(n=>t.includes(`${s}.${n}`)):!0}function V(t,s,n=null){const e=[];for(const i of t){if(i.hidden||i.permission&&!N(s,i.permission))continue;const o=typeof i.url=="string"?i.url:"";if(n&&o.startsWith("#/")&&!te(n,o.slice(1)))continue;const l=Array.isArray(i.items)&&i.items.length>0,a=l?V(i.items,s,n):[];l&&!a.length&&!o||e.push({...i,items:a})}return e}async function me(t){if(!N(t,"projects"))return[];let s=[];try{s=await p.projects.listWithCounts()}catch{return[]}if(!Array.isArray(s))return[];const n=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(e=>{const o="#/projects/"+encodeURIComponent(e.slug),l=e.counts||{},a=[{text:"Overview",url:o,icon:e.icon||"folder"}];for(const u of n){const m=Number(l[u.key])||0;m<=0||a.push({text:`${u.text} (${m})`,url:o+u.path,icon:u.icon})}a.push({text:"Settings",url:o+"/settings",icon:"settings"});const c=e.enabled===!1;return{text:e.name||e.slug,icon:e.icon||"folder",items:a,...c&&{colour:"neutral",badge:{text:"off"}}}})}function fe(t,s){for(const n of t)if(!n.hidden&&(n.key==="projects"||!n.key&&(n.text||"").toLowerCase()==="projects")){n.items=Array.isArray(n.items)?n.items.slice():[],n.items.push(...s);return}}function G(t,s){const n=Y.includes(s.tone)?s.tone:"info",e=s.label||String(s.count);s.details?P.set(t,s.details):P.delete(t);const i
|
|
1
|
+
import{api as p}from"../api.js";import{isPathAllowed as te}from"./admin-scope.js";import{colourToCss as X,applyFloat as ne,applyHighlight as se,itemHighlightAttrs as ie}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{placeTools as oe,ensureLockedTools as ae,managePluginsItem as re,stripItemByUrl as le,insertFoldersBeforeSystem as ce,pruneEmptyToolsFolder as de,MANAGE_PLUGINS_URL as K}from"./sidebar-grouping.js";function h(t){return String(t??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}const ue=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu",permission:"menus"}];function N(t,s){return s?!t||!t.length?!1:t.includes(s)?!0:["read","create","update","delete"].some(n=>t.includes(`${s}.${n}`)):!0}function V(t,s,n=null){const e=[];for(const i of t){if(i.hidden||i.permission&&!N(s,i.permission))continue;const o=typeof i.url=="string"?i.url:"";if(n&&o.startsWith("#/")&&!te(n,o.slice(1)))continue;const l=Array.isArray(i.items)&&i.items.length>0,a=l?V(i.items,s,n):[];l&&!a.length&&!o||e.push({...i,items:a})}return e}async function me(t){if(!N(t,"projects"))return[];let s=[];try{s=await p.projects.listWithCounts()}catch{return[]}if(!Array.isArray(s))return[];const n=[{key:"pages",text:"Pages",icon:"file-text",path:"/pages"},{key:"collections",text:"Collections",icon:"database",path:"/collections"},{key:"forms",text:"Forms",icon:"layout",path:"/forms"},{key:"actions",text:"Actions",icon:"zap",path:"/actions"},{key:"menus",text:"Menus",icon:"menu",path:"/menus"},{key:"blocks",text:"Blocks",icon:"box",path:"/blocks"},{key:"components",text:"Components",icon:"component",path:"/components"},{key:"views",text:"Views",icon:"eye",path:"/views"},{key:"roles",text:"Roles",icon:"shield",path:"/roles"},{key:"users",text:"Users",icon:"users",path:"/users"},{key:"apis",text:"APIs",icon:"code",path:"/apis"}];return s.map(e=>{const o="#/projects/"+encodeURIComponent(e.slug),l=e.counts||{},a=[{text:"Overview",url:o,icon:e.icon||"folder"}];for(const u of n){const m=Number(l[u.key])||0;m<=0||a.push({text:`${u.text} (${m})`,url:o+u.path,icon:u.icon})}a.push({text:"Settings",url:o+"/settings",icon:"settings"});const c=e.enabled===!1;return{text:e.name||e.slug,icon:e.icon||"folder",items:a,...c&&{colour:"neutral",badge:{text:"off"}}}})}function fe(t,s){for(const n of t)if(!n.hidden&&(n.key==="projects"||!n.key&&(n.text||"").toLowerCase()==="projects")){n.items=Array.isArray(n.items)?n.items.slice():[],n.items.push(...s);return}}function G(t,s){const n=Y.includes(s.tone)?s.tone:"info",e=s.label||String(s.count);s.details?P.set(t,s.details):P.delete(t);const i=!s.inventory&&C.some(o=>o.url===t&&!o.inventory)?" data-live":"";return`<span class="sidebar-badge sidebar-badge--${n}" data-count-for="${h(t)}"${i} data-badge-label="${h(e)}" aria-label="${h(e)}">${h(String(s.count))}</span>`}function J(t){for(const s of t.querySelectorAll("details")){const n=s.querySelector(":scope > summary");if(!n)continue;let e=0,i=!1;const o=[];for(const m of s.querySelectorAll(":scope > .sidebar-children .sidebar-badge[data-live]")){const v=Number(m.textContent)||0;v&&(e+=v,i||(i=m.classList.contains("sidebar-badge--danger")),o.push({text:m.closest("a")?.querySelector(".sidebar-text")?.textContent.trim()??"",meta:m.dataset.badgeLabel,tone:m.classList.contains("sidebar-badge--danger")?"danger":void 0}))}const l=`folder:${s.dataset.stateKey||n.textContent.trim()}`;let a=n.querySelector(":scope > .sidebar-badge--sum");if(!e){a?.remove(),P.delete(l);continue}a||(a=document.createElement("span"),n.querySelector(".sidebar-text")?.after(a));const c=n.querySelector(".sidebar-text")?.textContent.trim()||"This folder",u=o.map(m=>`${m.text}: ${m.meta}`).join(" \xB7 ");a.className=`sidebar-badge sidebar-badge--sum sidebar-badge--${i?"danger":"info"}`,a.dataset.countFor=l,a.dataset.badgeLabel=u,a.setAttribute("aria-label",u),a.textContent=String(e),P.set(l,{title:c,items:o})}}const P=new Map;function pe(t,s){if(!t.url||!s)return"";const n=s[t.url];return!n||!(n.count>0)?"":G(t.url,n)}const Y=["info","attention","danger","live"],he={"#/pages":["page","pages"],"#/media":["file in the media library","files in the media library"],"#/menus":["menu","menus"],"#/context-menus":["context menu","context menus"],"#/theme":["element override in the theme","element overrides in the theme"],"#/collections":["collection","collections"],"#/forms":["form","forms"],"#/views":["view","views"],"#/actions":["action","actions"],"#/blocks":["block","blocks"],"#/components":["component","components"],"#/users":["user","users"],"#/plugins":["plugin installed","plugins installed"],"#/system/notifications":["unread notification","unread notifications"]};function U(t,{fallbackLabel:s="",tone:n="info"}={}){const e=Number(typeof t=="object"&&t!==null?t.count:t);if(!Number.isFinite(e)||e<0)return null;const i=typeof t?.label=="string"&&t.label||(s?`${e} - ${s}`:String(e)),o={count:Math.round(e),label:i,tone:Y.includes(t?.tone)?t.tone:n};return t?.details&&typeof t.details=="object"&&(o.details=t.details),t?.inventory===!0&&(o.inventory=!0),o}function ge(t){const s=(t.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),n=(t.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${n?`project-${decodeURIComponent(n[1])}.`:""}${s}`}function Q(t,s){if(t&&t.type==="separator")return'<hr class="sidebar-divider">';if(t&&t.type==="spacer"){const y=["xs","sm","md","lg","xl","flex"].includes(t.size)?t.size:"md";return`<div class="sidebar-spacer sidebar-spacer--${h(y)}" aria-hidden="true"></div>`}const n=t.icon?`<span data-icon="${h(t.icon)}"></span>`:"",e=h(t.text||""),i=t.url?h(t.url):"",o=Array.isArray(t.items)&&t.items.length>0,l=t.colour?X(t.colour):"",a=ie(t),c=[a.hover&&` data-menu-item-hover="${h(a.hover)}"`,a.active&&` data-menu-item-active="${h(a.active)}"`].filter(Boolean).join(""),u=[l&&`color:${h(l)}`,a.accent&&`--dm-menu-highlight:${h(a.accent)}`].filter(Boolean).join(";"),m=u?` style="${u}"`:"";let v="";if(t.badge&&t.badge.text!=null&&t.badge.text!==""){const y=t.badge.variant?X(t.badge.variant):"";v=`<span class="dm-menu-badge"${y?` style="background:${h(y)};color:var(--dm-text-inverse,#fff)"`:""}>${h(String(t.badge.text))}</span>`}if(o){const y=ge(t),k=S.get(y)!==!1,E=t.items.map(w=>Q(w,s)).join("");return`<details data-state-key="${h(y)}"${k?" open":""}>
|
|
2
2
|
<summary${m}>${n} <span class="sidebar-text">${e}</span>${v}</summary>
|
|
3
3
|
<div class="sidebar-children">${E}</div>
|
|
4
4
|
</details>`}return`<a href="${i}" class="sidebar-link" data-url="${i}"${m}${c}>${n} <span class="sidebar-text">${e}</span>${pe(t,s)}${v}</a>`}async function be(t=[],s=[]){const n=d=>d.then(g=>Array.isArray(g)?g.length:Array.isArray(g?.entries)?g.entries.length:0).catch(()=>0);C=xe(t);const e=new Set(C.map(d=>d.url)),i={pages:"#/pages",media:"#/media",menus:"#/menus","context-menus":"#/context-menus",theme:"#/theme",collections:"#/collections",forms:"#/forms",views:"#/views",actions:"#/actions",blocks:"#/blocks",components:"#/components",users:"#/users",plugins:"#/plugins",notifications:"#/system/notifications"},o=(d,g)=>N(s,d)&&!e.has(i[d])?g():Promise.resolve(0),[l,a,c,u,m,v,y,k,E,w,R,B,q,T]=await Promise.all([o("pages",()=>n(p.pages.list())),o("media",()=>n(p.media.list())),o("menus",()=>n(p.menus.list())),o("context-menus",()=>n(p.contextMenus.list())),o("theme",()=>p.theme.get().then(d=>(d?.overrides||[]).length).catch(()=>0)),o("collections",()=>n(p.collections.list())),o("forms",()=>n(p.forms.list())),o("views",()=>n(p.views.list())),o("actions",()=>n(p.actions.list())),o("blocks",()=>n(p.blocks.list())),o("components",()=>n(p.components.list())),o("users",()=>n(p.users.list())),o("plugins",()=>n(p.plugins.list())),o("notifications",()=>p.system?.notifications?.unreadCount?.().then(d=>d?.count??0).catch(()=>0)??Promise.resolve(0))]),j={};await Promise.all(C.map(async d=>{const g=await Z(d);g&&(j[d.url]=g)}));const I={"#/pages":l,"#/media":a,"#/menus":c,"#/context-menus":u,"#/theme":m,"#/collections":v,"#/forms":y,"#/views":k,"#/actions":E,"#/blocks":w,"#/components":R,"#/users":B,"#/plugins":q,"#/system/notifications":T},x={};for(const[d,g]of Object.entries(I)){if(e.has(d))continue;const[L,_]=he[d]??["item","items"],A=U(g);A&&(x[d]={...A,label:`${A.count} ${A.count===1?L:_}`})}return{...x,...j}}let C=[];const ye="dm:sidebar-count",ve=6e4;function xe(t=[]){const s=[],n=e=>{if(e){e.url&&typeof e.countUrl=="string"&&e.countUrl&&s.push({url:e.url,countUrl:e.countUrl,text:e.text||"",countLabel:e.countLabel||"",inventory:e.inventory===!0});for(const i of[...e.children||[],...e.items||[]])n(i)}};for(const e of t??[])n(e?.item??e);return s}async function Z(t){try{const s=new Date,n=`${s.getFullYear()}-${String(s.getMonth()+1).padStart(2,"0")}-${String(s.getDate()).padStart(2,"0")}`,e=t.countUrl+(t.countUrl.includes("?")?"&":"?")+`today=${n}`;return U(await H.get(e),{fallbackLabel:t.countLabel||t.text})}catch{return null}}function ee(t,s,n){const e=[...t.querySelectorAll("a.sidebar-link")].find(o=>o.dataset.url===s);if(!e)return;const i=e.querySelector(".sidebar-badge[data-count-for]");if(!n||!(n.count>0))i?.remove();else{const o=document.createRange();o.selectNodeContents(e);const l=o.createContextualFragment(G(s,n)).firstElementChild;i?i.replaceWith(l):e.querySelector(".sidebar-text")?.after(l)}O.set(s,n),J(t)}const O=new Map;async function W(t,s=null){const n=s?C.filter(e=>e.url===s):C;await Promise.all(n.map(async e=>{const i=await Z(e),o=O.get(e.url);JSON.stringify(o??null)!==JSON.stringify(i)&&ee(t,e.url,i)}))}function $e(t){t.dataset.liveCounts||(t.dataset.liveCounts="1",window.addEventListener(ye,s=>{const n=s.detail||{};if(n.url)if(n.count!==void 0){const e=C.find(i=>i.url===n.url);ee(t,n.url,U(n,{fallbackLabel:e?.countLabel||e?.text||""}))}else W(t,n.url)}),setInterval(()=>{!document.hidden&&document.body.contains(t)&&W(t)},ve),document.addEventListener("visibilitychange",()=>{document.hidden||W(t)}),Se(t))}function Ce(t,s,n){const e=(l,a,c)=>{const u=document.createElement(l);return a&&(u.className=a),c!=null&&(u.textContent=String(c)),u};if(!n){t.textContent=s;return}t.appendChild(e("strong","dm-badge-pop-title",n.title||s)),(n.rows?.length||!n.items?.length)&&t.appendChild(e("p","dm-badge-pop-sub",s));const i=Array.isArray(n.rows)?n.rows.slice(0,6):[];if(i.length){const l=e("dl","dm-badge-pop-rows");for(const a of i)l.appendChild(e("dt",null,a.label)),l.appendChild(e("dd",a.tone==="danger"?"is-danger":null,a.value));t.appendChild(l)}const o=Array.isArray(n.items)?n.items.slice(0,6):[];if(o.length){const l=e("ul","dm-badge-pop-items");for(const a of o){const c=e("li",a.tone==="danger"?"is-danger":null);c.appendChild(e("span","dm-badge-pop-text",a.text)),a.meta&&c.appendChild(e("span","dm-badge-pop-meta",a.meta)),l.appendChild(c)}t.appendChild(l)}else n.empty&&t.appendChild(e("p","dm-badge-pop-empty",n.empty))}function Se(t){let s=null,n=null;const e=()=>{clearTimeout(n),s?.remove(),s=null},i=o=>{e();const l=o?.dataset.badgeLabel;l&&(n=setTimeout(()=>{s=document.createElement("div"),s.className="dm-help-pop dm-badge-pop",s.setAttribute("role","tooltip"),Ce(s,l,P.get(o.dataset.countFor)),document.body.appendChild(s);const a=o.getBoundingClientRect(),c=s.getBoundingClientRect(),u=Math.min(a.right+10,innerWidth-c.width-8);s.style.left=`${Math.max(8,u)}px`,s.style.top=`${Math.max(8,Math.min(a.top+a.height/2-c.height/2,innerHeight-c.height-8))}px`},150))};t.addEventListener("mouseover",o=>{const l=o.target.closest?.(".sidebar-badge[data-badge-label]");l&&i(l)}),t.addEventListener("mouseout",o=>{o.target.closest?.(".sidebar-badge[data-badge-label]")&&e()}),t.addEventListener("focusin",o=>{const l=o.target.closest?.("a, summary")?.querySelector(":scope > .sidebar-badge[data-badge-label]:not([hidden])");l?i(l):e()}),t.addEventListener("focusout",e),window.addEventListener("scroll",e,!0),window.addEventListener("hashchange",e)}async function we(t=[]){if(!N(t,"settings"))return"Admin";try{const s=await p.settings.get();return s?.adminBrand?.title||s?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:t,permissions:s,adminScope:n=null}){const e=$(t).get(0);if(!e)return;let i,o=null,l=null,a=null,c=null,u=null;try{const r=await p.get("/sidebar/menu");i=Array.isArray(r?.items)?r.items:null,o=r?.variant||null,l=r?.position||null,a=r?.style||null,c=r?.float||null,u=r?.highlight||null}catch{i=null}(!i||!i.some(r=>r&&(!r.hidden||r.restricted)))&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),i=ue.slice());let m=[];try{m=await H.get("/api/sidebar/registered-items")||[]}catch{}i=le(i,K),i=oe(i,m.filter(r=>(r?.item??r)?.url!==K)),i=ae(i),i=ce(i,[re()]);const v=await me(s);v.length&&fe(i,v),i=V(i,s,n),i=de(i);const[y,k]=await Promise.all([be(m.filter(r=>N(s,r?.item?.permission)),s).catch(()=>({})),we(s)]),E=o?` dm-admin-sidebar--${h(o)}`:"";function w(r,b="px"){if(r==null)return r;const f=String(r).trim();return/^\d+(\.\d+)?$/.test(f)?`${f}${b}`:f}let R="";if(a){const r=[];a.fontFamily&&r.push(`font-family: ${a.fontFamily}, sans-serif`),a.fontSize&&r.push(`font-size: ${w(a.fontSize)}`),a.fontWeight&&r.push(`font-weight: ${a.fontWeight}`),a.letterSpacing&&r.push(`letter-spacing: ${w(a.letterSpacing,"em")}`);const b=[];if(r.length&&b.push(`#admin-sidebar .dm-admin-sidebar, #admin-sidebar .dm-admin-sidebar .sidebar-link, #admin-sidebar .dm-admin-sidebar summary, #admin-sidebar .dm-admin-sidebar .sidebar-text { ${r.join("; ")} }`),a.iconSize){const f=w(a.iconSize);b.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${f} !important; height: ${f} !important; }`)}b.length&&(R=`<style data-admin-sidebar-style>${b.join(" ")}</style>`)}const B=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${h(k)}</div>`,q=i.some(r=>r&&r.type==="spacer"&&r.size==="flex")?" dm-admin-sidebar--has-flex-spacer":"",T=`${R}<nav class="dm-admin-sidebar${E}${q}">${B}${i.map(r=>Q(r,y)).join("")}</nav>`,j=document.createRange();j.selectNodeContents(e);const I=j.createContextualFragment(T);e.replaceChildren(I),Domma.icons.scan(e);for(const r of C)O.set(r.url,y[r.url]??null);J(e),$e(e);const x=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(x)&&x>=180&&x<=480&&(e.style.width=x+"px"),e.removeAttribute("data-menu-hover"),e.removeAttribute("data-menu-active"),e.style.removeProperty("--dm-menu-highlight"),se(e,{highlight:u});const d=document.querySelector(".dashboard-wrapper");if(["position","top","bottom","left","right","zIndex","maxHeight","transform"].forEach(r=>{e.style[r]=""}),e.classList.remove("dm-menu-floating"),d&&(d.style.paddingLeft="",d.style.paddingRight=""),l==="floating"){ne(e,{position:"floating",float:c||{}});const b=c&&c.anchor||"TL",f=Number.isFinite(Number(c?.offsetX))?Number(c.offsetX):16,F=e.getBoundingClientRect().width||x||260;if(d){const z=Math.round(F+f*2)+"px";b==="R"||b==="TR"||b==="BR"?d.style.paddingRight=z:d.style.paddingLeft=z}}const g=document.createElement("div");g.className="dm-admin-sidebar-handle",g.setAttribute("aria-label","Resize sidebar"),e.appendChild(g),e.querySelectorAll("details").forEach(r=>{r.addEventListener("toggle",function(){const b=this.getAttribute("data-state-key");b&&S.set(b,this.open);const f=this.querySelector(":scope > .sidebar-children");if(f)if(this.open){const F=f.scrollHeight;f.style.maxHeight="0",requestAnimationFrame(()=>{f.style.maxHeight=F+"px",f.addEventListener("transitionend",function z(){f.style.maxHeight="none",f.removeEventListener("transitionend",z)})})}else{const F=f.scrollHeight;f.style.maxHeight=F+"px",requestAnimationFrame(()=>{f.style.maxHeight="0"})}})});let L=!1,_=0,A=0;g.addEventListener("mousedown",r=>{L=!0,_=r.clientX,A=e.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",r.preventDefault()}),e.dataset.resizeWired||(e.dataset.resizeWired="1",document.addEventListener("mousemove",r=>e.__dmResize?.move(r)),document.addEventListener("mouseup",()=>e.__dmResize?.up()),M.subscribe("router:afterChange",()=>e.__dmSetActive?.())),e.__dmResize={move(r){if(!L)return;const b=Math.max(180,Math.min(480,A+(r.clientX-_)));e.style.width=b+"px"},up(){L&&(L=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(e.style.width,10)))}};function D(){const r=location.hash||"#/";$(e).find(".sidebar-link").removeClass("active"),$(e).find(`.sidebar-link[data-url="${r}"]`).addClass("active")}D(),e.__dmSetActive=D}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<div class="ntx">
|
|
5
5
|
<p class="ntx-note">Messages for this site's administrators - maintenance, updates and warnings, mostly from the
|
|
6
6
|
manager that hosts it.
|
|
7
|
-
<span data-help-title="Notifications" data-help="Opening this screen marks everything in it as read - the ones that were unread stay marked New until you leave. Click a notification to read all of it; right-click it (or ⋮, or Shift+F10) to open its link, copy it, mark it unread again, dismiss it or delete it. Dismissing hides it for you only; deleting removes it for everyone and needs the delete permission. A notification with an end date disappears by itself once it passes."></span></p>
|
|
7
|
+
<span data-help-title="Notifications" data-help="Opening this screen marks everything in it as read - the ones that were unread stay marked New until you leave. Click a notification to read all of it; right-click it (or ⋮, or Shift+F10) to open its link, copy it, mark it unread again, dismiss it or delete it. Dismissing hides it for you only; deleting removes it for everyone and needs the delete permission. A notification with an end date disappears by itself once it passes. The cog in the banner turns on desktop notifications for this browser."></span></p>
|
|
8
8
|
|
|
9
9
|
<div class="ntx-card">
|
|
10
10
|
<div class="ntx-bar">
|
|
@@ -70,5 +70,25 @@
|
|
|
70
70
|
.ntx-chip.is-new { background: var(--dm-primary); color: var(--dm-primary-text); }
|
|
71
71
|
.ntx-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; }
|
|
72
72
|
.ntx-kebab:hover { color: var(--dm-primary); }
|
|
73
|
+
/* The settings slideover (it sits outside .ntx, in the page body). */
|
|
74
|
+
.ntx-panel { display: flex; flex-direction: column; gap: 1rem; padding: .25rem 0 .5rem; }
|
|
75
|
+
.ntx-panel [hidden] { display: none !important; }
|
|
76
|
+
.ntx-panel .form-label { display: flex; align-items: center; font-weight: 500; margin-bottom: .3rem; }
|
|
77
|
+
.ntx-setting-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
|
|
78
|
+
.ntx-panel-meta { font-size: var(--dm-text-xs); color: var(--dm-text-muted); margin-top: -.6rem; }
|
|
79
|
+
.ntx-panel-meta.is-bad { color: var(--dm-danger); }
|
|
80
|
+
.ntx-panel-actions { display: flex; align-items: center; gap: .5rem; padding-top: .75rem; border-top: 1px solid var(--dm-border); }
|
|
81
|
+
.ntx-panel-actions .spacer { flex: 1; }
|
|
82
|
+
.ntx-panel-h { display: flex; align-items: center; gap: .3rem; margin: .5rem 0 0; font-size: var(--dm-text-base); font-weight: 600; }
|
|
83
|
+
.ntx-sources { display: flex; flex-direction: column; gap: .5rem; border-top: 1px solid var(--dm-border); padding-top: .75rem; }
|
|
84
|
+
.ntx-src-list { display: flex; flex-direction: column; margin-top: .6rem; }
|
|
85
|
+
.ntx-src { display: flex; flex-direction: column; gap: .25rem; padding: .55rem 0; border-bottom: 1px solid var(--dm-border); }
|
|
86
|
+
.ntx-src:last-child { border-bottom: 0; }
|
|
87
|
+
.ntx-src-head { display: flex; align-items: flex-start; justify-content: space-between; gap: .75rem; }
|
|
88
|
+
.ntx-src-name { display: flex; flex-direction: column; gap: .1rem; min-width: 0; }
|
|
89
|
+
.ntx-src-name small, .ntx-src-desc { color: var(--dm-text-muted); font-size: var(--dm-text-xs); }
|
|
90
|
+
.ntx-src-foot { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
|
|
91
|
+
.ntx-src-aud { max-width: 18rem; padding-top: .2rem; padding-bottom: .2rem; font-size: var(--dm-text-sm); }
|
|
92
|
+
.ntx-src-err { color: var(--dm-danger); font-size: var(--dm-text-xs); }
|
|
73
93
|
@media (max-width: 640px) { .ntx-preview { white-space: normal; } }
|
|
74
94
|
</style>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as g,apiRequest as m}from"../api.js";import{addBannerAction as U}from"../lib/plugin-chrome.js";import{helpPopovers as ee}from"../lib/help-popover.js";import{onceClosed as Ae,openMenuAt as te,sidebarCount as Ne,viewLifetime as Re}from"../lib/tool-kit.js";import{DEFAULT_SETTINGS as se,FILTERS as Le,arrangeNotifications as De,filterCounts as $e,normaliseSettings as Ie,notificationRow as ne,sourceRow as Oe}from"../lib/notifications-arrange.js";import{desktopPermission as ae,notificationSettings as oe,primeSeen as Pe,requestDesktopPermission as qe,showTestNotification as Ue}from"../lib/desktop-notify.js";const ie=/^#\/system\/notifications\/?(\?.*)?$/,_e="#/system/notifications",_="notifications.filter",Be=`
|
|
2
2
|
<div class="ntx-row" data-id="{{id}}" tabindex="0" data-bind-class="rowCls" data-bind-aria-expanded="open ? 'true' : 'false'">
|
|
3
3
|
<span class="ntx-dot" aria-hidden="true"></span>
|
|
4
4
|
<span class="ntx-main">
|
|
@@ -9,10 +9,73 @@ import{api as d,apiRequest as b}from"../api.js";import{addBannerAction as O}from
|
|
|
9
9
|
<span class="ntx-preview" data-bind-hidden="open">{{preview}}</span>
|
|
10
10
|
<span class="ntx-body" data-bind-hidden="!open">{{body}}</span>
|
|
11
11
|
<span class="ntx-actions" data-bind-hidden="!open || !link">
|
|
12
|
-
<a class="btn btn-sm btn-outline" href="{{link}}" target="
|
|
12
|
+
<a class="btn btn-sm btn-outline" href="{{link}}" target="{{linkTarget}}" rel="noopener">{{linkText}}</a>
|
|
13
13
|
</span>
|
|
14
14
|
</span>
|
|
15
15
|
<button type="button" class="ntx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
|
|
16
|
-
</div>`,
|
|
16
|
+
</div>`,We=r=>String(r??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),Fe=`
|
|
17
|
+
<div class="ntx-src" data-id="{{id}}">
|
|
18
|
+
<div class="ntx-src-head">
|
|
19
|
+
<span class="ntx-src-name"><strong>{{label}}</strong><small>{{meta}}</small></span>
|
|
20
|
+
<input type="checkbox" class="form-switch ntx-src-on" aria-label="Send: {{label}}" data-bind-checked="enabled">
|
|
21
|
+
</div>
|
|
22
|
+
<small class="ntx-src-desc">{{description}}</small>
|
|
23
|
+
<div class="ntx-src-foot" data-bind-hidden="!enabled">
|
|
24
|
+
<select class="form-input ntx-src-aud" aria-label="Who gets these: {{label}}">
|
|
25
|
+
<option value="super-admin">Super Admins</option>
|
|
26
|
+
<option value="admins">Admins and Super Admins</option>
|
|
27
|
+
<option value="everyone">Everyone who can read notifications</option>
|
|
28
|
+
</select>
|
|
29
|
+
<button type="button" class="btn btn-sm btn-ghost ntx-src-run" data-bind-hidden="!every">Run now</button>
|
|
30
|
+
</div>
|
|
31
|
+
<small class="ntx-src-err" data-bind-hidden="!lastError">{{lastError}}</small>
|
|
32
|
+
</div>`,C={granted:"Allowed in this browser.",default:"Your browser will ask for permission when you turn this on.",denied:"Blocked in this browser. Allow notifications for this site in the browser's site settings (the icon by the address), then turn this on.",unsupported:"This browser cannot show desktop notifications here - they need a current browser and an https address."};function je(r){const c=document.createElement("template");return c.innerHTML=r.trim(),c.content.firstElementChild}const p=Re(ie);async function f(){Ne(_e);try{const{count:r}=await g.system.notifications.unreadCount(),c=$("#topbar-bell-badge");r>0?c.text(r>99?"99+":String(r)).removeAttr("hidden"):c.attr("hidden","").text("")}catch{}}export const notificationsView={templateUrl:"/admin/js/templates/notifications.html",async onMount(r){p.reset();const c=r.get(0).querySelector(".ntx")||r.get(0),A=()=>ie.test(location.hash)&&c.isConnected,i=M.observable([]),k=M.observable(!0),N=M.observable("");let B="all";try{B=Le.includes(S.get(_))?S.get(_):"all"}catch{}const h=M.observable(B),W=M.observable(""),x=M.observable("");p.add(M.effect(()=>{const e=h.value;try{S.set(_,e)}catch{}}));const F=M.computed(()=>$e(i.value));p.add(M.effect(()=>{const e=F.value;!k.value&&h.peek()!=="all"&&!e[h.peek()]&&(h.value="all")}));const j=M.observable([]);p.add(M.effect(()=>{const e=Date.now(),t=x.value;j.value=De(i.value,{filter:h.value,query:W.value}).map(n=>ne(n,{now:e,expanded:t}))}));const re=M.computed(()=>i.value.length?"No notifications match.":"No notifications - you're all caught up."),d=M.observable(oe.read());p.add(M.effect(()=>oe.write(d.value)));const R=(e,t)=>{d.value=Ie({...d.peek(),[e]:t})},H=e=>i.peek().find(t=>String(t.id)===String(e)),le=(e,t)=>{i.value=i.peek().map(n=>String(n.id)===String(e)?t(n):n)},G=e=>{i.value=i.peek().filter(t=>String(t.id)!==String(e))};async function T({markRead:e=!0}={}){try{const t=await g.system.notifications.list();if(!A())return;const n=Array.isArray(t)?t:[];if(i.value=n.map(l=>({...l,isNew:!!l.unread})),N.value="",e&&n.some(l=>l.unread)){if(await m("/system/notifications/read-all",{method:"POST"}).catch(()=>{}),!A())return;i.value=i.peek().map(l=>({...l,unread:!1})),f()}}catch(t){N.value=`Could not load notifications: ${t.message||t}`}finally{k.value=!1}}async function K(e,t){try{t?await g.system.notifications.markRead(e.id):await m(`/system/notifications/${encodeURIComponent(e.id)}/unread`,{method:"POST"}),le(e.id,n=>({...n,unread:!t})),f()}catch(n){E.toast(`Not changed: ${n.message||n}`,{type:"error"})}}async function Y(e){try{await g.system.notifications.dismiss(e.id),G(e.id),f()}catch(t){E.toast(`Could not dismiss it: ${t.message||t}`,{type:"error"})}}async function ce(e){if(await E.confirm(`Delete "${We(e.data?.title||"this notification")}" for everyone? Dismissing hides it for you only.`))try{await g.system.notifications.remove(e.id),G(e.id),E.toast("Notification deleted.",{type:"success"}),f()}catch(t){E.toast(/403|forbidden|permission/i.test(String(t.message||t))?"Deleting needs the Notifications delete permission - Dismiss hides it for you.":`Could not delete it: ${t.message||t}`,{type:"error"})}}async function de(){const e=i.peek().length;if(!e){E.toast("Nothing to dismiss.",{type:"info"});return}if(await E.confirm(`Dismiss all ${e} notification${e===1?"":"s"}? They are hidden for you only.`))try{await m("/system/notifications/dismiss-all",{method:"POST"}),i.value=[],f()}catch(t){E.toast(`Could not dismiss them: ${t.message||t}`,{type:"error"})}}async function pe(e){const t=e.data||{},n=[t.title,t.body,t.link].filter(Boolean).join(`
|
|
17
33
|
|
|
18
|
-
`);try{await navigator.clipboard.writeText(n),E.toast("Copied.",{type:"success"})}catch{E.toast("Copying is blocked here - select the text instead.",{type:"warning"})}}const
|
|
34
|
+
`);try{await navigator.clipboard.writeText(n),E.toast("Copied.",{type:"success"})}catch{E.toast("Copying is blocked here - select the text instead.",{type:"warning"})}}const L=e=>{x.value=x.peek()===String(e)?"":String(e)};function ue(e){const t=H(e);if(!t)return[];const n=ne(t);return[{header:n.title},{label:n.open||x.peek()===n.id?"Close":"Read it",icon:"eye",action:()=>L(e)},...n.link?[n.linkTarget==="_self"?{label:"Go there",icon:"arrow-right",action:()=>{location.hash=n.link}}:{label:"Open link",icon:"external-link",action:()=>window.open(n.link,"_blank","noopener")}]:[],{label:"Copy text",icon:"copy",action:()=>pe(t)},t.unread?{label:"Mark as read",icon:"check",action:()=>K(t,!0)}:{label:"Mark as unread",icon:"mail",action:()=>K(t,!1)},{type:"divider"},{label:"Dismiss",icon:"x",action:()=>Y(t)},{label:"Delete for everyone\u2026",icon:"trash",danger:!0,action:()=>ce(t)}]}const b=c.querySelector(".ntx-list");b.innerHTML=Be;const y=e=>()=>{h.value=e},he=M.applyBindings({items:i,loading:k,loadError:N,filter:h,query:W,rows:j,counts:F,emptyText:re},c,{methods:{showAll:y("all"),showNew:y("new"),showCritical:y("critical"),showWarning:y("warning"),showInfo:y("info"),showSuccess:y("success")}});p.add(()=>he.dispose()),p.add(ee(c,{position:"bottom"}));const me=E.contextMenu(b,{cursor:"glyph",match:".ntx-row",inherit:!1,density:"compact",items:e=>ue(e.dataset.id)});p.add(()=>me?.destroy?.());const z=e=>{const t=e.target.closest(".ntx-row");if(!t)return;const n=e.target.closest(".ntx-kebab");if(n){te(n);return}e.target.closest("a")||L(t.dataset.id)},J=e=>{const t=e.target.closest(".ntx-row");if(t){if(e.key==="F10"&&e.shiftKey)e.preventDefault(),te(t);else if(e.key==="Enter"&&e.target===t)L(t.dataset.id);else if(e.key==="Delete"&&e.target===t){const n=H(t.dataset.id);n&&Y(n)}}};b.addEventListener("click",z),b.addEventListener("keydown",J),p.add(()=>{b.removeEventListener("click",z),b.removeEventListener("keydown",J)});async function fe(){try{const{added:e}=await m("/system/notifications/samples",{method:"POST"});await T({markRead:!1}),f(),E.toast(`${e} sample notifications added.${d.peek().desktop?" They pop up on the desktop within a minute.":""}`,{type:"success"})}catch(e){E.toast(/403|forbidden|permission/i.test(String(e.message||e))?"Adding samples needs the Notifications delete permission.":`Could not add samples: ${e.message||e}`,{type:"error"})}}function be(){const e=E.slideover({title:"Notifications settings",size:"md",position:"right"}),t=je(`
|
|
35
|
+
<div class="ntx-panel">
|
|
36
|
+
<div class="ntx-setting-row">
|
|
37
|
+
<label class="form-label" for="ntx-set-desktop" style="margin:0">Desktop notifications
|
|
38
|
+
<span data-help-title="Desktop notifications" data-help="New notifications pop up on your desktop, even when the admin is in a background tab - as long as it is open somewhere. They are checked every minute. Click one to come back here. This is for this browser only: turn it on in each browser you use."></span>
|
|
39
|
+
</label>
|
|
40
|
+
<input id="ntx-set-desktop" type="checkbox" class="form-switch" data-model="desktop.value">
|
|
41
|
+
</div>
|
|
42
|
+
<small class="ntx-panel-meta" data-bind-class="permTone.value" data-bind-text="permText.value"></small>
|
|
43
|
+
<div data-bind-hidden="!desktop.value">
|
|
44
|
+
<label class="form-label" for="ntx-set-level">Which ones
|
|
45
|
+
<span data-help="Everything still arrives here and on the bell - this only decides what is worth interrupting you for. Critical ones stay on the desktop until you close them."></span>
|
|
46
|
+
</label>
|
|
47
|
+
<select id="ntx-set-level" class="form-input" data-model="desktopLevel.value">
|
|
48
|
+
<option value="all">All of them</option>
|
|
49
|
+
<option value="warning">Warnings and critical</option>
|
|
50
|
+
<option value="critical">Critical only</option>
|
|
51
|
+
</select>
|
|
52
|
+
</div>
|
|
53
|
+
<div data-bind-hidden="!canTest.value">
|
|
54
|
+
<button type="button" class="btn btn-sm btn-outline" data-on-click="test">Send a test</button>
|
|
55
|
+
</div>
|
|
56
|
+
<div class="ntx-setting-row">
|
|
57
|
+
<label class="form-label" for="ntx-set-toasts" style="margin:0">Pop-ups when you sign in
|
|
58
|
+
<span data-help="On signing in, unread warnings and critical notifications (up to three) are shown as messages that stay until you close them."></span>
|
|
59
|
+
</label>
|
|
60
|
+
<input id="ntx-set-toasts" type="checkbox" class="form-switch" data-model="signInToasts.value">
|
|
61
|
+
</div>
|
|
62
|
+
<small class="ntx-panel-meta">Saved in this browser as you change them.</small>
|
|
63
|
+
<div class="ntx-setting-row">
|
|
64
|
+
<label class="form-label" style="margin:0">Sample notifications
|
|
65
|
+
<span data-help-title="Sample notifications" data-help="Adds four made-up notifications - one critical, warning, info and success - to see how they look, and to try the bell, the sidebar badge and desktop notifications. They are added for everyone, stay unread until you open this screen again, and go by themselves after a week. Adding them again replaces the earlier ones. Needs the Notifications delete permission."></span>
|
|
66
|
+
</label>
|
|
67
|
+
<button type="button" class="btn btn-sm btn-outline" data-on-click="samples">Add samples</button>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="ntx-sources" data-bind-hidden="!sourcesShown.value">
|
|
70
|
+
<h4 class="ntx-panel-h">What this site tells you
|
|
71
|
+
<span data-help-title="Sources" data-help="Everything the site and its plugins can notify about. Switch one off and it stops sending; choose who gets each one. These settings are for the whole site and only Super Admins can see or change them. Checks that run on a timer can be run now to try them."></span>
|
|
72
|
+
</h4>
|
|
73
|
+
<small class="ntx-panel-meta">For the whole site - only Super Admins see this.</small>
|
|
74
|
+
<div class="ntx-src-list" data-each="sourceRows.value key=id"></div>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="ntx-panel-actions">
|
|
77
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="reset">Reset to defaults</button>
|
|
78
|
+
<span class="spacer"></span>
|
|
79
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="done">Done</button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>`),n=t.querySelector("#ntx-set-desktop"),l=t.querySelector(".ntx-src-list");l.innerHTML=Fe;const w=M.observable([]),D=M.observable([]),O=M.observable(!1),ye=M.effect(()=>{D.value=w.value.map(s=>Oe(s))}),ve=M.effect(()=>{const s=D.value;setTimeout(()=>s.forEach(a=>{const o=l.querySelector(`.ntx-src[data-id="${CSS.escape(a.id)}"] .ntx-src-aud`);o&&(o.value=a.audience)}),0)}),P=s=>{w.value=w.peek().map(a=>a.id===s.id?s:a)},V=s=>`/system/notifications/sources/${encodeURIComponent(s)}`;m("/system/notifications/sources",{method:"GET"}).then(s=>{w.value=s.sources||[],O.value=!0}).catch(()=>{O.value=!1});async function X(s,a){const o=w.peek().find(v=>v.id===s);try{const{setting:v}=await m(V(s),{method:"PUT",body:JSON.stringify(a)});P({...o,...v})}catch(v){P({...o}),E.toast(`Not saved: ${v.message||v}`,{type:"error"})}}async function ge(s,a){a.disabled=!0;try{const o=await m(`${V(s)}/run`,{method:"POST"});o.source&&P(o.source),E.toast(o.ok?"Done - anything it found is in the list.":`It failed: ${o.error}`,{type:o.ok?"success":"error"}),await T({markRead:!1}),f()}catch(o){E.toast(`Could not run it: ${o.message||o}`,{type:"error"})}finally{a.disabled=!1}}const Q=s=>{const a=s.target.closest(".ntx-src");a&&(s.target.matches(".ntx-src-on")?X(a.dataset.id,{enabled:s.target.checked}):s.target.matches(".ntx-src-aud")&&X(a.dataset.id,{audience:s.target.value}))},Z=s=>{const a=s.target.closest(".ntx-src-run");a&&ge(a.closest(".ntx-src").dataset.id,a)};l.addEventListener("change",Q),l.addEventListener("click",Z);const u=M.observable(ae()),we=M.computed(()=>d.value.desktop&&u.value!=="granted"?`On, but not showing: ${C[u.value]}`:C[u.value]),ke=M.computed(()=>u.value==="denied"||u.value==="unsupported"?"is-bad":""),xe=M.computed(()=>d.value.desktop&&u.value==="granted"),q=()=>{n.checked=d.peek().desktop};function Se(){const s=ae();if(s==="unsupported"||s==="denied"){u.value=s,E.toast(C[s],{type:"warning"}),q();return}(s==="granted"?Promise.resolve("granted"):qe()).then(async o=>{u.value=o,o==="granted"?(Pe(await g.system.notifications.list().catch(()=>[])),R("desktop",!0),E.toast("Desktop notifications are on for this browser.",{type:"success"})):E.toast(o==="denied"?C.denied:"Not turned on - the browser was not given permission.",{type:"warning"}),q()})}const Ee=s=>M.computed({read:()=>d.value[s],write:a=>R(s,a)}),Te={...Object.fromEntries(Object.keys(se).map(s=>[s,Ee(s)])),desktop:M.computed({read:()=>d.value.desktop,write:s=>s?Se():R("desktop",!1)}),permText:we,permTone:ke,canTest:xe,sourceRows:D,sourcesShown:O};e.element.appendChild(t);const Me=ee(t),Ce=M.applyBindings(Te,t,{methods:{done:()=>e.close(),reset:()=>{d.value={...se},q()},samples:()=>{fe()},test:()=>{Ue()||E.toast("The browser would not show it - check its notification settings.",{type:"warning"})}}});Ae(e,()=>{Ce.dispose(),Me(),ye(),ve(),l.removeEventListener("change",Q),l.removeEventListener("click",Z)}),e.open()}U(r,{id:"notifications-settings",icon:"settings",title:"Notifications settings",onClick:be}),U(r,{id:"notifications-dismiss-all",icon:"check-circle",title:"Dismiss all",onClick:()=>de()}),U(r,{id:"notifications-refresh",icon:"refresh-cw",title:"Refresh",onClick:()=>{k.value=!0,T()}}),I.scan(c),await T(),A()&&I.scan(c)}};
|
package/package.json
CHANGED
|
@@ -53,12 +53,15 @@ export function apiTokensBadge(tokens = [], now = Date.now()) {
|
|
|
53
53
|
const d = Math.ceil((at(t) - now) / DAY);
|
|
54
54
|
return d <= 0 ? 'today' : d === 1 ? 'tomorrow' : `in ${d} days`;
|
|
55
55
|
};
|
|
56
|
+
// A number either way (0.80): how many tokens while none needs anyone - an
|
|
57
|
+
// inventory - and the expired and expiring ones when some do.
|
|
56
58
|
return {
|
|
57
|
-
count: need,
|
|
59
|
+
count: need || tokens.length,
|
|
60
|
+
...(!need && {inventory: true}),
|
|
58
61
|
label: need
|
|
59
62
|
? [expired.length && `${plural(expired.length, 'token')} expired`, soon.length && `${soon.length} expiring soon`].filter(Boolean).join(', ')
|
|
60
63
|
: `${plural(tokens.length, 'token')}, none expiring soon`,
|
|
61
|
-
tone: expired.length ? 'danger' : 'attention',
|
|
64
|
+
tone: expired.length ? 'danger' : need ? 'attention' : 'info',
|
|
62
65
|
details: {
|
|
63
66
|
title: 'API Tokens',
|
|
64
67
|
rows: [
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
import {getProfile, updateProfile} from '../../services/userProfiles.js';
|
|
32
32
|
import {buildClearedSessionCookie, buildSessionCookie} from '../../services/viewerSession.js';
|
|
33
33
|
import {createTransport, sendEmail} from '../../services/email.js';
|
|
34
|
+
import {loginFailed, loginRateLimited} from '../../services/notification-sources.js';
|
|
34
35
|
|
|
35
36
|
const { accessTokenExpiry, refreshTokenExpiry } = config.auth;
|
|
36
37
|
|
|
@@ -94,7 +95,7 @@ export async function authRoutes(fastify) {
|
|
|
94
95
|
});
|
|
95
96
|
|
|
96
97
|
// POST /api/auth/login
|
|
97
|
-
fastify.post('/auth/login', { config: { rateLimit: { max: 5, timeWindow: '1 minute' } } }, async (request, reply) => {
|
|
98
|
+
fastify.post('/auth/login', { config: { rateLimit: { max: 5, timeWindow: '1 minute', onExceeded: (req) => { loginRateLimited(req.ip); } } } }, async (request, reply) => {
|
|
98
99
|
const { email, password } = request.body || {};
|
|
99
100
|
if (!email || !password) {
|
|
100
101
|
return reply.status(400).send({ error: 'email and password are required' });
|
|
@@ -102,11 +103,13 @@ export async function authRoutes(fastify) {
|
|
|
102
103
|
|
|
103
104
|
const user = await getUserByEmail(email);
|
|
104
105
|
if (!user || !user.isActive) {
|
|
106
|
+
loginFailed(email, request.ip);
|
|
105
107
|
return reply.status(401).send({ error: 'Invalid credentials' });
|
|
106
108
|
}
|
|
107
109
|
|
|
108
110
|
const valid = await validatePassword(password, user.password);
|
|
109
111
|
if (!valid) {
|
|
112
|
+
loginFailed(email, request.ip);
|
|
110
113
|
return reply.status(401).send({ error: 'Invalid credentials' });
|
|
111
114
|
}
|
|
112
115
|
|
|
@@ -156,6 +159,17 @@ export async function authRoutes(fastify) {
|
|
|
156
159
|
return {...user, profile: profileEntry?.data || {}};
|
|
157
160
|
});
|
|
158
161
|
|
|
162
|
+
// GET /api/auth/me/_count - My Profile's sidebar badge: profile details left empty
|
|
163
|
+
fastify.get('/auth/me/_count', {preHandler: [authenticate]}, async (request, reply) => {
|
|
164
|
+
const user = await getUserById(request.user.id);
|
|
165
|
+
if (!user) return reply.status(404).send({error: 'User not found'});
|
|
166
|
+
const {getCollection} = await import('../../services/collections.js');
|
|
167
|
+
const schema = await getCollection('user-profiles').catch(() => null);
|
|
168
|
+
const profileEntry = await getProfile(request.user.id);
|
|
169
|
+
const {profileBadge} = await import('../../services/sidebarBadges.js');
|
|
170
|
+
return profileBadge(user, profileEntry?.data || {}, schema?.fields || []);
|
|
171
|
+
});
|
|
172
|
+
|
|
159
173
|
// PUT /api/auth/me - self-service update (name, email, password, profile)
|
|
160
174
|
fastify.put('/auth/me', {preHandler: [authenticate]}, async (request, reply) => {
|
|
161
175
|
const id = request.user.id;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Effects API Routes
|
|
3
3
|
* GET /api/effects/settings - public (read by public site IIFE)
|
|
4
4
|
* PUT /api/effects/settings - admin-only (save user overrides)
|
|
5
|
+
* GET /api/effects/_count - the sidebar badge: how many effects the editor offers (an inventory)
|
|
5
6
|
*
|
|
6
7
|
* The PUT whitelists and checks what it stores (normaliseEffectsSettings): it
|
|
7
8
|
* used to write the request body to config/effects.json verbatim, and GET
|
|
@@ -60,12 +61,47 @@ function loadSettings() {
|
|
|
60
61
|
* @param {import('fastify').FastifyInstance} fastify
|
|
61
62
|
* @param {{authenticate?: Function, requireAdmin?: Function}} [opts] - injectable for tests
|
|
62
63
|
*/
|
|
64
|
+
/**
|
|
65
|
+
* The sidebar badge on Effects: how many effects the page editor offers, by
|
|
66
|
+
* category, and whether the scroll-reveal defaults have been changed. An
|
|
67
|
+
* inventory, so a closed System folder does not add it up.
|
|
68
|
+
*
|
|
69
|
+
* @param {{category: string, effects: object[]}[]} byCategory - effectsByCategory()
|
|
70
|
+
* @param {object} settings - config/effects.json
|
|
71
|
+
* @param {object} defaults - the shipped defaults
|
|
72
|
+
*/
|
|
73
|
+
export function effectsBadge(byCategory = [], settings = {}, defaults = {}) {
|
|
74
|
+
const n = byCategory.reduce((t, c) => t + c.effects.length, 0);
|
|
75
|
+
const changed = Object.keys(defaults).filter(k => settings[k] !== undefined && settings[k] !== defaults[k]);
|
|
76
|
+
return {
|
|
77
|
+
count: n,
|
|
78
|
+
inventory: true,
|
|
79
|
+
label: `${n} effect${n === 1 ? '' : 's'} for the page editor`,
|
|
80
|
+
tone: 'info',
|
|
81
|
+
details: {
|
|
82
|
+
title: 'Effects',
|
|
83
|
+
rows: [
|
|
84
|
+
...byCategory.map(c => ({label: c.category, value: String(c.effects.length)})),
|
|
85
|
+
{label: 'Reveal defaults', value: changed.length ? `${changed.length} changed` : 'as shipped'}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
63
91
|
export async function effectsRoutes(fastify, opts = {}) {
|
|
64
92
|
const authenticate = opts.authenticate || defaultAuthenticate;
|
|
65
93
|
const requireAdmin = opts.requireAdmin || defaultRequireAdmin;
|
|
66
94
|
|
|
67
95
|
fastify.get('/effects/settings', async () => loadSettings());
|
|
68
96
|
|
|
97
|
+
fastify.get('/effects/_count', {preHandler: [authenticate]}, async () => {
|
|
98
|
+
const {effectsByCategory} = await import('../../../admin/js/lib/effect-defs.js');
|
|
99
|
+
const {DEFAULT_SETTINGS} = await import('../../../admin/js/lib/effects-arrange.js');
|
|
100
|
+
let settings = {};
|
|
101
|
+
try { settings = getConfig('effects') || {}; } catch { /* no file yet: the defaults */ }
|
|
102
|
+
return effectsBadge(effectsByCategory(), settings, DEFAULT_SETTINGS);
|
|
103
|
+
});
|
|
104
|
+
|
|
69
105
|
fastify.put('/effects/settings', {preHandler: [authenticate, requireAdmin]}, async (request, reply) => {
|
|
70
106
|
const {errors, settings} = normaliseEffectsSettings(request.body);
|
|
71
107
|
if (errors.length) return reply.status(400).send({error: errors.join('; ')});
|
|
@@ -25,6 +25,7 @@ import path from 'path';
|
|
|
25
25
|
import {deleteForm, ensureCollectionForForm, ensureFormsDir, listForms, readForm, slugify, writeForm} from '../../services/forms.js';
|
|
26
26
|
import {executeAction} from '../../services/actions.js';
|
|
27
27
|
import {createTransport, sendFormEmail} from '../../services/email.js';
|
|
28
|
+
import {formActionFailed} from '../../services/notification-sources.js';
|
|
28
29
|
import {
|
|
29
30
|
clearEntries,
|
|
30
31
|
createCollection,
|
|
@@ -914,6 +915,7 @@ export async function formsRoutes(fastify) {
|
|
|
914
915
|
});
|
|
915
916
|
} catch (err) {
|
|
916
917
|
fastify.log.warn(`[forms] Email send failed for "${slug}": ${err.message}`);
|
|
918
|
+
formActionFailed(slug, form.title, 'notification email', err.message);
|
|
917
919
|
}
|
|
918
920
|
}
|
|
919
921
|
|
|
@@ -928,6 +930,7 @@ export async function formsRoutes(fastify) {
|
|
|
928
930
|
});
|
|
929
931
|
} catch (err) {
|
|
930
932
|
fastify.log.warn(`[forms] Webhook failed for "${slug}": ${err.message}`);
|
|
933
|
+
formActionFailed(slug, form.title, 'webhook', err.message);
|
|
931
934
|
}
|
|
932
935
|
}
|
|
933
936
|
|
|
@@ -940,6 +943,7 @@ export async function formsRoutes(fastify) {
|
|
|
940
943
|
await executeAction(actionSlug, entry.id, { user: submittingUser });
|
|
941
944
|
} catch (err) {
|
|
942
945
|
fastify.log.warn(`[forms] Action "${actionSlug}" failed for form "${slug}": ${err.message}`);
|
|
946
|
+
formActionFailed(slug, form.title, `action "${actionSlug}"`, err.message);
|
|
943
947
|
}
|
|
944
948
|
}
|
|
945
949
|
|
|
@@ -953,6 +957,7 @@ export async function formsRoutes(fastify) {
|
|
|
953
957
|
await executeAction(event.action.slug, entry.id, {user: submittingUser});
|
|
954
958
|
} catch (err) {
|
|
955
959
|
fastify.log.warn(`[forms] Trigger action "${event.action.slug}" failed for form "${slug}": ${err.message}`);
|
|
960
|
+
formActionFailed(slug, form.title, `action "${event.action.slug}"`, err.message);
|
|
956
961
|
}
|
|
957
962
|
} else if (event.type === 'redirect' && event.action.url && !triggerRedirect) {
|
|
958
963
|
// First one wins - two triggers redirecting at once is an
|
|
@@ -122,12 +122,16 @@ export function layoutsBadge(presets = {}, usage = {byKey: {}, missing: []}) {
|
|
|
122
122
|
const missing = usage.missing || [];
|
|
123
123
|
const inUse = Object.values(usage.byKey || {}).filter(list => list.length).length;
|
|
124
124
|
const n = missing.length;
|
|
125
|
+
// A number either way (0.80): how many layouts while all is well - an
|
|
126
|
+
// inventory, left out of the System folder's total - and the pages on a
|
|
127
|
+
// missing layout, in red, when not.
|
|
125
128
|
return {
|
|
126
|
-
count: n,
|
|
129
|
+
count: n || all.length,
|
|
130
|
+
...(!n && {inventory: true}),
|
|
127
131
|
label: n
|
|
128
132
|
? `${n} page${n === 1 ? '' : 's'} use${n === 1 ? 's' : ''} a layout that no longer exists`
|
|
129
133
|
: `${all.length} layout${all.length === 1 ? '' : 's'}, every page's layout exists`,
|
|
130
|
-
tone: 'attention',
|
|
134
|
+
tone: n ? 'attention' : 'info',
|
|
131
135
|
details: {
|
|
132
136
|
title: 'Layouts',
|
|
133
137
|
rows: [
|