domma-cms 1.6.39 → 1.6.41

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.
@@ -1,9 +1,9 @@
1
1
  /*!
2
- * Domma Tools CSS v0.51.13
2
+ * Domma Tools CSS v0.51.14
3
3
  * Dynamic Object Manipulation & Modeling API
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-10-13T15:14:47.576Z
6
- * Commit: 85d3945
5
+ * Built: 2026-10-13T15:59:11.655Z
6
+ * Commit: 9c8f7d3
7
7
  */
8
8
 
9
9
  /*!
@@ -1,9 +1,9 @@
1
1
  /*!
2
- * Domma Tools v0.51.13
2
+ * Domma Tools v0.51.14
3
3
  * Developer tools: Theme Roller & Page Roller
4
4
  * (c) 2026 Darryl Waterhouse & DCBW-IT
5
- * Built: 2026-10-13T15:14:31.868Z
6
- * Commit: 85d3945
5
+ * Built: 2026-10-13T15:58:56.767Z
6
+ * Commit: 9c8f7d3
7
7
  *
8
8
  * Requires: domma.min.js
9
9
  */
@@ -1,2 +1,2 @@
1
- export const FILTERS=["all","on","off","attention"],BROWSE_FILTERS=["all","installed","available","updates"],BLOCKING=new Set(["expired","unlicensed"]);const c={grace:{text:"Renew",tone:"warn",title:"The rental term has ended - still running during the grace period"},"support-ended":{text:"Support ended",tone:"muted",title:"Owned outright and still running, but no longer entitled to updates"},expired:{text:"Expired",tone:"danger",title:"The rental term and grace period have ended - this plugin is not running"},unlicensed:{text:"Unlicensed",tone:"danger",title:"No valid entitlement for this site - this plugin is not running"}};export function compareVersions(e,a){const r=o=>String(o??"").split(".").map(s=>parseInt(s,10)||0),t=r(e),n=r(a);for(let o=0;o<Math.max(t.length,n.length);o++){const s=(t[o]??0)-(n[o]??0);if(s)return s>0?1:-1}return 0}export function pluginStatus(e){return e.core?{key:"core",text:"Built in",tone:"info",running:!0}:e.enabled&&BLOCKING.has(e.entitlement)?{key:"blocked",text:"Not running",tone:"danger",running:!1}:e.enabled&&e.supersededBy?{key:"replaced",text:"Replaced",tone:"muted",running:!1}:e.enabled&&e.blockedBy?{key:"blocked",text:`Needs ${e.blockedBy}`,tone:"danger",running:!1}:e.enabled?{key:"on",text:"On",tone:"ok",running:!0}:{key:"off",text:"Off",tone:"muted",running:!1}}export function updateFor(e,a){const r=a?.get?.(e.name);return r&&compareVersions(r,e.version)>0?r:""}export function needsAttention(e,a){return pluginStatus(e).key==="blocked"||e.entitlement==="grace"||!!updateFor(e,a)}export function fmtReleased(e){if(!e||!/^\d{4}-\d{2}-\d{2}/.test(String(e)))return"";const a=new Date(e);if(Number.isNaN(a.getTime()))return"";const r=t=>String(t).padStart(2,"0");return`${r(a.getDate())}.${r(a.getMonth()+1)}.${a.getFullYear()}`}export function arrangePlugins(e,{filter:a="all",query:r="",latest:t=new Map}={}){const n=String(r).trim().toLowerCase(),o={all:()=>!0,on:i=>pluginStatus(i).running,off:i=>!pluginStatus(i).running,attention:i=>needsAttention(i,t)}[a]||(()=>!0),s=i=>needsAttention(i,t)?0:pluginStatus(i).running?1:2;return[...e||[]].filter(o).filter(i=>!n||[i.name,i.displayName,i.description,i.author].some(l=>String(l||"").toLowerCase().includes(n))).sort((i,l)=>s(i)-s(l)||String(i.displayName||i.name).localeCompare(String(l.displayName||l.name)))}export function pluginRow(e,a=new Map){const r=pluginStatus(e),t=c[e.entitlement]||null,n=updateFor(e,a),o=fmtReleased(e.date);return{name:e.name,displayName:e.displayName||e.name,icon:e.icon||"package",meta:[`v${e.version}`,e.author?`by ${e.author}`:"",o?`released ${o}`:"",e.supersededBy?`replaced by ${e.supersededBy}`:"",e.locked?"locked by your manager":""].filter(Boolean).join(" \xB7 "),description:e.description||"",statusText:r.text,statusCls:`is-${r.tone}`,licensed:e.closedSource===!0,entText:t?.text||"",entCls:t?`is-${t.tone}`:"",entTitle:t?.title||"",update:n,updateText:n?`Update to ${n}`:"",configurable:settingsFields(e).length>0,locked:e.locked===!0,rowCls:[r.running?"":"is-off",needsAttention(e,a)?"is-attention":""].filter(Boolean).join(" ")}}export function formatPrice(e){if(e?.tier!=="paid")return"Free";const a=e.terms?.managed?.monthly;return a==null||!Number.isFinite(Number(a))?"Paid":`${e.terms?.currency==="GBP"?"\xA3":`${e.terms?.currency||""} `}${(Number(a)/100).toFixed(2)}/mo`}export function catalogueAction(e){return e.installed&&e.updatable&&e.entitled?e.needsCms?"needs-cms":"update":e.installed?"installed":e.entitled?e.needsCms?"needs-cms":"install":e.buyUrl?"buy":"locked"}export function arrangeCatalogue(e,{filter:a="all",query:r=""}={}){const t=String(r).trim().toLowerCase(),n={all:()=>!0,installed:s=>s.installed,available:s=>catalogueAction(s)==="install",updates:s=>catalogueAction(s)==="update"}[a]||(()=>!0),o={update:0,install:1,buy:2,installed:3,"needs-cms":4,locked:5};return[...e||[]].filter(n).filter(s=>!t||[s.slug,s.displayName,s.description].some(i=>String(i||"").toLowerCase().includes(t))).sort((s,i)=>o[catalogueAction(s)]-o[catalogueAction(i)]||String(s.displayName||s.slug).localeCompare(String(i.displayName||i.slug)))}export function catalogueRow(e){const a=catalogueAction(e),r=e.entitled&&e.tier==="paid";return{slug:e.slug,displayName:e.displayName||e.slug,icon:e.icon||"package",meta:[e.installed&&e.installedVersion&&e.installedVersion!==e.version?`v${e.installedVersion} \u2192 v${e.version}`:`v${e.version}`,e.installed?"installed":"",a==="needs-cms"&&e.minCmsVersion?`needs Domma CMS ${String(e.minCmsVersion).replace(/^[^\d]+/,"")} or later`:""].filter(Boolean).join(" \xB7 "),description:e.description||"",price:formatPrice(e),licText:r?e.licenceStatus==="grace"?"Renew":"Licensed":"",licCls:e.licenceStatus==="grace"?"is-warn":"is-ok",action:a,actionText:{update:"Update",install:"Install",installed:"Installed","needs-cms":"Needs newer CMS",buy:e.licenceStatus==="expired"?"Renew\u2026":"Buy\u2026",locked:e.licenceStatus==="expired"?"Licence expired":"Not licensed"}[a],rowCls:a==="locked"||a==="needs-cms"?"is-off":a==="update"?"is-attention":""}}export const isSecretKey=e=>/pass(word)?|secret|token|api[-_]?key|private|credential|uri$|dsn/i.test(String(e));export function labelFor(e){const a=String(e).replace(/[_-]+/g," ").replace(/([a-z0-9])([A-Z])/g,"$1 $2").trim().toLowerCase();return a?a[0].toUpperCase()+a.slice(1):String(e)}export function settingsFields(e){const a=e?.settings||{},r={...e?.effectiveSettings||{},...a};return Array.isArray(e?.settingsSchema)&&e.settingsSchema.length?e.settingsSchema.filter(t=>t&&t.key).map(t=>{const n=t.type==="select"?"select":t.type==="number"?"number":t.type==="boolean"||t.type==="checkbox"?"checkbox":t.type==="textarea"?"textarea":"text",o=r[t.key]??t.default??(n==="checkbox"?!1:"");return{key:t.key,label:t.label||labelFor(t.key),type:n,value:n==="checkbox"?o===!0:String(o??""),options:(t.options||[]).map(s=>typeof s=="object"?{value:String(s.value),label:String(s.label??s.value)}:{value:String(s),label:String(s)}),min:t.min,max:t.max,help:t.help||"",secret:t.secret===!0||isSecretKey(t.key)}}):Object.entries(r).map(([t,n])=>{const o=isSecretKey(t);if(typeof n=="boolean")return{key:t,label:labelFor(t),type:"checkbox",value:n,secret:o};if(typeof n=="number")return{key:t,label:labelFor(t),type:"number",value:String(n),secret:o};if(n!==null&&typeof n=="object")return{key:t,label:labelFor(t),type:"json",value:JSON.stringify(n,null,2),secret:o};const s=String(n??"");return{key:t,label:labelFor(t),type:s.includes(`
2
- `)||s.length>80?"textarea":"text",value:s,secret:o}})}export function readSettings(e,a){const r={},t={};for(const n of e){const o=a[n.key];if(n.type==="checkbox"){!!o!=!!n.value&&(t[n.key]=!!o);continue}const s=String(o??"");if(s!==String(n.value))if(n.type==="number"){const i=Number(s);if(s.trim()===""||!Number.isFinite(i)){r[n.key]="A number, please.";continue}if(n.min!=null&&i<n.min){r[n.key]=`At least ${n.min}.`;continue}if(n.max!=null&&i>n.max){r[n.key]=`At most ${n.max}.`;continue}t[n.key]=i}else if(n.type==="json")try{t[n.key]=JSON.parse(s)}catch{r[n.key]="Not valid JSON."}else t[n.key]=s}return{errors:r,changed:t}}export function slugifyPlugin(e){return String(e??"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^[^a-z]+/,"").replace(/-+$/g,"").slice(0,40).replace(/-+$/,"")}export function checkScaffold(e,a=[]){const r=s=>String(e?.[s]??"").trim(),t={displayName:r("displayName"),slug:r("slug"),description:r("description"),author:r("author"),icon:r("icon")||"box"},n={},o=/[<>"\\]|\*\//;t.displayName?t.displayName.length>60&&(n.displayName="60 characters at most."):n.displayName="A plugin needs a name.",/^[a-z][a-z0-9-]{1,39}$/.test(t.slug)?a.includes(t.slug)&&(n.slug=`"${t.slug}" is already installed.`):n.slug="2-40 characters: lowercase letters, digits and hyphens, starting with a letter.",t.description.length>300&&(n.description="300 characters at most."),t.author.length>80&&(n.author="80 characters at most."),/^[a-z][a-z0-9-]{0,39}$/.test(t.icon)||(n.icon="A lowercase icon name (letters, digits, hyphens).");for(const s of["displayName","description","author"])!n[s]&&o.test(t[s])&&(n[s]='No <, >, ", backslash or */.');return{errors:n,payload:t}}export function planLines(e){const a=[];for(const r of e?.cascaded||[])a.push(`${r.displayName||r.name} is switched ${r.enabled?"on":"off"} too - ${r.enabled?`${r.becauseName||r.because} needs it`:`it needs ${r.becauseName||r.because}`}.`);for(const r of e?.losses||[])a.push(`${r.displayName||r.name} stays on without ${r.toolName||r.tool}${r.reason?`, but loses: ${r.reason.replace(/\.$/,"")}`:""}.`);return a}
1
+ export const FILTERS=["all","on","off","attention"],BROWSE_FILTERS=["all","installed","available","updates"],BLOCKING=new Set(["expired","unlicensed"]);const m={grace:{text:"Renew",tone:"warn",title:"The rental term has ended - still running during the grace period"},"support-ended":{text:"Support ended",tone:"muted",title:"Owned outright and still running, but no longer entitled to updates"},expired:{text:"Expired",tone:"danger",title:"The rental term and grace period have ended - this plugin is not running"},unlicensed:{text:"Unlicensed",tone:"danger",title:"No valid entitlement for this site - this plugin is not running"}};export function compareVersions(e,s){const r=o=>String(o??"").split(".").map(a=>parseInt(a,10)||0),t=r(e),n=r(s);for(let o=0;o<Math.max(t.length,n.length);o++){const a=(t[o]??0)-(n[o]??0);if(a)return a>0?1:-1}return 0}export function pluginStatus(e){return e.core?{key:"core",text:"Built in",tone:"info",running:!0}:e.enabled&&BLOCKING.has(e.entitlement)?{key:"blocked",text:"Not running",tone:"danger",running:!1}:e.enabled&&e.supersededBy?{key:"replaced",text:"Replaced",tone:"muted",running:!1}:e.enabled&&e.blockedBy?{key:"blocked",text:`Needs ${e.blockedBy}`,tone:"danger",running:!1}:e.enabled?{key:"on",text:"On",tone:"ok",running:!0}:{key:"off",text:"Off",tone:"muted",running:!1}}export function updateFor(e,s){const r=s?.get?.(e.name);return r&&compareVersions(r,e.version)>0?r:""}export function needsAttention(e,s){return pluginStatus(e).key==="blocked"||e.entitlement==="grace"||!!updateFor(e,s)}export function fmtReleased(e){if(!e||!/^\d{4}-\d{2}-\d{2}/.test(String(e)))return"";const s=new Date(e);if(Number.isNaN(s.getTime()))return"";const r=t=>String(t).padStart(2,"0");return`${r(s.getDate())}.${r(s.getMonth()+1)}.${s.getFullYear()}`}export function arrangePlugins(e,{filter:s="all",query:r="",latest:t=new Map}={}){const n=String(r).trim().toLowerCase(),o={all:()=>!0,on:i=>pluginStatus(i).running,off:i=>!pluginStatus(i).running,attention:i=>needsAttention(i,t)}[s]||(()=>!0),a=i=>needsAttention(i,t)?0:pluginStatus(i).running?1:2;return[...e||[]].filter(o).filter(i=>!n||[i.name,i.displayName,i.description,i.author].some(c=>String(c||"").toLowerCase().includes(n))).sort((i,c)=>a(i)-a(c)||String(i.displayName||i.name).localeCompare(String(c.displayName||c.name)))}export function pluginRow(e,s=new Map){const r=pluginStatus(e),t=m[e.entitlement]||null,n=updateFor(e,s),o=fmtReleased(e.date);return{name:e.name,displayName:e.displayName||e.name,icon:e.icon||"package",meta:[`v${e.version}`,e.author?`by ${e.author}`:"",o?`released ${o}`:"",e.supersededBy?`replaced by ${e.supersededBy}`:"",e.locked?"locked by your manager":""].filter(Boolean).join(" \xB7 "),description:e.description||"",statusText:r.text,statusCls:`is-${r.tone}`,licensed:e.closedSource===!0,entText:t?.text||"",entCls:t?`is-${t.tone}`:"",entTitle:t?.title||"",update:n,updateText:n?`Update to ${n}`:"",configurable:settingsFields(e).length>0,locked:e.locked===!0,rowCls:[r.running?"":"is-off",needsAttention(e,s)?"is-attention":""].filter(Boolean).join(" ")}}export function formatPrice(e){if(e?.tier!=="paid")return"Free";const s=e.terms?.managed?.monthly;return s==null||!Number.isFinite(Number(s))?"Paid":`${e.terms?.currency==="GBP"?"\xA3":`${e.terms?.currency||""} `}${(Number(s)/100).toFixed(2)}/mo`}export function catalogueAction(e){return e.installed&&e.updatable&&e.entitled?e.needsCms?"needs-cms":"update":e.installed?"installed":e.entitled?e.needsCms?"needs-cms":"install":e.buyUrl?"buy":"locked"}export function arrangeCatalogue(e,{filter:s="all",query:r=""}={}){const t=String(r).trim().toLowerCase(),n={all:()=>!0,installed:a=>a.installed,available:a=>catalogueAction(a)==="install",updates:a=>catalogueAction(a)==="update"}[s]||(()=>!0),o={update:0,install:1,buy:2,installed:3,"needs-cms":4,locked:5};return[...e||[]].filter(n).filter(a=>!t||[a.slug,a.displayName,a.description].some(i=>String(i||"").toLowerCase().includes(t))).sort((a,i)=>o[catalogueAction(a)]-o[catalogueAction(i)]||String(a.displayName||a.slug).localeCompare(String(i.displayName||i.slug)))}export function catalogueRow(e){const s=catalogueAction(e),r=e.entitled&&e.tier==="paid";return{slug:e.slug,displayName:e.displayName||e.slug,icon:e.icon||"package",meta:[e.installed&&e.installedVersion&&e.installedVersion!==e.version?`v${e.installedVersion} \u2192 v${e.version}`:`v${e.version}`,e.installed?"installed":"",s==="needs-cms"&&e.minCmsVersion?`needs Domma CMS ${String(e.minCmsVersion).replace(/^[^\d]+/,"")} or later`:""].filter(Boolean).join(" \xB7 "),description:e.description||"",price:formatPrice(e),licText:r?e.licenceStatus==="grace"?"Renew":"Licensed":"",licCls:e.licenceStatus==="grace"?"is-warn":"is-ok",action:s,actionText:{update:"Update",install:"Install",installed:"Installed","needs-cms":"Needs newer CMS",buy:e.licenceStatus==="expired"?"Renew\u2026":"Buy\u2026",locked:e.licenceStatus==="expired"?"Licence expired":"Not licensed"}[s],rowCls:s==="locked"||s==="needs-cms"?"is-off":s==="update"?"is-attention":""}}export const isSecretKey=e=>/pass(word)?|secret|token|api[-_]?key|private|credential|uri$|dsn/i.test(String(e));export function labelFor(e){const s=String(e).replace(/[_-]+/g," ").replace(/([a-z0-9])([A-Z])/g,"$1 $2").trim().toLowerCase();return s?s[0].toUpperCase()+s.slice(1):String(e)}export function settingsFields(e){const s=e?.settings||{},r={...e?.effectiveSettings||{},...s};return Array.isArray(e?.settingsSchema)&&e.settingsSchema.length?e.settingsSchema.filter(t=>t&&t.key).map(t=>{const n=t.type==="select"?"select":t.type==="number"?"number":t.type==="boolean"||t.type==="checkbox"?"checkbox":t.type==="textarea"?"textarea":"text",o=r[t.key]??t.default??(n==="checkbox"?!1:"");return{key:t.key,label:t.label||labelFor(t.key),type:n,value:n==="checkbox"?o===!0:String(o??""),options:(t.options||[]).map(a=>typeof a=="object"?{value:String(a.value),label:String(a.label??a.value)}:{value:String(a),label:String(a)}),min:t.min,max:t.max,help:t.help||"",secret:t.secret===!0||isSecretKey(t.key)}}):Object.entries(r).map(([t,n])=>{const o=isSecretKey(t);if(typeof n=="boolean")return{key:t,label:labelFor(t),type:"checkbox",value:n,secret:o};if(typeof n=="number")return{key:t,label:labelFor(t),type:"number",value:String(n),secret:o};if(n!==null&&typeof n=="object")return{key:t,label:labelFor(t),type:"json",value:JSON.stringify(n,null,2),secret:o};const a=String(n??"");return{key:t,label:labelFor(t),type:a.includes(`
2
+ `)||a.length>80?"textarea":"text",value:a,secret:o}})}export function readSettings(e,s){const r={},t={};for(const n of e){const o=s[n.key];if(n.type==="checkbox"){!!o!=!!n.value&&(t[n.key]=!!o);continue}const a=String(o??"");if(a!==String(n.value))if(n.type==="number"){const i=Number(a);if(a.trim()===""||!Number.isFinite(i)){r[n.key]="A number, please.";continue}if(n.min!=null&&i<n.min){r[n.key]=`At least ${n.min}.`;continue}if(n.max!=null&&i>n.max){r[n.key]=`At most ${n.max}.`;continue}t[n.key]=i}else if(n.type==="json")try{t[n.key]=JSON.parse(a)}catch{r[n.key]="Not valid JSON."}else t[n.key]=a}return{errors:r,changed:t}}export function slugifyPlugin(e){return String(e??"").toLowerCase().trim().replace(/[^a-z0-9]+/g,"-").replace(/^[^a-z]+/,"").replace(/-+$/g,"").slice(0,40).replace(/-+$/,"")}export function checkScaffold(e,s=[]){const r=a=>String(e?.[a]??"").trim(),t={displayName:r("displayName"),slug:r("slug"),description:r("description"),author:r("author"),icon:r("icon")||"box"},n={},o=/[<>"\\]|\*\//;t.displayName?t.displayName.length>60&&(n.displayName="60 characters at most."):n.displayName="A plugin needs a name.",/^[a-z][a-z0-9-]{1,39}$/.test(t.slug)?s.includes(t.slug)&&(n.slug=`"${t.slug}" is already installed.`):n.slug="2-40 characters: lowercase letters, digits and hyphens, starting with a letter.",t.description.length>300&&(n.description="300 characters at most."),t.author.length>80&&(n.author="80 characters at most."),/^[a-z][a-z0-9-]{0,39}$/.test(t.icon)||(n.icon="A lowercase icon name (letters, digits, hyphens).");for(const a of["displayName","description","author"])!n[a]&&o.test(t[a])&&(n[a]='No <, >, ", backslash or */.');return{errors:n,payload:t}}export function planLines(e){const s=[];for(const r of e?.cascaded||[])s.push(`${r.displayName||r.name} is switched ${r.enabled?"on":"off"} too - ${r.enabled?`${r.becauseName||r.because} needs it`:`it needs ${r.becauseName||r.because}`}.`);for(const r of e?.losses||[])s.push(`${r.displayName||r.name} stays on without ${r.toolName||r.tool}${r.reason?`, but loses: ${r.reason.replace(/\.$/,"")}`:""}.`);return s}export function dependencyRows(e,{plugins:s=[],tools:r=[],catalogue:t=[]}={}){const n={on:["On","is-ok"],off:["Off","is-muted"],missing:["Not installed","is-warn"]},o=l=>{const u=r.find(d=>d.name===l);if(u)return{displayName:u.displayName||l,state:u.enabled===!1?"off":"on"};const p=s.find(d=>d.name===l);return p?{displayName:p.displayName||l,state:pluginStatus(p).running?"on":"off"}:{displayName:t.find(d=>d.slug===l)?.displayName||l,state:"missing"}},a=(l,u="")=>{const{displayName:p,state:g}=o(l),[d,y]=n[g];return{name:l,displayName:p,reason:u,state:g,stateText:d,stateCls:y}},i=Array.isArray(e?.requires)?e.requires.filter(l=>typeof l=="string"&&l):[],c=e?.uses,f=Array.isArray(c)?c.filter(l=>typeof l=="string").map(l=>[l,""]):Object.entries(c&&typeof c=="object"?c:{}).map(([l,u])=>[l,typeof u=="string"?u:""]);return{needs:i.map(l=>a(l)),worksWith:f.map(([l,u])=>a(l,u))}}export function dependencyText(e){return(e||[]).map(s=>`${s.displayName} (${s.stateText.toLowerCase()})`).join(", ")}
@@ -94,6 +94,15 @@
94
94
  next restart, with its data.</li>
95
95
  </ul>
96
96
 
97
+ <h3>Dependencies</h3>
98
+ <p>Open a plugin on the Installed tab to see its dependencies. <strong>Needs</strong> lists the Tools
99
+ it cannot run without, each with its state here - On, Off or Not installed. Switching the
100
+ plugin on switches those on too (you are shown the list and asked first). When your site's
101
+ manager grants you a Pro Tool, what it needs is switched on with it; a paid Tool it needs that
102
+ your site has no licence for is not added for you - the manager is told, and the Pro Tool
103
+ stays off until it is. <strong>Works better with</strong> lists optional Tools and what each
104
+ adds; nothing there is switched on for you.</p>
105
+
97
106
  <h3>Plugin settings</h3>
98
107
  <p>Open a plugin and choose Configure. Settings are merged with the plugin's defaults and saved to
99
108
  <code>config/plugins.json</code>. Many plugins also have a settings cog in their own screen's
@@ -100,6 +100,9 @@
100
100
  .plx-facts { display: grid; grid-template-columns: max-content 1fr; gap: .3rem .9rem; margin: 0; font-size: var(--dm-text-sm); }
101
101
  .plx-facts dt { color: var(--dm-text-muted); }
102
102
  .plx-facts dd { margin: 0; }
103
+ .plx-deps { list-style: none; margin: .25rem 0 0; padding: 0; display: grid; gap: .35rem; font-size: var(--dm-text-sm); }
104
+ .plx-dep { display: flex; flex-wrap: wrap; align-items: center; gap: .2rem .5rem; }
105
+ .plx-dep .plx-hint { flex-basis: 100%; margin-top: 0; }
103
106
  .plx-desc { margin: 0; }
104
107
  .plx-sub { margin: .25rem 0 0; font-size: var(--dm-text-sm); font-weight: 600; }
105
108
  .plx-err { display: block; color: var(--dm-danger); font-size: var(--dm-text-xs); margin-top: .25rem; }
@@ -1,4 +1,4 @@
1
- import{api as k,getUser as Ze}from"../api.js";import{addBannerAction as Ae}from"../lib/plugin-chrome.js";import{SHAPES as et,whileLoading as tt}from"../lib/loading.js";import{helpPopovers as Oe}from"../lib/help-popover.js";import{onceClosed as at,openMenuAt as Ue,sidebarCount as nt,viewLifetime as st}from"../lib/tool-kit.js";import{pluginAccent as Le}from"../lib/plugin-accent.js";import{reportRestart as lt}from"../lib/server-restart.js";import{FILTERS as it,arrangeCatalogue as ot,arrangePlugins as rt,catalogueAction as ee,catalogueRow as ct,checkScaffold as dt,formatPrice as Re,fmtReleased as ut,needsAttention as pt,planLines as mt,pluginRow as bt,pluginStatus as je,readSettings as ft,settingsFields as Be,slugifyPlugin as vt,updateFor as De}from"../lib/plugins-arrange.js";const Fe=/^#\/plugins\/?(\?.*)?$/,ht="#/plugins",de="plugins.filter",te="plugins.draft",gt=`
1
+ import{api as x,getUser as et}from"../api.js";import{addBannerAction as Ae}from"../lib/plugin-chrome.js";import{SHAPES as tt,whileLoading as at}from"../lib/loading.js";import{helpPopovers as Le}from"../lib/help-popover.js";import{onceClosed as nt,openMenuAt as Ue,sidebarCount as st,viewLifetime as lt}from"../lib/tool-kit.js";import{pluginAccent as Re}from"../lib/plugin-accent.js";import{reportRestart as it}from"../lib/server-restart.js";import{FILTERS as ot,arrangeCatalogue as rt,arrangePlugins as dt,catalogueAction as te,catalogueRow as ct,checkScaffold as ut,dependencyRows as pt,formatPrice as je,fmtReleased as mt,needsAttention as bt,planLines as ht,pluginRow as ft,pluginStatus as Be,readSettings as vt,settingsFields as De,slugifyPlugin as gt,updateFor as Fe}from"../lib/plugins-arrange.js";const qe=/^#\/plugins\/?(\?.*)?$/,yt="#/plugins",ue="plugins.filter",ae="plugins.draft",wt=`
2
2
  <div class="plx-row" data-name="{{name}}" tabindex="0" data-bind-class="rowCls" style="--plugin-accent: {{accent}}">
3
3
  <span class="plx-icon" aria-hidden="true"><span data-icon="{{icon}}" data-icon-size="17"></span></span>
4
4
  <span class="plx-main"><strong>{{displayName}}</strong><small data-bind-text="meta"></small></span>
@@ -10,7 +10,7 @@ import{api as k,getUser as Ze}from"../api.js";import{addBannerAction as Ae}from"
10
10
  <span class="plx-chip" data-bind-class="statusCls" data-bind-text="statusText"></span>
11
11
  </span>
12
12
  <button type="button" class="plx-kebab" aria-label="Actions for {{displayName}}" title="Actions">\u22EE</button>
13
- </div>`,yt=`
13
+ </div>`,xt=`
14
14
  <div class="plx-row" data-tool="{{name}}" tabindex="0" data-bind-class="rowCls">
15
15
  <span class="plx-icon" aria-hidden="true"><span data-icon="{{icon}}" data-icon-size="17"></span></span>
16
16
  <span class="plx-main"><strong>{{displayName}}</strong><small data-bind-text="meta"></small></span>
@@ -19,7 +19,7 @@ import{api as k,getUser as Ze}from"../api.js";import{addBannerAction as Ae}from"
19
19
  <span class="plx-chip" data-bind-class="statusCls" data-bind-text="statusText"></span>
20
20
  </span>
21
21
  <button type="button" class="plx-kebab" aria-label="Actions for {{displayName}}" title="Actions">\u22EE</button>
22
- </div>`,wt=`
22
+ </div>`,kt=`
23
23
  <div class="plx-row" data-slug="{{slug}}" tabindex="0" data-bind-class="rowCls" style="--plugin-accent: {{accent}}">
24
24
  <span class="plx-icon" aria-hidden="true"><span data-icon="{{icon}}" data-icon-size="17"></span></span>
25
25
  <span class="plx-main"><strong>{{displayName}}</strong><small>{{meta}}</small></span>
@@ -29,20 +29,35 @@ import{api as k,getUser as Ze}from"../api.js";import{addBannerAction as Ae}from"
29
29
  </span>
30
30
  <button type="button" class="btn btn-sm plx-act" data-bind-class="actCls" data-bind-disabled="actDisabled" data-bind-text="actionText"></button>
31
31
  <button type="button" class="plx-kebab" aria-label="Actions for {{displayName}}" title="Actions">\u22EE</button>
32
- </div>`,r=O=>String(O??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function ue(O){const b=document.createElement("template");return b.innerHTML=O.trim(),b.content.firstElementChild}const g=st(Fe);export const pluginsView={templateUrl:"/admin/js/templates/plugins.html",async onMount(O){g.reset();const b=O.get(0).querySelector(".plx")||O.get(0),$=()=>Fe.test(location.hash)&&b.isConnected,L=M.observable([]),q=M.observable([]),pe=M.computed(()=>q.value.map(e=>{const t=[...(e.requiredBy||[]).map(a=>a.displayName),...(e.usedBy||[]).map(a=>a.displayName)];return{name:e.name,displayName:e.displayName,icon:e.icon,meta:[e.description,t.length?`used by ${t.join(", ")}`:"",e.locked?"locked by your manager":""].filter(Boolean).join(" \xB7 "),locked:e.locked===!0,statusText:e.enabled?"On":"Off",statusCls:e.enabled?"is-ok":"is-muted",rowCls:e.enabled?"":"is-off"}})),f=M.observable({available:null,site:null,items:[]}),me=M.observable(!0),ae=M.observable(""),m=M.observable("");let be="all";try{be=it.includes(S.get(de))?S.get(de):"all"}catch{}const T=M.observable(be),H=M.observable(""),R=M.observable("all"),fe=M.observable("");g.add(M.effect(()=>{const e=T.value;try{S.set(de,e)}catch{}}));const V=M.computed(()=>new Map((f.value.items||[]).map(e=>[e.slug,e.version]))),P=e=>(f.peek().items||[]).find(t=>t.slug===e),ne=M.observable([]);g.add(M.effect(()=>{const e=V.value;ne.value=rt(L.value,{filter:T.value,query:H.value,latest:e}).map(t=>({...bt(t,e),accent:Le(t.name)}))}));const qe=M.computed(()=>{const e=L.value,t=V.value,a=e.filter(n=>je(n).running).length;return{all:e.length,on:a,off:e.length-a,attention:e.filter(n=>pt(n,t)).length}}),Y=M.computed(()=>(f.value.items||[]).filter(e=>ee(e)==="update").length),Pe=M.computed(()=>L.value.length?"No plugins match.":"No plugins installed. Browse the catalogue, or Install from file in the banner."),se=M.observable([]);g.add(M.effect(()=>{se.value=ot(f.value.items,{filter:R.value,query:fe.value}).map(e=>{const t=ct(e),a=t.action==="update"||t.action==="install"||t.action==="buy";return{...t,accent:Le(e.slug),actCls:a?"btn-primary":"btn-ghost",actDisabled:!a}})}));const ze=M.computed(()=>{const e=f.value.items||[];return{all:e.length,installed:e.filter(t=>t.installed).length,available:e.filter(t=>ee(t)==="install").length,updates:Y.value}}),J=e=>L.peek().find(t=>t.name===e);async function le(){try{const[e,t]=await tt(b.querySelector(".dm-loading-slot"),Promise.all([k.plugins.list(),k.tools.list().catch(()=>[])]),et.iconRows);if(!$())return;L.value=Array.isArray(e)?e:[],q.value=Array.isArray(t)?t:[],ae.value=""}catch(e){ae.value=`Could not load the plugins: ${e.message||e}`}finally{me.value=!1}}async function ve(){try{const e=await k.marketplace.catalogue();if(!$())return;f.value={available:!!e?.available,site:e?.site||null,items:e?.catalogue||[]}}catch{f.value={available:!1,site:null,items:[],failed:!0}}}const z=async()=>{await Promise.all([le(),ve()]),nt(ht)};async function j(e,t,a="plugin"){if(e.core)return;const n=t?"on":"off";if(e.locked){E.toast(`${e.displayName||e.name} is locked by your site's manager - ask them to switch it ${n}.`,{type:"info",duration:6e3});return}if(!await E.confirm(`Switch ${r(e.displayName||e.name)} ${n}? The server restarts to apply it, which takes a few seconds.`)||m.peek())return;const l=i=>a==="tool"?k.tools.update(e.name,{enabled:t,cascade:i}):k.plugins.update(e.name,{enabled:t,cascade:i});m.value=`Switching ${e.displayName||e.name} ${n}\u2026`;try{let i;try{i=await l(!1)}catch(c){if(c.status!==409||!c.body?.needsConfirm)throw c;const u=mt(c.body.plan);if(m.value="",!await E.confirm(`<p>Switching ${r(e.displayName||e.name)} ${n} also means:</p><ul>${u.map(o=>`<li>${r(o)}</li>`).join("")}</ul><p>Go ahead?</p>`))return;m.value=`Switching ${e.displayName||e.name} ${n}\u2026`,i=await l(!0)}await lt(i,{onStatus:c=>{m.value=c}}),E.toast(`${e.displayName||e.name} is ${n}.${t?" Reload the admin to open its screens.":""}`,{type:"success",duration:6e3});try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}catch(i){E.toast(`Not switched ${n}: ${i.message||i}`,{type:"error"})}finally{m.value="",$()&&await z()}}async function B(e,{update:t=!1}={}){const a=r(e.displayName||e.slug),n=t?`<p>Update ${a} from ${r(e.installedVersion)} to ${r(e.version)}?</p> <p class="text-muted">The plugin keeps its data, and the current version is put back if the update fails.</p>`:`<p>Install ${a} v${r(e.version)}?</p> <p class="text-muted">It arrives licensed to this site and takes effect after a restart.</p>`;if(await E.confirm(n,{title:t?"Update plugin":"Install plugin",confirmText:t?"Update":"Install",cancelText:"Cancel",html:!0})){try{const l=await k.marketplace.install(e.slug,e.version,{update:t}),i=l?.action==="upgrade"?`${e.slug} updated${l.from?` from ${l.from}`:""} to ${l.version}.`:`${e.slug} ${l?.version??e.version} installed.`;E.toast(`${i} Restart the server to activate it.`,{type:"success",duration:6e3})}catch(l){E.toast(l.message||"Install failed.",{type:"error"})}$()&&await z()}}function he(e){e?.buyUrl&&(window.open(e.buyUrl,"_blank","noopener"),E.toast(`Manage my site opened in a new tab. We email you a sign-in link; ${e.displayName||e.slug} is ticked ready once you are in. Install it here when it is paid for.`,{type:"info",duration:9e3}))}async function ge(e,t){const n=P(e)?.tier==="paid"?` <p class="text-muted">It is a paid Tool: your site's manager is told, and if you pay for it on its own it comes off your monthly bill (the rest of this month is credited).</p>`:"";if(await E.confirm(`<p>Uninstall ${r(t||e)}?</p> <p class="text-muted">Its files are removed, and its <code>data/</code> folder with them. Roles it added are removed and their users moved to <code>user</code>. Restart the server to finish.</p>${n}`,{title:"Uninstall plugin",confirmText:"Uninstall",cancelText:"Cancel",html:!0})){try{const i=(await k.marketplace.uninstall(e))?.release?.removed?" It is off your bill.":"";E.toast(`${t||e} uninstalled.${i} Restart the server to apply.`,{type:"success",duration:6e3})}catch(l){E.toast(`Uninstall failed: ${l.message||l}`,{type:"error"})}$()&&await z()}}async function ye(e){try{await k.plugins.exportBundle(e.name)}catch(t){E.toast(t.message||"Export failed.",{type:"error"})}}const we=e=>{location.hash=`#/plugins/code/${encodeURIComponent(e.name)}`},K=b.querySelector(".plx-file");async function Ke(e){if(m.peek())return;m.value=`Installing ${e.name}\u2026`;const t={};try{for(let a=0;a<4;a++)try{const n=await k.plugins.installFile(e,t),l=n.action==="upgrade"?`${n.name} updated${n.from?` from ${n.from}`:""} to ${n.version}.`:`${n.name} ${n.version} installed.`;E.toast(`${l} Restart the server to activate it.`,{type:"success",duration:6e3});return}catch(n){if(m.value="",n.code==="CONFLICT"&&!t.confirmUpgrade){if(!await E.confirm(`<p>${r(n.name)} ${r(n.installedVersion||"the installed version")} is already installed. Update it to ${r(n.incomingVersion||"this file")}?</p> <p class="text-muted">The plugin keeps its data, and the current version is put back if the update fails.</p>`,{title:"Update plugin",confirmText:"Update",cancelText:"Cancel",html:!0}))return;t.confirmUpgrade=!0}else if(/refusing to downgrade/i.test(n.message)&&!t.confirmDowngrade){if(!await E.confirm(`<p>${r(n.message)} Install it anyway?</p> <p class="text-muted">An older version may not understand data written by the newer one.</p>`,{title:"Downgrade plugin",confirmText:"Downgrade",cancelText:"Cancel",html:!0}))return;t.confirmDowngrade=!0}else if(/unsigned|signature/i.test(n.message)&&!t.confirmUnsigned){if(!await E.confirm(`<p>${r(n.message)}</p> <p class="text-muted">An unsigned plugin has not been verified as coming from the marketplace. Its code runs on this server with full access. Install it anyway?</p>`,{title:"Unsigned plugin",confirmText:"Install anyway",cancelText:"Cancel",html:!0}))return;t.confirmUnsigned=!0}else{E.toast(n.message||"Install failed.",{type:"error"});return}m.value=`Installing ${e.name}\u2026`}}finally{m.value="",$()&&await z()}}const xe=async()=>{const e=K.files?.[0];K.value="",e&&await Ke(e)};K.addEventListener("change",xe),g.add(()=>K.removeEventListener("change",xe));const Q=new Set;let U=null,ke=!1;function $e({title:e,size:t="md",panel:a,model:n,methods:l,dirty:i=()=>!1,form:c=null,cleanup:u=()=>{}}){const o=E.slideover({title:r(e),size:t,position:"right"});o.element.appendChild(a);const C=Oe(a),w=M.applyBindings(n,a,{methods:l});c&&(U=c),Q.add(o),at(o,()=>{u(),w.dispose(),C(),Q.delete(o),U===c&&(U=null)});const x=o.close.bind(o);let v=!1,N=!1;return o.close=async D=>{if(!v&&D!==!0&&i()){if(N)return;N=!0;const F=await E.confirm("Discard the changes you have not saved?");if(N=!1,!F)return}return v=!0,x()},I.scan(a),o.open(),o}function ie(e,t=null){const a=J(e);if(!a)return;const n=je(a),l=Be(a),i=P(a.name),c=De(a,V.peek()),u=!!(c&&i?.entitled&&!i?.needsCms),C={...Object.fromEntries(l.map(s=>[s.key,s.value])),...Object.fromEntries(Object.entries(t||{}).filter(([s])=>!l.find(p=>p.key===s)?.secret))},w=Object.fromEntries(l.map(s=>[s.key,M.observable(C[s.key])])),x=M.computed(()=>ft(l,Object.fromEntries(l.map(s=>[s.key,w[s.key].value])))),v=M.observable(!1),N=()=>Object.keys(x.peek().changed).length>0||Object.keys(x.peek().errors).length>0,D=[["Version",`v${a.version}${c?` (v${c} available)`:""}`],["Status",n.text+(a.supersededBy?` - replaced by ${a.supersededBy}`:"")],["Author",a.author],["Released",ut(a.date)],["Licence",a.licence],["Slug",a.name]].filter(([,s])=>s),F=(s,p)=>{const A=`plx-set-${p}`,X=s.help?` <span data-help="${r(s.help)}"></span>`:"",W=`<label class="form-label" for="${A}">${r(s.label)}${X}</label>`,_=`data-model="v${p}.value"`,Z=`<small class="plx-err" data-bind-hidden="!err${p}.value" data-bind-text="err${p}.value"></small>`;if(s.type==="checkbox")return`<div class="plx-setting">${W}<input id="${A}" type="checkbox" class="form-switch" ${_}></div>`;if(s.type==="select")return`<div>${W}<select id="${A}" class="form-input" ${_}>${s.options.map(Ie=>`<option value="${r(Ie.value)}">${r(Ie.label)}</option>`).join("")}</select>${Z}</div>`;if(s.type==="json")return`<div>${W}<textarea id="${A}" class="form-input plx-json" rows="5" spellcheck="false" ${_}></textarea>${Z}</div>`;if(s.type==="textarea")return`<div>${W}<textarea id="${A}" class="form-input" rows="3" ${_}></textarea>${Z}</div>`;const Ge=s.type==="number"?"number":s.secret?"password":"text",Xe=`${s.min!=null?` min="${r(s.min)}"`:""}${s.max!=null?` max="${r(s.max)}"`:""}`;return`<div>${W}<input id="${A}" type="${Ge}" class="form-input" autocomplete="${s.secret?"new-password":"off"}"${Xe} ${_}>${Z}</div>`},d={saving:v,hasFields:l.length>0,cannot:M.computed(()=>v.value||!Object.keys(x.value.changed).length||Object.keys(x.value.errors).length>0),...Object.fromEntries(l.flatMap((s,p)=>[[`v${p}`,w[s.key]],[`err${p}`,M.computed(()=>x.value.errors[s.key]||"")]]))},h=ue(`
32
+ </div>`,$t=`
33
+ <li class="plx-dep">
34
+ <strong data-bind-text="displayName"></strong>
35
+ <span class="plx-chip" data-bind-class="stateCls" data-bind-text="stateText"></span>
36
+ <small class="plx-hint" data-bind-hidden="!reason" data-bind-text="reason"></small>
37
+ </li>`,d=A=>String(A??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function pe(A){const b=document.createElement("template");return b.innerHTML=A.trim(),b.content.firstElementChild}const v=lt(qe);export const pluginsView={templateUrl:"/admin/js/templates/plugins.html",async onMount(A){v.reset();const b=A.get(0).querySelector(".plx")||A.get(0),k=()=>qe.test(location.hash)&&b.isConnected,L=M.observable([]),R=M.observable([]),me=M.computed(()=>R.value.map(e=>{const t=[...(e.requiredBy||[]).map(a=>a.displayName),...(e.usedBy||[]).map(a=>a.displayName)];return{name:e.name,displayName:e.displayName,icon:e.icon,meta:[e.description,t.length?`used by ${t.join(", ")}`:"",e.locked?"locked by your manager":""].filter(Boolean).join(" \xB7 "),locked:e.locked===!0,statusText:e.enabled?"On":"Off",statusCls:e.enabled?"is-ok":"is-muted",rowCls:e.enabled?"":"is-off"}})),h=M.observable({available:null,site:null,items:[]}),be=M.observable(!0),ne=M.observable(""),m=M.observable("");let he="all";try{he=ot.includes(S.get(ue))?S.get(ue):"all"}catch{}const T=M.observable(he),V=M.observable(""),j=M.observable("all"),fe=M.observable("");v.add(M.effect(()=>{const e=T.value;try{S.set(ue,e)}catch{}}));const Y=M.computed(()=>new Map((h.value.items||[]).map(e=>[e.slug,e.version]))),W=e=>(h.peek().items||[]).find(t=>t.slug===e),se=M.observable([]);v.add(M.effect(()=>{const e=Y.value;se.value=dt(L.value,{filter:T.value,query:V.value,latest:e}).map(t=>({...ft(t,e),accent:Re(t.name)}))}));const We=M.computed(()=>{const e=L.value,t=Y.value,a=e.filter(n=>Be(n).running).length;return{all:e.length,on:a,off:e.length-a,attention:e.filter(n=>bt(n,t)).length}}),J=M.computed(()=>(h.value.items||[]).filter(e=>te(e)==="update").length),Pe=M.computed(()=>L.value.length?"No plugins match.":"No plugins installed. Browse the catalogue, or Install from file in the banner."),le=M.observable([]);v.add(M.effect(()=>{le.value=rt(h.value.items,{filter:j.value,query:fe.value}).map(e=>{const t=ct(e),a=t.action==="update"||t.action==="install"||t.action==="buy";return{...t,accent:Re(e.slug),actCls:a?"btn-primary":"btn-ghost",actDisabled:!a}})}));const ze=M.computed(()=>{const e=h.value.items||[];return{all:e.length,installed:e.filter(t=>t.installed).length,available:e.filter(t=>te(t)==="install").length,updates:J.value}}),Q=e=>L.peek().find(t=>t.name===e);async function ie(){try{const[e,t]=await at(b.querySelector(".dm-loading-slot"),Promise.all([x.plugins.list(),x.tools.list().catch(()=>[])]),tt.iconRows);if(!k())return;L.value=Array.isArray(e)?e:[],R.value=Array.isArray(t)?t:[],ne.value=""}catch(e){ne.value=`Could not load the plugins: ${e.message||e}`}finally{be.value=!1}}async function ve(){try{const e=await x.marketplace.catalogue();if(!k())return;h.value={available:!!e?.available,site:e?.site||null,items:e?.catalogue||[]}}catch{h.value={available:!1,site:null,items:[],failed:!0}}}const P=async()=>{await Promise.all([ie(),ve()]),st(yt)};async function B(e,t,a="plugin"){if(e.core)return;const n=t?"on":"off";if(e.locked){E.toast(`${e.displayName||e.name} is locked by your site's manager - ask them to switch it ${n}.`,{type:"info",duration:6e3});return}if(!await E.confirm(`Switch ${d(e.displayName||e.name)} ${n}? The server restarts to apply it, which takes a few seconds.`)||m.peek())return;const l=i=>a==="tool"?x.tools.update(e.name,{enabled:t,cascade:i}):x.plugins.update(e.name,{enabled:t,cascade:i});m.value=`Switching ${e.displayName||e.name} ${n}\u2026`;try{let i;try{i=await l(!1)}catch(c){if(c.status!==409||!c.body?.needsConfirm)throw c;const u=ht(c.body.plan);if(m.value="",!await E.confirm(`<p>Switching ${d(e.displayName||e.name)} ${n} also means:</p><ul>${u.map(o=>`<li>${d(o)}</li>`).join("")}</ul><p>Go ahead?</p>`))return;m.value=`Switching ${e.displayName||e.name} ${n}\u2026`,i=await l(!0)}await it(i,{onStatus:c=>{m.value=c}}),E.toast(`${e.displayName||e.name} is ${n}.${t?" Reload the admin to open its screens.":""}`,{type:"success",duration:6e3});try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}catch(i){E.toast(`Not switched ${n}: ${i.message||i}`,{type:"error"})}finally{m.value="",k()&&await P()}}async function D(e,{update:t=!1}={}){const a=d(e.displayName||e.slug),n=t?`<p>Update ${a} from ${d(e.installedVersion)} to ${d(e.version)}?</p> <p class="text-muted">The plugin keeps its data, and the current version is put back if the update fails.</p>`:`<p>Install ${a} v${d(e.version)}?</p> <p class="text-muted">It arrives licensed to this site and takes effect after a restart.</p>`;if(await E.confirm(n,{title:t?"Update plugin":"Install plugin",confirmText:t?"Update":"Install",cancelText:"Cancel",html:!0})){try{const l=await x.marketplace.install(e.slug,e.version,{update:t}),i=l?.action==="upgrade"?`${e.slug} updated${l.from?` from ${l.from}`:""} to ${l.version}.`:`${e.slug} ${l?.version??e.version} installed.`;E.toast(`${i} Restart the server to activate it.`,{type:"success",duration:6e3})}catch(l){E.toast(l.message||"Install failed.",{type:"error"})}k()&&await P()}}function ge(e){e?.buyUrl&&(window.open(e.buyUrl,"_blank","noopener"),E.toast(`Manage my site opened in a new tab. We email you a sign-in link; ${e.displayName||e.slug} is ticked ready once you are in. Install it here when it is paid for.`,{type:"info",duration:9e3}))}async function ye(e,t){const n=W(e)?.tier==="paid"?` <p class="text-muted">It is a paid Tool: your site's manager is told, and if you pay for it on its own it comes off your monthly bill (the rest of this month is credited).</p>`:"";if(await E.confirm(`<p>Uninstall ${d(t||e)}?</p> <p class="text-muted">Its files are removed, and its <code>data/</code> folder with them. Roles it added are removed and their users moved to <code>user</code>. Restart the server to finish.</p>${n}`,{title:"Uninstall plugin",confirmText:"Uninstall",cancelText:"Cancel",html:!0})){try{const i=(await x.marketplace.uninstall(e))?.release?.removed?" It is off your bill.":"";E.toast(`${t||e} uninstalled.${i} Restart the server to apply.`,{type:"success",duration:6e3})}catch(l){E.toast(`Uninstall failed: ${l.message||l}`,{type:"error"})}k()&&await P()}}async function we(e){try{await x.plugins.exportBundle(e.name)}catch(t){E.toast(t.message||"Export failed.",{type:"error"})}}const xe=e=>{location.hash=`#/plugins/code/${encodeURIComponent(e.name)}`},z=b.querySelector(".plx-file");async function _e(e){if(m.peek())return;m.value=`Installing ${e.name}\u2026`;const t={};try{for(let a=0;a<4;a++)try{const n=await x.plugins.installFile(e,t),l=n.action==="upgrade"?`${n.name} updated${n.from?` from ${n.from}`:""} to ${n.version}.`:`${n.name} ${n.version} installed.`;E.toast(`${l} Restart the server to activate it.`,{type:"success",duration:6e3});return}catch(n){if(m.value="",n.code==="CONFLICT"&&!t.confirmUpgrade){if(!await E.confirm(`<p>${d(n.name)} ${d(n.installedVersion||"the installed version")} is already installed. Update it to ${d(n.incomingVersion||"this file")}?</p> <p class="text-muted">The plugin keeps its data, and the current version is put back if the update fails.</p>`,{title:"Update plugin",confirmText:"Update",cancelText:"Cancel",html:!0}))return;t.confirmUpgrade=!0}else if(/refusing to downgrade/i.test(n.message)&&!t.confirmDowngrade){if(!await E.confirm(`<p>${d(n.message)} Install it anyway?</p> <p class="text-muted">An older version may not understand data written by the newer one.</p>`,{title:"Downgrade plugin",confirmText:"Downgrade",cancelText:"Cancel",html:!0}))return;t.confirmDowngrade=!0}else if(/unsigned|signature/i.test(n.message)&&!t.confirmUnsigned){if(!await E.confirm(`<p>${d(n.message)}</p> <p class="text-muted">An unsigned plugin has not been verified as coming from the marketplace. Its code runs on this server with full access. Install it anyway?</p>`,{title:"Unsigned plugin",confirmText:"Install anyway",cancelText:"Cancel",html:!0}))return;t.confirmUnsigned=!0}else{E.toast(n.message||"Install failed.",{type:"error"});return}m.value=`Installing ${e.name}\u2026`}}finally{m.value="",k()&&await P()}}const ke=async()=>{const e=z.files?.[0];z.value="",e&&await _e(e)};z.addEventListener("change",ke),v.add(()=>z.removeEventListener("change",ke));const G=new Set;let U=null,$e=!1;function Ee({title:e,size:t="md",panel:a,model:n,methods:l,dirty:i=()=>!1,form:c=null,cleanup:u=()=>{}}){const o=E.slideover({title:d(e),size:t,position:"right"});o.element.appendChild(a);const $=Le(a),y=M.applyBindings(n,a,{methods:l});c&&(U=c),G.add(o),nt(o,()=>{u(),y.dispose(),$(),G.delete(o),U===c&&(U=null)});const w=o.close.bind(o);let f=!1,N=!1;return o.close=async F=>{if(!f&&F!==!0&&i()){if(N)return;N=!0;const q=await E.confirm("Discard the changes you have not saved?");if(N=!1,!q)return}return f=!0,w()},I.scan(a),o.open(),o}function oe(e,t=null){const a=Q(e);if(!a)return;const n=Be(a),l=De(a),i=W(a.name),c=Fe(a,Y.peek()),u=!!(c&&i?.entitled&&!i?.needsCms),$={...Object.fromEntries(l.map(s=>[s.key,s.value])),...Object.fromEntries(Object.entries(t||{}).filter(([s])=>!l.find(p=>p.key===s)?.secret))},y=Object.fromEntries(l.map(s=>[s.key,M.observable($[s.key])])),w=M.computed(()=>vt(l,Object.fromEntries(l.map(s=>[s.key,y[s.key].value])))),f=M.observable(!1),N=()=>Object.keys(w.peek().changed).length>0||Object.keys(w.peek().errors).length>0,F=[["Version",`v${a.version}${c?` (v${c} available)`:""}`],["Status",n.text+(a.supersededBy?` - replaced by ${a.supersededBy}`:"")],["Author",a.author],["Released",mt(a.date)],["Licence",a.licence],["Slug",a.name]].filter(([,s])=>s),q=(s,p)=>{const O=`plx-set-${p}`,Z=s.help?` <span data-help="${d(s.help)}"></span>`:"",H=`<label class="form-label" for="${O}">${d(s.label)}${Z}</label>`,K=`data-model="v${p}.value"`,ee=`<small class="plx-err" data-bind-hidden="!err${p}.value" data-bind-text="err${p}.value"></small>`;if(s.type==="checkbox")return`<div class="plx-setting">${H}<input id="${O}" type="checkbox" class="form-switch" ${K}></div>`;if(s.type==="select")return`<div>${H}<select id="${O}" class="form-input" ${K}>${s.options.map(Oe=>`<option value="${d(Oe.value)}">${d(Oe.label)}</option>`).join("")}</select>${ee}</div>`;if(s.type==="json")return`<div>${H}<textarea id="${O}" class="form-input plx-json" rows="5" spellcheck="false" ${K}></textarea>${ee}</div>`;if(s.type==="textarea")return`<div>${H}<textarea id="${O}" class="form-input" rows="3" ${K}></textarea>${ee}</div>`;const Xe=s.type==="number"?"number":s.secret?"password":"text",Ze=`${s.min!=null?` min="${d(s.min)}"`:""}${s.max!=null?` max="${d(s.max)}"`:""}`;return`<div>${H}<input id="${O}" type="${Xe}" class="form-input" autocomplete="${s.secret?"new-password":"off"}"${Ze} ${K}>${ee}</div>`},r=pt(a,{plugins:L.peek(),tools:R.peek(),catalogue:h.peek().items||[]}),C={saving:f,needs:M.observable(r.needs),worksWith:M.observable(r.worksWith),hasNeeds:r.needs.length>0,hasWorksWith:r.worksWith.length>0,hasFields:l.length>0,cannot:M.computed(()=>f.value||!Object.keys(w.value.changed).length||Object.keys(w.value.errors).length>0),...Object.fromEntries(l.flatMap((s,p)=>[[`v${p}`,y[s.key]],[`err${p}`,M.computed(()=>w.value.errors[s.key]||"")]]))},_=pe(`
33
38
  <div class="plx-form">
34
39
  <p class="plx-desc"></p>
35
40
  <dl class="plx-facts"></dl>
41
+ <div data-bind-hidden="!hasNeeds">
42
+ <p class="plx-sub">Needs
43
+ <span data-help-title="Needs" data-help="Tools this one cannot run without. Switching it on switches these on too, and switching one of these off switches this off with it. One that is not installed, or that this site has no licence for, has to be added first - ask your site's manager."></span></p>
44
+ <ul class="plx-deps" data-each="needs.value key=name"></ul>
45
+ </div>
46
+ <div data-bind-hidden="!hasWorksWith">
47
+ <p class="plx-sub">Works better with
48
+ <span data-help-title="Works better with" data-help="Optional. This Tool runs without them, and gains what is named beside each when they are on. Nothing here is switched on for you."></span></p>
49
+ <ul class="plx-deps" data-each="worksWith.value key=name"></ul>
50
+ </div>
36
51
  <div data-if="hasFields">
37
52
  <p class="plx-sub">Settings
38
53
  <span data-help-title="Settings" data-help="Saved to config/plugins.json for this site. Only what you change is stored - anything you leave keeps the plugin's own default, so a later version can improve it. Most plugins read their settings as they run; a few only at start-up."></span></p>
39
- <div class="plx-form">${l.map(F).join("")}</div>
54
+ <div class="plx-form">${l.map(q).join("")}</div>
40
55
  </div>
41
56
  <p class="plx-hint" data-if="!hasFields">This plugin has no settings to change here.</p>
42
57
  <div class="plx-actions">
43
58
  ${a.core?"":`<button type="button" class="btn btn-sm btn-ghost" data-on-click="toggle"><span data-icon="${a.enabled?"pause":"play"}" data-icon-size="13"></span> ${a.enabled?"Switch off":"Switch on"}</button>`}
44
- ${u?`<button type="button" class="btn btn-sm btn-ghost" data-on-click="update"><span data-icon="download" data-icon-size="13"></span> Update to v${r(c)}</button>`:""}
45
- ${c&&i?.needsCms?`<span class="plx-hint">v${r(c)}: ${r(i.needsCms)}</span>`:""}
59
+ ${u?`<button type="button" class="btn btn-sm btn-ghost" data-on-click="update"><span data-icon="download" data-icon-size="13"></span> Update to v${d(c)}</button>`:""}
60
+ ${c&&i?.needsCms?`<span class="plx-hint">v${d(c)}: ${d(i.needsCms)}</span>`:""}
46
61
  ${a.sourceEditable!==!1?'<button type="button" class="btn btn-sm btn-ghost" data-on-click="source"><span data-icon="code" data-icon-size="13"></span> Source</button>':""}
47
62
  ${a.closedSource?"":'<button type="button" class="btn btn-sm btn-ghost" data-on-click="exportIt" title="Download as a .dcmsplugin file"><span data-icon="download" data-icon-size="13"></span> Export</button>'}
48
63
  <span class="plx-spacer"></span>
@@ -50,7 +65,7 @@ import{api as k,getUser as Ze}from"../api.js";import{addBannerAction as Ae}from"
50
65
  <button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-if="hasFields" data-bind-disabled="cannot.value">
51
66
  <span data-icon="check" data-icon-size="13"></span> Save settings</button>
52
67
  </div>
53
- </div>`);h.querySelector(".plx-desc").textContent=a.description||"";const Qe=h.querySelector(".plx-facts");for(const[s,p]of D){const A=document.createElement("dt");A.textContent=s;const X=document.createElement("dd");X.textContent=p,Qe.append(A,X)}async function Ne(){if(d.cannot.peek())return;const{changed:s}=x.peek();v.value=!0;try{await k.plugins.update(a.name,{settings:{...a.settings||{},...s}}),E.toast(`${a.displayName||a.name}: settings saved.`,{type:"success"}),G.close(!0),$()&&await le()}catch(p){E.toast(`Not saved: ${p.message||p}`,{type:"error"})}finally{v.value=!1}}const ce=s=>async()=>{await G.close(),Q.has(G)||s()},G=$e({title:a.displayName||a.name,size:"md",panel:h,model:d,dirty:N,form:l.length?{kind:"settings",name:a.name,dirty:N,values:()=>{const s={};for(const p of l)p.secret||(s[p.key]=w[p.key].peek());return s}}:null,methods:{save:Ne,cancel:()=>G.close(),toggle:ce(()=>j(a,!a.enabled)),update:ce(()=>B(i,{update:!0})),source:ce(()=>we(a)),exportIt:()=>ye(a)}});h.addEventListener("keydown",s=>{(s.ctrlKey||s.metaKey)&&s.key==="Enter"&&(s.preventDefault(),Ne())})}function Ee(e=null){const t=Ze(),a={displayName:M.observable(e?.displayName??""),slug:M.observable(e?.slug??""),description:M.observable(e?.description??""),author:M.observable(e?.author??(t?.name||t?.username||"")),icon:M.observable(e?.icon??"box")},n=JSON.stringify(Object.fromEntries(Object.entries(a).map(([d,h])=>[d,h.peek()]))),l=()=>Object.fromEntries(Object.entries(a).map(([d,h])=>[d,h.peek()])),i=()=>JSON.stringify(l())!==n||!!e;let c=!!e?.slug;const u=M.effect(()=>{const d=a.displayName.value;c||(a.slug.value=vt(d))}),o=M.observable(!1),C=M.computed(()=>dt(Object.fromEntries(Object.entries(a).map(([d,h])=>[d,h.value])),L.value.map(d=>d.name))),w=d=>M.computed(()=>C.value.errors[d]||""),x={...a,saving:o,nameErr:w("displayName"),slugErr:w("slug"),descErr:w("description"),authorErr:w("author"),iconErr:w("icon"),cannot:M.computed(()=>o.value||Object.keys(C.value.errors).length>0)},v=ue(`
68
+ </div>`);_.querySelector(".plx-desc").textContent=a.description||"",_.querySelectorAll(".plx-deps").forEach(s=>{s.innerHTML=$t});const Ge=_.querySelector(".plx-facts");for(const[s,p]of F){const O=document.createElement("dt");O.textContent=s;const Z=document.createElement("dd");Z.textContent=p,Ge.append(O,Z)}async function Ie(){if(C.cannot.peek())return;const{changed:s}=w.peek();f.value=!0;try{await x.plugins.update(a.name,{settings:{...a.settings||{},...s}}),E.toast(`${a.displayName||a.name}: settings saved.`,{type:"success"}),X.close(!0),k()&&await ie()}catch(p){E.toast(`Not saved: ${p.message||p}`,{type:"error"})}finally{f.value=!1}}const ce=s=>async()=>{await X.close(),G.has(X)||s()},X=Ee({title:a.displayName||a.name,size:"md",panel:_,model:C,dirty:N,form:l.length?{kind:"settings",name:a.name,dirty:N,values:()=>{const s={};for(const p of l)p.secret||(s[p.key]=y[p.key].peek());return s}}:null,methods:{save:Ie,cancel:()=>X.close(),toggle:ce(()=>B(a,!a.enabled)),update:ce(()=>D(i,{update:!0})),source:ce(()=>xe(a)),exportIt:()=>we(a)}});_.addEventListener("keydown",s=>{(s.ctrlKey||s.metaKey)&&s.key==="Enter"&&(s.preventDefault(),Ie())})}function Se(e=null){const t=et(),a={displayName:M.observable(e?.displayName??""),slug:M.observable(e?.slug??""),description:M.observable(e?.description??""),author:M.observable(e?.author??(t?.name||t?.username||"")),icon:M.observable(e?.icon??"box")},n=JSON.stringify(Object.fromEntries(Object.entries(a).map(([r,C])=>[r,C.peek()]))),l=()=>Object.fromEntries(Object.entries(a).map(([r,C])=>[r,C.peek()])),i=()=>JSON.stringify(l())!==n||!!e;let c=!!e?.slug;const u=M.effect(()=>{const r=a.displayName.value;c||(a.slug.value=gt(r))}),o=M.observable(!1),$=M.computed(()=>ut(Object.fromEntries(Object.entries(a).map(([r,C])=>[r,C.value])),L.value.map(r=>r.name))),y=r=>M.computed(()=>$.value.errors[r]||""),w={...a,saving:o,nameErr:y("displayName"),slugErr:y("slug"),descErr:y("description"),authorErr:y("author"),iconErr:y("icon"),cannot:M.computed(()=>o.value||Object.keys($.value.errors).length>0)},f=pe(`
54
69
  <div class="plx-form">
55
70
  <div>
56
71
  <label class="form-label" for="plx-np-name">Display name <span class="text-danger">*</span></label>
@@ -86,7 +101,7 @@ import{api as k,getUser as Ze}from"../api.js";import{addBannerAction as Ae}from"
86
101
  <button type="button" class="btn btn-sm btn-primary" data-on-click="create" data-bind-disabled="cannot.value">
87
102
  <span data-icon="plus" data-icon-size="13"></span> Create plugin</button>
88
103
  </div>
89
- </div>`);async function N(){if(x.cannot.peek())return;o.value=!0;const{payload:d}=C.peek();try{await k.plugins.scaffold(d),E.toast(`"${d.displayName}" created. Restart the server to activate it.`,{type:"success",duration:6e3}),D.close(!0),$()&&await z()}catch(h){E.toast(h.message||"Could not create the plugin.",{type:"error"})}finally{o.value=!1}}const D=$e({title:"New plugin",size:"sm",panel:v,model:x,dirty:i,form:{kind:"new",name:null,dirty:i,values:l},cleanup:()=>u(),methods:{create:N,cancel:()=>D.close()}}),F=v.querySelector("#plx-np-slug");F.addEventListener("input",()=>{c=!!F.value}),v.addEventListener("keydown",d=>{(d.ctrlKey||d.metaKey)&&d.key==="Enter"&&(d.preventDefault(),N())}),v.querySelector("#plx-np-name")?.focus()}function We(e){const t=J(e);if(!t)return[];const a=P(t.name),n=De(t,V.peek()),l=!!m.peek();return[{header:t.displayName||t.name},{label:Be(t).length?"Details and settings\u2026":"Details\u2026",icon:"settings",action:()=>ie(t.name)},...t.core?[]:[t.enabled?{label:"Switch off\u2026",icon:"pause",disabled:l,action:()=>j(t,!1)}:{label:"Switch on\u2026",icon:"play",disabled:l,action:()=>j(t,!0)}],...n?[{label:a?.needsCms?`Update to v${n} - needs a newer Domma CMS`:`Update to v${n}\u2026`,icon:"download",disabled:l||!a?.entitled||!!a?.needsCms,action:()=>B(a,{update:!0})}]:[],{type:"divider"},{label:"View source",icon:"code",disabled:t.sourceEditable===!1,action:()=>we(t)},{label:"Export .dcmsplugin",icon:"download",disabled:t.closedSource===!0,action:()=>ye(t)},...t.core?[]:[{type:"divider"},{label:"Uninstall\u2026",icon:"trash",danger:!0,disabled:l,action:()=>ge(t.name,t.displayName)}]]}function _e(e){const t=q.peek().find(n=>n.name===e);if(!t)return[];const a=!!m.peek();return[{header:t.displayName},t.enabled?{label:t.locked?"Switch off (locked)":"Switch off\u2026",icon:"pause",disabled:a||t.locked,action:()=>j(t,!1,"tool")}:{label:t.locked?"Switch on (locked)":"Switch on\u2026",icon:"play",disabled:a||t.locked,action:()=>j(t,!0,"tool")}]}const He=e=>{const t=q.peek().find(a=>a.name===e);t&&j(t,!t.enabled,"tool")};function Ve(e){const t=P(e);if(!t)return[];const a=ee(t),n=!!m.peek();return[{header:t.displayName||t.slug},...a==="install"?[{label:`Install v${t.version}\u2026`,icon:"download",disabled:n,action:()=>B(t)}]:[],...a==="update"?[{label:`Update to v${t.version}\u2026`,icon:"download",disabled:n,action:()=>B(t,{update:!0})}]:[],...t.installed?[{label:"Open in Installed",icon:"package",action:()=>{H.value=t.slug,T.value="all",Ce.activate?.(0)}},{type:"divider"},{label:"Uninstall\u2026",icon:"trash",danger:!0,disabled:n,action:()=>ge(t.slug,t.displayName)}]:[],...a==="buy"?[{label:`${t.licenceStatus==="expired"?"Renew":"Buy"}${Re(t)!=="Paid"?` - ${Re(t)}`:""}\u2026`,icon:"shopping-cart",action:()=>he(t)}]:[],...a==="locked"?[{label:"Not licensed for this site",icon:"lock",disabled:!0,action:()=>{}}]:[],...a==="needs-cms"?[{label:t.needsCms,icon:"alert-triangle",disabled:!0,action:()=>{}}]:[]]}function Ye(e){const t=P(e);if(!t)return;const a=ee(t);a==="install"?B(t):a==="update"?B(t,{update:!0}):a==="buy"?he(t):a==="needs-cms"?E.toast(t.needsCms,{type:"warning",duration:6e3}):t.installed&&(H.value=t.slug,T.value="all",Ce.activate?.(0))}function oe(e,t,a,n){const l=E.contextMenu(e,{cursor:"glyph",match:".plx-row",inherit:!1,density:"compact",items:u=>a(u.dataset[t])});g.add(()=>l?.destroy?.());const i=u=>{const o=u.target.closest(".plx-row");if(!o)return;const C=u.target.closest(".plx-kebab");if(C){Ue(C);return}if(u.target.closest(".plx-act")){u.target.closest("button").disabled||n(o.dataset[t]);return}n(o.dataset[t])},c=u=>{const o=u.target.closest(".plx-row");o&&(u.key==="F10"&&u.shiftKey?(u.preventDefault(),Ue(o)):u.key==="Enter"&&u.target===o&&n(o.dataset[t]))};e.addEventListener("click",i),e.addEventListener("keydown",c),g.add(()=>{e.removeEventListener("click",i),e.removeEventListener("keydown",c)})}const re=b.querySelector(".plx-browse");re.appendChild(ue(`
104
+ </div>`);async function N(){if(w.cannot.peek())return;o.value=!0;const{payload:r}=$.peek();try{await x.plugins.scaffold(r),E.toast(`"${r.displayName}" created. Restart the server to activate it.`,{type:"success",duration:6e3}),F.close(!0),k()&&await P()}catch(C){E.toast(C.message||"Could not create the plugin.",{type:"error"})}finally{o.value=!1}}const F=Ee({title:"New plugin",size:"sm",panel:f,model:w,dirty:i,form:{kind:"new",name:null,dirty:i,values:l},cleanup:()=>u(),methods:{create:N,cancel:()=>F.close()}}),q=f.querySelector("#plx-np-slug");q.addEventListener("input",()=>{c=!!q.value}),f.addEventListener("keydown",r=>{(r.ctrlKey||r.metaKey)&&r.key==="Enter"&&(r.preventDefault(),N())}),f.querySelector("#plx-np-name")?.focus()}function He(e){const t=Q(e);if(!t)return[];const a=W(t.name),n=Fe(t,Y.peek()),l=!!m.peek();return[{header:t.displayName||t.name},{label:De(t).length?"Details and settings\u2026":"Details\u2026",icon:"settings",action:()=>oe(t.name)},...t.core?[]:[t.enabled?{label:"Switch off\u2026",icon:"pause",disabled:l,action:()=>B(t,!1)}:{label:"Switch on\u2026",icon:"play",disabled:l,action:()=>B(t,!0)}],...n?[{label:a?.needsCms?`Update to v${n} - needs a newer Domma CMS`:`Update to v${n}\u2026`,icon:"download",disabled:l||!a?.entitled||!!a?.needsCms,action:()=>D(a,{update:!0})}]:[],{type:"divider"},{label:"View source",icon:"code",disabled:t.sourceEditable===!1,action:()=>xe(t)},{label:"Export .dcmsplugin",icon:"download",disabled:t.closedSource===!0,action:()=>we(t)},...t.core?[]:[{type:"divider"},{label:"Uninstall\u2026",icon:"trash",danger:!0,disabled:l,action:()=>ye(t.name,t.displayName)}]]}function Ke(e){const t=R.peek().find(n=>n.name===e);if(!t)return[];const a=!!m.peek();return[{header:t.displayName},t.enabled?{label:t.locked?"Switch off (locked)":"Switch off\u2026",icon:"pause",disabled:a||t.locked,action:()=>B(t,!1,"tool")}:{label:t.locked?"Switch on (locked)":"Switch on\u2026",icon:"play",disabled:a||t.locked,action:()=>B(t,!0,"tool")}]}const Ve=e=>{const t=R.peek().find(a=>a.name===e);t&&B(t,!t.enabled,"tool")};function Ye(e){const t=W(e);if(!t)return[];const a=te(t),n=!!m.peek();return[{header:t.displayName||t.slug},...a==="install"?[{label:`Install v${t.version}\u2026`,icon:"download",disabled:n,action:()=>D(t)}]:[],...a==="update"?[{label:`Update to v${t.version}\u2026`,icon:"download",disabled:n,action:()=>D(t,{update:!0})}]:[],...t.installed?[{label:"Open in Installed",icon:"package",action:()=>{V.value=t.slug,T.value="all",Te.activate?.(0)}},{type:"divider"},{label:"Uninstall\u2026",icon:"trash",danger:!0,disabled:n,action:()=>ye(t.slug,t.displayName)}]:[],...a==="buy"?[{label:`${t.licenceStatus==="expired"?"Renew":"Buy"}${je(t)!=="Paid"?` - ${je(t)}`:""}\u2026`,icon:"shopping-cart",action:()=>ge(t)}]:[],...a==="locked"?[{label:"Not licensed for this site",icon:"lock",disabled:!0,action:()=>{}}]:[],...a==="needs-cms"?[{label:t.needsCms,icon:"alert-triangle",disabled:!0,action:()=>{}}]:[]]}function Je(e){const t=W(e);if(!t)return;const a=te(t);a==="install"?D(t):a==="update"?D(t,{update:!0}):a==="buy"?ge(t):a==="needs-cms"?E.toast(t.needsCms,{type:"warning",duration:6e3}):t.installed&&(V.value=t.slug,T.value="all",Te.activate?.(0))}function re(e,t,a,n){const l=E.contextMenu(e,{cursor:"glyph",match:".plx-row",inherit:!1,density:"compact",items:u=>a(u.dataset[t])});v.add(()=>l?.destroy?.());const i=u=>{const o=u.target.closest(".plx-row");if(!o)return;const $=u.target.closest(".plx-kebab");if($){Ue($);return}if(u.target.closest(".plx-act")){u.target.closest("button").disabled||n(o.dataset[t]);return}n(o.dataset[t])},c=u=>{const o=u.target.closest(".plx-row");o&&(u.key==="F10"&&u.shiftKey?(u.preventDefault(),Ue(o)):u.key==="Enter"&&u.target===o&&n(o.dataset[t]))};e.addEventListener("click",i),e.addEventListener("keydown",c),v.add(()=>{e.removeEventListener("click",i),e.removeEventListener("keydown",c)})}const de=b.querySelector(".plx-browse");de.appendChild(pe(`
90
105
  <div class="plx-card">
91
106
  <p class="plx-note" data-bind-hidden="browseState.value !== 'loading'">Loading the catalogue\u2026</p>
92
107
  <p class="plx-note" data-bind-hidden="browseState.value !== 'error'">The catalogue could not be loaded - it needs an administrator, and a manager that is reachable.</p>
@@ -109,4 +124,4 @@ import{api as k,getUser as Ze}from"../api.js";import{addBannerAction as Ae}from"
109
124
  <p class="plx-note" data-bind-hidden="browseRows.value.length">Nothing in the catalogue matches.</p>
110
125
  <div class="plx-list plx-catalogue" data-each="browseRows.value key=slug"></div>
111
126
  </div>
112
- </div>`)),re.querySelector(".plx-catalogue").innerHTML=wt;const Se=b.querySelector(".plx-installed");Se.innerHTML=gt;const Me=b.querySelector(".plx-tools");Me.innerHTML=yt;const Ce=E.tabs(b.querySelector(".plx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Je=M.applyBindings({plugins:L,tools:q,toolRows:pe,catalogue:f,loading:me,loadError:ae,restarting:m,filter:T,query:H,rows:ne,counts:qe,emptyText:Pe,updateCount:Y,updateCountText:M.computed(()=>`${Y.value} update${Y.value===1?"":"s"}`),browseFilter:R,browseQuery:fe,browseRows:se,browseCounts:ze,siteText:M.computed(()=>f.value.site?`Licensed to ${f.value.site}.`:""),browseState:M.computed(()=>f.value.available===null?"loading":f.value.failed?"error":f.value.available?"list":"file")},b,{methods:{showAll:()=>{T.value="all"},showOn:()=>{T.value="on"},showOff:()=>{T.value="off"},showAttention:()=>{T.value="attention"},bAll:()=>{R.value="all"},bAvailable:()=>{R.value="available"},bInstalled:()=>{R.value="installed"},bUpdates:()=>{R.value="updates"}}});g.add(()=>Je.dispose()),g.add(Oe(b,{position:"bottom"})),g.add(M.effect(()=>{ne.value,pe.value,se.value,requestAnimationFrame(()=>{b.isConnected&&I.scan(b)})})),oe(Se,"name",We,e=>ie(e)),oe(Me,"tool",_e,He),oe(re.querySelector(".plx-catalogue"),"slug",Ve,Ye),Ae(O,{id:"plugins-install-file",icon:"upload",title:"Install from file (.dcmsplugin)",onClick:()=>K.click()}),Ae(O,{id:"plugins-new",icon:"plus",title:"New plugin",onClick:()=>Ee()});const Te=e=>{(m.peek()||U?.dirty())&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Te),g.add(()=>window.removeEventListener("beforeunload",Te)),g.add(()=>{if(U?.dirty())try{S.set(te,{kind:U.kind,name:U.name,values:U.values(),at:new Date().toISOString()})}catch{}ke=!0,[...Q].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(b),await Promise.all([le(),ve()]),!$())return;let y=null;try{y=S.get(te)}catch{}if(y?.values&&!ke){try{S.remove?S.remove(te):S.set(te,null)}catch{}const e=y.kind==="new"?"the new plugin you started":`your settings for ${r(J(y.name)?.displayName||y.name)}`;(y.kind==="new"||J(y.name))&&await E.confirm(`You left ${e} unsaved. Pick up where you left off?`)&&$()&&(y.kind==="new"?Ee(y.values):ie(y.name,y.values))}}};
127
+ </div>`)),de.querySelector(".plx-catalogue").innerHTML=kt;const Me=b.querySelector(".plx-installed");Me.innerHTML=wt;const Ce=b.querySelector(".plx-tools");Ce.innerHTML=xt;const Te=E.tabs(b.querySelector(".plx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Qe=M.applyBindings({plugins:L,tools:R,toolRows:me,catalogue:h,loading:be,loadError:ne,restarting:m,filter:T,query:V,rows:se,counts:We,emptyText:Pe,updateCount:J,updateCountText:M.computed(()=>`${J.value} update${J.value===1?"":"s"}`),browseFilter:j,browseQuery:fe,browseRows:le,browseCounts:ze,siteText:M.computed(()=>h.value.site?`Licensed to ${h.value.site}.`:""),browseState:M.computed(()=>h.value.available===null?"loading":h.value.failed?"error":h.value.available?"list":"file")},b,{methods:{showAll:()=>{T.value="all"},showOn:()=>{T.value="on"},showOff:()=>{T.value="off"},showAttention:()=>{T.value="attention"},bAll:()=>{j.value="all"},bAvailable:()=>{j.value="available"},bInstalled:()=>{j.value="installed"},bUpdates:()=>{j.value="updates"}}});v.add(()=>Qe.dispose()),v.add(Le(b,{position:"bottom"})),v.add(M.effect(()=>{se.value,me.value,le.value,requestAnimationFrame(()=>{b.isConnected&&I.scan(b)})})),re(Me,"name",He,e=>oe(e)),re(Ce,"tool",Ke,Ve),re(de.querySelector(".plx-catalogue"),"slug",Ye,Je),Ae(A,{id:"plugins-install-file",icon:"upload",title:"Install from file (.dcmsplugin)",onClick:()=>z.click()}),Ae(A,{id:"plugins-new",icon:"plus",title:"New plugin",onClick:()=>Se()});const Ne=e=>{(m.peek()||U?.dirty())&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",Ne),v.add(()=>window.removeEventListener("beforeunload",Ne)),v.add(()=>{if(U?.dirty())try{S.set(ae,{kind:U.kind,name:U.name,values:U.values(),at:new Date().toISOString()})}catch{}$e=!0,[...G].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(b),await Promise.all([ie(),ve()]),!k())return;let g=null;try{g=S.get(ae)}catch{}if(g?.values&&!$e){try{S.remove?S.remove(ae):S.set(ae,null)}catch{}const e=g.kind==="new"?"the new plugin you started":`your settings for ${d(Q(g.name)?.displayName||g.name)}`;(g.kind==="new"||Q(g.name))&&await E.confirm(`You left ${e} unsaved. Pick up where you left off?`)&&k()&&(g.kind==="new"?Se(g.values):oe(g.name,g.values))}}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.6.39",
3
+ "version": "1.6.41",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -84,7 +84,7 @@
84
84
  "@fastify/rate-limit": "^10.3.0",
85
85
  "@fastify/static": "9.1.1",
86
86
  "bcryptjs": "^3.0.3",
87
- "domma-js": "0.51.13",
87
+ "domma-js": "0.51.14",
88
88
  "dotenv": "^17.2.3",
89
89
  "fastify": "5.8.5",
90
90
  "gray-matter": "^4.0.3",