kempo 4.5.7 → 4.6.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.
Files changed (57) hide show
  1. package/README.md +1 -1
  2. package/dist/admin/components/tableControls/ExtensionDetails.js +2 -0
  3. package/dist/admin/components/tableControls/ExtensionDetails.js.map +1 -0
  4. package/dist/admin/components/tableControls/InstallExtension.js.map +1 -1
  5. package/dist/admin/components/tableControls/ToggleExtension.js +2 -0
  6. package/dist/admin/components/tableControls/ToggleExtension.js.map +1 -0
  7. package/dist/admin/components/tableControls/UninstallExtension.js +1 -1
  8. package/dist/admin/components/tableControls/UninstallExtension.js.map +1 -1
  9. package/dist/admin/extensions/browse/index.page.html +118 -0
  10. package/dist/admin/extensions/details/index.page.html +254 -0
  11. package/dist/admin/extensions/index.page.html +8 -2
  12. package/dist/kempo/api/extensions/DELETE.js +1 -1
  13. package/dist/kempo/api/extensions/DELETE.js.map +1 -1
  14. package/dist/kempo/api/extensions/POST.js +1 -1
  15. package/dist/kempo/api/extensions/POST.js.map +1 -1
  16. package/dist/kempo/api/extensions/[name]/dependents/GET.js +2 -0
  17. package/dist/kempo/api/extensions/[name]/dependents/GET.js.map +1 -0
  18. package/dist/kempo/api/extensions/[name]/disable/POST.js +1 -1
  19. package/dist/kempo/api/extensions/[name]/disable/POST.js.map +1 -1
  20. package/dist/kempo/api/extensions/[name]/enable/POST.js +1 -1
  21. package/dist/kempo/api/extensions/[name]/enable/POST.js.map +1 -1
  22. package/dist/kempo/api/extensions/[name]/requirements/GET.js +2 -0
  23. package/dist/kempo/api/extensions/[name]/requirements/GET.js.map +1 -0
  24. package/dist/kempo/api/extensions/available/GET.js +1 -1
  25. package/dist/kempo/api/extensions/available/GET.js.map +1 -1
  26. package/dist/kempo/api/extensions/known/GET.js +1 -1
  27. package/dist/kempo/api/extensions/known/GET.js.map +1 -1
  28. package/dist/kempo/api/extensions/rating/PUT.js +2 -0
  29. package/dist/kempo/api/extensions/rating/PUT.js.map +1 -0
  30. package/dist/kempo/icons/payments.svg +1 -0
  31. package/dist/kempo/icons/toll.svg +1 -0
  32. package/dist/kempo/icons/wallet.svg +1 -0
  33. package/dist/kempo/sdk.js +1 -1
  34. package/dist/kempo/sdk.js.map +1 -1
  35. package/middleware/kempo.js +9 -6
  36. package/package.json +1 -1
  37. package/server/utils/extensions/catalogClient.js +203 -0
  38. package/server/utils/extensions/disableExtension.js +2 -32
  39. package/server/utils/extensions/disableExtensionTree.js +46 -0
  40. package/server/utils/extensions/enableExtension.js +2 -33
  41. package/server/utils/extensions/enableExtensionTree.js +54 -0
  42. package/server/utils/extensions/getDependentTree.js +47 -0
  43. package/server/utils/extensions/getExtension.js +4 -3
  44. package/server/utils/extensions/getMarketplaceClient.js +20 -0
  45. package/server/utils/extensions/getRequirementTree.js +43 -0
  46. package/server/utils/extensions/getSiteInstallId.js +21 -0
  47. package/server/utils/extensions/installExtension.js +2 -6
  48. package/server/utils/extensions/installFromCatalog.js +127 -0
  49. package/server/utils/extensions/installPackage.js +36 -0
  50. package/server/utils/extensions/listExtensions.js +8 -7
  51. package/server/utils/extensions/readKempoConfig.js +20 -0
  52. package/server/utils/extensions/scopeCache.js +14 -5
  53. package/server/utils/extensions/uninstallExtension.js +2 -124
  54. package/server/utils/extensions/uninstallExtensionTree.js +165 -0
  55. package/server/utils/extensions/updateExtension.js +9 -7
  56. package/server/utils/hooks/triggerHook.js +8 -0
  57. package/dist/admin/known-extensions.json +0 -1
package/README.md CHANGED
@@ -359,7 +359,7 @@ Extensions can push live updates to browsers: declare a channel, publish to it f
359
359
 
360
360
  ## Admin Dashboard
361
361
 
362
- The admin home page is a grid of widgets one to four blocks wide and one to three tall. Kempo provides widgets for pages, content, users, notifications and extensions; extensions add their own with `kempo.widgets` or `registerDashboardWidget`. Each person chooses which are shown, their order and size, kept in their browser's localStorage, and a widget only loads its script and data once it is shown. Existing sites need `npx drizzle-kit push` to add the `dashboardWidget` table (until then only Kempo's own widgets appear). See [docs/dashboard.md](docs/dashboard.md).
362
+ The admin home page is a grid of widgets one to four blocks wide and one to three tall. Kempo provides widgets for pages, content, users, notifications and extensions; extensions add their own with `widgets` in `kempo-config.json` or `registerDashboardWidget`. Each person chooses which are shown, their order and size, kept in their browser's localStorage, and a widget only loads its script and data once it is shown. Existing sites need `npx drizzle-kit push` to add the `dashboardWidget` table (until then only Kempo's own widgets appear). See [docs/dashboard.md](docs/dashboard.md).
363
363
 
364
364
  ## Notifications
365
365
 
