domma-cms 0.25.16 → 0.31.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/lib/sidebar-renderer.js +4 -4
- package/admin/js/templates/project-settings.html +7 -0
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/projects.js +3 -7
- package/package.json +1 -1
- package/public/js/site.js +1 -1
- package/server/routes/api/collections.js +10 -1
- package/server/routes/api/projects.js +9 -1
- package/server/routes/docs-public.js +43 -0
- package/server/routes/public.js +11 -2
- package/server/server.js +13 -1
- package/server/services/docs.js +358 -0
- package/server/services/presetCollections.js +1 -0
- package/server/services/projects.js +72 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as c}from"../api.js";import{colourToCss as F}from"/public/js/menu-decor.mjs";import{groupPluginItems as W,stripItemByUrl as D,insertFoldersBeforeSystem as _,pruneEmptySynthesisedFolders as K,MANAGE_PLUGINS_URL as X}from"./sidebar-grouping.js";function
|
|
2
|
-
<summary${
|
|
3
|
-
<div class="sidebar-children">${
|
|
4
|
-
</details>`}return`<a href="${s}" class="sidebar-link" data-url="${s}"${
|
|
1
|
+
import{api as c}from"../api.js";import{colourToCss as F}from"/public/js/menu-decor.mjs";import{groupPluginItems as W,stripItemByUrl as D,insertFoldersBeforeSystem as _,pruneEmptySynthesisedFolders as K,MANAGE_PLUGINS_URL as X}from"./sidebar-grouping.js";function h(e){return String(e??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}const G=[{text:"Dashboard",url:"#/",icon:"home"},{text:"Menus",url:"#/menus",icon:"menu"}];function B(e,i){return i?!e||!e.length?!1:e.includes(i)?!0:["read","create","update","delete"].some(t=>e.includes(`${i}.${t}`)):!0}function I(e,i){const t=[];for(const n of e){if(n.hidden||n.permission&&!B(i,n.permission))continue;const s=Array.isArray(n.items)&&n.items.length?I(n.items,i):[];t.push({...n,items:s})}return t}function O(e,i){if(!i||!i.length)return e;const t=(n,s)=>{for(const l of n){if((l.text||"").toLowerCase()===s.toLowerCase())return l;if(Array.isArray(l.items)){const r=t(l.items,s);if(r)return r}}return null};for(const n of i)if(n.parent){const s=t(e,n.parent);s?(s.items=Array.isArray(s.items)?s.items:[],s.items.push(n.item)):e.push(n.item)}else e.push(n.item);return e}async function V(e){if(!B(e,"projects"))return[];let i=[];try{i=await c.projects.list()}catch{return[]}const t={};await Promise.all(i.map(async s=>{try{const l=await c.projects.artefacts(s.slug);t[s.slug]=l}catch{t[s.slug]={}}}));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 i.map(s=>{const r="#/projects/"+encodeURIComponent(s.slug),g=t[s.slug]||{},d=[{text:"Overview",url:r,icon:s.icon||"folder"}];for(const m of n){const y=g[m.key],p=Array.isArray(y)?y.length:0;p<=0||d.push({text:`${m.text} (${p})`,url:r+m.path,icon:m.icon})}d.push({text:"Settings",url:r+"/settings",icon:"settings"});const u=s.enabled===!1;return{text:s.name||s.slug,icon:s.icon||"folder",items:d,...u&&{colour:"#9ca3af",badge:{text:"off"}}}})}function Y(e,i){for(const t of e)if((t.text||"").toLowerCase()==="projects"){t.items=Array.isArray(t.items)?t.items.slice():[],t.items.push(...i);return}}function J(e,i){if(!e.url||!i)return"";const t=i[e.url];return t==null||t<=0?"":`<span class="sidebar-badge">${h(String(t))}</span>`}function Q(e){const i=(e.text||"").replace(/\s*\(\d+\)\s*$/,"").toLowerCase().replace(/\s+/g,"-"),t=(e.url||"").match(/^#\/projects\/([^/]+)/);return`sidebar.expanded.${t?`project-${decodeURIComponent(t[1])}.`:""}${i}`}function z(e,i){if(e&&e.type==="separator")return'<hr class="sidebar-divider">';const t=e.icon?`<span data-icon="${h(e.icon)}"></span>`:"",n=h(e.text||""),s=e.url?h(e.url):"",l=Array.isArray(e.items)&&e.items.length>0,r=e.colour?F(e.colour):"",g=r?` style="color:${h(r)}"`:"";let d="";if(e.badge&&e.badge.text!=null&&e.badge.text!==""){const u=e.badge.variant?F(e.badge.variant):"";d=`<span class="dm-menu-badge"${u?` style="background:${h(u)};color:#fff"`:""}>${h(String(e.badge.text))}</span>`}if(l){const u=Q(e),m=S.get(u)!==!1,y=e.items.map(p=>z(p,i)).join("");return`<details data-state-key="${h(u)}"${m?" open":""}>
|
|
2
|
+
<summary${g}>${t} <span class="sidebar-text">${n}</span>${d}</summary>
|
|
3
|
+
<div class="sidebar-children">${y}</div>
|
|
4
|
+
</details>`}return`<a href="${s}" class="sidebar-link" data-url="${s}"${g}>${t} <span class="sidebar-text">${n}</span>${J(e,i)}${d}</a>`}async function Z(){const e=p=>p.then(b=>Array.isArray(b)?b.length:Array.isArray(b?.entries)?b.entries.length:0).catch(()=>0),[i,t,n,s,l,r,g,d,u,m,y]=await Promise.all([e(c.pages.list()),e(c.media.list()),e(c.collections.list()),e(c.forms.list()),e(c.views.list()),e(c.actions.list()),e(c.blocks.list()),e(c.components.list()),e(c.users.list()),e(c.plugins.list()),c.system?.notifications?.unreadCount?.().then(p=>p?.count??0).catch(()=>0)??Promise.resolve(0)]);return{"#/pages":i,"#/media":t,"#/collections":n,"#/forms":s,"#/views":l,"#/actions":r,"#/blocks":g,"#/components":d,"#/users":u,"#/plugins":m,"#/system/notifications":y}}async function ee(){try{const e=await c.settings.get();return e?.adminBrand?.title||e?.title||"Admin"}catch{return"Admin"}}export async function renderAdminSidebar({mount:e,permissions:i}){const t=$(e).get(0);if(!t)return;let n,s=null,l=null,r=null;try{const a=await c.menus.get("admin-sidebar");n=Array.isArray(a?.items)?a.items:null,s=a?.variant||null,l=a?.position||null,r=a?.style||null}catch{n=null}(!n||!n.length)&&(console.warn("[admin-sidebar] No admin-sidebar menu found; using fallback tree"),n=G.slice());let g=[];try{g=await H.get("/api/sidebar/registered-items")||[]}catch{}n=D(n,X);const{toolsFolder:d,pluginsFolder:u,parented:m}=W(g);n=O(n,m),n=_(n,[d,u]);const y=await V(i);y.length&&Y(n,y),n=I(n,i),n=K(n);const[p,b]=await Promise.all([Z().catch(()=>({})),ee()]),R=s?` dm-admin-sidebar--${h(s)}`:"";function w(a,f="px"){if(a==null)return a;const o=String(a).trim();return/^\d+(\.\d+)?$/.test(o)?`${o}${f}`:o}let C="";if(r){const a=[];r.fontFamily&&a.push(`font-family: ${r.fontFamily}, sans-serif`),r.fontSize&&a.push(`font-size: ${w(r.fontSize)}`),r.fontWeight&&a.push(`font-weight: ${r.fontWeight}`),r.letterSpacing&&a.push(`letter-spacing: ${w(r.letterSpacing,"em")}`);const f=[];if(a.length&&f.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 { ${a.join("; ")} }`),r.iconSize){const o=w(r.iconSize);f.push(`#admin-sidebar .dm-admin-sidebar [data-icon], #admin-sidebar .dm-admin-sidebar [data-icon] svg { width: ${o} !important; height: ${o} !important; }`)}f.length&&(C=`<style data-admin-sidebar-style>${f.join(" ")}</style>`)}const N=`<div class="dm-admin-sidebar-header"><span data-icon="layout"></span> ${h(b)}</div>`,U=`${C}<nav class="dm-admin-sidebar${R}">${N}${n.map(a=>z(a,p)).join("")}</nav>`,j=document.createRange();j.selectNodeContents(t);const q=j.createContextualFragment(U);t.replaceChildren(q),Domma.icons.scan(t);const x=Number.parseInt(S.get("sidebar.width"),10);Number.isFinite(x)&&x>=180&&x<=480&&(t.style.width=x+"px");const A=document.createElement("div");A.className="dm-admin-sidebar-handle",A.setAttribute("aria-label","Resize sidebar"),t.appendChild(A),t.querySelectorAll("details").forEach(a=>{a.addEventListener("toggle",function(){const f=this.getAttribute("data-state-key");f&&S.set(f,this.open);const o=this.querySelector(":scope > .sidebar-children");if(o)if(this.open){const k=o.scrollHeight;o.style.maxHeight="0",requestAnimationFrame(()=>{o.style.maxHeight=k+"px",o.addEventListener("transitionend",function T(){o.style.maxHeight="none",o.removeEventListener("transitionend",T)})})}else{const k=o.scrollHeight;o.style.maxHeight=k+"px",requestAnimationFrame(()=>{o.style.maxHeight="0"})}})});let v=!1,P=0,E=0;A.addEventListener("mousedown",a=>{v=!0,P=a.clientX,E=t.getBoundingClientRect().width,document.body.style.cursor="col-resize",document.body.style.userSelect="none",a.preventDefault()}),document.addEventListener("mousemove",a=>{if(!v)return;const f=Math.max(180,Math.min(480,E+(a.clientX-P)));t.style.width=f+"px"}),document.addEventListener("mouseup",()=>{v&&(v=!1,document.body.style.cursor="",document.body.style.userSelect="",S.set("sidebar.width",parseInt(t.style.width,10)))});function L(){const a=location.hash||"#/";$(t).find(".sidebar-link").removeClass("active"),$(t).find(`.sidebar-link[data-url="${a}"]`).addClass("active")}L(),M.subscribe("router:afterChange",L)}
|
|
@@ -40,6 +40,13 @@
|
|
|
40
40
|
<input type="number" id="ps-sortOrder" name="sortOrder" class="form-input" value="0">
|
|
41
41
|
<small class="form-hint">Lower = higher in the sidebar</small>
|
|
42
42
|
</div>
|
|
43
|
+
<div class="form-group col-span-2" id="ps-enabled-group">
|
|
44
|
+
<label class="dm-checkbox-label" style="display:inline-flex;align-items:center;gap:.5rem;">
|
|
45
|
+
<input type="checkbox" id="ps-enabled" name="enabled" checked>
|
|
46
|
+
<span>Enabled</span>
|
|
47
|
+
</label>
|
|
48
|
+
<small class="form-hint">Off = hard kill-switch: public pages 404 and the external API stops responding. Admin editing of its artefacts is unaffected.</small>
|
|
49
|
+
</div>
|
|
43
50
|
</div>
|
|
44
51
|
</div>
|
|
45
52
|
</div>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as c}from"../api.js";import{makeIconInput as
|
|
1
|
+
import{api as c}from"../api.js";import{makeIconInput as h}from"../lib/shortcode-modal.js";export const projectSettingsView={templateUrl:"/admin/js/templates/project-settings.html",async onMount(e){const i=location.hash.split("?")[0].match(/^#\/projects\/([^/]+)\/settings$/),o=i?decodeURIComponent(i[1]):null;if(!o){location.hash="#/projects";return}let s;try{s=await c.projects.get(o)}catch(t){E.toast(`Failed to load project: ${t.message||t}`,{type:"error"}),location.hash="#/projects";return}const m=encodeURIComponent(o);e.find("#ps-title").text(`Settings \u2014 ${s.name||o}`),e.find("#ps-back").attr("href","#/projects/"+m);const u=e.find("#ps-slug"),l=e.find("#ps-name"),p=e.find("#ps-description"),a=e.find("#ps-rootUrl"),d=e.find("#ps-sortOrder"),f=e.find("#ps-enabled");if(u.val(s.slug||""),l.val(s.name||""),p.val(s.description||""),a.val(s.rootUrl||""),d.val(s.sortOrder??0),f.prop("checked",s.enabled!==!1),s.protected){a.attr("disabled","disabled");const t=a.get(0)?.nextElementSibling;t?.classList.contains("form-hint")&&(t.textContent="Locked \u2014 built-in project"),e.find("#ps-danger-zone").css("display","none"),e.find("#ps-enabled-group").css("display","none")}const g=e.find("#ps-icon-mount").get(0),n=h("e.g. folder, users, box",s.icon||"folder");n.input.id="ps-icon",n.input.classList.add("form-input"),g.appendChild(n.el),Domma.icons.scan(e.get(0)),e.find("#ps-save").on("click",async()=>{const t={name:l.val().trim(),description:p.val().trim(),icon:n.input.value.trim()||"folder",sortOrder:Number.parseInt(d.val(),10)||0};if(!s.protected){const r=a.val().trim();r&&(t.rootUrl=r),t.enabled=f.prop("checked")}try{await c.projects.update(o,t),E.toast("Saved.",{type:"success"})}catch(r){E.toast(`Save failed: ${r.message||r}`,{type:"error"})}}),e.find("#ps-untag-all").on("click",async()=>{if(await E.confirm(`Untag all artefacts from project "${o}"? They will become site-wide.`))try{const t=await c.projects.untagAll(o);E.toast(`Untagged: ${JSON.stringify(t.untagged)}`,{type:"success"}),setTimeout(()=>location.reload(),600)}catch(t){E.toast(`Untag failed: ${t.message||t}`,{type:"error"})}}),e.find("#ps-delete").on("click",async()=>{if(await E.confirm(`Delete project "${o}"? The server refuses if any artefacts are still tagged.`))try{await c.projects.remove(o),E.toast("Project deleted.",{type:"success"}),location.hash="#/projects"}catch(t){E.toast(`Delete refused: ${t.message||t}`,{type:"error"})}})}};
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<a class="btn btn-sm btn-ghost" href="#/projects/${encodeURIComponent(e.slug)}/settings" data-tooltip="Settings"><span data-icon="settings"></span></a>
|
|
5
|
-
${e.protected?"":`<button class="btn btn-sm btn-danger btn-delete-project" data-slug="${o(e.slug)}" data-tooltip="Delete (refused while tagged)"><span data-icon="trash"></span></button>`}
|
|
6
|
-
</span>
|
|
7
|
-
`}]}),Domma.icons.scan(a.get(0)),document.querySelectorAll("#projects-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),a.off("click",".btn-delete-project").on("click",".btn-delete-project",async function(){const t=$(this).data("slug");if(await E.confirm(`Delete project "${t}"? This cannot be undone.`))try{await s.projects.remove(t),E.toast("Project deleted.",{type:"success"}),location.reload()}catch(e){E.toast(`Delete refused: ${e.message||e}`,{type:"error"})}})}};
|
|
1
|
+
import{api as l}from"../api.js";function s(n){return String(n??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}export const projectsView={templateUrl:"/admin/js/templates/projects.html",async onMount(n){const r=await l.projects.list().catch(()=>[]),p=await Promise.all(r.map(t=>l.projects.artefacts(t.slug).then(e=>Object.values(e).reduce((a,c)=>a+c.length,0)).catch(()=>0))),d=r.map((t,e)=>({...t,artefactCount:p[e]})),o=t=>t.enabled===!1;T.create("#projects-table",{data:d,emptyMessage:'No projects yet. Click "New project" to create one.',columns:[{key:"slug",title:"Slug",sortable:!0,render:(t,e)=>o(e)?`<span class="text-muted">${s(t)}</span>`:s(t)},{key:"name",title:"Name",sortable:!0,render:(t,e)=>{const a=o(e)?`<span class="text-muted">${s(t)}</span>`:s(t),c=e.protected?' <span class="badge badge-secondary" data-tooltip="Built-in project \u2014 cannot be deleted">System</span>':"",i=o(e)?' <span class="badge badge-warning" data-tooltip="Disabled \u2014 public pages and external API are off">Disabled</span>':"";return a+c+i}},{key:"icon",title:"Icon",render:(t,e)=>t?`<span class="${o(e)?"text-muted":""}" data-icon="${s(t)}"></span>`:""},{key:"rootUrl",title:"Root URL",render:t=>t?`<code>${s(t)}</code>`:'<span class="text-muted">\u2014</span>'},{key:"artefactCount",title:"Artefacts",render:t=>`<span class="badge badge-info">${t??0}</span>`},{key:"_actions",title:"Actions",render:(t,e)=>{const a=encodeURIComponent(e.slug),c=o(e)?`<span class="btn btn-sm btn-ghost disabled" aria-disabled="true" style="opacity:.4;pointer-events:none;"><span data-icon="eye"></span></span>
|
|
2
|
+
<span class="btn btn-sm btn-ghost disabled" aria-disabled="true" style="opacity:.4;pointer-events:none;"><span data-icon="settings"></span></span>`:`<a class="btn btn-sm btn-ghost" href="#/projects/${a}" data-tooltip="Overview"><span data-icon="eye"></span></a>
|
|
3
|
+
<a class="btn btn-sm btn-ghost" href="#/projects/${a}/settings" data-tooltip="Settings"><span data-icon="settings"></span></a>`,b=e.slug==="core"?"":o(e)?`<button class="btn btn-sm btn-ghost btn-toggle-project" data-slug="${s(e.slug)}" data-enable="1" data-tooltip="Enable"><span class="text-success" data-icon="play"></span></button>`:`<button class="btn btn-sm btn-ghost btn-toggle-project" data-slug="${s(e.slug)}" data-enable="0" data-tooltip="Disable"><span class="text-warning" data-icon="pause"></span></button>`,g=e.protected?"":`<button class="btn btn-sm btn-danger btn-delete-project" data-slug="${s(e.slug)}" data-tooltip="Delete (refused while tagged)"><span data-icon="trash"></span></button>`;return`<span style="display:inline-flex;gap:0.25rem;">${c}${b}${g}</span>`}}]}),Domma.icons.scan(n.get(0)),document.querySelectorAll("#projects-table [data-tooltip]").forEach(t=>{E.tooltip(t,{content:t.getAttribute("data-tooltip"),position:"top"})}),n.off("click",".btn-toggle-project").on("click",".btn-toggle-project",async function(){const t=$(this).data("slug"),e=String($(this).data("enable"))==="1";if(!(!e&&!await E.confirm(`Disable project "${t}"? Its public pages will 404 and its external API will stop responding. Admin editing is unaffected.`)))try{await l.projects.update(t,{enabled:e}),E.toast(e?"Project enabled.":"Project disabled.",{type:"success"}),location.reload()}catch(a){E.toast(`Failed: ${a.message||a}`,{type:"error"})}}),n.off("click",".btn-delete-project").on("click",".btn-delete-project",async function(){const t=$(this).data("slug");if(await E.confirm(`Delete project "${t}"? This cannot be undone.`))try{await l.projects.remove(t),E.toast("Project deleted.",{type:"success"}),location.reload()}catch(e){E.toast(`Delete refused: ${e.message||e}`,{type:"error"})}})}};
|
package/package.json
CHANGED
package/public/js/site.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{makeBadgeEl as _,applyColour as A,applyPill as I}from"/public/js/menu-decor.mjs";$(()=>{const C=window.__CMS_NAV__||{},g=window.__CMS_SITE__||{};if(g.autoTheme?.enabled){let o=function(e){const f=(e||"07:00").split(":");return+f[0]*60+(+f[1]||0)},a=function(){const e=new Date,f=e.getHours()*60+e.getMinutes();return f>=o(t.dayStart)&&f<o(t.nightStart)?t.dayTheme:t.nightTheme};const t=g.autoTheme;Domma.theme.set(a()),setInterval(()=>Domma.theme.set(a()),6e4)}const b=$("#site-navbar");if(b.length&&C.brand){let a=function(i,n=0){return Array.isArray(i)?i.filter(c=>!(c&&c.type==="separator"&&n===0)).map(c=>c&&c.type==="separator"?{divider:!0}:Array.isArray(c?.items)?{...c,items:a(c.items,n+1)}:c):i},e=function(i){return Array.isArray(i)?i.filter(n=>!(n&&n.type==="separator")):[]};const t={...C.brand},o=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let i="";t.logo?i+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(i+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(i+=`<span class="navbar-brand-text${o}">${t.text}</span>`),t.tagline&&(i+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=i}else o&&t.text&&(t.html=`<span class="navbar-brand-text${o}">${t.text}</span>`);Domma.elements.navbar("#site-navbar",{brand:t,items:a(C.items||[]),variant:C.variant||"dark",position:C.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar");const f=b.get(0);if(C.appearOnHover){const i=window.matchMedia("(min-width: 993px)");f.querySelectorAll(".navbar-dropdown").forEach(n=>{n.addEventListener("mouseenter",()=>{i.matches&&n.classList.add("open")}),n.addEventListener("mouseleave",()=>{i.matches&&n.classList.remove("open")})})}const d=f.querySelector(".navbar-toggle"),l=f.querySelector(".navbar-collapse");if(d&&l){let i=null;const n=()=>{l.classList.contains("show")&&d.click()},c=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},s=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",n),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};d.addEventListener("click",()=>{requestAnimationFrame(()=>{l.classList.contains("show")?s():c()})}),l.addEventListener("click",u=>{u.target.closest("a")&&requestAnimationFrame(c)}),document.addEventListener("keydown",u=>{u.key==="Escape"&&l.classList.contains("show")&&n()})}const y=e(C.items||[]);f.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const n=y[parseInt(i.dataset.index,10)];if(!n)return;const c=i.dataset.subindex!=null,s=c&&Array.isArray(n.items)?n.items[parseInt(i.dataset.subindex,10)]:n;if(!s)return;s.colour&&A(i,s.colour),s.pill&&!c&&I(i,s.pill);const u=_(s.badge);u&&i.appendChild(u)})}const v=$("#site-footer");if(v.length){let f=function(r){return String(r??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},d=function(r){return f(r).replace(/'/g,"'")},l=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';const m=d(r.url||"#"),h=f(r.text||""),k=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(l).join("")}</ul>`:"";return`<li><a href="${m}">${h}</a>${k}</li>`},y=function(r,m){return(r||[]).forEach(h=>{h&&h.type==="separator"||(m.push(h),Array.isArray(h.items)&&h.items.length&&y(h.items,m))}),m};const t=g.social||{},o={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let a='<div class="footer-inner container">';const e=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(e.copyright&&(a+=`<p>${f(e.copyright)}</p>`),e.primary.length&&(a+=`<nav class="footer-nav footer-nav--primary"><ul>${e.primary.map(l).join("")}</ul></nav>`),e.legal.length&&(a+=`<nav class="footer-nav footer-nav--legal"><ul>${e.legal.map(l).join("")}</ul></nav>`),g.footer){const r=Object.keys(o).filter(m=>t[m]);r.length&&(a+='<div class="footer-social">',r.forEach(m=>{const{label:h,svg:k}=o[m];a+=`<a href="${t[m]}" target="_blank" rel="noopener noreferrer" aria-label="${h}" class="footer-social-link">${k}</a>`}),a+="</div>")}a+="</div>",v.html(a);const i=[...y(e.primary,[]),...y(e.legal,[])];v.get(0).querySelectorAll(".footer-nav a").forEach((r,m)=>{const h=i[m];if(!h)return;h.colour&&A(r,h.colour);const k=_(h.badge);k&&r.appendChild(k)});const c=S.get("reduced_motion"),s=c!==null?!!c:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),u=v.get(0).querySelector(".footer-inner");if(u){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=s,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const m=document.createElement("span");m.className="form-switch-label",m.textContent="Reduce motion";const h=document.createElement("label");h.className="form-switch footer-motion-switch",h.title="Reduce motion",h.appendChild(r),h.appendChild(m),u.appendChild(h)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan();const p=$(".page-body");if(p.length){p.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),p.find(".tabs").each(function(){Domma.elements.tabs(this)}),p.find(".carousel").each(function(){const t={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const o=parseInt(this.dataset.animationDuration,10);Number.isFinite(o)&&(t.animationDuration=o)}this.dataset.animationEasing&&(t.animationEasing=this.dataset.animationEasing),Domma.elements.carousel(this,t)}),p.find(".dm-countdown").each(function(){const t={autoStart:!0};this.dataset.to&&(t.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(t.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(t.format=this.dataset.format),Domma.elements.timer(this,t)}),p.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),p.find(".dm-progression").each(function(){Domma.elements.progression(this,{layout:this.dataset.layout||"vertical",theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(t=>{const o=t.dataset.revealMode||"stagger",a=t.dataset.revealAnimation||"slide-up",e=parseInt(t.dataset.revealDuration,10)||400,f=parseInt(t.dataset.revealStagger,10)||60,d=parseInt(t.dataset.revealDelay,10)||0,l=t.dataset.revealDirection==="rtl",y=Array.from(t.children),i={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};y.forEach((n,c)=>{n.style.opacity="0",n.style.transform=i[a]||"",n.style.transition=`opacity ${e}ms ease, transform ${e}ms ease`;const s=l?y.length-1-c:c;n.style.transitionDelay=o==="stagger"?`${d+s*f}ms`:`${d}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const n=new IntersectionObserver(c=>{c.forEach(s=>{s.isIntersecting&&(s.target.offsetWidth,s.target.style.opacity="1",s.target.style.transform="none",n.unobserve(s.target))})},{threshold:.1});y.forEach(c=>n.observe(c))}))}),p.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),p.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,o=document.getElementById(t);if(!o)return;const a=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",a.setContent(o),a.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const a=atob(o.dataset.config),e=JSON.parse(a);Object.assign(t,e)}catch{}}),Object.keys(t).length>0){const o={};for(const[a,e]of Object.entries(t)){const f=e?.events?.click,{confirm:d,toast:l,alert:y,prompt:i,...n}=f||{};d||l||y||i?($(a).on("click",async function(s){if(s.preventDefault(),d&&!await E.confirm(d))return;let u=null;if(!(i&&(u=await E.prompt(i,{inputPlaceholder:n.promptPlaceholder||"",inputValue:n.promptDefault||""}),u===null))){if(n.target){const r=$(n.target);n.toggleClass&&r.toggleClass(n.toggleClass),n.addClass&&r.addClass(n.addClass),n.removeClass&&r.removeClass(n.removeClass),u!==null&&(n.setText&&r.text(u),n.setVal&&r.val(u),n.setAttr&&r.attr(n.setAttr,u))}n.href&&(window.location.href=n.href),l&&E.toast(l,{type:n.toastType||"success"}),y&&E.alert(y)}}),Object.keys(n).length&&(o[a]={...e,events:{...e.events,click:n}})):o[a]=e}$.setup(o)}}p.length&&x(p.get(0))});function x(C){C.querySelectorAll(".dm-cta-trigger").forEach(g=>{g.__ctaWired||(g.__ctaWired=!0,g.addEventListener("click",async()=>{const b=g.dataset.action,v=g.dataset.entry,w=g.dataset.confirm;let p=S.get("auth_token");if(!p){E.toast("Please log in to perform this action.",{type:"warning"});return}if(w&&!await E.confirm(w))return;const t=Array.from(g.childNodes).map(a=>a.cloneNode(!0));g.disabled=!0,g.textContent="Running\u2026";const o=a=>fetch(`/api/actions/${b}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({entryId:v})});try{let a=await o(p);if(a.status===401){const f=S.get("auth_refresh_token");if(f){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:f})});if(d.ok){const{token:l}=await d.json();S.set("auth_token",l),p=l,a=await o(p)}}}const e=await a.json().catch(()=>({}));if(!a.ok)throw new Error(e.error||e.message||`Error ${a.status}`);E.toast(e.message||"Action completed.",{type:"success"})}catch(a){E.toast(a.message||"Action failed.",{type:"error"})}finally{g.disabled=!1,g.textContent="",t.forEach(a=>g.appendChild(a)),Domma.icons.scan(g)}}))})}(function(){const g=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!g.length)return;const b=S.get("auth_token");function v(p,t){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const a=document.createElement("p");if(t){a.appendChild(document.createTextNode("Please "));const e=document.createElement("a");e.href="/login",e.textContent="sign in",a.appendChild(e),a.appendChild(document.createTextNode(" to view your entries."))}else a.textContent=p;return o.appendChild(a),o}function w(p,t){const o=document.createElement("template");o.innerHTML=t||"",p.replaceWith(o.content)}g.forEach(async p=>{const t=p.dataset.collectionAttrs;if(!t)return;if(!b){p.replaceChildren(v("",!0));return}const o=a=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({attrs:t})});try{let a=b,e=await o(a);if(e.status===401){const d=S.get("auth_refresh_token");if(d){const l=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:d})});if(l.ok){const{token:y}=await l.json();S.set("auth_token",y),a=y,e=await o(a)}}}if(!e.ok){const{error:d}=await e.json().catch(()=>({}));p.replaceChildren(v(d||"Unable to load entries.",!1));return}const{html:f}=await e.json();w(p,f)}catch{p.replaceChildren(v("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),x(document.body)},100)})(),(function(){const g=document.querySelectorAll("[data-collection-table]");!g.length||typeof T>"u"||!T.create||g.forEach(b=>{let v;try{v=JSON.parse(atob(b.dataset.payload))}catch{return}const{columns:w,rows:p,search:t,sortable:o,exportable:a,pageSize:e,empty:f,ctaConfig:d}=v;if(!w?.length)return;if(d){const i=w.findIndex(n=>n.key==="_cta");i!==-1&&(w[i]={key:"_cta",title:"",render:(n,c)=>{const s=document.createElement("button");if(s.className=`btn btn-${d.style||"primary"} dm-cta-trigger`,s.dataset.action=d.action||"",s.dataset.entry=c._entryId||"",d.confirm&&(s.dataset.confirm=d.confirm),d.icon){const u=document.createElement("span");u.dataset.icon=d.icon,s.appendChild(u),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(d.label||"Run")),s}})}const l="col-table-"+Math.random().toString(36).slice(2,7),y=document.createElement("div");y.id=l,b.replaceChildren(y),T.create("#"+l,{data:p,columns:w,search:t,sortable:o,exportable:a,pageSize:e,emptyMessage:f}),d&&x(y)})})(),(function(){const g=document.querySelectorAll("[data-form-inline]");if(!g.length||typeof F>"u")return;function b(t,o){const a={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const f=e.type==="checkbox"?"boolean":e.type==="date"?"string":e.type,d={...e.formConfig||{}};d.span==="full"&&o&&(d.span=o);const l={type:f,label:e.label,required:e.required,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...d}};e.type==="chooser"&&(e.variant&&(l.variant=e.variant),e.multiple&&(l.multiple=!0),e.density&&(l.density=e.density),e.columns&&(l.columns=e.columns),e.accent&&(l.accent=e.accent),e.accentStyle&&(l.accentStyle=e.accentStyle),e.glow&&(l.glow=!0),e.glowColour&&(l.glowColour=e.glowColour),e.shadow&&(l.shadow=e.shadow),e.shadowColour&&(l.shadowColour=e.shadowColour)),a[e.name]=l}),a}function v(t){const o={};return(t||[]).forEach(a=>{if(!(!a.name||a.type==="page-break"||a.type==="spacer")&&(a.type==="select"||a.type==="multiselect")&&a.required){const e=(a.options||[])[0];e&&(o[a.name]=typeof e=="object"?e.value:e)}}),o}function w(t,o){(o||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const e=t.querySelector(`[name="${a.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function p(t,o,a){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=o,e.style.cssText=a?"color:var(--danger,#f87171);margin-top:.75rem;":"color:var(--success,#4ade80);margin-top:.75rem;"}g.forEach(t=>{let o;try{o=JSON.parse(atob(t.dataset.formInline))}catch{return}const a=o.fields||[],e=o.settings||{},f=e.columns||1,d=e.layout||"stacked",l=a.some(c=>c.type==="page-break"),y=async c=>{try{const s=t.querySelector('[name="website"]'),u=t.querySelector('[name="_t"]'),r=Object.assign({},c);s!==null&&(r._hp=s.value),u!==null&&(r._t=u.value);const m=await H.post(`/api/forms/submit/${o.slug}`,r);if(m&&m.error){p(t,m.error,!0);const h=new Error(m.error);throw h.formSubmitFailed=!0,h}if(m?.redirect){window.location.href=m.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);p(t,m?.message||e.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||p(t,s.message||"Submission failed. Please try again.",!0),s}};function i(c){const s=c.querySelector("form");if(!s)return;const u=document.createElement("div");u.className="fb-form-honeypot",u.setAttribute("aria-hidden","true");const r=document.createElement("input");r.name="website",r.type="text",r.tabIndex=-1,r.autocomplete="url",r.placeholder="https://",u.appendChild(r);const m=document.createElement("input");m.name="_t",m.type="hidden",m.value=Date.now(),u.appendChild(m),s.appendChild(u)}function n(){if(!window.FormLogicEngine||!a.some(s=>s.logic))return;const c=new window.FormLogicEngine.FormLogicRuntime(o,t);if(c.init(),t._formLogicRuntime=c,t.parentNode&&typeof MutationObserver<"u"){const s=new MutationObserver(function(u){for(const r of u)for(const m of r.removedNodes)if(m===t||m.nodeType===1&&m.contains&&m.contains(t)){c.destroy(),s.disconnect();return}});s.observe(t.parentNode,{childList:!0,subtree:!1})}}if(l&&F.wizard){const c=[];let s=[],u=o.title||"Step 1",r="";a.forEach(h=>{h.type==="page-break"?(c.push({title:u,description:r,fields:b(s,f)}),s=[],u=h.label||`Step ${c.length+1}`,r=h.description||""):h.type!=="spacer"&&s.push(h)}),c.push({title:u,description:r,fields:b(s,f)});const m=F.wizard(t,{schema:{steps:c},onSubmit:y});Promise.resolve(m).then(function(){w(t,a),e.honeypot!==!1&&i(t),n()})}else if(F.render){const c=F.render(t,b(a,f),v(a),{submitText:e.submitText||"Submit",layout:d,columns:f,onSubmit:y});Promise.resolve(c).then(function(){if(d==="grid"&&e.submitSpan==="full"){const s=t.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}w(t,a),e.honeypot!==!1&&i(t),n()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
|
|
1
|
+
import{makeBadgeEl as _,applyColour as A,applyPill as D}from"/public/js/menu-decor.mjs";$(()=>{const C=window.__CMS_NAV__||{},g=window.__CMS_SITE__||{};if(g.autoTheme?.enabled){let o=function(e){const f=(e||"07:00").split(":");return+f[0]*60+(+f[1]||0)},a=function(){const e=new Date,f=e.getHours()*60+e.getMinutes();return f>=o(t.dayStart)&&f<o(t.nightStart)?t.dayTheme:t.nightTheme};const t=g.autoTheme;Domma.theme.set(a()),setInterval(()=>Domma.theme.set(a()),6e4)}const b=$("#site-navbar");if(b.length&&C.brand){let a=function(i,n=0){return Array.isArray(i)?i.filter(c=>!(c&&c.type==="separator"&&n===0)).map(c=>c&&c.type==="separator"?{divider:!0}:Array.isArray(c?.items)?{...c,items:a(c.items,n+1)}:c):i},e=function(i){return Array.isArray(i)?i.filter(n=>!(n&&n.type==="separator")):[]};const t={...C.brand},o=t.size&&t.size!=="md"?` navbar-brand-${t.size}`:"";if(t.logo||t.icon||t.tagline){let i="";t.logo?i+=`<img src="${t.logo}" class="navbar-brand-logo" alt="${t.text||""}">`:t.icon&&(i+=`<span data-icon="${t.icon}" style="width:1.1em;height:1.1em;margin-right:.35em;vertical-align:middle;"></span>`),t.text&&(i+=`<span class="navbar-brand-text${o}">${t.text}</span>`),t.tagline&&(i+=`<small class="navbar-brand-tagline">${t.tagline}</small>`),t.html=i}else o&&t.text&&(t.html=`<span class="navbar-brand-text${o}">${t.text}</span>`);Domma.elements.navbar("#site-navbar",{brand:t,items:a(C.items||[]),variant:C.variant||"dark",position:C.position||"sticky",collapsible:!0}),Domma.icons.scan("#site-navbar");const f=b.get(0);if(C.appearOnHover){const i=window.matchMedia("(min-width: 993px)");f.querySelectorAll(".navbar-dropdown").forEach(n=>{n.addEventListener("mouseenter",()=>{i.matches&&n.classList.add("open")}),n.addEventListener("mouseleave",()=>{i.matches&&n.classList.remove("open")})})}const d=f.querySelector(".navbar-toggle"),l=f.querySelector(".navbar-collapse");if(d&&l){let i=null;const n=()=>{l.classList.contains("show")&&d.click()},c=()=>{i&&(i.remove(),i=null),document.body.classList.remove("dm-drawer-open")},s=()=>{i||(i=document.createElement("div"),i.className="dm-navbar-backdrop",i.addEventListener("click",n),document.body.appendChild(i),document.body.classList.add("dm-drawer-open"))};d.addEventListener("click",()=>{requestAnimationFrame(()=>{l.classList.contains("show")?s():c()})}),l.addEventListener("click",u=>{u.target.closest("a")&&requestAnimationFrame(c)}),document.addEventListener("keydown",u=>{u.key==="Escape"&&l.classList.contains("show")&&n()})}const y=e(C.items||[]);f.querySelectorAll(".navbar-link[data-index], .navbar-dropdown-item[data-index]").forEach(i=>{const n=y[parseInt(i.dataset.index,10)];if(!n)return;const c=i.dataset.subindex!=null,s=c&&Array.isArray(n.items)?n.items[parseInt(i.dataset.subindex,10)]:n;if(!s)return;s.colour&&A(i,s.colour),s.pill&&!c&&D(i,s.pill);const u=_(s.badge);u&&i.appendChild(u)})}const v=$("#site-footer");if(v.length){let f=function(r){return String(r??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")},d=function(r){return f(r).replace(/'/g,"'")},l=function(r){if(r&&r.type==="separator")return'<li class="footer-sep"><hr></li>';const m=d(r.url||"#"),h=f(r.text||""),k=Array.isArray(r.items)&&r.items.length?`<ul>${r.items.map(l).join("")}</ul>`:"";return`<li><a href="${m}">${h}</a>${k}</li>`},y=function(r,m){return(r||[]).forEach(h=>{h&&h.type==="separator"||(m.push(h),Array.isArray(h.items)&&h.items.length&&y(h.items,m))}),m};const t=g.social||{},o={twitter:{label:"X / Twitter",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.742l7.73-8.835L1.254 2.25H8.08l4.259 5.629L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>'},facebook:{label:"Facebook",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>'},instagram:{label:"Instagram",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>'},linkedin:{label:"LinkedIn",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.064 2.064 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'},github:{label:"GitHub",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>'},youtube:{label:"YouTube",svg:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.495 6.205a3.007 3.007 0 00-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 00.527 6.205a31.247 31.247 0 00-.522 5.805 31.247 31.247 0 00.522 5.783 3.007 3.007 0 002.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 002.088-2.088 31.247 31.247 0 00.5-5.783 31.247 31.247 0 00-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/></svg>'}};let a='<div class="footer-inner container">';const e=window.__CMS_FOOTER__||{primary:[],legal:[],copyright:""};if(e.copyright&&(a+=`<p>${f(e.copyright)}</p>`),e.primary.length&&(a+=`<nav class="footer-nav footer-nav--primary"><ul>${e.primary.map(l).join("")}</ul></nav>`),e.legal.length&&(a+=`<nav class="footer-nav footer-nav--legal"><ul>${e.legal.map(l).join("")}</ul></nav>`),g.footer){const r=Object.keys(o).filter(m=>t[m]);r.length&&(a+='<div class="footer-social">',r.forEach(m=>{const{label:h,svg:k}=o[m];a+=`<a href="${t[m]}" target="_blank" rel="noopener noreferrer" aria-label="${h}" class="footer-social-link">${k}</a>`}),a+="</div>")}a+="</div>",v.html(a);const i=[...y(e.primary,[]),...y(e.legal,[])];v.get(0).querySelectorAll(".footer-nav a").forEach((r,m)=>{const h=i[m];if(!h)return;h.colour&&A(r,h.colour);const k=_(h.badge);k&&r.appendChild(k)});const c=S.get("reduced_motion"),s=c!==null?!!c:!!(window.matchMedia&&window.matchMedia("(prefers-reduced-motion: reduce)").matches),u=v.get(0).querySelector(".footer-inner");if(u){const r=document.createElement("input");r.type="checkbox",r.className="form-switch-input",r.id="dm-motion-switch",r.checked=s,r.addEventListener("change",function(){S.set("reduced_motion",this.checked),window.location.reload()});const m=document.createElement("span");m.className="form-switch-label",m.textContent="Reduce motion";const h=document.createElement("label");h.className="form-switch footer-motion-switch",h.title="Reduce motion",h.appendChild(r),h.appendChild(m),u.appendChild(h)}}$("#site-sidebar").length&&Domma.elements.sidebar("#site-sidebar",{autoGenerate:!0,selector:"h2, h3",collapsible:!1,push:!0,contentSelector:".site-content"}),Domma.icons.scan(),Domma.syntax?.scan&&Domma.syntax.scan();const p=$(".page-body");if(p.length){p.find(".accordion").each(function(){Domma.elements.accordion(this,{allowMultiple:this.dataset.multi==="true"})}),p.find(".tabs").each(function(){Domma.elements.tabs(this)}),p.find(".carousel").each(function(){const t={autoplay:this.dataset.autoplay==="true",interval:parseInt(this.dataset.interval,10)||5e3,loop:this.dataset.loop!=="false",animation:this.dataset.animation||"slide"};if(this.dataset.animationDuration){const o=parseInt(this.dataset.animationDuration,10);Number.isFinite(o)&&(t.animationDuration=o)}this.dataset.animationEasing&&(t.animationEasing=this.dataset.animationEasing),Domma.elements.carousel(this,t)}),p.find(".dm-countdown").each(function(){const t={autoStart:!0};this.dataset.to&&(t.targetDate=new Date(this.dataset.to)),this.dataset.duration&&(t.duration=parseInt(this.dataset.duration,10)),this.dataset.format&&(t.format=this.dataset.format),Domma.elements.timer(this,t)}),p.find("[data-tooltip]").each(function(){Domma.elements.tooltip(this,{content:$(this).data("tooltip"),position:$(this).data("tooltip-position")||"top"})}),p.find(".dm-progression").each(function(){Domma.elements.progression(this,{layout:this.dataset.layout||"vertical",theme:this.dataset.theme||"minimal",mode:this.dataset.mode||"timeline",statusIcons:!0})});try{Domma.effects.reveal(".page-body .hero",{animation:"slide-up",duration:480,threshold:.06,stagger:60,once:!1})}catch{}document.querySelectorAll(".page-body .row[data-reveal]").forEach(t=>{const o=t.dataset.revealMode||"stagger",a=t.dataset.revealAnimation||"slide-up",e=parseInt(t.dataset.revealDuration,10)||400,f=parseInt(t.dataset.revealStagger,10)||60,d=parseInt(t.dataset.revealDelay,10)||0,l=t.dataset.revealDirection==="rtl",y=Array.from(t.children),i={"slide-up":"translateY(30px)","slide-down":"translateY(-30px)","slide-left":"translateX(30px)","slide-right":"translateX(-30px)",zoom:"scale(0.85)",flip:"perspective(600px) rotateX(15deg)"};y.forEach((n,c)=>{n.style.opacity="0",n.style.transform=i[a]||"",n.style.transition=`opacity ${e}ms ease, transform ${e}ms ease`;const s=l?y.length-1-c:c;n.style.transitionDelay=o==="stagger"?`${d+s*f}ms`:`${d}ms`}),requestAnimationFrame(()=>requestAnimationFrame(()=>{const n=new IntersectionObserver(c=>{c.forEach(s=>{s.isIntersecting&&(s.target.offsetWidth,s.target.style.opacity="1",s.target.style.transform="none",n.unobserve(s.target))})},{threshold:.1});y.forEach(c=>n.observe(c))}))}),p.find(".card[data-collapsible]").each(function(){const t=this.querySelector(".card-header");t&&t.addEventListener("click",()=>this.classList.toggle("is-collapsed"))}),p.find(".dm-so-trigger").each(function(){this.addEventListener("click",()=>{const t=this.dataset.soTarget,o=document.getElementById(t);if(!o)return;const a=E.slideover({title:o.dataset.soTitle||"",size:o.dataset.soSize||"md",position:o.dataset.soPosition||"right"});o.style.display="",a.setContent(o),a.open()})})}if(typeof $.setup=="function"){const t=Object.assign({},window.__CMS_DCONFIG__||{});if(document.querySelectorAll(".dm-page-config[data-config]").forEach(o=>{try{const a=atob(o.dataset.config),e=JSON.parse(a);Object.assign(t,e)}catch{}}),Object.keys(t).length>0){const o={};for(const[a,e]of Object.entries(t)){const f=e?.events?.click,{confirm:d,toast:l,alert:y,prompt:i,...n}=f||{};d||l||y||i?($(a).on("click",async function(s){if(s.preventDefault(),d&&!await E.confirm(d))return;let u=null;if(!(i&&(u=await E.prompt(i,{inputPlaceholder:n.promptPlaceholder||"",inputValue:n.promptDefault||""}),u===null))){if(n.target){const r=$(n.target);n.toggleClass&&r.toggleClass(n.toggleClass),n.addClass&&r.addClass(n.addClass),n.removeClass&&r.removeClass(n.removeClass),u!==null&&(n.setText&&r.text(u),n.setVal&&r.val(u),n.setAttr&&r.attr(n.setAttr,u))}n.href&&(window.location.href=n.href),l&&E.toast(l,{type:n.toastType||"success"}),y&&E.alert(y)}}),Object.keys(n).length&&(o[a]={...e,events:{...e.events,click:n}})):o[a]=e}$.setup(o)}}p.length&&x(p.get(0))});function x(C){C.querySelectorAll(".dm-cta-trigger").forEach(g=>{g.__ctaWired||(g.__ctaWired=!0,g.addEventListener("click",async()=>{const b=g.dataset.action,v=g.dataset.entry,w=g.dataset.confirm;let p=S.get("auth_token");if(!p){E.toast("Please log in to perform this action.",{type:"warning"});return}if(w&&!await E.confirm(w))return;const t=Array.from(g.childNodes).map(a=>a.cloneNode(!0));g.disabled=!0,g.textContent="Running\u2026";const o=a=>fetch(`/api/actions/${b}/public`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({entryId:v})});try{let a=await o(p);if(a.status===401){const f=S.get("auth_refresh_token");if(f){const d=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:f})});if(d.ok){const{token:l}=await d.json();S.set("auth_token",l),p=l,a=await o(p)}}}const e=await a.json().catch(()=>({}));if(!a.ok)throw new Error(e.error||e.message||`Error ${a.status}`);E.toast(e.message||"Action completed.",{type:"success"})}catch(a){E.toast(a.message||"Action failed.",{type:"error"})}finally{g.disabled=!1,g.textContent="",t.forEach(a=>g.appendChild(a)),Domma.icons.scan(g)}}))})}(function(){const g=document.querySelectorAll('.dm-collection-hydrate[data-collection-scope="mine"]');if(!g.length)return;const b=S.get("auth_token");function v(p,t){const o=document.createElement("div");o.className="dm-collection-display dm-collection-empty";const a=document.createElement("p");if(t){a.appendChild(document.createTextNode("Please "));const e=document.createElement("a");e.href="/login",e.textContent="sign in",a.appendChild(e),a.appendChild(document.createTextNode(" to view your entries."))}else a.textContent=p;return o.appendChild(a),o}function w(p,t){const o=document.createElement("template");o.innerHTML=t||"",p.replaceWith(o.content)}g.forEach(async p=>{const t=p.dataset.collectionAttrs;if(!t)return;if(!b){p.replaceChildren(v("",!0));return}const o=a=>fetch("/api/collections/render-scope",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${a}`},body:JSON.stringify({attrs:t})});try{let a=b,e=await o(a);if(e.status===401){const d=S.get("auth_refresh_token");if(d){const l=await fetch("/api/auth/refresh",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({refreshToken:d})});if(l.ok){const{token:y}=await l.json();S.set("auth_token",y),a=y,e=await o(a)}}}if(!e.ok){const{error:d}=await e.json().catch(()=>({}));p.replaceChildren(v(d||"Unable to load entries.",!1));return}const{html:f}=await e.json();w(p,f)}catch{p.replaceChildren(v("Unable to load entries.",!1))}}),setTimeout(()=>{typeof Domma<"u"&&Domma.icons?.scan&&Domma.icons.scan(),typeof Domma<"u"&&Domma.syntax?.scan&&Domma.syntax.scan(),x(document.body)},100)})(),(function(){const g=document.querySelectorAll("[data-collection-table]");!g.length||typeof T>"u"||!T.create||g.forEach(b=>{let v;try{v=JSON.parse(atob(b.dataset.payload))}catch{return}const{columns:w,rows:p,search:t,sortable:o,exportable:a,pageSize:e,empty:f,ctaConfig:d}=v;if(!w?.length)return;if(d){const i=w.findIndex(n=>n.key==="_cta");i!==-1&&(w[i]={key:"_cta",title:"",render:(n,c)=>{const s=document.createElement("button");if(s.className=`btn btn-${d.style||"primary"} dm-cta-trigger`,s.dataset.action=d.action||"",s.dataset.entry=c._entryId||"",d.confirm&&(s.dataset.confirm=d.confirm),d.icon){const u=document.createElement("span");u.dataset.icon=d.icon,s.appendChild(u),s.appendChild(document.createTextNode(" "))}return s.appendChild(document.createTextNode(d.label||"Run")),s}})}const l="col-table-"+Math.random().toString(36).slice(2,7),y=document.createElement("div");y.id=l,b.replaceChildren(y),T.create("#"+l,{data:p,columns:w,search:t,sortable:o,exportable:a,pageSize:e,emptyMessage:f}),d&&x(y)})})(),(function(){const g=document.querySelectorAll("[data-form-inline]");if(!g.length||typeof F>"u")return;function b(t,o){const a={};return(t||[]).forEach(e=>{if(e.type==="page-break"||e.type==="spacer"||!e.name)return;const f=e.type==="checkbox"?"boolean":e.type==="date"?"string":e.type,d={...e.formConfig||{}};d.span==="full"&&o&&(d.span=o);const l={type:f,label:e.label,required:e.required,options:e.options,formConfig:{...e.placeholder&&{placeholder:e.placeholder},...e.helper&&{hint:e.helper},...d}};e.type==="chooser"&&(e.variant&&(l.variant=e.variant),e.multiple&&(l.multiple=!0),e.density&&(l.density=e.density),e.columns&&(l.columns=e.columns),e.accent&&(l.accent=e.accent),e.accentStyle&&(l.accentStyle=e.accentStyle),e.glow&&(l.glow=!0),e.glowColour&&(l.glowColour=e.glowColour),e.shadow&&(l.shadow=e.shadow),e.shadowColour&&(l.shadowColour=e.shadowColour)),a[e.name]=l}),a}function v(t){const o={};return(t||[]).forEach(a=>{if(!(!a.name||a.type==="page-break"||a.type==="spacer")&&(a.type==="select"||a.type==="multiselect")&&a.required){const e=(a.options||[])[0];e&&(o[a.name]=typeof e=="object"?e.value:e)}}),o}function w(t,o){(o||[]).forEach(a=>{if(a.type!=="date"||!a.name)return;const e=t.querySelector(`[name="${a.name}"]`);e&&e.type!=="date"&&(e.type="date")})}function p(t,o,a){let e=t.querySelector(".cms-form-message");e||(e=document.createElement("p"),e.className="cms-form-message",t.appendChild(e)),e.textContent=o,e.style.cssText=a?"color:var(--danger,#f87171);margin-top:.75rem;":"color:var(--success,#4ade80);margin-top:.75rem;"}g.forEach(t=>{let o;try{o=JSON.parse(atob(t.dataset.formInline))}catch{return}const a=o.fields||[],e=o.settings||{},f=e.columns||1,d=e.layout||"stacked",l=a.some(c=>c.type==="page-break"),y=async c=>{try{const s=t.querySelector('[name="website"]'),u=t.querySelector('[name="_t"]'),r=Object.assign({},c);s!==null&&(r._hp=s.value),u!==null&&(r._t=u.value);const m=await H.post(`/api/forms/submit/${o.slug}`,r);if(m&&m.error){p(t,m.error,!0);const h=new Error(m.error);throw h.formSubmitFailed=!0,h}if(m?.redirect){window.location.href=m.redirect;return}for(;t.firstChild;)t.removeChild(t.firstChild);p(t,m?.message||e.successMessage||"Thank you for your submission.",!1)}catch(s){throw s.formSubmitFailed||p(t,s.message||"Submission failed. Please try again.",!0),s}};function i(c){const s=c.querySelector("form");if(!s)return;const u=document.createElement("div");u.className="fb-form-honeypot",u.setAttribute("aria-hidden","true");const r=document.createElement("input");r.name="website",r.type="text",r.tabIndex=-1,r.autocomplete="url",r.placeholder="https://",u.appendChild(r);const m=document.createElement("input");m.name="_t",m.type="hidden",m.value=Date.now(),u.appendChild(m),s.appendChild(u)}function n(){if(!window.FormLogicEngine||!a.some(s=>s.logic))return;const c=new window.FormLogicEngine.FormLogicRuntime(o,t);if(c.init(),t._formLogicRuntime=c,t.parentNode&&typeof MutationObserver<"u"){const s=new MutationObserver(function(u){for(const r of u)for(const m of r.removedNodes)if(m===t||m.nodeType===1&&m.contains&&m.contains(t)){c.destroy(),s.disconnect();return}});s.observe(t.parentNode,{childList:!0,subtree:!1})}}if(l&&F.wizard){const c=[];let s=[],u=o.title||"Step 1",r="";a.forEach(h=>{h.type==="page-break"?(c.push({title:u,description:r,fields:b(s,f)}),s=[],u=h.label||`Step ${c.length+1}`,r=h.description||""):h.type!=="spacer"&&s.push(h)}),c.push({title:u,description:r,fields:b(s,f)});const m=F.wizard(t,{schema:{steps:c},onSubmit:y});Promise.resolve(m).then(function(){w(t,a),e.honeypot!==!1&&i(t),n()})}else if(F.render){const c=F.render(t,b(a,f),v(a),{submitText:e.submitText||"Submit",layout:d,columns:f,onSubmit:y});Promise.resolve(c).then(function(){if(d==="grid"&&e.submitSpan==="full"){const s=t.querySelector(".form-buttons");s&&s.classList.add("col-span-full")}w(t,a),e.honeypot!==!1&&i(t),n()})}}),$(document).on("click",".dm-banner__dismiss",function(){$(this).closest(".dm-banner").remove()})})();
|
|
@@ -60,7 +60,7 @@ import {PRESET_COLLECTION_SLUGS} from '../../services/presetCollections.js';
|
|
|
60
60
|
import {ensureFormForCollection} from '../../services/forms.js';
|
|
61
61
|
import {hooks} from '../../services/hooks.js';
|
|
62
62
|
import {scopeAllows, validateToken} from '../../services/apiTokens.js';
|
|
63
|
-
import {resolveArtefactProject} from '../../services/projects.js';
|
|
63
|
+
import {isProjectEnabled, resolveArtefactProject} from '../../services/projects.js';
|
|
64
64
|
|
|
65
65
|
const ALL_PRESET_SLUGS = new Set(['roles', 'user-profiles', ...PRESET_COLLECTION_SLUGS]);
|
|
66
66
|
|
|
@@ -146,6 +146,15 @@ export function applyReadFieldAllowlist(schema, payload) {
|
|
|
146
146
|
* @returns {Promise<object|undefined>}
|
|
147
147
|
*/
|
|
148
148
|
export async function checkPublicAccess(schema, operation, request, reply) {
|
|
149
|
+
// Hard kill-switch: if the artefact's project is disabled, the whole
|
|
150
|
+
// external surface is off — 404 (no oracle). This covers /api/v1
|
|
151
|
+
// collections, the synthesized-schema custom endpoints (/api/x/*, whose
|
|
152
|
+
// meta.project is the endpoint's project), and any token bound to the
|
|
153
|
+
// project — all funnel through here.
|
|
154
|
+
if (!(await isProjectEnabled(resolveArtefactProject(schema)))) {
|
|
155
|
+
return reply.status(404).send({ error: 'Not found' });
|
|
156
|
+
}
|
|
157
|
+
|
|
149
158
|
const access = schema.api?.[operation];
|
|
150
159
|
if (!access?.enabled) {
|
|
151
160
|
return reply.status(403).send({ error: `Public ${operation} is disabled for this collection` });
|
|
@@ -80,6 +80,10 @@ export async function projectsRoutes(fastify, opts = {}) {
|
|
|
80
80
|
if (request.params.slug === CORE_PROJECT_SLUG) {
|
|
81
81
|
return reply.status(403).send({error: 'Cannot delete the core project'});
|
|
82
82
|
}
|
|
83
|
+
const target = await getProject(request.params.slug);
|
|
84
|
+
if (target?.protected === true) {
|
|
85
|
+
return reply.status(403).send({error: 'This project is protected and cannot be deleted'});
|
|
86
|
+
}
|
|
83
87
|
const grouped = await getArtefactsForProject(request.params.slug);
|
|
84
88
|
const total = Object.values(grouped).reduce((sum, arr) => sum + arr.length, 0);
|
|
85
89
|
if (total > 0) {
|
|
@@ -105,7 +109,11 @@ export async function projectsRoutes(fastify, opts = {}) {
|
|
|
105
109
|
if (request.params.slug === CORE_PROJECT_SLUG) {
|
|
106
110
|
return reply.status(403).send({error: 'Cannot untag the core project — artefacts would immediately resolve back to it'});
|
|
107
111
|
}
|
|
108
|
-
|
|
112
|
+
const target = await getProject(request.params.slug);
|
|
113
|
+
if (target?.protected === true) {
|
|
114
|
+
return reply.status(403).send({error: 'This project is protected and cannot be untagged'});
|
|
115
|
+
}
|
|
116
|
+
if (!target) {
|
|
109
117
|
return reply.status(404).send({error: 'Project not found'});
|
|
110
118
|
}
|
|
111
119
|
const counts = await untagAllForProject(request.params.slug);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domma Docs — public surface
|
|
3
|
+
*
|
|
4
|
+
* Serves the handbook at request time from the admin doc templates:
|
|
5
|
+
* GET /domma-docs → home landing
|
|
6
|
+
* GET /domma-docs/<path> → section landing or leaf page
|
|
7
|
+
*
|
|
8
|
+
* The publish switch is the `domma-docs` project's enabled flag: when the
|
|
9
|
+
* project is disabled the whole surface 404s (no existence oracle), identical
|
|
10
|
+
* to the catch-all's project kill-switch. Unknown paths 404 the same way.
|
|
11
|
+
*
|
|
12
|
+
* This dedicated /domma-docs(/*) prefix out-prioritises public.js's `/*`
|
|
13
|
+
* catch-all (Fastify ranks static segments above wildcards), exactly as
|
|
14
|
+
* routes/api/endpoints-public.js `/x/*` coexists with it — so no per-path
|
|
15
|
+
* special-casing is needed in public.js. Registered at app root (not behind a
|
|
16
|
+
* prefixed plugin) so the specificity ranking shares one router scope.
|
|
17
|
+
*/
|
|
18
|
+
import {isProjectEnabled} from '../services/projects.js';
|
|
19
|
+
import {renderDocPage} from '../services/docs.js';
|
|
20
|
+
import {getBaseUrl, render404} from './public.js';
|
|
21
|
+
|
|
22
|
+
const DOCS_PROJECT_SLUG = 'domma-docs';
|
|
23
|
+
|
|
24
|
+
export async function docsPublicRoutes(fastify) {
|
|
25
|
+
const handle = async (request, reply) => {
|
|
26
|
+
// Hard kill-switch: docs disabled → 404 the entire surface.
|
|
27
|
+
if (!(await isProjectEnabled(DOCS_PROJECT_SLUG))) {
|
|
28
|
+
reply.status(404);
|
|
29
|
+
return reply.type('text/html').send(await render404(request.url.split('?')[0]));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const docPath = String(request.params['*'] || '');
|
|
33
|
+
const html = await renderDocPage(docPath, {user: null, baseUrl: getBaseUrl(request)});
|
|
34
|
+
if (html == null) {
|
|
35
|
+
reply.status(404);
|
|
36
|
+
return reply.type('text/html').send(await render404(`/domma-docs${docPath ? '/' + docPath : ''}`));
|
|
37
|
+
}
|
|
38
|
+
return reply.type('text/html').send(html);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
fastify.get('/domma-docs', handle); // exact — params['*'] === '' (home landing)
|
|
42
|
+
fastify.get('/domma-docs/*', handle); // sections + leaves
|
|
43
|
+
}
|
package/server/routes/public.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* The admin panel is excluded (handled by static serving).
|
|
6
6
|
*/
|
|
7
7
|
import {getPage} from '../services/content.js';
|
|
8
|
+
import {getProjectForPage, isProjectEnabled} from '../services/projects.js';
|
|
8
9
|
import {renderPage} from '../services/renderer.js';
|
|
9
10
|
import {buildRobotsTxt, generate as generateSitemap} from '../services/sitemap.js';
|
|
10
11
|
import {checkVisibility} from '../middleware/auth.js';
|
|
@@ -25,7 +26,7 @@ import * as cache from '../services/cache/index.js';
|
|
|
25
26
|
* @param {import('fastify').FastifyRequest} request
|
|
26
27
|
* @returns {string} e.g. 'https://example.com' (no trailing slash)
|
|
27
28
|
*/
|
|
28
|
-
function getBaseUrl(request) {
|
|
29
|
+
export function getBaseUrl(request) {
|
|
29
30
|
const override = (getConfig('site') || {}).baseUrl;
|
|
30
31
|
if (override) return override.toString().trim().replace(/\/+$/, '');
|
|
31
32
|
const host = request.host || request.headers.host || request.hostname;
|
|
@@ -108,6 +109,14 @@ export async function publicRoutes(fastify) {
|
|
|
108
109
|
return reply.type('text/html').send(await render404(urlPath));
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
// Hard kill-switch: a disabled project takes its pages off the public
|
|
113
|
+
// site entirely — 404 (no oracle), regardless of page visibility.
|
|
114
|
+
const pageProject = await getProjectForPage(page.urlPath || urlPath, page.project);
|
|
115
|
+
if (!(await isProjectEnabled(pageProject))) {
|
|
116
|
+
reply.status(404);
|
|
117
|
+
return reply.type('text/html').send(await render404(urlPath));
|
|
118
|
+
}
|
|
119
|
+
|
|
111
120
|
// Enforce page visibility — role only resolved for gated pages,
|
|
112
121
|
// so public pages share a single cache entry keyed `roleanon`.
|
|
113
122
|
//
|
|
@@ -169,7 +178,7 @@ export async function publicRoutes(fastify) {
|
|
|
169
178
|
* Render a 404 response — tries content/pages/404.md first, falls back to
|
|
170
179
|
* a minimal inline page so the site theme is applied when possible.
|
|
171
180
|
*/
|
|
172
|
-
async function render404(urlPath) {
|
|
181
|
+
export async function render404(urlPath) {
|
|
173
182
|
try {
|
|
174
183
|
const page404 = await getPage('/404');
|
|
175
184
|
if (page404 && page404.status === 'published') {
|
package/server/server.js
CHANGED
|
@@ -22,7 +22,7 @@ import {getLoadedPlugins, getPluginSettings, registerPlugins} from './services/p
|
|
|
22
22
|
import {load as loadRoles, seed as seedRoles} from './services/roles.js';
|
|
23
23
|
import {ensureAllProfiles, seed as seedUserProfiles} from './services/userProfiles.js';
|
|
24
24
|
import {seedAll as seedPresetCollections} from './services/presetCollections.js';
|
|
25
|
-
import {seedCoreProject} from './services/projects.js';
|
|
25
|
+
import {seedCoreProject, seedDocsProject} from './services/projects.js';
|
|
26
26
|
import {seedDefaultBlocks} from './services/blocks.js';
|
|
27
27
|
import {seedDefaultComponents} from './services/components.js';
|
|
28
28
|
import {refreshComponentTagAllowlist} from './services/markdown.js';
|
|
@@ -186,6 +186,7 @@ await seedUserProfiles();
|
|
|
186
186
|
await ensureAllProfiles();
|
|
187
187
|
await seedPresetCollections();
|
|
188
188
|
await seedCoreProject();
|
|
189
|
+
await seedDocsProject(); // built-in Domma Docs project — seeded disabled by default
|
|
189
190
|
await seedDefaultBlocks();
|
|
190
191
|
await seedDefaultComponents();
|
|
191
192
|
await refreshComponentTagAllowlist();
|
|
@@ -223,6 +224,11 @@ try {
|
|
|
223
224
|
await cache.initCache(config.cache);
|
|
224
225
|
console.log(`[cache] driver=${cache.isEnabled() ? config.cache.driver : 'disabled'}`);
|
|
225
226
|
|
|
227
|
+
// Docs are rendered at request time from the shipped admin templates; clear any
|
|
228
|
+
// previously-cached docs HTML so a version bump (new templates) never serves
|
|
229
|
+
// stale pages. No-op on a cold memory cache; matters for the Redis driver.
|
|
230
|
+
await cache.invalidateTags(['docs']);
|
|
231
|
+
|
|
226
232
|
// Serve uploaded media files — nosniff prevents browsers rendering spoofed content types
|
|
227
233
|
await app.register(staticPlugin, {
|
|
228
234
|
root: mediaDir,
|
|
@@ -369,6 +375,11 @@ for (const [name, plugin] of Object.entries(getLoadedPlugins())) {
|
|
|
369
375
|
// Public Site (catch-all — must be last)
|
|
370
376
|
// ---------------------------------------------------------------------------
|
|
371
377
|
|
|
378
|
+
// Domma Docs (request-time handbook) — dedicated /domma-docs(/*) prefix that
|
|
379
|
+
// out-prioritises the catch-all; must be registered before it.
|
|
380
|
+
const { docsPublicRoutes } = await import('./routes/docs-public.js');
|
|
381
|
+
await app.register(docsPublicRoutes);
|
|
382
|
+
|
|
372
383
|
const { publicRoutes } = await import('./routes/public.js');
|
|
373
384
|
await app.register(publicRoutes);
|
|
374
385
|
|
|
@@ -395,3 +406,4 @@ try {
|
|
|
395
406
|
process.exit(1);
|
|
396
407
|
}
|
|
397
408
|
// scaffold-restart 1779615684
|
|
409
|
+
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domma Docs — request-time renderer
|
|
3
|
+
*
|
|
4
|
+
* Serves the Domma CMS handbook at /domma-docs/* directly from the admin doc
|
|
5
|
+
* HTML templates (admin/js/templates/docs/*.html) at request time. There is no
|
|
6
|
+
* baked content: a domma-cms version bump ships new templates and the docs
|
|
7
|
+
* update everywhere automatically — no porter, no per-site provisioning, no
|
|
8
|
+
* staleness. (Replaces the one-shot scripts/migrate-docs.js bake.)
|
|
9
|
+
*
|
|
10
|
+
* The publish switch is the `domma-docs` project's enabled flag, enforced by
|
|
11
|
+
* the route (server/routes/docs-public.js); this service only renders.
|
|
12
|
+
*
|
|
13
|
+
* Pipeline per leaf page: read template → strip admin chrome → rewrite links →
|
|
14
|
+
* extract inline <style> → dedent → escape code brackets → structure body into
|
|
15
|
+
* native Domma shortcodes ([card]/[tabs]/[accordion]) → wrap with hero, nav,
|
|
16
|
+
* breadcrumb and prev/next → parseMarkdown → renderPage. Section landing pages
|
|
17
|
+
* (/domma-docs and the Usage/Tutorials/API indexes) are synthesized from the
|
|
18
|
+
* MAP so the nav never links to a 404.
|
|
19
|
+
*/
|
|
20
|
+
import {readFile} from 'node:fs/promises';
|
|
21
|
+
import {fileURLToPath} from 'node:url';
|
|
22
|
+
import {dirname, join, resolve} from 'node:path';
|
|
23
|
+
|
|
24
|
+
import {parseMarkdown} from './markdown.js';
|
|
25
|
+
import {renderPage} from './renderer.js';
|
|
26
|
+
import * as cache from './cache/index.js';
|
|
27
|
+
|
|
28
|
+
// import.meta.url-relative (NOT cwd-relative): the templates live beside the
|
|
29
|
+
// engine's admin dir in both the npm-package layout and each child site's
|
|
30
|
+
// physical copy. server/services/docs.js → ../../admin/js/templates/docs.
|
|
31
|
+
const TPL_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../..', 'admin/js/templates/docs');
|
|
32
|
+
|
|
33
|
+
// Functional nav chrome only (breadcrumb + prev/next + section nav). Everything
|
|
34
|
+
// else (hero, cards, tabs, accordion, buttons) is a native Domma component, so
|
|
35
|
+
// it follows the theme. Injected as an inline <style> per docs page at render
|
|
36
|
+
// time — no dependency on content/custom.css.
|
|
37
|
+
const DOCS_BODY_CSS = `.doc-breadcrumb { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; max-width: 80ch; margin: 0 auto 1.25rem; font-size: .85rem; color: var(--dm-text-muted); }
|
|
38
|
+
.doc-breadcrumb a { color: var(--dm-primary); text-decoration: none; }
|
|
39
|
+
.doc-breadcrumb a:hover { text-decoration: underline; }
|
|
40
|
+
.doc-breadcrumb [aria-current] { color: var(--dm-text); font-weight: 600; }
|
|
41
|
+
.doc-pagenav { display: flex; flex-wrap: wrap; gap: .75rem; justify-content: space-between; align-items: center; max-width: 80ch; margin: 2.5rem auto 3rem; }
|
|
42
|
+
.doc-pagenav .btn-secondary { margin-inline: auto; }
|
|
43
|
+
.doc-nav { display: flex; flex-wrap: wrap; gap: .4rem; justify-content: center; max-width: 80ch; margin: 0 auto 1.75rem; padding: .5rem; border: 1px solid var(--dm-card-border, var(--dm-border)); border-radius: var(--dm-radius-lg, 14px); background: var(--dm-card-bg, var(--dm-surface)); }`;
|
|
44
|
+
|
|
45
|
+
// template basename → [url tail (= file path), title]
|
|
46
|
+
const MAP = [
|
|
47
|
+
['usage-pages', 'usage/pages', 'Pages'],
|
|
48
|
+
['usage-media', 'usage/media', 'Media'],
|
|
49
|
+
['usage-navigation', 'usage/navigation', 'Navigation'],
|
|
50
|
+
['usage-dconfig', 'usage/dconfig', 'DConfig'],
|
|
51
|
+
['usage-shortcodes', 'usage/shortcodes', 'Shortcodes'],
|
|
52
|
+
['usage-site-settings', 'usage/site-settings', 'Site Settings'],
|
|
53
|
+
['usage-plugins', 'usage/plugins', 'Plugins'],
|
|
54
|
+
['usage-users-roles', 'usage/users-roles', 'Users & Roles'],
|
|
55
|
+
['usage-views', 'usage/views', 'Views'],
|
|
56
|
+
['usage-actions', 'usage/actions', 'Actions'],
|
|
57
|
+
['usage-cta-shortcode', 'usage/cta-shortcode', 'CTA Shortcode'],
|
|
58
|
+
['tutorial-crud', 'tutorials/crud', 'Building a CRUD App'],
|
|
59
|
+
['tutorial-plugin', 'tutorials/plugin', 'Writing a Plugin'],
|
|
60
|
+
['tutorial-forms', 'tutorials/forms', 'Form Follow-Up'],
|
|
61
|
+
['components-reference', 'components', 'Components Reference'],
|
|
62
|
+
['components-howto', 'components/howto', 'Components How-To'],
|
|
63
|
+
['components-walkthrough','components/walkthrough','Components Walkthrough'],
|
|
64
|
+
['components-rules', 'components/rules', 'Components Rules'],
|
|
65
|
+
['api-authentication', 'api/authentication', 'Authentication'],
|
|
66
|
+
['api-pages', 'api/pages', 'Pages API'],
|
|
67
|
+
['api-settings', 'api/settings', 'Settings API'],
|
|
68
|
+
['api-layouts', 'api/layouts', 'Layouts API'],
|
|
69
|
+
['api-navigation', 'api/navigation', 'Navigation API'],
|
|
70
|
+
['api-media', 'api/media', 'Media API'],
|
|
71
|
+
['api-users', 'api/users', 'Users API'],
|
|
72
|
+
['api-plugins', 'api/plugins', 'Plugins API'],
|
|
73
|
+
['api-collections', 'api/collections', 'Collections API'],
|
|
74
|
+
['api-views', 'api/views', 'Views API'],
|
|
75
|
+
['api-actions', 'api/actions', 'Actions API']
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
// Admin-SPA hash routes and stale /resources/… paths don't resolve on the
|
|
79
|
+
// public site — rewrite doc-to-doc links to /domma-docs/…, admin-feature links
|
|
80
|
+
// to the admin SPA, and unwrap the rest.
|
|
81
|
+
const LINK_MAP = {
|
|
82
|
+
'#/docs/components-walkthrough': '/domma-docs/components/walkthrough',
|
|
83
|
+
'#/docs/components-rules': '/domma-docs/components/rules',
|
|
84
|
+
'#/docs/components-howto': '/domma-docs/components/howto',
|
|
85
|
+
'#/docs/components': '/domma-docs/components',
|
|
86
|
+
'#/components': '/domma-docs/components',
|
|
87
|
+
'#/tutorials/plugin': '/domma-docs/tutorials/plugin',
|
|
88
|
+
'#/tutorials/forms': '/domma-docs/tutorials/forms',
|
|
89
|
+
'#/tutorials': '/domma-docs/tutorials',
|
|
90
|
+
'#/documentation': '/domma-docs',
|
|
91
|
+
'#/actions': '/admin#/actions',
|
|
92
|
+
'#/collections': '/admin#/collections',
|
|
93
|
+
'#/forms': '/admin#/forms',
|
|
94
|
+
'#/plugins': '/admin#/plugins',
|
|
95
|
+
'#/settings': '/admin#/settings',
|
|
96
|
+
'/resources/shortcodes': '/domma-docs/usage/shortcodes',
|
|
97
|
+
'/resources/components': '/domma-docs/components'
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// The four top-level doc sections — rendered as a persistent nav on every page.
|
|
101
|
+
const SECTIONS = [
|
|
102
|
+
{label: 'Usage', href: '/domma-docs/usage'},
|
|
103
|
+
{label: 'Tutorials', href: '/domma-docs/tutorials'},
|
|
104
|
+
{label: 'Components', href: '/domma-docs/components'},
|
|
105
|
+
{label: 'API Reference', href: '/domma-docs/api'}
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
// Section landing pages keyed by url tail → section label. The Components
|
|
109
|
+
// landing is the Components Reference leaf (MAP tail 'components'), so it is
|
|
110
|
+
// NOT synthesized here.
|
|
111
|
+
const LANDINGS = {
|
|
112
|
+
'': 'Home',
|
|
113
|
+
'usage': 'Usage',
|
|
114
|
+
'tutorials': 'Tutorials',
|
|
115
|
+
'api': 'API Reference'
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// ── pure transform helpers (relocated from scripts/migrate-docs.js) ──────────
|
|
119
|
+
|
|
120
|
+
function escText(s) {
|
|
121
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
122
|
+
}
|
|
123
|
+
function heroAttr(s) {
|
|
124
|
+
return String(s).replace(/"/g, '"');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Drop the admin-only header block (<div class="view-header"> … </div>).
|
|
128
|
+
function stripHeader(html) {
|
|
129
|
+
return html.replace(/<div class="view-header">[\s\S]*?<\/div>\s*/, '');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function rewriteLinks(html) {
|
|
133
|
+
let out = html;
|
|
134
|
+
for (const [from, to] of Object.entries(LINK_MAP)) {
|
|
135
|
+
out = out.split(`href="${from}"`).join(`href="${to}"`);
|
|
136
|
+
}
|
|
137
|
+
// Any remaining /resources/… link has no doc equivalent — unwrap to text.
|
|
138
|
+
out = out.replace(/<a\b[^>]*\shref="\/resources\/[^"]*"[^>]*>([\s\S]*?)<\/a>/gi, '$1');
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Pull every inline <style> block out of the body, returning the cleaned HTML
|
|
143
|
+
// plus the extracted (deduped) CSS — collected PER CALL (no shared module state,
|
|
144
|
+
// which would leak across requests in a long-lived server).
|
|
145
|
+
function stripStyleBlocks(html) {
|
|
146
|
+
const styles = new Set();
|
|
147
|
+
const out = html.replace(/<style\b[^>]*>([\s\S]*?)<\/style>\s*/gi, (_m, css) => {
|
|
148
|
+
const trimmed = css.trim();
|
|
149
|
+
if (trimmed) styles.add(trimmed);
|
|
150
|
+
return '';
|
|
151
|
+
});
|
|
152
|
+
return {html: out, styles: [...styles]};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Markdown treats any line indented 4+ spaces as a code block. Strip leading
|
|
156
|
+
// indentation OUTSIDE <pre> so structural HTML passes through as raw HTML;
|
|
157
|
+
// inside <pre> whitespace is significant and preserved.
|
|
158
|
+
function dedentOutsidePre(html) {
|
|
159
|
+
const parts = html.split(/(<pre\b[^>]*>[\s\S]*?<\/pre>)/gi);
|
|
160
|
+
return parts.map((part, i) =>
|
|
161
|
+
(i % 2 === 1) ? part : part.replace(/^[ \t]+/gm, '')
|
|
162
|
+
).join('');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Escape square brackets inside <pre> and <code> so shortcode examples render
|
|
166
|
+
// literally instead of being expanded by the render pipeline.
|
|
167
|
+
function escapeCodeBrackets(html) {
|
|
168
|
+
const esc = (region) => region.replace(/\[/g, '[').replace(/\]/g, ']');
|
|
169
|
+
html = html.replace(/<pre\b[^>]*>[\s\S]*?<\/pre>/gi, esc);
|
|
170
|
+
html = html.replace(/<code\b[^>]*>[\s\S]*?<\/code>/gi, esc);
|
|
171
|
+
return html;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Strip the consistent .row > .col-12 > .docs-body shell to get the bare inner.
|
|
175
|
+
function unwrapDocsBody(html) {
|
|
176
|
+
return html.trim()
|
|
177
|
+
.replace(/^<div class="row">\s*<div class="col-12">\s*<div class="docs-body">/, '')
|
|
178
|
+
.replace(/<\/div>\s*<\/div>\s*<\/div>\s*$/, '')
|
|
179
|
+
.trim();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Plain-text heading title, safe as a shortcode attribute value.
|
|
183
|
+
function headingText(inner) {
|
|
184
|
+
return inner.replace(/<[^>]+>/g, '')
|
|
185
|
+
.replace(/&/g, '&').replace(/—/g, '—').replace(/–/g, '–')
|
|
186
|
+
.replace(/"/g, "'").replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>')
|
|
187
|
+
.replace(/\s+/g, ' ').trim().replace(/"/g, "'");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Split inner content at top-level <hN> headings into {preamble, sections}.
|
|
191
|
+
function splitSections(inner, level) {
|
|
192
|
+
const re = new RegExp(`<h${level}\\b[^>]*>([\\s\\S]*?)<\\/h${level}>`, 'gi');
|
|
193
|
+
const matches = [...inner.matchAll(re)];
|
|
194
|
+
if (!matches.length) return {preamble: inner, sections: []};
|
|
195
|
+
const preamble = inner.slice(0, matches[0].index).trim();
|
|
196
|
+
const sections = matches.map((m, i) => ({
|
|
197
|
+
title: headingText(m[1]),
|
|
198
|
+
body: inner.slice(m.index + m[0].length, i + 1 < matches.length ? matches[i + 1].index : inner.length).trim()
|
|
199
|
+
}));
|
|
200
|
+
return {preamble, sections};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ≥7 sections → accordion; 2–6 → tabs; <2 → a single card.
|
|
204
|
+
function structureBody(body) {
|
|
205
|
+
const inner = unwrapDocsBody(body);
|
|
206
|
+
const level = /<h2\b/i.test(inner) ? 2 : 3;
|
|
207
|
+
const {preamble, sections} = splitSections(inner, level);
|
|
208
|
+
if (sections.length < 2) return `[card]\n${inner}\n[/card]`;
|
|
209
|
+
const lead = preamble ? `[card]\n${preamble}\n[/card]\n\n` : '';
|
|
210
|
+
if (sections.length <= 6) {
|
|
211
|
+
return lead + '[tabs]\n' +
|
|
212
|
+
sections.map(s => `[tab title="${s.title}"]\n${s.body}\n[/tab]`).join('\n') + '\n[/tabs]';
|
|
213
|
+
}
|
|
214
|
+
return lead + '[accordion multiple="true"]\n' +
|
|
215
|
+
sections.map(s => `[item title="${s.title}"]\n${s.body}\n[/item]`).join('\n') + '\n[/accordion]';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Persistent section nav (Home + the four sections, current one highlighted).
|
|
219
|
+
function docNav(activeLabel) {
|
|
220
|
+
const links = [{label: 'Home', href: '/domma-docs'}, ...SECTIONS].map(s => {
|
|
221
|
+
const active = s.label === activeLabel;
|
|
222
|
+
return `<a class="btn btn-sm ${active ? 'btn-primary' : 'btn-ghost'}" href="${s.href}">${escText(s.label)}</a>`;
|
|
223
|
+
}).join('');
|
|
224
|
+
return `<nav class="doc-nav">${links}</nav>`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Section label + landing URL for the hero eyebrow, derived from the page path.
|
|
228
|
+
function sectionFor(path) {
|
|
229
|
+
if (path.startsWith('usage/')) return {label: 'Usage', href: '/domma-docs/usage'};
|
|
230
|
+
if (path.startsWith('tutorials/')) return {label: 'Tutorials', href: '/domma-docs/tutorials'};
|
|
231
|
+
if (path.startsWith('api/')) return {label: 'API Reference', href: '/domma-docs/api'};
|
|
232
|
+
if (path === 'components' || path.startsWith('components/')) return {label: 'Components', href: '/domma-docs/components'};
|
|
233
|
+
return {label: 'Documentation', href: '/domma-docs'};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Assemble a leaf page: hero, persistent nav, breadcrumb, structured body, and
|
|
237
|
+
// a prev / back-to-section / next nav bar.
|
|
238
|
+
function buildPage(section, title, body, prev, next) {
|
|
239
|
+
const hero = `[hero title="${heroAttr(title)}" tagline="${heroAttr(section.label)}" variant="gradient" align="center" fullwidth="true"][/hero]`;
|
|
240
|
+
const nav = docNav(section.label);
|
|
241
|
+
const crumb = `<nav class="doc-breadcrumb"><a href="/domma-docs">Docs</a><span>/</span>` +
|
|
242
|
+
`<a href="${section.href}">${escText(section.label)}</a><span>/</span>` +
|
|
243
|
+
`<span aria-current="page">${escText(title)}</span></nav>`;
|
|
244
|
+
const prevA = prev ? `<a class="btn btn-ghost" href="/domma-docs/${prev[1]}">← ${escText(prev[2])}</a>` : '<span></span>';
|
|
245
|
+
const nextA = next ? `<a class="btn btn-ghost" href="/domma-docs/${next[1]}">${escText(next[2])} →</a>` : '<span></span>';
|
|
246
|
+
const pagenav = `<nav class="doc-pagenav">${prevA}` +
|
|
247
|
+
`<a class="btn btn-secondary" href="${section.href}">All ${escText(section.label)}</a>${nextA}</nav>`;
|
|
248
|
+
return `${hero}\n\n${nav}\n\n${crumb}\n\n${structureBody(body)}\n\n${pagenav}`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── landing pages ────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
// Build a section landing body: hero + nav + a grid of cards linking each leaf.
|
|
254
|
+
function buildSectionLanding(label) {
|
|
255
|
+
const section = SECTIONS.find(s => s.label === label);
|
|
256
|
+
const pages = MAP.filter(m => sectionFor(m[1]).label === label);
|
|
257
|
+
const hero = `[hero title="${heroAttr(label)}" tagline="Domma Docs" variant="gradient" align="center" fullwidth="true"][/hero]`;
|
|
258
|
+
const cards = pages.map(([, tail, title]) =>
|
|
259
|
+
`[card]\n### ${escText(title)}\n\n[button label="Read ${heroAttr(title)} →" href="/domma-docs/${tail}" variant="ghost" /]\n[/card]`
|
|
260
|
+
).join('\n');
|
|
261
|
+
return {title: label, body: `${hero}\n\n${docNav(label)}\n\n[grid cols="3"]\n${cards}\n[/grid]`};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Build the home landing: hero + nav + a card per top-level section.
|
|
265
|
+
function buildHomeLanding() {
|
|
266
|
+
const hero = `[hero title="Domma Docs" tagline="The Domma CMS handbook" variant="gradient" align="center" fullwidth="true"][/hero]`;
|
|
267
|
+
const cards = [
|
|
268
|
+
{label: 'Usage', href: '/domma-docs/usage', icon: 'book-open', blurb: 'Day-to-day guides for running the CMS — pages, media, navigation, shortcodes and more.'},
|
|
269
|
+
{label: 'Tutorials', href: '/domma-docs/tutorials', icon: 'academic-cap', blurb: 'Step-by-step walkthroughs that build something real from start to finish.'},
|
|
270
|
+
{label: 'Components', href: '/domma-docs/components', icon: 'puzzle', blurb: 'The full component reference — shortcodes, how-tos, walkthroughs and rules.'},
|
|
271
|
+
{label: 'API Reference', href: '/domma-docs/api', icon: 'code', blurb: 'Every REST endpoint for headless and external integrations.'}
|
|
272
|
+
].map(s =>
|
|
273
|
+
`[card icon="${s.icon}" title="${heroAttr(s.label)}"]\n${escText(s.blurb)}\n\n[button label="Browse ${heroAttr(s.label)} →" href="${s.href}" variant="primary" /]\n[/card]`
|
|
274
|
+
).join('\n');
|
|
275
|
+
return {title: 'Documentation', body: `${hero}\n\n${docNav('Home')}\n\n[grid cols="2"]\n${cards}\n[/grid]`};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── render ───────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
// Build the shortcode body for a doc path, or null if the path is unknown.
|
|
281
|
+
// Returns {title, body, styles[]} — `styles` is the inline CSS extracted from a
|
|
282
|
+
// leaf template (empty for synthesized landings).
|
|
283
|
+
async function buildDocBody(docPath) {
|
|
284
|
+
if (docPath in LANDINGS) {
|
|
285
|
+
const built = docPath === '' ? buildHomeLanding() : buildSectionLanding(LANDINGS[docPath]);
|
|
286
|
+
return {...built, styles: []};
|
|
287
|
+
}
|
|
288
|
+
const entry = MAP.find(m => m[1] === docPath);
|
|
289
|
+
if (!entry) return null;
|
|
290
|
+
const [tpl, path, title] = entry;
|
|
291
|
+
let src;
|
|
292
|
+
try {
|
|
293
|
+
src = await readFile(join(TPL_DIR, `${tpl}.html`), 'utf8');
|
|
294
|
+
} catch {
|
|
295
|
+
return null; // template missing — treat as a 404
|
|
296
|
+
}
|
|
297
|
+
const {html: stripped, styles} = stripStyleBlocks(stripHeader(src));
|
|
298
|
+
const body = escapeCodeBrackets(dedentOutsidePre(rewriteLinks(stripped))).trim();
|
|
299
|
+
const section = sectionFor(path);
|
|
300
|
+
// prev / next are the section-mates either side of this page in MAP order.
|
|
301
|
+
const mates = MAP.filter(m => sectionFor(m[1]).label === section.label);
|
|
302
|
+
const mi = mates.findIndex(m => m[1] === path);
|
|
303
|
+
const prev = mi > 0 ? mates[mi - 1] : null;
|
|
304
|
+
const next = mi >= 0 && mi < mates.length - 1 ? mates[mi + 1] : null;
|
|
305
|
+
return {title, body: buildPage(section, title, body, prev, next), styles};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Render a Domma Docs page to full HTML, or return null for an unknown path
|
|
310
|
+
* (the route turns that into a 404). Cached per path+origin under the static
|
|
311
|
+
* 'docs' tag — invalidated on boot so a version bump never serves stale HTML.
|
|
312
|
+
*
|
|
313
|
+
* @param {string} docPath url tail after /domma-docs (e.g. 'usage/pages', '' for home)
|
|
314
|
+
* @param {{user?: object|null, baseUrl?: string}} [opts]
|
|
315
|
+
* @returns {Promise<string|null>}
|
|
316
|
+
*/
|
|
317
|
+
export async function renderDocPage(docPath, opts = {}) {
|
|
318
|
+
const path = String(docPath || '').replace(/^\/+|\/+$/g, '');
|
|
319
|
+
const built = await buildDocBody(path);
|
|
320
|
+
if (!built) return null;
|
|
321
|
+
|
|
322
|
+
const {user = null, baseUrl} = opts;
|
|
323
|
+
return cache.wrap(
|
|
324
|
+
`docs:${path}:o${baseUrl || ''}`,
|
|
325
|
+
async () => {
|
|
326
|
+
const parsed = await parseMarkdown(built.body, {user});
|
|
327
|
+
const css = [DOCS_BODY_CSS, ...built.styles].filter(Boolean).join('\n\n');
|
|
328
|
+
const html = `<style>${css}</style>\n${parsed.html}`;
|
|
329
|
+
const page = {
|
|
330
|
+
title: built.title,
|
|
331
|
+
layout: 'default',
|
|
332
|
+
status: 'published',
|
|
333
|
+
visibility: 'public',
|
|
334
|
+
project: 'domma-docs',
|
|
335
|
+
urlPath: `/domma-docs${path ? '/' + path : ''}`,
|
|
336
|
+
html,
|
|
337
|
+
usedComponents: parsed.usedComponents || [],
|
|
338
|
+
dconfig: parsed.data?.dconfig || null,
|
|
339
|
+
seo: {title: built.title},
|
|
340
|
+
sidebar: false,
|
|
341
|
+
tags: ['docs']
|
|
342
|
+
};
|
|
343
|
+
return renderPage(page, {baseUrl, user});
|
|
344
|
+
},
|
|
345
|
+
{tags: ['docs']}
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Every valid doc path (home + 29 leaves + 3 section landings). The Components
|
|
351
|
+
* landing is the 'components' leaf, already in MAP. `renderDocPage` returning
|
|
352
|
+
* null is the authoritative miss; this is an enumeration aid (e.g. sitemap).
|
|
353
|
+
*
|
|
354
|
+
* @returns {string[]}
|
|
355
|
+
*/
|
|
356
|
+
export function listDocPaths() {
|
|
357
|
+
return ['', ...MAP.map(m => m[1]), 'usage', 'tutorials', 'api'];
|
|
358
|
+
}
|
|
@@ -51,6 +51,7 @@ const PRESETS = [
|
|
|
51
51
|
{name: 'icon', label: 'Icon', type: 'text', default: 'folder'},
|
|
52
52
|
{name: 'rootUrl', label: 'Root URL', type: 'text'},
|
|
53
53
|
{name: 'sortOrder', label: 'Sort Order', type: 'number', default: 0},
|
|
54
|
+
{name: 'enabled', label: 'Enabled', type: 'boolean', default: true},
|
|
54
55
|
{name: 'protected', label: 'Protected', type: 'boolean', default: false}
|
|
55
56
|
],
|
|
56
57
|
api: {
|
|
@@ -36,6 +36,23 @@ const CORE_PROJECT_SEED = {
|
|
|
36
36
|
icon: 'home',
|
|
37
37
|
rootUrl: '/',
|
|
38
38
|
sortOrder: -1,
|
|
39
|
+
enabled: true,
|
|
40
|
+
protected: true
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** Slug of the built-in, disabled-by-default documentation project. */
|
|
44
|
+
export const DOCS_PROJECT_SLUG = 'domma-docs';
|
|
45
|
+
|
|
46
|
+
/** Seed record for the Domma Docs project (created at boot if absent). */
|
|
47
|
+
const DOCS_PROJECT_SEED = {
|
|
48
|
+
slug: DOCS_PROJECT_SLUG,
|
|
49
|
+
name: 'Domma Docs',
|
|
50
|
+
description: 'The Domma CMS handbook — guides, tutorials, component & API ' +
|
|
51
|
+
'reference. Switch on to publish it on your own site.',
|
|
52
|
+
icon: 'document',
|
|
53
|
+
rootUrl: '/domma-docs',
|
|
54
|
+
sortOrder: 0,
|
|
55
|
+
enabled: false,
|
|
39
56
|
protected: true
|
|
40
57
|
};
|
|
41
58
|
|
|
@@ -105,6 +122,7 @@ export async function createProject(input) {
|
|
|
105
122
|
icon: input.icon || 'folder',
|
|
106
123
|
...(input.rootUrl != null && {rootUrl: input.rootUrl}),
|
|
107
124
|
sortOrder: Number.isFinite(input.sortOrder) ? input.sortOrder : 0,
|
|
125
|
+
enabled: input.enabled !== false,
|
|
108
126
|
...(typeof input.createdBy === 'string' && input.createdBy.trim() && {createdBy: input.createdBy.trim()}),
|
|
109
127
|
createdAt: now,
|
|
110
128
|
updatedAt: now
|
|
@@ -127,8 +145,9 @@ export async function seedCoreProject() {
|
|
|
127
145
|
const {entries} = await listEntries(PROJECTS_COLLECTION_SLUG, {limit: 0});
|
|
128
146
|
const existing = entries.find(e => e.data?.slug === CORE_PROJECT_SLUG);
|
|
129
147
|
if (existing) {
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
// Core is always protected and always enabled — adopt both invariants.
|
|
149
|
+
if (existing.data.protected === true && existing.data.enabled !== false) return normalise(existing);
|
|
150
|
+
const adopted = {...existing.data, protected: true, enabled: true, updatedAt: new Date().toISOString()};
|
|
132
151
|
await updateEntry(PROJECTS_COLLECTION_SLUG, existing.id, adopted);
|
|
133
152
|
return adopted;
|
|
134
153
|
}
|
|
@@ -138,6 +157,24 @@ export async function seedCoreProject() {
|
|
|
138
157
|
return data;
|
|
139
158
|
}
|
|
140
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Seed the built-in Domma Docs project at boot. Create-if-absent by slug — an
|
|
162
|
+
* existing record (including the admin's enabled/disabled choice and any edits)
|
|
163
|
+
* is returned untouched. Disabled by default: a fresh site ships with the docs
|
|
164
|
+
* off until an admin enables the project.
|
|
165
|
+
*
|
|
166
|
+
* @returns {Promise<object>} The docs project record.
|
|
167
|
+
*/
|
|
168
|
+
export async function seedDocsProject() {
|
|
169
|
+
const {entries} = await listEntries(PROJECTS_COLLECTION_SLUG, {limit: 0});
|
|
170
|
+
const existing = entries.find(e => e.data?.slug === DOCS_PROJECT_SLUG);
|
|
171
|
+
if (existing) return normalise(existing);
|
|
172
|
+
const now = new Date().toISOString();
|
|
173
|
+
const data = {...DOCS_PROJECT_SEED, createdAt: now, updatedAt: now};
|
|
174
|
+
await createEntry(PROJECTS_COLLECTION_SLUG, data);
|
|
175
|
+
return data;
|
|
176
|
+
}
|
|
177
|
+
|
|
141
178
|
/**
|
|
142
179
|
* Update a project. Slug is immutable. Preserves createdAt; bumps updatedAt.
|
|
143
180
|
* The core project's `rootUrl` and `protected` flag are locked; its name,
|
|
@@ -159,6 +196,9 @@ export async function updateProject(slug, input) {
|
|
|
159
196
|
if (input.protected != null && input.protected !== existing.protected) {
|
|
160
197
|
throw new Error("Cannot change the core project's protected flag");
|
|
161
198
|
}
|
|
199
|
+
if (input.enabled === false) {
|
|
200
|
+
throw new Error('Cannot disable the core project');
|
|
201
|
+
}
|
|
162
202
|
}
|
|
163
203
|
const merged = {
|
|
164
204
|
...existing,
|
|
@@ -167,6 +207,7 @@ export async function updateProject(slug, input) {
|
|
|
167
207
|
...(input.icon != null && {icon: input.icon}),
|
|
168
208
|
...(input.rootUrl != null && {rootUrl: input.rootUrl}),
|
|
169
209
|
...(Number.isFinite(input.sortOrder) && {sortOrder: input.sortOrder}),
|
|
210
|
+
...(typeof input.enabled === 'boolean' && {enabled: input.enabled}),
|
|
170
211
|
slug,
|
|
171
212
|
createdAt: existing.createdAt,
|
|
172
213
|
updatedAt: new Date().toISOString()
|
|
@@ -196,6 +237,9 @@ export async function deleteProject(slug) {
|
|
|
196
237
|
const {entries} = await listEntries(PROJECTS_COLLECTION_SLUG, {limit: 0});
|
|
197
238
|
const entry = entries.find(e => e.data?.slug === slug);
|
|
198
239
|
if (!entry) return false;
|
|
240
|
+
if (entry.data?.protected === true) {
|
|
241
|
+
throw new Error(`Cannot delete the protected "${slug}" project`);
|
|
242
|
+
}
|
|
199
243
|
await deleteEntry(PROJECTS_COLLECTION_SLUG, entry.id);
|
|
200
244
|
return true;
|
|
201
245
|
}
|
|
@@ -229,7 +273,28 @@ export function validateProject(project) {
|
|
|
229
273
|
}
|
|
230
274
|
|
|
231
275
|
function normalise(entry) {
|
|
232
|
-
|
|
276
|
+
const data = entry.data || {};
|
|
277
|
+
// `enabled` is default-on: legacy records (and any record missing the
|
|
278
|
+
// field) are treated as enabled. Only an explicit `false` disables.
|
|
279
|
+
return {...data, enabled: data.enabled !== false};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Whether the given project is enabled (i.e. not switched off). A disabled
|
|
284
|
+
* project is a hard kill-switch on its PUBLIC surfaces — public pages 404 and
|
|
285
|
+
* its external API (collections + custom endpoints + tokens) is refused.
|
|
286
|
+
*
|
|
287
|
+
* The core project is always enabled. An unknown slug resolves to core by
|
|
288
|
+
* fallback elsewhere, so it is reported enabled here (never a block).
|
|
289
|
+
*
|
|
290
|
+
* @param {string} slug
|
|
291
|
+
* @returns {Promise<boolean>}
|
|
292
|
+
*/
|
|
293
|
+
export async function isProjectEnabled(slug) {
|
|
294
|
+
if (!slug || slug === CORE_PROJECT_SLUG) return true;
|
|
295
|
+
const project = await getProject(slug);
|
|
296
|
+
if (!project) return true;
|
|
297
|
+
return project.enabled !== false;
|
|
233
298
|
}
|
|
234
299
|
|
|
235
300
|
/**
|
|
@@ -455,6 +520,10 @@ export async function untagAllForProject(projectSlug) {
|
|
|
455
520
|
if (projectSlug === CORE_PROJECT_SLUG) {
|
|
456
521
|
throw new Error('Cannot untag the core project — artefacts would immediately resolve back to it');
|
|
457
522
|
}
|
|
523
|
+
const proj = await getProject(projectSlug);
|
|
524
|
+
if (proj?.protected === true) {
|
|
525
|
+
throw new Error(`Cannot untag the protected "${projectSlug}" project — its pages would resolve back to core`);
|
|
526
|
+
}
|
|
458
527
|
const counts = {
|
|
459
528
|
pages: 0, collections: 0, forms: 0, actions: 0,
|
|
460
529
|
menus: 0, blocks: 0, components: 0, views: 0, roles: 0, users: 0, apis: 0
|