domma-cms 1.8.24 → 1.8.26

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.26] - 2026-10-18
4
+
5
+ - **Updates clear out their old backups.** Every `npx domma-cms update` copies the engine and plugins into `.domma-backups/` first, and nothing ever removed those copies - some sites held 250+ of up to 490 MB each. After a successful update it now keeps the newest 7, plus one about a week, a month, three, six and twelve months old (at most 16), and nothing older than a year. Backups you put in that folder yourself, under any other name, are never touched.
6
+
7
+ ## [1.8.25] - 2026-10-18
8
+
9
+ - **Tutorial: The Editor.** A walk through a working session in the editor - the list, the tabs, coloured text, the live preview, saving and drafts, and the settings (font, wrapping, smooth typing, the admin menu folding away). Under Documentation › Tutorials, first in the list on every site.
10
+
3
11
  ## [1.8.24] - 2026-10-18
4
12
 
5
13
  - **The page editor is a workspace.** Pages and blocks open side by side, a tab each, the way a code editor works. On the left, a list: **This page** (the blocks the open page uses), every page as a tree and every block, with a filter box - click one and it opens in a tab; **+** starts a new page. Across the top, the open tabs: a dot marks unsaved work, **×** or a middle-click closes one (unsaved changes are kept as a draft and come back when you open it again). The toolbar's three buttons - or **Ctrl+\** - switch between editor and preview, editor only and preview only; preview only has the width presets, Refresh and Share link that the Live Preview tab used to. **Ctrl+Shift+E** hides the list; fullscreen hides the admin. The page's details, SEO, DConfig and history are in a dock on the right (the sliders button). The status bar at the bottom shows where you are, **Unsaved**, **Discard**, **Save** (Ctrl+S - the tab you are on: a page together with the changed blocks it uses) and **Save all** (every open page and block). Each page is in the form on its own: switch tabs freely, nothing is lost. A page address reached from the Pages list or a link opens in a tab of the workspace already up; the open tabs come back after a reload. A block no open page uses is previewed on its own instead of "the preview cannot show it".
package/CLAUDE.md CHANGED
@@ -583,9 +583,33 @@ ones plugins open. The width is keyed off the slideover's TITLE
583
583
  **View width**: `.view-container` caps at 1200px with no `margin: 0 auto`, so a view
584
584
  sits left with the remainder empty. Plugin views lift the cap via
585
585
  `:has(> .dm-plugin-shell)` - the shell is stamped by `plugin-chrome.js`, so this
586
- reaches every plugin without the author doing anything. Four core editors lift it the
587
- same way, keyed on a marker element (`#editor-meta-tabs`, `#block-editor-body`,
588
- `#ep-editor-tabs`, `.fb-fields-layout`).
586
+ reaches every plugin without the author doing anything. Three core editors lift it the
587
+ same way, keyed on a marker element (`#pe-workspace`, `#ep-editor-tabs`, `.fb-fields-layout`).
588
+
589
+ ## The editor workspace
590
+
591
+ Pages, blocks and components are all edited in ONE screen, `admin/js/views/page-editor.js` +
592
+ `templates/page-editor.html`, which answers every editor address (`/pages/new`, `/pages/edit<path>`,
593
+ `/blocks/new`, `/blocks/edit/<name>`, `/components/new`, `/components/edit/<name>` - `SCREEN` in
594
+ `lib/page-editor-arrange.js`). There is no separate Blocks or Components editor any more (1.8.24); a
595
+ list's Edit, a link or a reload opens a tab in the workspace, and a `…/new` address asks for a name.
596
+ Pure parts: `lib/editor-workspace-arrange.js` (tabs, explorer rows, layouts, keys, sample values),
597
+ `lib/page-blocks-arrange.js`, `lib/editor-hop.js` (the HELD set + drafts per kind: a closed tab's
598
+ draft comes back without a question; leaving the workspace clears the held set).
599
+
600
+ - One page at a time is in the form (`cur`); other open pages are parked state in `docs`. Blocks are
601
+ working copies in `working`, components in `comps` (compiled as typed through `POST /api/components/compile`).
602
+ - **A block's and a component's source are written in the body's own editor** (`lib/plain-editor.js`):
603
+ the same `textarea.editor-textarea` + `.editor-line-numbers` gutter under `#editor-body`, so the
604
+ lettering settings (`--pe-font/size/line`, admin.css "Editor lettering") reach them by the same rule.
605
+ The syntax colours are a `<pre>` behind the textarea carrying the SAME `editor-textarea` class, so the
606
+ two layers cannot take different fonts (`tests/admin/plain-editor.test.js` guards it). Never give the
607
+ text and its colour layer separate font rules - that is how the caret ended up beside the wrong letter.
608
+ `lib/simple-editor.js` (its own overlay and font rules) is for the plugin code editor only.
609
+ - The address bar follows the active tab (`addressFor`); `history.state.pe` marks the workspace's own
610
+ address so a reload restores the remembered tabs (`startingTabs`). The router guard opens an editor
611
+ address in place (`active.open`) and treats a workspace whose `#pe-workspace` root is gone (the 404
612
+ page replaced it without `router:afterChange`) as dead.
589
613
 
590
614
  ## Admin loading states
591
615
 
@@ -626,8 +650,10 @@ Keep `E.loader` for work in progress (a save, an upload), where there is no shap
626
650
  authenticate via the `dm_session` cookie (`server/services/viewerSession.js`), which is read **only** by the public
627
651
  page renderer; `authenticate()` stays Bearer-only, so no write route accepts ambient credentials.
628
652
  **Editor preview**: `POST /api/pages/preview/full` renders unsaved `{urlPath, frontmatter, body}` through the real