@@ -0,0 +1,2 @@
1
+ import e from"../AdminTableControl.js";import{html as n}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Icon.js";export default class t extends e{connectedCallback(){super.connectedCallback(),this.hasAttribute("title")||(this.title="Extension Details")}handleAction(){this.record&&(window.location.href=`/admin/extensions/details/?name=${encodeURIComponent(this.record.name)}`)}render(){return n`<k-icon name="info"></k-icon>`}}customElements.define("admin-extension-details",t);
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\tableControls\ExtensionDetails.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\admin\\components\\tableControls\\ExtensionDetails.js","names":["AdminTableControl","html","ExtensionDetails","connectedCallback","super","this","hasAttribute","title","handleAction","record","window","location","href","encodeURIComponent","name","render","customElements","define"],"sources":["0"],"sourcesContent":["import AdminTableControl from '../AdminTableControl.js';\nimport { html } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Icon.js';\n\n/*\n Opens the full details page for a row's extension (README, what it requires and adds, rating,\n install). The page shows whatever the marketplace knows; for an extension it does not list it says so.\n*/\nexport default class ExtensionDetails extends AdminTableControl {\n connectedCallback(){\n super.connectedCallback();\n if(!this.hasAttribute('title')) this.title = 'Extension Details';\n }\n\n handleAction(){\n if(!this.record) return;\n window.location.href = `/admin/extensions/details/?name=${encodeURIComponent(this.record.name)}`;\n }\n\n render(){ return html`<k-icon name=\"info\"></k-icon>`; }\n}\n\ncustomElements.define('admin-extension-details', ExtensionDetails);\n"],"mappings":"OAAOA,MAAuB,yCACrBC,MAAY,iCACd,8CAMQ,MAAMC,UAAyBF,EAC5C,iBAAAG,GACEC,MAAMD,oBACFE,KAAKC,aAAa,WAAUD,KAAKE,MAAQ,oBAC/C,CAEA,YAAAC,GACMH,KAAKI,SACTC,OAAOC,SAASC,KAAO,mCAAmCC,mBAAmBR,KAAKI,OAAOK,QAC3F,CAEA,MAAAC,GAAU,OAAOd,CAAI,+BAAiC,EAGxDe,eAAeC,OAAO,0BAA2Bf","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"src\\admin\\components\\tableControls\\InstallExtension.js","names":["AdminTableControl","html","Dialog","Toast","InstallExtension","install","async","this","record","confirm","name","confirmed","installExtension","error","msg","success","table","deleteRecord","connectedCallback","super","hasAttribute","title","handleAction","render","customElements","define"],"sources":["0"],"sourcesContent":["import AdminTableControl from '../AdminTableControl.js';\r\nimport { html } from '/kempo-ui/lit-all.min.js';\r\nimport Dialog from '/kempo-ui/components/Dialog.js';\r\nimport Toast from '/kempo-ui/components/Toast.js';\r\nimport '/kempo-ui/components/Icon.js';\r\n\r\nexport default class InstallExtension extends AdminTableControl {\r\n install = async () => {\r\n if(!this.record) return;\r\n\r\n Dialog.confirm(`Install \"${this.record.name}\"?`, async confirmed => {\r\n if(!confirmed) return;\r\n\r\n const { installExtension } = await import('/kempo/sdk.js');\r\n const [error] = await installExtension(this.record.name);\r\n\r\n if(error){\r\n Toast.error(error.msg || 'Failed to install extension');\r\n return;\r\n }\r\n\r\n Toast.success(`\"${this.record.name}\" installed successfully`);\r\n this.table.deleteRecord(this.record);\r\n });\r\n };\r\n\r\n connectedCallback(){\r\n super.connectedCallback();\r\n if(!this.hasAttribute('title')) this.title = 'Install Extension';\r\n }\r\n\r\n handleAction(){ this.install(); }\r\n\r\n render(){ return html`<k-icon name=\"extension_add\"></k-icon>`; }\r\n}\r\n\r\ncustomElements.define('admin-install-extension', InstallExtension);\r\n"],"mappings":"OAAOA,MAAuB,yCACrBC,MAAY,kCACdC,MAAY,wCACZC,MAAW,sCACX,8CAEQ,MAAMC,UAAyBJ,EAC5CK,QAAUC,UACJC,KAAKC,QAETN,EAAOO,QAAQ,YAAYF,KAAKC,OAAOE,SAAUJ,UAC/C,IAAIK,EAAW,OAEf,MAAMC,iBAAEA,gBAAkC,mBACnCC,SAAeD,EAAiBL,KAAKC,OAAOE,MAEhDG,EACDV,EAAMU,MAAMA,EAAMC,KAAO,gCAI3BX,EAAMY,QAAQ,IAAIR,KAAKC,OAAOE,gCAC9BH,KAAKS,MAAMC,aAAaV,KAAKC,YAIjC,iBAAAU,GACEC,MAAMD,oBACFX,KAAKa,aAAa,WAAUb,KAAKc,MAAQ,oBAC/C,CAEA,YAAAC,GAAgBf,KAAKF,SAAW,CAEhC,MAAAkB,GAAU,OAAOtB,CAAI,wCAA0C,EAGjEuB,eAAeC,OAAO,0BAA2BrB","ignoreList":[]}
1
+ {"version":3,"file":"src\\admin\\components\\tableControls\\InstallExtension.js","names":["AdminTableControl","html","Dialog","Toast","InstallExtension","install","async","this","record","confirm","name","confirmed","installExtension","error","msg","success","table","deleteRecord","connectedCallback","super","hasAttribute","title","handleAction","render","customElements","define"],"sources":["0"],"sourcesContent":["import AdminTableControl from '../AdminTableControl.js';\nimport { html } from '/kempo-ui/lit-all.min.js';\nimport Dialog from '/kempo-ui/components/Dialog.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport '/kempo-ui/components/Icon.js';\n\nexport default class InstallExtension extends AdminTableControl {\n install = async () => {\n if(!this.record) return;\n\n Dialog.confirm(`Install \"${this.record.name}\"?`, async confirmed => {\n if(!confirmed) return;\n\n const { installExtension } = await import('/kempo/sdk.js');\n const [error] = await installExtension(this.record.name);\n\n if(error){\n Toast.error(error.msg || 'Failed to install extension');\n return;\n }\n\n Toast.success(`\"${this.record.name}\" installed successfully`);\n this.table.deleteRecord(this.record);\n });\n };\n\n connectedCallback(){\n super.connectedCallback();\n if(!this.hasAttribute('title')) this.title = 'Install Extension';\n }\n\n handleAction(){ this.install(); }\n\n render(){ return html`<k-icon name=\"extension_add\"></k-icon>`; }\n}\n\ncustomElements.define('admin-install-extension', InstallExtension);\n"],"mappings":"OAAOA,MAAuB,yCACrBC,MAAY,kCACdC,MAAY,wCACZC,MAAW,sCACX,8CAEQ,MAAMC,UAAyBJ,EAC5CK,QAAUC,UACJC,KAAKC,QAETN,EAAOO,QAAQ,YAAYF,KAAKC,OAAOE,SAAUJ,UAC/C,IAAIK,EAAW,OAEf,MAAMC,iBAAEA,gBAAkC,mBACnCC,SAAeD,EAAiBL,KAAKC,OAAOE,MAEhDG,EACDV,EAAMU,MAAMA,EAAMC,KAAO,gCAI3BX,EAAMY,QAAQ,IAAIR,KAAKC,OAAOE,gCAC9BH,KAAKS,MAAMC,aAAaV,KAAKC,YAIjC,iBAAAU,GACEC,MAAMD,oBACFX,KAAKa,aAAa,WAAUb,KAAKc,MAAQ,oBAC/C,CAEA,YAAAC,GAAgBf,KAAKF,SAAW,CAEhC,MAAAkB,GAAU,OAAOtB,CAAI,wCAA0C,EAGjEuB,eAAeC,OAAO,0BAA2BrB","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ import e from"../AdminTableControl.js";import{html as n}from"/kempo-ui/lit-all.min.js";import t from"/kempo-ui/components/Dialog.js";import s from"/kempo-ui/components/Toast.js";import"/kempo-ui/components/Icon.js";export default class i extends e{#e=({title:e,intro:n,items:s,confirmText:i,danger:a})=>{const o=document.createElement("div");o.className="p";const l=document.createElement("p");if(l.textContent=n,o.append(l),s.length){const e=document.createElement("ul");for(const n of s){const t=document.createElement("li");t.textContent=n,e.append(t)}o.append(e)}return new Promise(n=>{t.create(o,{title:e,closeBtn:!1,overlayClose:!1,confirmText:i,confirmClasses:a?"danger ml":"primary ml",cancelText:"Cancel",confirmAction:()=>n(!0),cancelAction:()=>n(!1)})})};#n=(e,n)=>{for(const t of e){const e=this.table.records.find(e=>e.name===t);e&&this.table.updateRecord(e,{enabled:n})}};#t=async(e,n)=>{const[t,i]=await n.getExtensionRequirements(e.name);if(t)return void s.error(t.msg||"Failed to check what this extension requires");if(i.missing.length)return void s.error(`"${e.name}" requires ${i.missing.join(", ")}, which ${1===i.missing.length?"is":"are"} not installed`);const a=i.disabled;if(a.length){if(!await this.#e({title:`Enable ${a.length+1} Extensions`,intro:`"${e.name}" cannot run without these, which are switched off. Enabling it also enables:`,items:a.map(e=>e.requires.length?`${e.name} (needs ${e.requires.join(", ")})`:e.name),confirmText:`Enable ${a.length+1} extensions`,danger:!1}))return}const[o,l]=await n.enableExtension(e.name,{cascade:!0});o?s.error(o.msg||"Failed to enable extension"):(this.#n(l.enabledNames,!0),s.success(l.enabledNames.length>1?`Enabled ${l.enabledNames.join(", ")}`:`"${e.name}" enabled`),this.requestUpdate())};#s=async(e,n)=>{const[t,i]=await n.getExtensionDependents(e.name);if(t)return void s.error(t.msg||"Failed to check what depends on this extension");const a=i.dependents;if(!await this.#e({title:a.length?`Disable ${a.length+1} Extensions`:"Disable Extension",intro:a.length?`Disabling "${e.name}" also disables everything that depends on it:`:`Disable "${e.name}"? It stays installed and keeps its data, but its pages, files, widgets and hooks stop being used until you enable it again.`,items:a.map(e=>`${e.name} (needs ${e.requires.join(", ")})`),confirmText:a.length?`Disable ${a.length+1} extensions`:"Disable",danger:!0}))return;const[o,l]=await n.disableExtension(e.name,{cascade:!0});o?s.error(o.msg||"Failed to disable extension"):(this.#n(l.disabled,!1),s.success(l.disabled.length>1?`Disabled ${l.disabled.join(", ")}`:`"${e.name}" disabled`),this.requestUpdate())};toggle=async()=>{const e=this.record;if(!e)return;const n=await(import("/kempo/sdk.js"));!1===e.enabled?await this.#t(e,n):await this.#s(e,n)};handleAction(){this.toggle()}render(){const e=!1===this.record?.enabled;return this.title=e?"Enable Extension":"Disable Extension",n`<k-icon name="${e?"play":"pause"}"></k-icon>`}}customElements.define("admin-toggle-extension",i);
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\tableControls\ToggleExtension.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\admin\\components\\tableControls\\ToggleExtension.js","names":["AdminTableControl","html","Dialog","Toast","ToggleExtension","confirm","title","intro","items","confirmText","danger","contents","document","createElement","className","text","textContent","append","length","list","entry","item","Promise","resolve","create","closeBtn","overlayClose","confirmClasses","cancelText","confirmAction","cancelAction","markRows","names","enabled","name","row","this","table","records","find","candidate","updateRecord","enable","async","record","sdk","requirementsError","data","getExtensionRequirements","error","msg","missing","join","requirements","disabled","map","requirement","requires","result","enableExtension","cascade","enabledNames","success","requestUpdate","disable","dependentsError","getExtensionDependents","dependents","dependent","disableExtension","toggle","handleAction","render","customElements","define"],"sources":["0"],"sourcesContent":["import AdminTableControl from '../AdminTableControl.js';\nimport { html } from '/kempo-ui/lit-all.min.js';\nimport Dialog from '/kempo-ui/components/Dialog.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport '/kempo-ui/components/Icon.js';\n\n/*\n Switches an installed extension off or on without uninstalling it.\n\n Disabled, an extension stays installed (its data, settings, groups and hooks are kept) but the CMS\n does not use it: its public files, admin pages, dashboard widgets and hooks are all left out. Turning\n it back on picks up exactly where it was.\n\n An extension cannot run without what it requires, so both directions act on the whole chain, and\n show it before anything happens:\n disabling also disables what depends on it, and what depends on those, and so on\n enabling also enables what it requires that is switched off, and what those require, and so on\n Enabling never turns on anything else that happened to be switched off with it.\n*/\nexport default class ToggleExtension extends AdminTableControl {\n #confirm = ({ title, intro, items, confirmText, danger }) => {\n const contents = document.createElement('div');\n contents.className = 'p';\n\n const text = document.createElement('p');\n text.textContent = intro;\n contents.append(text);\n\n if(items.length){\n const list = document.createElement('ul');\n for(const entry of items){\n const item = document.createElement('li');\n item.textContent = entry;\n list.append(item);\n }\n contents.append(list);\n }\n\n return new Promise(resolve => {\n Dialog.create(contents, {\n title,\n closeBtn: false,\n overlayClose: false,\n confirmText,\n confirmClasses: danger ? 'danger ml' : 'primary ml',\n cancelText: 'Cancel',\n confirmAction: () => resolve(true),\n cancelAction: () => resolve(false),\n });\n });\n };\n\n #markRows = (names, enabled) => {\n for(const name of names){\n const row = this.table.records.find(candidate => candidate.name === name);\n if(row) this.table.updateRecord(row, { enabled });\n }\n };\n\n #enable = async (record, sdk) => {\n const [requirementsError, data] = await sdk.getExtensionRequirements(record.name);\n if(requirementsError){\n Toast.error(requirementsError.msg || 'Failed to check what this extension requires');\n return;\n }\n\n if(data.missing.length){\n Toast.error(`\"${record.name}\" requires ${data.missing.join(', ')}, which ${data.missing.length === 1 ? 'is' : 'are'} not installed`);\n return;\n }\n\n const requirements = data.disabled;\n if(requirements.length){\n const agreed = await this.#confirm({\n title: `Enable ${requirements.length + 1} Extensions`,\n intro: `\"${record.name}\" cannot run without these, which are switched off. Enabling it also enables:`,\n items: requirements.map(requirement => requirement.requires.length ? `${requirement.name} (needs ${requirement.requires.join(', ')})` : requirement.name),\n confirmText: `Enable ${requirements.length + 1} extensions`,\n danger: false,\n });\n if(!agreed) return;\n }\n\n const [error, result] = await sdk.enableExtension(record.name, { cascade: true });\n if(error){\n Toast.error(error.msg || 'Failed to enable extension');\n return;\n }\n\n this.#markRows(result.enabledNames, true);\n Toast.success(result.enabledNames.length > 1 ? `Enabled ${result.enabledNames.join(', ')}` : `\"${record.name}\" enabled`);\n this.requestUpdate();\n };\n\n #disable = async (record, sdk) => {\n const [dependentsError, data] = await sdk.getExtensionDependents(record.name);\n if(dependentsError){\n Toast.error(dependentsError.msg || 'Failed to check what depends on this extension');\n return;\n }\n\n const dependents = data.dependents;\n const agreed = await this.#confirm({\n title: dependents.length ? `Disable ${dependents.length + 1} Extensions` : 'Disable Extension',\n intro: dependents.length\n ? `Disabling \"${record.name}\" also disables everything that depends on it:`\n : `Disable \"${record.name}\"? It stays installed and keeps its data, but its pages, files, widgets and hooks stop being used until you enable it again.`,\n items: dependents.map(dependent => `${dependent.name} (needs ${dependent.requires.join(', ')})`),\n confirmText: dependents.length ? `Disable ${dependents.length + 1} extensions` : 'Disable',\n danger: true,\n });\n if(!agreed) return;\n\n const [error, result] = await sdk.disableExtension(record.name, { cascade: true });\n if(error){\n Toast.error(error.msg || 'Failed to disable extension');\n return;\n }\n\n this.#markRows(result.disabled, false);\n Toast.success(result.disabled.length > 1 ? `Disabled ${result.disabled.join(', ')}` : `\"${record.name}\" disabled`);\n this.requestUpdate();\n };\n\n toggle = async () => {\n const record = this.record;\n if(!record) return;\n\n const sdk = await import('/kempo/sdk.js');\n if(record.enabled === false) await this.#enable(record, sdk);\n else await this.#disable(record, sdk);\n };\n\n handleAction(){ this.toggle(); }\n\n render(){\n const disabled = this.record?.enabled === false;\n this.title = disabled ? 'Enable Extension' : 'Disable Extension';\n return html`<k-icon name=\"${disabled ? 'play' : 'pause'}\"></k-icon>`;\n }\n}\n\ncustomElements.define('admin-toggle-extension', ToggleExtension);\n"],"mappings":"OAAOA,MAAuB,yCACrBC,MAAY,kCACdC,MAAY,wCACZC,MAAW,sCACX,8CAeQ,MAAMC,UAAwBJ,EAC3CK,GAAW,EAAGC,QAAOC,QAAOC,QAAOC,cAAaC,aAC9C,MAAMC,EAAWC,SAASC,cAAc,OACxCF,EAASG,UAAY,IAErB,MAAMC,EAAOH,SAASC,cAAc,KAIpC,GAHAE,EAAKC,YAAcT,EACnBI,EAASM,OAAOF,GAEbP,EAAMU,OAAO,CACd,MAAMC,EAAOP,SAASC,cAAc,MACpC,IAAI,MAAMO,KAASZ,EAAM,CACvB,MAAMa,EAAOT,SAASC,cAAc,MACpCQ,EAAKL,YAAcI,EACnBD,EAAKF,OAAOI,EACd,CACAV,EAASM,OAAOE,EAClB,CAEA,OAAO,IAAIG,QAAQC,IACjBrB,EAAOsB,OAAOb,EAAU,CACtBL,QACAmB,UAAU,EACVC,cAAc,EACdjB,cACAkB,eAAgBjB,EAAS,YAAc,aACvCkB,WAAY,SACZC,cAAe,IAAMN,GAAQ,GAC7BO,aAAc,IAAMP,GAAQ,QAKlCQ,GAAY,CAACC,EAAOC,KAClB,IAAI,MAAMC,KAAQF,EAAM,CACtB,MAAMG,EAAMC,KAAKC,MAAMC,QAAQC,KAAKC,GAAaA,EAAUN,OAASA,GACjEC,GAAKC,KAAKC,MAAMI,aAAaN,EAAK,CAAEF,WACzC,GAGFS,GAAUC,MAAOC,EAAQC,KACvB,MAAOC,EAAmBC,SAAcF,EAAIG,yBAAyBJ,EAAOV,MAC5E,GAAGY,EAED,YADA3C,EAAM8C,MAAMH,EAAkBI,KAAO,gDAIvC,GAAGH,EAAKI,QAAQjC,OAEd,YADAf,EAAM8C,MAAM,IAAIL,EAAOV,kBAAkBa,EAAKI,QAAQC,KAAK,gBAAwC,IAAxBL,EAAKI,QAAQjC,OAAe,KAAO,uBAIhH,MAAMmC,EAAeN,EAAKO,SAC1B,GAAGD,EAAanC,OAAO,CAQrB,UAPqBkB,MAAK/B,EAAS,CACjCC,MAAO,UAAU+C,EAAanC,OAAS,eACvCX,MAAO,IAAIqC,EAAOV,oFAClB1B,MAAO6C,EAAaE,IAAIC,GAAeA,EAAYC,SAASvC,OAAS,GAAGsC,EAAYtB,eAAesB,EAAYC,SAASL,KAAK,SAAWI,EAAYtB,MACpJzB,YAAa,UAAU4C,EAAanC,OAAS,eAC7CR,QAAQ,IAEE,MACd,CAEA,MAAOuC,EAAOS,SAAgBb,EAAIc,gBAAgBf,EAAOV,KAAM,CAAE0B,SAAS,IACvEX,EACD9C,EAAM8C,MAAMA,EAAMC,KAAO,+BAI3Bd,MAAKL,EAAU2B,EAAOG,cAAc,GACpC1D,EAAM2D,QAAQJ,EAAOG,aAAa3C,OAAS,EAAI,WAAWwC,EAAOG,aAAaT,KAAK,QAAU,IAAIR,EAAOV,iBACxGE,KAAK2B,kBAGPC,GAAWrB,MAAOC,EAAQC,KACxB,MAAOoB,EAAiBlB,SAAcF,EAAIqB,uBAAuBtB,EAAOV,MACxE,GAAG+B,EAED,YADA9D,EAAM8C,MAAMgB,EAAgBf,KAAO,kDAIrC,MAAMiB,EAAapB,EAAKoB,WAUxB,UATqB/B,MAAK/B,EAAS,CACjCC,MAAO6D,EAAWjD,OAAS,WAAWiD,EAAWjD,OAAS,eAAiB,oBAC3EX,MAAO4D,EAAWjD,OACd,cAAc0B,EAAOV,qDACrB,YAAYU,EAAOV,mIACvB1B,MAAO2D,EAAWZ,IAAIa,GAAa,GAAGA,EAAUlC,eAAekC,EAAUX,SAASL,KAAK,UACvF3C,YAAa0D,EAAWjD,OAAS,WAAWiD,EAAWjD,OAAS,eAAiB,UACjFR,QAAQ,IAEE,OAEZ,MAAOuC,EAAOS,SAAgBb,EAAIwB,iBAAiBzB,EAAOV,KAAM,CAAE0B,SAAS,IACxEX,EACD9C,EAAM8C,MAAMA,EAAMC,KAAO,gCAI3Bd,MAAKL,EAAU2B,EAAOJ,UAAU,GAChCnD,EAAM2D,QAAQJ,EAAOJ,SAASpC,OAAS,EAAI,YAAYwC,EAAOJ,SAASF,KAAK,QAAU,IAAIR,EAAOV,kBACjGE,KAAK2B,kBAGPO,OAAS3B,UACP,MAAMC,EAASR,KAAKQ,OACpB,IAAIA,EAAQ,OAEZ,MAAMC,eAAmB,mBACH,IAAnBD,EAAOX,cAAyBG,MAAKM,EAAQE,EAAQC,SAC7CT,MAAK4B,EAASpB,EAAQC,IAGnC,YAAA0B,GAAgBnC,KAAKkC,QAAU,CAE/B,MAAAE,GACE,MAAMlB,GAAoC,IAAzBlB,KAAKQ,QAAQX,QAE9B,OADAG,KAAK9B,MAAQgD,EAAW,mBAAqB,oBACtCrD,CAAI,iBAAiBqD,EAAW,OAAS,oBAClD,EAGFmB,eAAeC,OAAO,yBAA0BtE","ignoreList":[]}
@@ -1,2 +1,2 @@
1
- import e from"../AdminTableControl.js";import{html as n}from"/kempo-ui/lit-all.min.js";import t from"/kempo-ui/components/Dialog.js";import s from"/kempo-ui/components/Toast.js";import"/kempo-ui/components/Icon.js";export default class a extends e{uninstall=async()=>{if(!this.record)return;const e=document.createElement("div");e.className="p",e.innerHTML=`\n <p>Uninstall "${this.record.name}"?</p>\n <p class="small tc-muted">Its data is kept by default, so reinstalling restores everything.</p>\n <label class="d-f" style="gap: var(--spacer_h); align-items: center">\n <input type="checkbox" name="purge-data" />\n <span class="tc-danger">Also permanently delete all of its data</span>\n </label>\n `,t.create(e,{title:"Uninstall Extension",closeBtn:!1,overlayClose:!1,confirmText:"Uninstall",confirmClasses:"danger ml",cancelText:"Cancel",confirmAction:async()=>{const n=e.querySelector('[name="purge-data"]').checked,{uninstallExtension:t}=await(import("/kempo/sdk.js")),[a]=await t(this.record.name,n);a?s.error(a.msg||"Failed to uninstall extension"):(s.success(n?`"${this.record.name}" uninstalled and its data deleted`:`"${this.record.name}" uninstalled, its data kept`),this.table.deleteRecord(this.record))}})};connectedCallback(){super.connectedCallback(),this.hasAttribute("title")||(this.title="Uninstall Extension")}handleAction(){this.uninstall()}render(){return n`<k-icon name="extension_remove"></k-icon>`}}customElements.define("admin-uninstall-extension",a);
1
+ import e from"../AdminTableControl.js";import{html as n}from"/kempo-ui/lit-all.min.js";import t from"/kempo-ui/components/Dialog.js";import s from"/kempo-ui/components/Toast.js";import"/kempo-ui/components/Icon.js";export default class a extends e{uninstall=async()=>{const e=this.record;if(!e)return;const{uninstallExtension:n,getExtensionDependents:a}=await(import("/kempo/sdk.js")),[l,o]=await a(e.name,{includeDisabled:!0});if(l)return void s.error(l.msg||"Failed to check what depends on this extension");const i=o.dependents,c=i.length+1,d=document.createElement("div");d.className="p";const r=document.createElement("p");if(r.textContent=i.length?`Uninstalling "${e.name}" also uninstalls everything that depends on it:`:`Uninstall "${e.name}"?`,d.append(r),i.length){const e=document.createElement("ul");for(const n of i){const t=document.createElement("li");t.textContent=`${n.name} (needs ${n.requires.join(", ")})${n.enabled?"":" - disabled"}`,e.append(t)}d.append(e)}const m=document.createElement("p");m.className="tc-muted",m.textContent=c>1?"Their data is kept by default, so reinstalling restores everything.":"Its data is kept by default, so reinstalling restores everything.",d.append(m);const p=document.createElement("label");p.className="d-f";const u=document.createElement("input");u.type="checkbox",u.name="purge-data";const h=document.createElement("span");h.className="tc-danger",h.textContent=c>1?`Also permanently delete all of the data of these ${c} extensions`:"Also permanently delete all of its data",p.append(u,h),d.append(p),t.create(d,{title:c>1?`Uninstall ${c} Extensions`:"Uninstall Extension",closeBtn:!1,overlayClose:!1,confirmText:c>1?`Uninstall ${c} extensions`:"Uninstall",confirmClasses:"danger ml",cancelText:"Cancel",confirmAction:async()=>{const t=u.checked,[a,l]=await n(e.name,t,{cascade:!0});if(a)return void s.error(a.msg||"Failed to uninstall extension");for(const e of l.uninstalled){const n=this.table.records.find(n=>n.name===e);n&&this.table.deleteRecord(n)}const o=l.uninstalled.length>1,i=o?l.uninstalled.join(", "):`"${e.name}"`;s.success(t?`${i} uninstalled and ${o?"their":"its"} data deleted`:`${i} uninstalled, data kept`)}})};connectedCallback(){super.connectedCallback(),this.hasAttribute("title")||(this.title="Uninstall Extension")}handleAction(){this.uninstall()}render(){return n`<k-icon name="extension_remove"></k-icon>`}}customElements.define("admin-uninstall-extension",a);
2
2
  //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\tableControls\UninstallExtension.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"src\\admin\\components\\tableControls\\UninstallExtension.js","names":["AdminTableControl","html","Dialog","Toast","UninstallExtension","uninstall","async","this","record","contents","document","createElement","className","innerHTML","name","create","title","closeBtn","overlayClose","confirmText","confirmClasses","cancelText","confirmAction","purgeData","querySelector","checked","uninstallExtension","error","msg","success","table","deleteRecord","connectedCallback","super","hasAttribute","handleAction","render","customElements","define"],"sources":["0"],"sourcesContent":["import AdminTableControl from '../AdminTableControl.js';\r\nimport { html } from '/kempo-ui/lit-all.min.js';\r\nimport Dialog from '/kempo-ui/components/Dialog.js';\r\nimport Toast from '/kempo-ui/components/Toast.js';\r\nimport '/kempo-ui/components/Icon.js';\r\n\r\nexport default class UninstallExtension extends AdminTableControl {\r\n uninstall = async () => {\r\n if(!this.record) return;\r\n\r\n /*\r\n Two genuinely different operations behind one button, so the destructive one is opt-in:\r\n deregistering leaves the extension's tables, settings, groups and memberships intact and a\r\n reinstall picks up exactly where it left off, which is what you want when reinstalling to\r\n apply a fix. Purging destroys all of it — for a blog that is every post and comment.\r\n */\r\n const contents = document.createElement('div');\r\n contents.className = 'p';\r\n contents.innerHTML = `\r\n <p>Uninstall \"${this.record.name}\"?</p>\r\n <p class=\"small tc-muted\">Its data is kept by default, so reinstalling restores everything.</p>\r\n <label class=\"d-f\" style=\"gap: var(--spacer_h); align-items: center\">\r\n <input type=\"checkbox\" name=\"purge-data\" />\r\n <span class=\"tc-danger\">Also permanently delete all of its data</span>\r\n </label>\r\n `;\r\n\r\n Dialog.create(contents, {\r\n title: 'Uninstall Extension',\r\n closeBtn: false,\r\n overlayClose: false,\r\n confirmText: 'Uninstall',\r\n confirmClasses: 'danger ml',\r\n cancelText: 'Cancel',\r\n confirmAction: async () => {\r\n const purgeData = contents.querySelector('[name=\"purge-data\"]').checked;\r\n const { uninstallExtension } = await import('/kempo/sdk.js');\r\n const [error] = await uninstallExtension(this.record.name, purgeData);\r\n\r\n if(error){\r\n Toast.error(error.msg || 'Failed to uninstall extension');\r\n return;\r\n }\r\n\r\n Toast.success(purgeData\r\n ? `\"${this.record.name}\" uninstalled and its data deleted`\r\n : `\"${this.record.name}\" uninstalled, its data kept`);\r\n this.table.deleteRecord(this.record);\r\n },\r\n });\r\n };\r\n\r\n connectedCallback(){\r\n super.connectedCallback();\r\n if(!this.hasAttribute('title')) this.title = 'Uninstall Extension';\r\n }\r\n\r\n handleAction(){ this.uninstall(); }\r\n\r\n render(){ return html`<k-icon name=\"extension_remove\"></k-icon>`; }\r\n}\r\n\r\ncustomElements.define('admin-uninstall-extension', UninstallExtension);\r\n"],"mappings":"OAAOA,MAAuB,yCACrBC,MAAY,kCACdC,MAAY,wCACZC,MAAW,sCACX,8CAEQ,MAAMC,UAA2BJ,EAC9CK,UAAYC,UACV,IAAIC,KAAKC,OAAQ,OAQjB,MAAMC,EAAWC,SAASC,cAAc,OACxCF,EAASG,UAAY,IACrBH,EAASI,UAAY,yBACHN,KAAKC,OAAOM,4VAQ9BZ,EAAOa,OAAON,EAAU,CACtBO,MAAO,sBACPC,UAAU,EACVC,cAAc,EACdC,YAAa,YACbC,eAAgB,YAChBC,WAAY,SACZC,cAAehB,UACb,MAAMiB,EAAYd,EAASe,cAAc,uBAAuBC,SAC1DC,mBAAEA,gBAAoC,mBACrCC,SAAeD,EAAmBnB,KAAKC,OAAOM,KAAMS,GAExDI,EACDxB,EAAMwB,MAAMA,EAAMC,KAAO,kCAI3BzB,EAAM0B,QAAQN,EACV,IAAIhB,KAAKC,OAAOM,yCAChB,IAAIP,KAAKC,OAAOM,oCACpBP,KAAKuB,MAAMC,aAAaxB,KAAKC,aAKnC,iBAAAwB,GACEC,MAAMD,oBACFzB,KAAK2B,aAAa,WAAU3B,KAAKS,MAAQ,sBAC/C,CAEA,YAAAmB,GAAgB5B,KAAKF,WAAa,CAElC,MAAA+B,GAAU,OAAOnC,CAAI,2CAA6C,EAGpEoC,eAAeC,OAAO,4BAA6BlC","ignoreList":[]}
1
+ {"version":3,"file":"src\\admin\\components\\tableControls\\UninstallExtension.js","names":["AdminTableControl","html","Dialog","Toast","UninstallExtension","uninstall","async","record","this","uninstallExtension","getExtensionDependents","dependentsError","data","name","includeDisabled","error","msg","dependents","count","length","contents","document","createElement","className","intro","textContent","append","list","dependent","item","requires","join","enabled","note","purgeLabel","purge","type","purgeText","create","title","closeBtn","overlayClose","confirmText","confirmClasses","cancelText","confirmAction","purgeData","checked","result","cascade","removed","uninstalled","row","table","records","find","candidate","deleteRecord","many","what","success","connectedCallback","super","hasAttribute","handleAction","render","customElements","define"],"sources":["0"],"sourcesContent":["import AdminTableControl from '../AdminTableControl.js';\nimport { html } from '/kempo-ui/lit-all.min.js';\nimport Dialog from '/kempo-ui/components/Dialog.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport '/kempo-ui/components/Icon.js';\n\nexport default class UninstallExtension extends AdminTableControl {\n uninstall = async () => {\n const record = this.record;\n if(!record) return;\n\n /*\n An extension cannot stay installed without what it requires, so uninstalling one that others\n require uninstalls them too. The whole chain is fetched and shown first (disabled ones\n included: installed but switched off is still installed), and nothing happens until the\n administrator agrees to all of it.\n */\n const { uninstallExtension, getExtensionDependents } = await import('/kempo/sdk.js');\n const [dependentsError, data] = await getExtensionDependents(record.name, { includeDisabled: true });\n if(dependentsError){\n Toast.error(dependentsError.msg || 'Failed to check what depends on this extension');\n return;\n }\n const dependents = data.dependents;\n const count = dependents.length + 1;\n\n /*\n Two genuinely different operations behind one button, so the destructive one is opt-in:\n deregistering leaves the extension's tables, settings, groups and memberships intact and a\n reinstall picks up exactly where it left off, which is what you want when reinstalling to\n apply a fix. Purging destroys all of that. For a blog that is every post and comment.\n */\n const contents = document.createElement('div');\n contents.className = 'p';\n\n const intro = document.createElement('p');\n intro.textContent = dependents.length\n ? `Uninstalling \"${record.name}\" also uninstalls everything that depends on it:`\n : `Uninstall \"${record.name}\"?`;\n contents.append(intro);\n\n if(dependents.length){\n const list = document.createElement('ul');\n for(const dependent of dependents){\n const item = document.createElement('li');\n item.textContent = `${dependent.name} (needs ${dependent.requires.join(', ')})${dependent.enabled ? '' : ' - disabled'}`;\n list.append(item);\n }\n contents.append(list);\n }\n\n const note = document.createElement('p');\n note.className = 'tc-muted';\n note.textContent = count > 1\n ? 'Their data is kept by default, so reinstalling restores everything.'\n : 'Its data is kept by default, so reinstalling restores everything.';\n contents.append(note);\n\n const purgeLabel = document.createElement('label');\n purgeLabel.className = 'd-f';\n const purge = document.createElement('input');\n purge.type = 'checkbox';\n purge.name = 'purge-data';\n const purgeText = document.createElement('span');\n purgeText.className = 'tc-danger';\n purgeText.textContent = count > 1\n ? `Also permanently delete all of the data of these ${count} extensions`\n : 'Also permanently delete all of its data';\n purgeLabel.append(purge, purgeText);\n contents.append(purgeLabel);\n\n Dialog.create(contents, {\n title: count > 1 ? `Uninstall ${count} Extensions` : 'Uninstall Extension',\n closeBtn: false,\n overlayClose: false,\n confirmText: count > 1 ? `Uninstall ${count} extensions` : 'Uninstall',\n confirmClasses: 'danger ml',\n cancelText: 'Cancel',\n confirmAction: async () => {\n const purgeData = purge.checked;\n const [error, result] = await uninstallExtension(record.name, purgeData, { cascade: true });\n\n if(error){\n Toast.error(error.msg || 'Failed to uninstall extension');\n return;\n }\n\n for(const removed of result.uninstalled){\n const row = this.table.records.find(candidate => candidate.name === removed);\n if(row) this.table.deleteRecord(row);\n }\n\n const many = result.uninstalled.length > 1;\n const what = many ? result.uninstalled.join(', ') : `\"${record.name}\"`;\n Toast.success(purgeData\n ? `${what} uninstalled and ${many ? 'their' : 'its'} data deleted`\n : `${what} uninstalled, data kept`);\n },\n });\n };\n\n connectedCallback(){\n super.connectedCallback();\n if(!this.hasAttribute('title')) this.title = 'Uninstall Extension';\n }\n\n handleAction(){ this.uninstall(); }\n\n render(){ return html`<k-icon name=\"extension_remove\"></k-icon>`; }\n}\n\ncustomElements.define('admin-uninstall-extension', UninstallExtension);\n"],"mappings":"OAAOA,MAAuB,yCACrBC,MAAY,kCACdC,MAAY,wCACZC,MAAW,sCACX,8CAEQ,MAAMC,UAA2BJ,EAC9CK,UAAYC,UACV,MAAMC,EAASC,KAAKD,OACpB,IAAIA,EAAQ,OAQZ,MAAME,mBAAEA,EAAkBC,uBAAEA,gBAAwC,mBAC7DC,EAAiBC,SAAcF,EAAuBH,EAAOM,KAAM,CAAEC,iBAAiB,IAC7F,GAAGH,EAED,YADAR,EAAMY,MAAMJ,EAAgBK,KAAO,kDAGrC,MAAMC,EAAaL,EAAKK,WAClBC,EAAQD,EAAWE,OAAS,EAQ5BC,EAAWC,SAASC,cAAc,OACxCF,EAASG,UAAY,IAErB,MAAMC,EAAQH,SAASC,cAAc,KAMrC,GALAE,EAAMC,YAAcR,EAAWE,OAC3B,iBAAiBZ,EAAOM,uDACxB,cAAcN,EAAOM,SACzBO,EAASM,OAAOF,GAEbP,EAAWE,OAAO,CACnB,MAAMQ,EAAON,SAASC,cAAc,MACpC,IAAI,MAAMM,KAAaX,EAAW,CAChC,MAAMY,EAAOR,SAASC,cAAc,MACpCO,EAAKJ,YAAc,GAAGG,EAAUf,eAAee,EAAUE,SAASC,KAAK,SAASH,EAAUI,QAAU,GAAK,gBACzGL,EAAKD,OAAOG,EACd,CACAT,EAASM,OAAOC,EAClB,CAEA,MAAMM,EAAOZ,SAASC,cAAc,KACpCW,EAAKV,UAAY,WACjBU,EAAKR,YAAcP,EAAQ,EACvB,sEACA,oEACJE,EAASM,OAAOO,GAEhB,MAAMC,EAAab,SAASC,cAAc,SAC1CY,EAAWX,UAAY,MACvB,MAAMY,EAAQd,SAASC,cAAc,SACrCa,EAAMC,KAAO,WACbD,EAAMtB,KAAO,aACb,MAAMwB,EAAYhB,SAASC,cAAc,QACzCe,EAAUd,UAAY,YACtBc,EAAUZ,YAAcP,EAAQ,EAC5B,oDAAoDA,eACpD,0CACJgB,EAAWR,OAAOS,EAAOE,GACzBjB,EAASM,OAAOQ,GAEhBhC,EAAOoC,OAAOlB,EAAU,CACtBmB,MAAOrB,EAAQ,EAAI,aAAaA,eAAqB,sBACrDsB,UAAU,EACVC,cAAc,EACdC,YAAaxB,EAAQ,EAAI,aAAaA,eAAqB,YAC3DyB,eAAgB,YAChBC,WAAY,SACZC,cAAevC,UACb,MAAMwC,EAAYX,EAAMY,SACjBhC,EAAOiC,SAAgBvC,EAAmBF,EAAOM,KAAMiC,EAAW,CAAEG,SAAS,IAEpF,GAAGlC,EAED,YADAZ,EAAMY,MAAMA,EAAMC,KAAO,iCAI3B,IAAI,MAAMkC,KAAWF,EAAOG,YAAY,CACtC,MAAMC,EAAM5C,KAAK6C,MAAMC,QAAQC,KAAKC,GAAaA,EAAU3C,OAASqC,GACjEE,GAAK5C,KAAK6C,MAAMI,aAAaL,EAClC,CAEA,MAAMM,EAAOV,EAAOG,YAAYhC,OAAS,EACnCwC,EAAOD,EAAOV,EAAOG,YAAYpB,KAAK,MAAQ,IAAIxB,EAAOM,QAC/DV,EAAMyD,QAAQd,EACV,GAAGa,qBAAwBD,EAAO,QAAU,qBAC5C,GAAGC,gCAKb,iBAAAE,GACEC,MAAMD,oBACFrD,KAAKuD,aAAa,WAAUvD,KAAK+B,MAAQ,sBAC/C,CAEA,YAAAyB,GAAgBxD,KAAKH,WAAa,CAElC,MAAA4D,GAAU,OAAOhE,CAAI,2CAA6C,EAGpEiE,eAAeC,OAAO,4BAA6B/D","ignoreList":[]}
@@ -0,0 +1,118 @@
1
+ <!--
2
+ locked: true
3
+ -->
4
+ <page title="Browse Extensions - Admin">
5
+ <content>
6
+ <script type="module" src="/kempo-ui/components/Icon.js"></script>
7
+ <script type="module" src="/kempo-ui/components/Rating.js"></script>
8
+ <script type="module" src="/kempo-ui/components/Pagination.js"></script>
9
+ <script type="module" src="/kempo-ui/components/Spinner.js"></script>
10
+ <script type="module" src="/kempo/components/Permission.js"></script>
11
+
12
+ <div class="d-f mb">
13
+ <a href="/admin/extensions" class="no-link">
14
+ <button><k-icon name="arrow" direction="left"></k-icon> Extensions</button>
15
+ </a>
16
+ </div>
17
+ <h1>Browse Extensions</h1>
18
+ <p class="tc-muted">Extensions from the Kempo marketplace. Pick one to see what it does and what it needs before installing it.</p>
19
+
20
+ <div class="row mb">
21
+ <div class="col mr">
22
+ <input id="search" type="search" placeholder="Search extensions" aria-label="Search extensions" class="full" />
23
+ </div>
24
+ <div>
25
+ <select id="sort" aria-label="Sort by">
26
+ <option value="installs">Most installed</option>
27
+ <option value="rating">Highest rated</option>
28
+ <option value="name">Name</option>
29
+ <option value="updated">Recently updated</option>
30
+ </select>
31
+ </div>
32
+ </div>
33
+
34
+ <div id="notice" class="tc-danger mb" hidden></div>
35
+ <div id="cards" class="d-g cols-3 t-cols-2 m-cols-1"></div>
36
+ <p id="empty" class="tc-muted" hidden>No extensions match that search.</p>
37
+ <k-pagination id="pagination" controls="simple" items-per-page="12" total-items="0" page="1"></k-pagination>
38
+
39
+ </content>
40
+ <content location="scripts">
41
+ <script type="module">
42
+ import { listKnownExtensions } from '/kempo/sdk.js';
43
+
44
+ const PAGE = 12;
45
+ const cards = document.getElementById('cards');
46
+ const search = document.getElementById('search');
47
+ const sort = document.getElementById('sort');
48
+ const notice = document.getElementById('notice');
49
+ const empty = document.getElementById('empty');
50
+ const pagination = document.getElementById('pagination');
51
+
52
+ let timer;
53
+ let loading = false;
54
+
55
+ const card = item => {
56
+ const link = document.createElement('a');
57
+ link.className = 'card no-link d-b';
58
+ link.href = `/admin/extensions/details/?name=${encodeURIComponent(item.name)}`;
59
+
60
+ const title = document.createElement('h3');
61
+ title.className = 'mb0';
62
+ title.textContent = item.name;
63
+
64
+ const text = document.createElement('p');
65
+ text.textContent = item.description || 'No description.';
66
+
67
+ const rating = document.createElement('k-rating');
68
+ rating.setAttribute('disabled', '');
69
+ rating.value = Math.round(item.rating.average || 0);
70
+
71
+ const meta = document.createElement('p');
72
+ meta.className = 'tc-muted';
73
+ meta.textContent = `${item.rating.count ? `${item.rating.average} (${item.rating.count}) · ` : ''}${item.installs} install${item.installs === 1 ? '' : 's'}`;
74
+
75
+ link.append(title, text, rating, meta);
76
+ return link;
77
+ };
78
+
79
+ const load = async page => {
80
+ loading = true;
81
+ const [error, data] = await listKnownExtensions({
82
+ search: search.value.trim() || undefined,
83
+ sort: sort.value,
84
+ limit: PAGE,
85
+ offset: (page - 1) * PAGE,
86
+ });
87
+
88
+ notice.hidden = !error;
89
+ if(error){
90
+ notice.textContent = error.msg;
91
+ cards.replaceChildren();
92
+ empty.hidden = true;
93
+ loading = false;
94
+ return;
95
+ }
96
+
97
+ cards.replaceChildren(...data.items.map(card));
98
+ empty.hidden = data.total > 0;
99
+ pagination.totalItems = data.total;
100
+ pagination.page = page;
101
+ pagination.classList.toggle('d-n', data.total <= PAGE);
102
+ loading = false;
103
+ };
104
+
105
+ pagination.addEventListener('page-change', event => {
106
+ if(!loading && event.detail.currentPage !== undefined) load(event.detail.currentPage);
107
+ });
108
+
109
+ search.addEventListener('input', () => {
110
+ clearTimeout(timer);
111
+ timer = setTimeout(() => load(1), 300);
112
+ });
113
+ sort.addEventListener('change', () => load(1));
114
+
115
+ load(1);
116
+ </script>
117
+ </content>
118
+ </page>
@@ -0,0 +1,254 @@
1
+ <!--
2
+ locked: true
3
+ -->
4
+ <page title="Extension - Admin">
5
+ <content>
6
+ <script type="module" src="/kempo-ui/components/Icon.js"></script>
7
+ <script type="module" src="/kempo-ui/components/Rating.js"></script>
8
+ <script type="module" src="/kempo-ui/components/Markdown.js"></script>
9
+ <script type="module" src="/kempo/components/Permission.js"></script>
10
+
11
+ <div class="d-f mb">
12
+ <a href="/admin/extensions/browse/" class="no-link mrh">
13
+ <button><k-icon name="arrow" direction="left"></k-icon> Browse</button>
14
+ </a>
15
+ <a href="/admin/extensions" class="no-link">
16
+ <button>Installed</button>
17
+ </a>
18
+ </div>
19
+
20
+ <div id="notFound" class="d-n">
21
+ <h1 id="notFoundTitle">Extension</h1>
22
+ <p id="notFoundMessage" class="tc-muted"></p>
23
+ </div>
24
+
25
+ <div id="content" class="d-n">
26
+ <div class="d-f mb">
27
+ <div class="flex">
28
+ <h1 id="name" class="mb0"></h1>
29
+ <p id="meta" class="tc-muted"></p>
30
+ </div>
31
+ <div>
32
+ <span id="installedBadge" class="tc-success d-n"><k-icon name="check"></k-icon> Installed</span>
33
+ <button id="installBtn" class="primary d-n">Install</button>
34
+ </div>
35
+ </div>
36
+
37
+ <p id="description"></p>
38
+ <div id="problem" class="tc-danger mb d-n"></div>
39
+
40
+ <div class="d-f mb">
41
+ <div class="mr">
42
+ <strong>Rating</strong><br />
43
+ <k-rating id="average" disabled></k-rating>
44
+ <span id="ratingText" class="tc-muted"></span>
45
+ </div>
46
+ <div id="mine" class="d-n">
47
+ <strong>Your rating</strong><br />
48
+ <k-rating id="myRating"></k-rating>
49
+ <span id="myRatingText" class="tc-muted"></span>
50
+ </div>
51
+ </div>
52
+
53
+ <div id="treeBlock" class="d-n">
54
+ <h2>Installing also changes</h2>
55
+ <p class="tc-muted">It cannot run without these, so they are installed or enabled first, in this order:</p>
56
+ <ol id="tree"></ol>
57
+ </div>
58
+
59
+ <div id="providesBlock" class="d-n">
60
+ <h2>What it adds</h2>
61
+ <div id="provides"></div>
62
+ </div>
63
+
64
+ <h2>README</h2>
65
+ <k-markdown id="readme"></k-markdown>
66
+ </div>
67
+
68
+ </content>
69
+ <content location="scripts">
70
+ <script type="module">
71
+ import { getKnownExtension, listExtensions, installExtension, rateExtension } from '/kempo/sdk.js';
72
+ import Dialog from '/kempo-ui/components/Dialog.js';
73
+ import Toast from '/kempo-ui/components/Toast.js';
74
+
75
+ const name = new URLSearchParams(location.search).get('name') || '';
76
+ const $ = id => document.getElementById(id);
77
+
78
+ const show = id => { $(id).classList.remove('d-n'); };
79
+
80
+ const list = (title, items) => {
81
+ if(!items.length) return null;
82
+ const wrap = document.createElement('div');
83
+ const heading = document.createElement('p');
84
+ heading.className = 'mb0';
85
+ const strong = document.createElement('strong');
86
+ strong.textContent = title;
87
+ heading.append(strong);
88
+ const ul = document.createElement('ul');
89
+ for(const text of items){
90
+ const li = document.createElement('li');
91
+ li.textContent = text;
92
+ ul.append(li);
93
+ }
94
+ wrap.append(heading, ul);
95
+ return wrap;
96
+ };
97
+
98
+ const render = async () => {
99
+ const [error, data] = await getKnownExtension(name);
100
+ if(error){
101
+ $('notFoundTitle').textContent = name || 'Extension';
102
+ $('notFoundMessage').textContent = error.code === 404
103
+ ? 'The marketplace does not list this extension.'
104
+ : error.msg;
105
+ show('notFound');
106
+ return;
107
+ }
108
+
109
+ const { item, local, myRating } = data;
110
+ document.title = `${item.name} - Admin`;
111
+ $('name').textContent = item.name;
112
+ $('description').textContent = item.description;
113
+ $('meta').textContent = [
114
+ item.author && `By ${item.author}`,
115
+ item.license,
116
+ item.version && `Version ${item.version}`,
117
+ `${item.installs} install${item.installs === 1 ? '' : 's'}`,
118
+ ].filter(Boolean).join(' · ');
119
+
120
+ $('average').value = Math.round(item.rating.average || 0);
121
+ $('ratingText').textContent = item.rating.count ? `${item.rating.average} (${item.rating.count} rating${item.rating.count === 1 ? '' : 's'})` : 'No ratings yet';
122
+ $('readme').value = item.readme || '*This extension has no README.*';
123
+
124
+ /*
125
+ What installing will change, worked out before anything is offered: the extensions it needs that
126
+ this site does not have yet (downloaded and installed), and the ones it has but has switched
127
+ off (enabled). An extension cannot run without what it requires, so the administrator approves
128
+ the whole list or none of it.
129
+ */
130
+ const [, installedData] = await listExtensions({ limit: 200 });
131
+ const installedState = new Map((installedData?.items || []).map(extension => [extension.name, extension.enabled]));
132
+ const missing = item.dependencies.extensions.filter(dependency => !installedState.has(dependency.name));
133
+ const switchedOff = item.dependencies.extensions.filter(dependency => installedState.get(dependency.name) === false);
134
+
135
+ if(missing.length || switchedOff.length){
136
+ $('tree').replaceChildren(...item.dependencies.extensions
137
+ .filter(dependency => missing.includes(dependency) || switchedOff.includes(dependency))
138
+ .map(dependency => {
139
+ const li = document.createElement('li');
140
+ const action = missing.includes(dependency) ? 'install' : 'enable';
141
+ li.textContent = `${dependency.name} ${dependency.version} (${action}${dependency.installable || action === 'enable' ? '' : ', not published yet'})`;
142
+ return li;
143
+ }));
144
+ show('treeBlock');
145
+ }
146
+
147
+ const blockers = [
148
+ ...item.dependencies.unresolved.map(unresolved => `Needs ${unresolved}, which the marketplace does not list.`),
149
+ ...(!item.installable ? [`${item.name} is listed but not published yet.`] : []),
150
+ ...missing.filter(dependency => !dependency.installable).map(dependency => `Needs ${dependency.name}, which is not published yet.`),
151
+ ];
152
+
153
+ const provides = [
154
+ list('Permissions', item.provides.permissions.map(permission => permission.description ? `${permission.name}: ${permission.description}` : permission.name)),
155
+ list('Settings', item.provides.settings.map(setting => setting.name)),
156
+ list('Groups', item.provides.groups.map(group => group.name)),
157
+ list('Dashboard widgets', item.provides.widgets.map(widget => widget.title || widget.key)),
158
+ ].filter(Boolean);
159
+ if(provides.length){
160
+ $('provides').replaceChildren(...provides);
161
+ show('providesBlock');
162
+ }
163
+
164
+ if(local.installed){
165
+ show('installedBadge');
166
+ show('mine');
167
+ const mine = $('myRating');
168
+ mine.value = myRating || 0;
169
+ $('myRatingText').textContent = myRating ? '' : 'Not rated yet';
170
+ await mine.updateComplete;
171
+ mine.addEventListener('change', async event => {
172
+ if(!event.detail.value || event.detail.value === myRating) return;
173
+ const [rateError] = await rateExtension(name, event.detail.value);
174
+ if(rateError){
175
+ Toast.error(rateError.msg || 'Failed to save the rating');
176
+ mine.value = myRating || 0;
177
+ return;
178
+ }
179
+ Toast.success('Rating saved');
180
+ $('myRatingText').textContent = '';
181
+ });
182
+ } else if(blockers.length){
183
+ $('problem').textContent = blockers.join(' ');
184
+ show('problem');
185
+ } else {
186
+ show('installBtn');
187
+ }
188
+
189
+ $('installBtn').addEventListener('click', () => {
190
+ const contents = document.createElement('div');
191
+ contents.className = 'p';
192
+
193
+ const total = missing.length + switchedOff.length + 1;
194
+
195
+ const intro = document.createElement('p');
196
+ intro.textContent = total > 1
197
+ ? `"${item.name}" cannot run without the extensions below, so installing it also changes these, in this order:`
198
+ : `Install "${item.name}"?`;
199
+ contents.append(intro);
200
+
201
+ const step = (title, dependencies) => {
202
+ if(!dependencies.length) return;
203
+ const heading = document.createElement('p');
204
+ heading.className = 'mb0';
205
+ const strong = document.createElement('strong');
206
+ strong.textContent = title;
207
+ heading.append(strong);
208
+ const ol = document.createElement('ol');
209
+ for(const dependency of dependencies){
210
+ const li = document.createElement('li');
211
+ li.textContent = `${dependency.name} ${dependency.version}`;
212
+ ol.append(li);
213
+ }
214
+ contents.append(heading, ol);
215
+ };
216
+ step('Downloaded and installed', missing);
217
+ step('Already installed, will be enabled', switchedOff);
218
+
219
+ const note = document.createElement('p');
220
+ note.className = 'tc-muted';
221
+ note.textContent = 'Extensions are code that runs on your site with its permissions. Only install what you trust.';
222
+ contents.append(note);
223
+
224
+ Dialog.create(contents, {
225
+ title: total > 1 ? `Install ${total} Extensions` : 'Install Extension',
226
+ closeBtn: false,
227
+ overlayClose: false,
228
+ confirmText: total > 1 ? `Install and enable ${total} extensions` : 'Install',
229
+ cancelText: 'Cancel',
230
+ confirmAction: async () => {
231
+ const [installError, result] = await installExtension(name, { withDependencies: true });
232
+ if(installError){
233
+ Toast.error(installError.msg || 'Failed to install the extension');
234
+ return;
235
+ }
236
+ const enabled = result.enabled?.length ? `, enabled ${result.enabled.join(', ')}` : '';
237
+ Toast.success(`Installed ${result.installed?.join(', ') || name}${enabled}`);
238
+ window.location.reload();
239
+ },
240
+ });
241
+ });
242
+
243
+ show('content');
244
+ };
245
+
246
+ if(!name){
247
+ $('notFoundMessage').textContent = 'No extension was chosen.';
248
+ show('notFound');
249
+ } else {
250
+ render();
251
+ }
252
+ </script>
253
+ </content>
254
+ </page>
@@ -11,15 +11,19 @@
11
11
  <script type="module" src="/kempo-ui/components/controls/TcPageSelect.js"></script>
12
12
  <script type="module" src="/kempo-ui/components/controls/TcPageSize.js"></script>
13
13
  <script type="module" src="/admin/components/tableControls/InstallExtension.js"></script>
14
+ <script type="module" src="/admin/components/tableControls/ExtensionDetails.js"></script>
15
+ <script type="module" src="/admin/components/tableControls/ToggleExtension.js"></script>
14
16
  <script type="module" src="/admin/components/tableControls/UninstallExtension.js"></script>
15
17
  <script type="module" src="/kempo/components/Permission.js"></script>
16
18
 
17
19
  <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
18
20
  <h1 class="flex mb0">Extensions</h1>
21
+ <a href="/admin/extensions/browse/" class="no-link"><button class="primary"><k-icon name="extension"></k-icon> Browse extensions</button></a>
19
22
  </div>
20
23
 
21
24
  <h2>Installed Extensions</h2>
22
25
  <k-table id="installedTable" enable-pages page-size="25" placeholder="No extensions are currently installed.">
26
+ <admin-toggle-extension slot="after"></admin-toggle-extension>
23
27
  <admin-uninstall-extension slot="after"></admin-uninstall-extension>
24
28
  <div class="d-f full" style="align-items: center;">
25
29
  <div>
@@ -35,9 +39,11 @@
35
39
  </div>
36
40
  </k-table>
37
41
 
38
- <h2>Available Extensions</h2>
39
- <k-table id="availableTable" enable-pages page-size="25" placeholder="No extensions are available to install. Install a kempo extension package via npm first.">
42
+ <h2>Downloaded, Not Installed</h2>
43
+ <p class="tc-muted">Extension packages already in this site. To find and download more, <a href="/admin/extensions/browse/">browse the marketplace</a>.</p>
44
+ <k-table id="availableTable" enable-pages page-size="25" placeholder="Every downloaded extension is installed.">
40
45
  <admin-install-extension slot="after"></admin-install-extension>
46
+ <admin-extension-details slot="after"></admin-extension-details>
41
47
  <div class="d-f full" style="align-items: center;">
42
48
  <div>
43
49
  <kc-tc-first-page></kc-tc-first-page>
@@ -1,2 +1,2 @@
1
- import s from"../../../../server/utils/permissions/currentUserHasPermission.js";import e from"../../../../server/utils/extensions/uninstallExtension.js";export default async(r,n)=>{const t=r.cookies.session_token,[o,i]=await s(t,"system:extensions:uninstall");if(o)return n.status(o.code).json({error:o.msg});if(!i)return n.status(403).json({error:"Insufficient permissions"});const{name:a,purgeData:u}=r.body,[m,c]=await e({name:a,purgeData:!0===u});if(m)return n.status(m.code).json({error:m.msg});n.json(c)};
1
+ import s from"../../../../server/utils/permissions/currentUserHasPermission.js";import e from"../../../../server/utils/extensions/uninstallExtensionTree.js";export default async(n,t)=>{const r=n.cookies.session_token,[o,i]=await s(r,"system:extensions:uninstall");if(o)return t.status(o.code).json({error:o.msg});if(!i)return t.status(403).json({error:"Insufficient permissions"});const{name:a,purgeData:u,cascade:c}=n.body,[d,m]=await e({name:a,purgeData:!0===u,cascade:!0===c});if(d)return t.status(d.code).json({error:d.msg,dependents:d.dependents,uninstalled:d.uninstalled});t.json(m)};
2
2
  //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\extensions\DELETE.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"src\\kempo\\api\\extensions\\DELETE.js","names":["currentUserHasPermission","uninstallExtension","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","name","purgeData","body","data"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../server/utils/permissions/currentUserHasPermission.js';\r\nimport uninstallExtension from '../../../../server/utils/extensions/uninstallExtension.js';\r\n\r\nexport default async (request, response) => {\r\n const token = request.cookies.session_token;\r\n const [permError, hasPermission] = await currentUserHasPermission(token, 'system:extensions:uninstall');\r\n\r\n if(permError){\r\n return response.status(permError.code).json({ error: permError.msg });\r\n }\r\n\r\n if(!hasPermission){\r\n return response.status(403).json({ error: 'Insufficient permissions' });\r\n }\r\n\r\n const { name, purgeData } = request.body;\r\n const [error, data] = await uninstallExtension({ name, purgeData: purgeData === true });\r\n\r\n if(error){\r\n return response.status(error.code).json({ error: error.msg });\r\n }\r\n\r\n response.json(data);\r\n};\r\n"],"mappings":"OAAOA,MAA8B,0EAC9BC,MAAwB,2EAEhBC,MAAOC,EAASC,KAC7B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBT,EAAyBK,EAAO,+BAEzE,GAAGG,EACD,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGjE,IAAIL,EACF,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG5C,MAAME,KAAEA,EAAIC,UAAEA,GAAcb,EAAQc,MAC7BJ,EAAOK,SAAcjB,EAAmB,CAAEc,OAAMC,WAAyB,IAAdA,IAElE,GAAGH,EACD,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGzDV,EAASQ,KAAKM","ignoreList":[]}
1
+ {"version":3,"file":"src\\kempo\\api\\extensions\\DELETE.js","names":["currentUserHasPermission","uninstallExtension","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","name","purgeData","cascade","body","data","dependents","uninstalled"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../server/utils/permissions/currentUserHasPermission.js';\nimport uninstallExtension from '../../../../server/utils/extensions/uninstallExtensionTree.js';\n\n/*\n DELETE /kempo/api/extensions { name, purgeData?, cascade? }\n\n Without `cascade`, an extension that others require is refused (409) with the whole tree of what\n depends on it as `dependents`. With `cascade: true` they are uninstalled first, then this one.\n*/\nexport default async (request, response) => {\n const token = request.cookies.session_token;\n const [permError, hasPermission] = await currentUserHasPermission(token, 'system:extensions:uninstall');\n\n if(permError){\n return response.status(permError.code).json({ error: permError.msg });\n }\n\n if(!hasPermission){\n return response.status(403).json({ error: 'Insufficient permissions' });\n }\n\n const { name, purgeData, cascade } = request.body;\n const [error, data] = await uninstallExtension({ name, purgeData: purgeData === true, cascade: cascade === true });\n\n if(error){\n return response.status(error.code).json({ error: error.msg, dependents: error.dependents, uninstalled: error.uninstalled });\n }\n\n response.json(data);\n};\n"],"mappings":"OAAOA,MAA8B,0EAC9BC,MAAwB,+EAQhBC,MAAOC,EAASC,KAC7B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBT,EAAyBK,EAAO,+BAEzE,GAAGG,EACD,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGjE,IAAIL,EACF,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG5C,MAAME,KAAEA,EAAIC,UAAEA,EAASC,QAAEA,GAAYd,EAAQe,MACtCL,EAAOM,SAAclB,EAAmB,CAAEc,OAAMC,WAAyB,IAAdA,EAAoBC,SAAqB,IAAZA,IAE/F,GAAGJ,EACD,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,IAAKM,WAAYP,EAAMO,WAAYC,YAAaR,EAAMQ,cAG/GjB,EAASQ,KAAKO","ignoreList":[]}
@@ -1,2 +1,2 @@
1
- import s from"../../../../server/utils/permissions/currentUserHasPermission.js";export default async(e,t)=>{const r=e.cookies.session_token,[n,o]=await s(r,"system:extensions:install");if(n)return t.status(n.code).json({error:n.msg});if(!o)return t.status(403).json({error:"Insufficient permissions"});const{name:i}=e.body,{default:a}=await(import("../../../../server/utils/extensions/installExtension.js")),[u,m]=await a({name:i});if(u)return t.status(u.code).json({error:u.msg});t.json(m)};
1
+ import e from"../../../../server/utils/permissions/currentUserHasPermission.js";export default async(s,t)=>{const i=s.cookies.session_token,[n,r]=await e(i,"system:extensions:install");if(n)return t.status(n.code).json({error:n.msg});if(!r)return t.status(403).json({error:"Insufficient permissions"});const{name:a,withDependencies:o}=s.body||{},{default:l}=await(import("../../../../server/utils/extensions/getMarketplaceClient.js")),{default:u}=await(import("../../../../server/utils/extensions/getSiteInstallId.js")),{default:d}=await(import("../../../../server/utils/extensions/installFromCatalog.js")),[,m]=await u(),[c,f]=await d({name:a,withDependencies:!0===o,client:await l(),installId:m});if(c)return t.status(c.code).json({error:c.msg,needs:c.needs,installed:c.installed});t.json(f)};
2
2
  //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\extensions\POST.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"src\\kempo\\api\\extensions\\POST.js","names":["currentUserHasPermission","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","name","body","default","installExtension","data"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../server/utils/permissions/currentUserHasPermission.js';\r\n\r\nexport default async (request, response) => {\r\n const token = request.cookies.session_token;\r\n const [permError, hasPermission] = await currentUserHasPermission(token, 'system:extensions:install');\r\n\r\n if(permError){\r\n return response.status(permError.code).json({ error: permError.msg });\r\n }\r\n\r\n if(!hasPermission){\r\n return response.status(403).json({ error: 'Insufficient permissions' });\r\n }\r\n\r\n const { name } = request.body;\r\n const { default: installExtension } = await import('../../../../server/utils/extensions/installExtension.js');\r\n const [error, data] = await installExtension({ name });\r\n\r\n if(error){\r\n return response.status(error.code).json({ error: error.msg });\r\n }\r\n\r\n response.json(data);\r\n};\r\n"],"mappings":"OAAOA,MAA8B,kFAEtBC,MAAOC,EAASC,KAC7B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBR,EAAyBI,EAAO,6BAEzE,GAAGG,EACD,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGjE,IAAIL,EACF,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG5C,MAAME,KAAEA,GAASZ,EAAQa,MACjBC,QAASC,gBAAkC,6DAC5CL,EAAOM,SAAcD,EAAiB,CAAEH,SAE/C,GAAGF,EACD,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGzDV,EAASQ,KAAKO","ignoreList":[]}
1
+ {"version":3,"file":"src\\kempo\\api\\extensions\\POST.js","names":["currentUserHasPermission","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","name","withDependencies","body","default","getMarketplaceClient","getSiteInstallId","installFromCatalog","installId","data","client","needs","installed"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../server/utils/permissions/currentUserHasPermission.js';\n\n/*\n POST /kempo/api/extensions { name, withDependencies? }\n\n Installs an extension. One that is not in node_modules yet is downloaded first, but only if the\n marketplace lists it: this route never runs `npm install` for a name the caller made up. With\n `withDependencies`, the extensions it needs are installed first, in order; the admin shows that list\n for approval before sending it.\n*/\nexport default async (request, response) => {\n const token = request.cookies.session_token;\n const [permError, hasPermission] = await currentUserHasPermission(token, 'system:extensions:install');\n\n if(permError){\n return response.status(permError.code).json({ error: permError.msg });\n }\n\n if(!hasPermission){\n return response.status(403).json({ error: 'Insufficient permissions' });\n }\n\n const { name, withDependencies } = request.body || {};\n\n const { default: getMarketplaceClient } = await import('../../../../server/utils/extensions/getMarketplaceClient.js');\n const { default: getSiteInstallId } = await import('../../../../server/utils/extensions/getSiteInstallId.js');\n const { default: installFromCatalog } = await import('../../../../server/utils/extensions/installFromCatalog.js');\n\n const [, installId] = await getSiteInstallId();\n const [error, data] = await installFromCatalog({\n name,\n withDependencies: withDependencies === true,\n client: await getMarketplaceClient(),\n installId,\n });\n\n if(error){\n return response.status(error.code).json({ error: error.msg, needs: error.needs, installed: error.installed });\n }\n\n response.json(data);\n};\n"],"mappings":"OAAOA,MAA8B,kFAUtBC,MAAOC,EAASC,KAC7B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBR,EAAyBI,EAAO,6BAEzE,GAAGG,EACD,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGjE,IAAIL,EACF,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG5C,MAAME,KAAEA,EAAIC,iBAAEA,GAAqBb,EAAQc,MAAQ,CAAC,GAE5CC,QAASC,gBAAsC,iEAC/CD,QAASE,gBAAkC,6DAC3CF,QAASG,gBAAoC,+DAE9C,CAAEC,SAAmBF,KACrBP,EAAOU,SAAcF,EAAmB,CAC7CN,OACAC,kBAAuC,IAArBA,EAClBQ,aAAcL,IACdG,cAGF,GAAGT,EACD,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,IAAKW,MAAOZ,EAAMY,MAAOC,UAAWb,EAAMa,YAGnGtB,EAASQ,KAAKW","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ import e from"../../../../../../server/utils/permissions/currentUserHasPermission.js";import s from"../../../../../../server/utils/extensions/getDependentTree.js";export default async(r,n)=>{const t=r.cookies.session_token,[i,o]=await e(t,"system:extensions:manage");if(i)return n.status(i.code).json({error:i.msg});if(!o)return n.status(403).json({error:"Insufficient permissions"});const a=await s(r.params.name,{includeDisabled:"true"===r.query.includeDisabled});n.json({dependents:a})};
2
+ //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\extensions\[name]\dependents\GET.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"src\\kempo\\api\\extensions\\[name]\\dependents\\GET.js","names":["currentUserHasPermission","getDependentTree","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","dependents","params","name","includeDisabled","query"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../../../server/utils/permissions/currentUserHasPermission.js';\nimport getDependentTree from '../../../../../../server/utils/extensions/getDependentTree.js';\n\n/*\n GET /kempo/api/extensions/<name>/dependents[?includeDisabled=true]\n\n What would have to go along with this extension: the extensions that require it, and those that\n require them, and so on, deepest first. Each says which of the affected ones it requires directly and\n whether it is enabled.\n\n By default only enabled ones are listed (what disabling affects). `includeDisabled=true` lists every\n installed one, which is what uninstalling affects.\n*/\nexport default async (request, response) => {\n const token = request.cookies.session_token;\n const [permError, hasPermission] = await currentUserHasPermission(token, 'system:extensions:manage');\n\n if(permError){\n return response.status(permError.code).json({ error: permError.msg });\n }\n\n if(!hasPermission){\n return response.status(403).json({ error: 'Insufficient permissions' });\n }\n\n const dependents = await getDependentTree(request.params.name, { includeDisabled: request.query.includeDisabled === 'true' });\n response.json({ dependents });\n};\n"],"mappings":"OAAOA,MAA8B,gFAC9BC,MAAsB,+EAYdC,MAAOC,EAASC,KAC7B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBT,EAAyBK,EAAO,4BAEzE,GAAGG,EACD,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGjE,IAAIL,EACF,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG5C,MAAME,QAAmBd,EAAiBE,EAAQa,OAAOC,KAAM,CAAEC,gBAAmD,SAAlCf,EAAQgB,MAAMD,kBAChGd,EAASQ,KAAK,CAAEG","ignoreList":[]}
@@ -1,2 +1,2 @@
1
- import s from"../../../../../../server/utils/permissions/currentUserHasPermission.js";import e from"../../../../../../server/utils/extensions/disableExtension.js";export default async(r,n)=>{const o=r.cookies.session_token,[t,i]=await s(o,"system:extensions:manage");if(t)return n.status(t.code).json({error:t.msg});if(!i)return n.status(403).json({error:"Insufficient permissions"});const a=r.params.name,[m,u]=await e({name:a});if(m)return n.status(m.code).json({error:m.msg});n.json(u)};
1
+ import s from"../../../../../../server/utils/permissions/currentUserHasPermission.js";import e from"../../../../../../server/utils/extensions/disableExtensionTree.js";export default async(r,n)=>{const t=r.cookies.session_token,[o,i]=await s(t,"system:extensions:manage");if(o)return n.status(o.code).json({error:o.msg});if(!i)return n.status(403).json({error:"Insufficient permissions"});const a=r.params.name,[m,c]=await e({name:a,cascade:!0===r.body?.cascade});if(m)return n.status(m.code).json({error:m.msg,dependents:m.dependents});n.json(c)};
2
2
  //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\extensions\[name]\disable\POST.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"src\\kempo\\api\\extensions\\[name]\\disable\\POST.js","names":["currentUserHasPermission","disableExtension","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","name","params","data"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../../../server/utils/permissions/currentUserHasPermission.js';\r\nimport disableExtension from '../../../../../../server/utils/extensions/disableExtension.js';\r\n\r\nexport default async (request, response) => {\r\n const token = request.cookies.session_token;\r\n const [permError, hasPermission] = await currentUserHasPermission(token, 'system:extensions:manage');\r\n\r\n if(permError){\r\n return response.status(permError.code).json({ error: permError.msg });\r\n }\r\n\r\n if(!hasPermission){\r\n return response.status(403).json({ error: 'Insufficient permissions' });\r\n }\r\n\r\n const name = request.params.name;\r\n const [error, data] = await disableExtension({ name });\r\n\r\n if(error){\r\n return response.status(error.code).json({ error: error.msg });\r\n }\r\n\r\n response.json(data);\r\n};\r\n"],"mappings":"OAAOA,MAA8B,gFAC9BC,MAAsB,+EAEdC,MAAOC,EAASC,KAC7B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBT,EAAyBK,EAAO,4BAEzE,GAAGG,EACD,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGjE,IAAIL,EACF,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG5C,MAAME,EAAOZ,EAAQa,OAAOD,MACrBF,EAAOI,SAAchB,EAAiB,CAAEc,SAE/C,GAAGF,EACD,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGzDV,EAASQ,KAAKK","ignoreList":[]}
1
+ {"version":3,"file":"src\\kempo\\api\\extensions\\[name]\\disable\\POST.js","names":["currentUserHasPermission","disableExtension","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","name","params","data","cascade","body","dependents"],"sources":["0"],"sourcesContent":["import currentUserHasPermission from '../../../../../../server/utils/permissions/currentUserHasPermission.js';\nimport disableExtension from '../../../../../../server/utils/extensions/disableExtensionTree.js';\n\n/*\n POST /kempo/api/extensions/<name>/disable { cascade? }\n\n Without `cascade`, an extension that others require is refused (409) with the whole tree of what\n depends on it as `dependents`. With `cascade: true` it and all of them are disabled together.\n*/\nexport default async (request, response) => {\n const token = request.cookies.session_token;\n const [permError, hasPermission] = await currentUserHasPermission(token, 'system:extensions:manage');\n\n if(permError){\n return response.status(permError.code).json({ error: permError.msg });\n }\n\n if(!hasPermission){\n return response.status(403).json({ error: 'Insufficient permissions' });\n }\n\n const name = request.params.name;\n const [error, data] = await disableExtension({ name, cascade: request.body?.cascade === true });\n\n if(error){\n return response.status(error.code).json({ error: error.msg, dependents: error.dependents });\n }\n\n response.json(data);\n};\n"],"mappings":"OAAOA,MAA8B,gFAC9BC,MAAsB,mFAQdC,MAAOC,EAASC,KAC7B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBT,EAAyBK,EAAO,4BAEzE,GAAGG,EACD,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGjE,IAAIL,EACF,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG5C,MAAME,EAAOZ,EAAQa,OAAOD,MACrBF,EAAOI,SAAchB,EAAiB,CAAEc,OAAMG,SAAmC,IAA1Bf,EAAQgB,MAAMD,UAE5E,GAAGL,EACD,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,IAAKM,WAAYP,EAAMO,aAGhFhB,EAASQ,KAAKK","ignoreList":[]}