629
- `renderPage`; the editor injects the result as iframe `srcdoc`. `collectFrontmatter()` in `page-editor.js` is shared
630
- by Save and preview on purpose - fork them and the preview starts lying.
653
+ `renderPage`; the editor injects the result as iframe `srcdoc`. `frontmatterFrom()` in `page-editor.js` is shared
654
+ by Save and preview on purpose - fork them and the preview starts lying. A block no open page uses is previewed
655
+ on its own as `[block template="…" <sample attributes> /]` (`blockAloneBody`); a component in the component
656
+ preview page (`admin/preview/component-preview.html`).
631
657
  **Share links**: `server/services/previewLinks.js` + `GET /_preview?token=…`. The token goes in the QUERY STRING, not
632
658
  a path param - Fastify caps a route param at 100 chars and a signed token is ~350, so `/_preview/:token` silently
633
659
  404s. Revocation needs the server-side index (`content/preview-links.json`); a JWT alone cannot be withdrawn.
package/admin/js/app.js CHANGED
@@ -1,4 +1,4 @@
1
- import{installSmoothCarets as K}from"./lib/smooth-caret.js";import{views as J}from"./views/index.js";import{withPluginChrome as O}from"./lib/plugin-chrome.js";import{api as c,getUser as x,isAuthenticated as m,logout as Y,sendToLogin as Q}from"./api.js";import{claimDrafts as X}from"./lib/draft-owner.js";import{isAuthScreen as b,loginHashFor as Z}from"./lib/login-arrange.js";import{installHttpInterceptor as tt}from"./http-interceptor.js";import{installResizableSlideovers as et}from"./lib/slideover-resizable.js";import{installActivityThrobber as it}from"./lib/activity-throbber.js";import{installUpdateNotice as ot}from"./lib/update-notice.js";import{isPathAllowed as at,redirectFor as st}from"./lib/admin-scope.js";import{deliverDesktop as nt,desktopActive as rt,notificationSettings as lt}from"./lib/desktop-notify.js";import{avatarUrl as ct}from"./lib/avatar.js";import{installCalculator as mt}from"./lib/calculator.js";import{takeOverviewPass as pt,withDashboardStrip as _}from"./lib/dashboards-strip.js";import{installHotkeys as dt}from"./lib/hotkeys-ui.js";import{applyAdminBrand as V}from"./lib/admin-brand.js";import{installRouterCatchUp as ut}from"./lib/router-catch-up.js";import{installSidebarDrawer as ht}from"./lib/sidebar-drawer.js";$(()=>{K(document),tt(),mt(),ot(),it(),et(),ht(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,l="/",w=!1,g=null;function D(t){if(P[t])return E.toast(`${P[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=y[t]||st(p,t);return i||((t.split("?")[0]||"/")==="/"&&l&&l!=="/"&&!pt()?y[l]||l:null)}const y={},P={};R.use(async(t,i,e)=>{if(b(t.path))return e();if(!m()){Q();return}const o=D(t.path);if(o){R.navigate(o);return}e()});async function f(){try{const t=await c.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,V(h),l=t.adminHome||"/";const i=t.permissions||[];return w=at(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),g=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,l="/",h||V(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let N=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(N){A=!0;return}N=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{N=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{b(t.path)&&(u&&(clearInterval(u),u=null),p=null,l="/",w=!1,g=null)});const W=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - 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 Tool - 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/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - 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:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - 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:"pageEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"pageEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"pageEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"pageEditor",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:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/wordpress-import",view:"wordpressImport",title:"Import from WordPress - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - 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:i})=>{if(!b(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),b(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await U();const e=await f(),o=D(t.path);if(o&&R.navigate(o),j(e),lt.read().signInToasts)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 v=d(s.data?.title||""),T=d((s.data?.body||"").slice(0,120));E.toast(`${v} - ${T}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}B()}}),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 H=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function q(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 i=t?String(t.path).split("?")[0]:"";if(!t||!q(i))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(i))return;let a=0;const n=60,s=1500,v=()=>{a+=n;for(const T of H){const C=document.querySelector(T);if(!C)continue;if(Array.from(C.options||[]).find(z=>z.value===o)){C.value=o,C.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(v,n)};setTimeout(v,n)});const L="cms_card_states",k=S.get(L)||{};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&&k[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 i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(L,k))}),dt(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const r={...J},G={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},wordpressImport:{displayName:"Import from WordPress",icon:"download"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site 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"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site 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"},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,i]of Object.entries(G))r[t]&&(r[t]=O(r[t],{plugin:t,licence:"Built in",...i}));r.dashboard=_(r.dashboard);const I=[...W];async function U(){if(m())try{const t=await c.plugins.adminConfig(),i=new Set(I.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||I.push(e);for(const[e,o]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${o.entry}`);r[e]=O(a[o.exportName],(t.meta||{})[e])}catch{}r.dashboard=_(r.dashboard);for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(y[`/${e}`]=o);for(const[e,{enabled:o,displayName:a}]of Object.entries(t.tools||{}))o===!1&&(P[`/${e}`]=a);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=o,document.head.appendChild(a)}}catch{}}function B(){const t=x();if(!t||$("#topbar-user-name").length)return;const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=ct(t);$("#topbar-user").html(`
1
+ import{installSmoothCarets as K}from"./lib/smooth-caret.js";import{views as J}from"./views/index.js";import{withPluginChrome as O}from"./lib/plugin-chrome.js";import{api as c,getUser as x,isAuthenticated as m,logout as Y,sendToLogin as Q}from"./api.js";import{claimDrafts as X}from"./lib/draft-owner.js";import{isAuthScreen as b,loginHashFor as Z}from"./lib/login-arrange.js";import{installHttpInterceptor as tt}from"./http-interceptor.js";import{installResizableSlideovers as et}from"./lib/slideover-resizable.js";import{installActivityThrobber as it}from"./lib/activity-throbber.js";import{installUpdateNotice as ot}from"./lib/update-notice.js";import{isPathAllowed as at,redirectFor as st}from"./lib/admin-scope.js";import{deliverDesktop as nt,desktopActive as rt,notificationSettings as lt}from"./lib/desktop-notify.js";import{avatarUrl as ct}from"./lib/avatar.js";import{installCalculator as mt}from"./lib/calculator.js";import{takeOverviewPass as pt,withDashboardStrip as _}from"./lib/dashboards-strip.js";import{installHotkeys as dt}from"./lib/hotkeys-ui.js";import{applyAdminBrand as V}from"./lib/admin-brand.js";import{installRouterCatchUp as ut}from"./lib/router-catch-up.js";import{installSidebarDrawer as ht}from"./lib/sidebar-drawer.js";$(()=>{K(document),tt(),mt(),ot(),it(),et(),ht(),(async()=>{const{applyAdminTheme:t,refreshAdminTheme:i}=await import("./lib/admin-theme.js");m()?await i(c,{init:!0}):t({dommaTheme:"charcoal-dark"},{init:!0})})();let p=null,h=null,l="/",w=!1,g=null;function D(t){if(P[t])return E.toast(`${P[t]} is switched off on this site.`,{type:"info"}),"/";if(t.split("?")[0]==="/my-dashboard")return"/dashboards";const i=y[t]||st(p,t);return i||((t.split("?")[0]||"/")==="/"&&l&&l!=="/"&&!pt()?y[l]||l:null)}const y={},P={};R.use(async(t,i,e)=>{if(b(t.path))return e();if(!m()){Q();return}const o=D(t.path);if(o){R.navigate(o);return}e()});async function f(){try{const t=await c.get("/auth/permissions");p=t.adminScope||null,h=t.adminBrand||null,V(h),l=t.adminHome||"/";const i=t.permissions||[];return w=at(p,"/settings")&&i.some(e=>e==="settings"||e.startsWith("settings.")),g=i.some(e=>e==="notifications"||e.startsWith("notifications.")),i}catch{return p=null,l="/",h||V(null),w=!1,g=null,[]}}document.addEventListener("dm:admin-home-changed",()=>{f()});let N=null,A=!1;document.addEventListener("dm:sidebar-rebuild",()=>{if(N){A=!0;return}N=(async()=>{do{A=!1;try{await j(await f())}catch{}}while(A)})().finally(()=>{N=null})});async function j(t){const{renderAdminSidebar:i}=await import("./lib/sidebar-renderer.js");await i({mount:"#admin-sidebar",permissions:t,adminScope:p,brand:h})}M.subscribe("router:afterChange",({to:t})=>{b(t.path)&&(u&&(clearInterval(u),u=null),p=null,l="/",w=!1,g=null)});const W=[{path:"/",view:"dashboard",title:"Dashboard - Domma CMS"},{path:"/dashboards",view:"dashboards",title:"Dashboards - Domma CMS"},{path:"/my-dashboard",view:"dashboards",title:"Dashboards - 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/editor",view:"tutorialEditor",title:"The Editor - Domma CMS"},{path:"/tutorials/plugin",view:"tutorialPlugin",title:"Writing a Tool - 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/usage/editions",view:"usageEditions",title:"Editions & Licences - Usage - Domma CMS"},{path:"/docs/usage/collections",view:"usageCollections",title:"Collections & Forms - Usage - Domma CMS"},{path:"/docs/usage/blocks",view:"usageBlocks",title:"Blocks - Usage - Domma CMS"},{path:"/docs/usage/tools",view:"usageTools",title:"Built-in Tools - Usage - Domma CMS"},{path:"/docs/usage/dashboard",view:"usageDashboard",title:"Dashboard - Usage - Domma CMS"},{path:"/docs/plugins",view:"pluginGuides",title:"Plugin Guides - Domma CMS"},{path:"/docs/plugins/*",view:"pluginGuide",title:"Plugin Guide - 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:"/docs/api/forms",view:"apiRefForms",title:"Forms API - Domma CMS"},{path:"/docs/api/scaffold",view:"apiRefScaffold",title:"Scaffold API - Domma CMS"},{path:"/docs/api/external",view:"apiRefExternal",title:"External API & Tokens - Domma CMS"},{path:"/docs/api/content",view:"apiRefContent",title:"Content API - Domma CMS"},{path:"/docs/api/builder",view:"apiRefBuilder",title:"API Builder - 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:"pageEditor",title:"New Block - Domma CMS"},{path:"/blocks/edit/:name",view:"pageEditor",title:"Edit Block - Domma CMS"},{path:"/components",view:"components",title:"Components - Domma CMS"},{path:"/components/new",view:"pageEditor",title:"New Component - Domma CMS"},{path:"/components/edit/:name",view:"pageEditor",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:"/content-api",view:"contentApi",title:"Content API - Domma CMS"},{path:"/notes",view:"notes",title:"Notes - Domma CMS"},{path:"/todo",view:"todo",title:"Todo - Domma CMS"},{path:"/calculator",view:"calculator",title:"Calculator - Domma CMS"},{path:"/analytics",view:"analytics",title:"Analytics - Domma CMS"},{path:"/seo",view:"seo",title:"SEO - Domma CMS"},{path:"/seo/:tab",view:"seo",title:"SEO - Domma CMS"},{path:"/wordpress-import",view:"wordpressImport",title:"Import from WordPress - Domma CMS"},{path:"/contacts",view:"contacts",title:"Contacts - Domma CMS"},{path:"/calls/new",view:"callNew",title:"Call now - 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:i})=>{if(!b(t.path)){if($("#admin-sidebar").show(),$("#admin-topbar").show(),b(i?.path)){$("#topbar-user-name").remove(),import("./lib/admin-theme.js").then(a=>a.refreshAdminTheme(c)),await U();const e=await f(),o=D(t.path);if(o&&R.navigate(o),j(e),lt.read().signInToasts)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 v=d(s.data?.title||""),T=d((s.data?.body||"").slice(0,120));E.toast(`${v} - ${T}`,{type:s.data?.severity==="critical"?"error":"warning",duration:0})}}catch{}}B()}}),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 H=["#field-project","#menu-project","#collection-project","#endpoint-project","#action-project","#view-project","#block-project","#component-project","#role-project",'[name="project"]','[name="ownedByProject"]'];function q(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 i=t?String(t.path).split("?")[0]:"";if(!t||!q(i))return;const o=new URLSearchParams(location.hash.split("?")[1]||"").get("project");if(!o||!/\/new$/.test(i))return;let a=0;const n=60,s=1500,v=()=>{a+=n;for(const T of H){const C=document.querySelector(T);if(!C)continue;if(Array.from(C.options||[]).find(z=>z.value===o)){C.value=o,C.dispatchEvent(new Event("change",{bubbles:!0}));return}}a<s&&setTimeout(v,n)};setTimeout(v,n)});const L="cms_card_states",k=S.get(L)||{};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&&k[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 i=$(this).closest(".card");i.toggleClass("card-collapsed");const e=$(this).find("h2, h3").first().text().trim();e&&(k[e]=i.hasClass("card-collapsed")?"collapsed":"open",S.set(L,k))}),dt(),document.addEventListener("keydown",t=>{if(!(t.ctrlKey||t.metaKey)||t.key!=="s"||window.location.hash.startsWith("#/login")||window.location.hash.startsWith("#/reset-password"))return;const i=document.querySelector("#view-container .view-header button.btn-primary");i&&(t.preventDefault(),i.click())});const r={...J},G={notes:{displayName:"Notes",icon:"edit-3"},todo:{displayName:"Todo",icon:"check-square"},calculator:{displayName:"Calculator",icon:"calculator"},analytics:{displayName:"Analytics",icon:"chart-bar"},seo:{displayName:"SEO",icon:"search"},wordpressImport:{displayName:"Import from WordPress",icon:"download"},contacts:{displayName:"Contacts",icon:"users"},projects:{displayName:"Projects",icon:"folder"},pages:{displayName:"Pages",icon:"file-text"},media:{displayName:"Media",icon:"image"},menus:{displayName:"Site 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"},contentApi:{displayName:"Content API",icon:"file-text"},dashboard:{displayName:"Dashboard",icon:"home"},dashboards:{displayName:"Dashboards",icon:"home"},pageEditor:{displayName:"Pages",section:"Edit",icon:"file-text"},menuEditor:{displayName:"Site menus",section:"Edit",icon:"menu"},menuLocations:{displayName:"Site 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"},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,i]of Object.entries(G))r[t]&&(r[t]=O(r[t],{plugin:t,licence:"Built in",...i}));r.dashboard=_(r.dashboard);const I=[...W];async function U(){if(m())try{const t=await c.plugins.adminConfig(),i=new Set(I.map(e=>e.path));for(const e of t.routes||[])i.has(e.path)||I.push(e);for(const[e,o]of Object.entries(t.views||{}))try{const a=await import(`/plugins/${o.entry}`);r[e]=O(a[o.exportName],(t.meta||{})[e])}catch{}r.dashboard=_(r.dashboard);for(const[e,{route:o}]of Object.entries(t.takeovers||{}))o&&(y[`/${e}`]=o);for(const[e,{enabled:o,displayName:a}]of Object.entries(t.tools||{}))o===!1&&(P[`/${e}`]=a);for(const{id:e,href:o}of t.css||[])if(!document.getElementById(e)){const a=document.createElement("link");a.id=e,a.rel="stylesheet",a.href=o,document.head.appendChild(a)}}catch{}}function B(){const t=x();if(!t||$("#topbar-user-name").length)return;const e={"super-admin":"Super Admin",admin:"Admin",user:"User"}[t.role]||t.role,o=ct(t);$("#topbar-user").html(`
2
2
  ${o?`<img id="topbar-avatar" class="topbar-avatar" src="${d(o)}" alt="">`:""}
3
3
  <span id="topbar-user-name" class="topbar-user-name">${d(t.name)}</span>
4
4
  <span class="topbar-role-badge topbar-role-badge--${d(t.role)}">${e}</span>
@@ -0,0 +1,156 @@
1
+ <div class="view-header">
2
+ <h1><span data-icon="edit"></span> The Editor</h1>
3
+ <a href="#/tutorials" class="btn btn-ghost btn-sm"><span data-icon="arrow-left"></span> All tutorials</a>
4
+ </div>
5
+
6
+ <div class="row">
7
+ <div class="col-12">
8
+ <div class="docs-body" id="tutorial-editor">
9
+
10
+ <p class="lead">
11
+ Pages, blocks and components are all written in one place: the editor. It works the way a code
12
+ editor does - a list on the left, a tab for everything you have open, the text and a live preview
13
+ in the middle, and a settings dock on the right. This tutorial walks through a working session:
14
+ opening things, writing with colour, previewing, saving, and setting the editor up the way you
15
+ like it, smooth typing included.
16
+ </p>
17
+
18
+ <hr>
19
+
20
+ <h2>The screen</h2>
21
+
22
+ <p>Open any page from <strong>Pages</strong> and you are in the editor. Five parts:</p>
23
+
24
+ <ul>
25
+ <li><strong>The list</strong> (left): <em>This page</em> - the blocks and components the open page uses -
26
+ then every page as a tree, every block and every component. A filter box at the top narrows all three.
27
+ <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd> hides and shows it.</li>
28
+ <li><strong>The tabs</strong> (top): one for each page, block or component you have open, in the order you
29
+ opened them. A dot on a tab means unsaved work.</li>
30
+ <li><strong>The editor and the preview</strong> (middle): the text on the left, the page as your site will
31
+ show it on the right. Drag the divider between them.</li>
32
+ <li><strong>The status bar</strong> (bottom): where you are, <strong>Unsaved</strong>, <strong>Discard</strong>,
33
+ <strong>Save</strong> and <strong>Save all</strong>.</li>
34
+ <li><strong>The dock</strong> (right, the sliders button at the top right): the page's details, SEO, DConfig
35
+ and history. On a block's tab it holds the block's details and sample values; on a component's, its details
36
+ and preview props.</li>
37
+ </ul>
38
+
39
+ <p>The banner above the editor names what you are on - <em>Pages › About</em>, <em>Blocks › contact-card</em>,
40
+ <em>Components › counter</em> - and the address bar follows the tab, so a bookmark or a link always opens the
41
+ right thing.</p>
42
+
43
+ <h2>Opening things</h2>
44
+
45
+ <p>Everything in the list opens with a click, as a tab. So does <strong>Edit</strong> in the Pages, Blocks and
46
+ Components lists, and so does an address someone sends you. If the editor is already up, the new tab joins the
47
+ ones you have; nothing you were doing is disturbed.</p>
48
+
49
+ <p>The <strong>+</strong> beside each section makes a new one. A new page opens straight away as a tab. A new
50
+ block or component asks for its name first (lowercase letters, digits and hyphens - it becomes the tag, so it
51
+ cannot change later), is created, and opens.</p>
52
+
53
+ <p>Close a tab with its <strong>×</strong>, a middle-click, or <kbd>Delete</kbd> with the tab focused. Right-click
54
+ a tab for more: close the others, close those to its right, close all, move it left or right, clone the page
55
+ or duplicate the block or component, or copy its link. The open tabs are remembered: reload, or come back
56
+ tomorrow, and they are where you left them.</p>
57
+
58
+ <div class="alert alert-info">
59
+ <strong>Try it:</strong> open <em>About</em>, then click a block under <em>This page</em>, then a component from
60
+ the <em>Components</em> section. Three tabs. Click between them - each keeps its own text, scroll position
61
+ and unsaved changes.
62
+ </div>
63
+
64
+ <h2>Writing</h2>
65
+
66
+ <h3>A page</h3>
67
+ <p>The page is Markdown with shortcodes, and the text is coloured as you type: headings, code, emphasis, links
68
+ and lists in Markdown's colours, and every <code>[shortcode]</code> - its opening tag, its closing tag, a
69
+ self-closing one - in its own, so a block or a card stands out from the prose around it. The toolbar inserts
70
+ shortcodes with a dialog each; right-click inside one to edit its attributes or jump to the block it names.
71
+ Long shortcodes fold (the chevrons in the gutter, or the fold-all button).</p>
72
+
73
+ <h3>A block</h3>
74
+ <p>A block's tab has two parts, <strong>HTML</strong> and <strong>CSS</strong>, in the same editor the page uses,
75
+ coloured for each language. The <strong>Format</strong> button tidies the HTML's indentation. The dock's
76
+ <strong>Block</strong> panel shows its short tag and shortcode (copy either), its project, <strong>Bundled</strong>
77
+ and where it is used; <strong>Samples</strong> is one value per <code>&#123;&#123;placeholder&#125;&#125;</code> - right-click one
78
+ to insert the placeholder into the template.</p>
79
+
80
+ <h3>A component</h3>
81
+ <p>A component's tab has four parts - <code>&lt;template&gt;</code>, <code>&lt;props&gt;</code>,
82
+ <code>&lt;script&gt;</code> and <code>&lt;style&gt;</code> - coloured as HTML, JSON, JS and CSS. It is
83
+ compiled as you type; the dot beside the parts says <em>Compiles</em> or <em>Does not compile</em>, and the
84
+ errors show above the text. The dock's <strong>Preview props</strong> are the values the preview tries - change
85
+ one and the preview updates; right-click a row to copy it as an attribute or a whole tag. A component from a
86
+ plugin opens read only, with <strong>Copy to a new component</strong> to make your own.</p>
87
+
88
+ <h2>Previewing</h2>
89
+
90
+ <p>Three layouts, from the toolbar's three buttons or <kbd>Ctrl</kbd>+<kbd>\</kbd> to cycle: editor and preview
91
+ side by side, editor only, preview only. Preview only adds the width presets (full, tablet, phone),
92
+ <strong>Refresh</strong> and <strong>Share link</strong> - a link that lets someone see the page before it is
93
+ published, without signing in, that expires and can be revoked.</p>
94
+
95
+ <p>The preview is the real page, rendered by your site with your theme, fonts and custom CSS, and it shows your
96
+ unsaved changes - to the page and to any open block it uses, outlined so you can find it. A block no open
97
+ page uses is drawn on its own, its placeholders filled from the sample values. A component is drawn on its
98
+ own too, with its preview props.</p>
99
+
100
+ <h2>Saving</h2>
101
+
102
+ <p><strong>Save</strong> (<kbd>Ctrl</kbd>+<kbd>S</kbd>) saves the tab you are on. On a page, that includes the
103
+ blocks of its you changed - and if one of them is also used elsewhere, you are told where and asked first,
104
+ once. <strong>Save all</strong> saves every open page, block and component with changes. A component that does
105
+ not compile is not saved, and the status bar says so.</p>
106
+
107
+ <p>Nothing is lost by closing a tab or leaving the editor: unsaved work is kept as a draft and comes back when
108
+ you open that page, block or component again. Leaving the editor itself with unsaved work asks first, and
109
+ <strong>Cancel</strong> keeps you where you were. <strong>Discard</strong> throws away the current tab's
110
+ changes.</p>
111
+
112
+ <h2>Setting the editor up</h2>
113
+
114
+ <p>The cog in the banner opens the editor settings, kept per browser:</p>
115
+
116
+ <ul>
117
+ <li><strong>Font, size and line spacing</strong>: the lettering of the text, the gutter and the colours - all
118
+ three follow the same setting, on pages, blocks and components alike. Monospace fonts keep shortcodes and
119
+ Markdown lined up.</li>
120
+ <li><strong>Wrap long lines</strong>: off, a long line scrolls sideways and each line number is exactly one
121
+ line.</li>
122
+ <li><strong>Caret movement</strong> - smooth typing. <em>Smooth</em> draws the caret as a small light that
123
+ glides from letter to letter as you type and move, instead of jumping, so your eye never loses it on a busy
124
+ line. Typing, selecting, undo and dictation are exactly as before: only the caret you see is animated.
125
+ <em>Automatic</em> is smooth unless your system asks for reduced motion; <em>Native</em> is the browser's
126
+ own caret. The choice reaches every text box in the admin and your site's forms in this browser.</li>
127
+ <li><strong>Close the admin menu when the editor opens</strong>: the left-hand menu folds to its icon rail for
128
+ the room while you edit, and comes back when you leave the editor.</li>
129
+ </ul>
130
+
131
+ <p>The fullscreen button hides the admin around the editor. On a narrow screen the list and the dock slide over
132
+ the text instead of sitting beside it.</p>
133
+
134
+ <h2>Keys</h2>
135
+
136
+ <table class="table">
137
+ <thead><tr><th>Key</th><th>Does</th></tr></thead>
138
+ <tbody>
139
+ <tr><td><kbd>Ctrl</kbd>+<kbd>S</kbd></td><td>Save the tab you are on</td></tr>
140
+ <tr><td><kbd>Ctrl</kbd>+<kbd>\</kbd></td><td>Next layout: split, editor only, preview only</td></tr>
141
+ <tr><td><kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd></td><td>Show or hide the list</td></tr>
142
+ <tr><td><kbd>Ctrl</kbd>+<kbd>B</kbd>, <kbd>Ctrl</kbd>+<kbd>I</kbd>, <kbd>Ctrl</kbd>+<kbd>K</kbd></td><td>Bold, italic, link in the page text</td></tr>
143
+ <tr><td><kbd>Tab</kbd> / <kbd>Shift</kbd>+<kbd>Tab</kbd></td><td>Indent and dedent in a block's or component's source</td></tr>
144
+ <tr><td><kbd>Delete</kbd> on a tab, <kbd>Shift</kbd>+<kbd>F10</kbd></td><td>Close it; its menu</td></tr>
145
+ </tbody>
146
+ </table>
147
+
148
+ <div class="alert alert-success">
149
+ <strong>Where next:</strong> <a href="#/docs/usage/pages">Pages</a> for frontmatter, visibility and history,
150
+ <a href="#/docs/usage/blocks">Blocks</a> for placeholders and collection displays,
151
+ <a href="#/docs/components-walkthrough">the Components walkthrough</a> to build one from scratch.
152
+ </div>
153
+
154
+ </div>
155
+ </div>
156
+ </div>
@@ -96,6 +96,10 @@
96
96
  <h2 style="margin-top:2rem;">Tutorials</h2>
97
97
  <p class="text-muted">End-to-end walkthroughs.</p>
98
98
  <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:1rem;margin-top:1rem;">
99
+ <a href="#/tutorials/editor" class="card" style="text-decoration:none;display:block;">
100
+ <div class="card-body"><h3 style="margin-top:0;"><span data-icon="edit"></span> The Editor</h3>
101
+ <p class="text-muted" style="margin-bottom:0;">One editor for pages, blocks and components: tabs, colour, preview, drafts, smooth typing.</p></div>
102
+ </a>
99
103
  <a href="#/tutorials/crud" class="card" style="text-decoration:none;display:block;">
100
104
  <div class="card-body"><h3 style="margin-top:0;"><span data-icon="database"></span> Building a CRUD App</h3>
101
105
  <p class="text-muted" style="margin-bottom:0;">Collections, forms, actions and pages composed into a working app.</p></div>
@@ -14,6 +14,16 @@
14
14
 
15
15
  <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:1rem;margin-top:1.5rem;">
16
16
 
17
+ <a href="#/tutorials/editor" class="card" style="text-decoration:none;display:block;">
18
+ <div class="card-body">
19
+ <h3 style="margin-top:0;"><span data-icon="edit"></span> The Editor</h3>
20
+ <p class="text-muted" style="margin-bottom:0;">
21
+ Pages, blocks and components in one editor: the list, the tabs, coloured text, the live preview,
22
+ saving and drafts, and the settings - fonts, wrapping, smooth typing.
23
+ </p>
24
+ </div>
25
+ </a>
26
+
17
27
  <a href="#/tutorials/crud" class="card" style="text-decoration:none;display:block;">
18
28
  <div class="card-body">
19
29
  <h3 style="margin-top:0;"><span data-icon="database"></span> Building a CRUD App</h3>
@@ -1 +1 @@
1
- import{mountScaffolder as i}from"../lib/crud-tutorial.js";function t(o,s){return{templateUrl:`/admin/js/templates/docs/${o}.html`,async onMount(e){typeof s=="function"&&s(e),Domma.icons.scan(),Domma.syntax.scan()}}}export const docViews={tutorialCrud:t("tutorial-crud",o=>{const s=o.find("#tutorial-scaffolder-mount").get(0);s&&i(s,null,null)}),tutorialPlugin:t("tutorial-plugin"),tutorialForms:t("tutorial-forms"),componentsReference:t("components-reference"),componentsHowto:t("components-howto"),componentsWalkthrough:t("components-walkthrough"),componentsRules:t("components-rules")};const a=["pages","media","navigation","dconfig","shortcodes","site-settings","plugins","users-roles","views","actions","cta-shortcode","editions","collections","blocks","tools","dashboard"],c=["authentication","pages","settings","layouts","navigation","media","users","plugins","collections","views","actions","forms","scaffold","external","content","builder"],n=o=>o.split("-").map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join("");for(const o of a)docViews["usage"+n(o)]=t(`usage-${o}`);for(const o of c)docViews["apiRef"+n(o)]=t(`api-${o}`);
1
+ import{mountScaffolder as i}from"../lib/crud-tutorial.js";function t(o,s){return{templateUrl:`/admin/js/templates/docs/${o}.html`,async onMount(e){typeof s=="function"&&s(e),Domma.icons.scan(),Domma.syntax.scan()}}}export const docViews={tutorialCrud:t("tutorial-crud",o=>{const s=o.find("#tutorial-scaffolder-mount").get(0);s&&i(s,null,null)}),tutorialPlugin:t("tutorial-plugin"),tutorialEditor:t("tutorial-editor"),tutorialForms:t("tutorial-forms"),componentsReference:t("components-reference"),componentsHowto:t("components-howto"),componentsWalkthrough:t("components-walkthrough"),componentsRules:t("components-rules")};const a=["pages","media","navigation","dconfig","shortcodes","site-settings","plugins","users-roles","views","actions","cta-shortcode","editions","collections","blocks","tools","dashboard"],c=["authentication","pages","settings","layouts","navigation","media","users","plugins","collections","views","actions","forms","scaffold","external","content","builder"],n=o=>o.split("-").map(s=>s.charAt(0).toUpperCase()+s.slice(1)).join("");for(const o of a)docViews["usage"+n(o)]=t(`usage-${o}`);for(const o of c)docViews["apiRef"+n(o)]=t(`api-${o}`);
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Update backups, for the engine updater.
3
+ *
4
+ * Every update copies the engine's code, structural configs and plugins into
5
+ * .domma-backups/<timestamp>/ (about 500 MB on a site with plugins) and nothing
6
+ * ever removed them: a site that rolled a few times a day had 250+ of them. The
7
+ * updater now keeps the newest few plus one backup per age band - about a week,
8
+ * a month, three months, six months and a year old - and nothing older than a
9
+ * year. Only the names the updater writes are counted: anything else in the
10
+ * folder was put there by hand and is left alone.
11
+ *
12
+ * Each band keeps its OLDEST backup - that is what lets one move up: it ages out
13
+ * of its band into the next. Past the first week a band also keeps its NEWEST,
14
+ * the next in line, so when the oldest moves on the band is never left empty
15
+ * (with only the oldest kept, the year band stood empty 44% of the time at three
16
+ * updates a day). So at most 7 + 1 + 2 x 4 = 16 are kept.
17
+ *
18
+ * @module bin/lib/backup-retention
19
+ */
20
+
21
+ import {existsSync, readdirSync, rmSync} from 'node:fs';
22
+ import path from 'node:path';
23
+
24
+ /** How many of the newest update backups a site keeps outright. */
25
+ export const KEEP_UPDATE_BACKUPS = 7;
26
+
27
+ const DAY = 24 * 60 * 60 * 1000;
28
+
29
+ /** Age bands, in days: [from, to). The first starts at 0 so a backup can survive long enough to reach a week. */
30
+ export const BACKUP_BANDS = [[0, 7], [7, 30], [30, 90], [90, 180], [180, 365]];
31
+
32
+ /** `new Date().toISOString()` with `:` and `.` made `-` - the updater's own names. */
33
+ const UPDATE_BACKUP_NAME = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/;
34
+
35
+ /** When an update backup was taken, from its name; null for a name the updater did not write. */
36
+ export function backupTime(name) {
37
+ const m = UPDATE_BACKUP_NAME.exec(name);
38
+ if (!m) return null;
39
+ const t = Date.parse(`${m[1]}T${m[2]}:${m[3]}:${m[4]}.${m[5]}Z`);
40
+ return Number.isNaN(t) ? null : t;
41
+ }
42
+
43
+ /** The update backups to remove from a listing, oldest first. Pure: `now` is a timestamp in ms. */
44
+ export function backupsToPrune(names, now = Date.now(), keep = KEEP_UPDATE_BACKUPS) {
45
+ const ours = names
46
+ .map(name => ({name, at: backupTime(name)}))
47
+ .filter(b => b.at !== null)
48
+ .sort((a, b) => a.at - b.at);
49
+ const kept = new Set(ours.slice(-keep).map(b => b.name));
50
+ BACKUP_BANDS.forEach(([from, to], i) => {
51
+ const inBand = ours.filter(b => now - b.at >= from * DAY && now - b.at < to * DAY);
52
+ if (!inBand.length) return;
53
+ kept.add(inBand[0].name);
54
+ if (i > 0) kept.add(inBand.at(-1).name);
55
+ });
56
+ return ours.filter(b => !kept.has(b.name)).map(b => b.name);
57
+ }
58
+
59
+ /** Apply the retention rule to `dir`. Returns the names removed. */
60
+ export function pruneUpdateBackups(dir, now = Date.now()) {
61
+ if (!existsSync(dir)) return [];
62
+ const names = readdirSync(dir, {withFileTypes: true}).filter(e => e.isDirectory()).map(e => e.name);
63
+ const doomed = backupsToPrune(names, now);
64
+ for (const name of doomed) rmSync(path.join(dir, name), {recursive: true, force: true});
65
+ return doomed;
66
+ }
package/bin/update.js CHANGED
@@ -15,6 +15,7 @@ import {createInterface} from 'node:readline';
15
15
  import {fileURLToPath} from 'node:url';
16
16
  import {deepMergeNewKeys} from './lib/config-merge.js';
17
17
  import {shouldReplacePlugin} from './lib/plugin-version.js';
18
+ import {KEEP_UPDATE_BACKUPS, pruneUpdateBackups} from './lib/backup-retention.js';
18
19
 
19
20
  // ---------------------------------------------------------------------------
20
21
  // Paths
@@ -567,6 +568,13 @@ export default async function update(_positional, flags) {
567
568
  console.log('');
568
569
  }
569
570
 
571
+ // -------------------------------------------------------------------------
572
+ // 13b. Thin the update backups (bin/lib/backup-retention.js) - done last, so a failed update
573
+ // never costs a backup, and only when this run took one.
574
+ // -------------------------------------------------------------------------
575
+
576
+ const prunedBackups = backupPath ? pruneUpdateBackups(path.join(cwd, '.domma-backups')) : [];
577
+
570
578
  // -------------------------------------------------------------------------
571
579
  // 14. Summary
572
580
  // -------------------------------------------------------------------------
@@ -581,6 +589,9 @@ export default async function update(_positional, flags) {
581
589
 
582
590
  if (backupPath) {
583
591
  info(`Backup saved to: .domma-backups/${path.basename(backupPath)}/`);
592
+ if (prunedBackups.length) {
593
+ info(`Removed ${prunedBackups.length} older update backup(s): the newest ${KEEP_UPDATE_BACKUPS} are kept, plus one about a week, a month, 3, 6 and 12 months old.`);
594
+ }
584
595
  }
585
596
 
586
597
  if (newPlugins.length > 0) {
@@ -1280,9 +1280,10 @@ The four Tools below were plugins until 0.67.0 and are now part of the CMS. They
1280
1280
  - Registers new plugins, disabled except the protective ones such as Security.
1281
1281
  - Adds only new keys to `server.json`, `auth.json`, `content.json` and `presets.json`.
1282
1282
  - Swaps the CMS dependencies but keeps dependencies you added, self-heals `.gitignore`, updates the marker and runs `npm install`.
1283
+ - Thins `.domma-backups/` after a successful update: the newest 7 update backups, plus the oldest and newest backup aged 7-30 days, 30-90 days, 90-180 days and 180-365 days (and the oldest under a week), so one about a week, a month, three, six and twelve months old is always there - at most 16. Nothing older than a year is kept, and backups not named by the updater are never touched (unreleased).
1283
1284
 
1284
1285
  Options: `--yes`, `--no-backup`, `--no-install`, `--dry-run` (preview only), `--force` (reinstall the same version).
1285
- Where: `bin/update.js`, `bin/lib/config-merge.js`, `bin/lib/plugin-version.js` - Since: 0.6.5.
1286
+ Where: `bin/update.js`, `bin/lib/config-merge.js`, `bin/lib/plugin-version.js`, `bin/lib/backup-retention.js` - Since: 0.6.5.
1286
1287
  - **`npm run setup`** - The terminal setup wizard:
1287
1288
  1. Generates `JWT_SECRET` and `INSTANCE_SECRET`.
1288
1289
  2. Creates the first admin if there are no users (password of at least 8 characters).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.8.24",
3
+ "version": "1.8.26",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -63,6 +63,7 @@ const MAP = [
63
63
  ['usage-tools', 'usage/tools', 'Built-in Tools'],
64
64
  ['usage-dashboard', 'usage/dashboard', 'Dashboard'],
65
65
  ['tutorial-crud', 'tutorials/crud', 'Building a CRUD App'],
66
+ ['tutorial-editor', 'tutorials/editor', 'The Editor'],
66
67
  ['tutorial-plugin', 'tutorials/plugin', 'Writing a Tool'],
67
68
  ['tutorial-forms', 'tutorials/forms', 'Form Follow-Up'],
68
69
  ['components-reference', 'components', 'Components Reference'],
@@ -110,6 +110,7 @@ function DOC_TUTORIALS_SUBMENU() {
110
110
  return {
111
111
  text: 'Tutorials', icon: 'document',
112
112
  items: [
113
+ {text: 'The Editor', url: '#/tutorials/editor', icon: 'edit'},
113
114
  {text: 'Building a CRUD App', url: '#/tutorials/crud', icon: 'database'},
114
115
  {text: 'Writing a Tool', url: '#/tutorials/plugin', icon: 'package'},
115
116
  {text: 'Form Follow-Up', url: '#/tutorials/forms', icon: 'layout'}
@@ -397,6 +398,21 @@ export async function ensureFolderKeys(opts = {}) {
397
398
  changed = true;
398
399
  }
399
400
 
401
+ // The Editor tutorial (1.8.25) joins an existing Tutorials submenu once (`meta.editorTutorial`),
402
+ // first in the list, wherever that submenu is and only if it is not there already.
403
+ if (!menu.meta?.editorTutorial && items.length) {
404
+ const add = (list) => (list || []).forEach(n => {
405
+ if (n && String(n.text || '').trim() === 'Tutorials' && Array.isArray(n.items)
406
+ && n.items.some(i => i?.url === '#/tutorials/crud') && !n.items.some(i => i?.url === '#/tutorials/editor')) {
407
+ n.items.unshift({text: 'The Editor', url: '#/tutorials/editor', icon: 'edit'});
408
+ }
409
+ if (Array.isArray(n?.items)) add(n.items);
410
+ });
411
+ add(items);
412
+ menu.meta = {...(menu.meta || {}), editorTutorial: true};
413
+ changed = true;
414
+ }
415
+
400
416
  // An empty menu is left empty: the renderer's minimal fallback (Dashboard,
401
417
  // Menus) is what an admin needs to repair it, and a lone Tools folder would
402
418
  // stop that fallback from applying.