kempo 4.9.5 → 4.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/admin/accounts/groups/[name]/index.page.html +5 -5
- package/dist/admin/accounts/groups/index.page.html +12 -10
- package/dist/admin/accounts/permissions/index.page.html +1 -1
- package/dist/admin/accounts/users/[id]/index.page.html +81 -63
- package/dist/admin/accounts/users/index.page.html +7 -9
- package/dist/admin/components/EmailTemplates.js +11 -19
- package/dist/admin/components/EmailTemplates.js.map +1 -1
- package/dist/admin/components/Emails.js +32 -29
- package/dist/admin/components/Emails.js.map +1 -1
- package/dist/admin/components/Updates.js +17 -22
- package/dist/admin/components/Updates.js.map +1 -1
- package/dist/admin/components/settings/SettingsPanel.js +1 -1
- package/dist/admin/components/settings/SettingsPanel.js.map +1 -1
- package/dist/admin/components/tableControls/EditEmailTemplate.js +2 -0
- package/dist/admin/components/tableControls/EditEmailTemplate.js.map +1 -0
- package/dist/admin/components/tableControls/UpdateItem.js +2 -0
- package/dist/admin/components/tableControls/UpdateItem.js.map +1 -0
- package/dist/admin/components/tableControls/UpdateSelected.js +2 -0
- package/dist/admin/components/tableControls/UpdateSelected.js.map +1 -0
- package/dist/admin/components/tableControls/ViewEmail.js +2 -0
- package/dist/admin/components/tableControls/ViewEmail.js.map +1 -0
- package/dist/admin/content/fragments/index.page.html +12 -12
- package/dist/admin/content/globals/index.page.html +12 -11
- package/dist/admin/content/pages/index.page.html +11 -23
- package/dist/admin/content/templates/index.page.html +13 -25
- package/dist/admin/emails/index.page.html +1 -1
- package/dist/admin/extensions/index.page.html +10 -9
- package/dist/admin/settings/index.page.html +13 -3
- package/middleware/kempo.js +1 -0
- package/package.json +2 -2
- package/server/utils/sessions/pruneExpiredSessions.js +32 -0
- package/server/utils/settings/settingRules.js +1 -0
- package/server/utils/settings/systemSettings.js +1 -0
- package/dist/admin/components/settings/AdminSettings.js +0 -7
- package/dist/admin/components/settings/AdminSettings.js.map +0 -1
- package/dist/admin/components/tableControls/ExtensionDetails.js +0 -2
- package/dist/admin/components/tableControls/ExtensionDetails.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src\\admin\\components\\Emails.js","names":["ShadowComponent","html","nothing","Toast","listEmails","getEmail","retryEmail","sendTestEmail","getAllCurrentUserPermissions","STATUS_CLASS","sent","failed","sending","queued","when","value","Date","toLocaleString","Emails","static","emails","state","total","offset","status","search","loading","error","canManage","open","testTo","busy","constructor","super","this","connectedCallback","then","data","permissions","includes","load","async","undefined","limit","msg","filter","Object","assign","changes","page","Math","max","step","show","found","id","updateComplete","renderRoot","querySelector","retry","result","success","close","openTest","confirmTest","event","keepDialogOpen","sendTest","trim","renderDetail","mail","subject","attempts","provider","recipient","sender","type","createdAt","sentAt","lastError","renderTest","target","render","last","min","map","length","preventDefault","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Spinner.js';\nimport '/kempo-ui/components/Dialog.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport { listEmails, getEmail, retryEmail, sendTestEmail, getAllCurrentUserPermissions } from '/kempo/sdk.js';\n\n/*\n The log of emails, served at /admin/components/Emails.js:\n\n <k-admin-emails></k-admin-emails>\n\n Every email kempo was asked to send is here, newest first, with how it went: `queued` (waiting for a sender or a retry), `sending`,\n `sent` or `failed`. Click one to read it, with what it said and the reason it did not go. Someone who can manage emails can try a\n waiting or failed one again and send a test email, which is the quickest way to see that an email extension is set up.\n*/\nconst PAGE_SIZE = 25;\nconst STATUS_CLASS = { sent: 'tc-success', failed: 'tc-danger', sending: 'tc-warning', queued: 'tc-muted' };\n\nconst when = value => (value ? new Date(value).toLocaleString() : '');\n\nexport default class Emails extends ShadowComponent {\n static properties = {\n emails: { state: true },\n total: { state: true },\n offset: { state: true },\n status: { state: true },\n search: { state: true },\n loading: { state: true },\n error: { state: true },\n canManage: { state: true },\n open: { state: true },\n testTo: { state: true },\n busy: { state: true },\n };\n\n constructor(){\n super();\n this.emails = [];\n this.total = 0;\n this.offset = 0;\n this.status = '';\n this.search = '';\n this.loading = true;\n this.error = '';\n this.canManage = false;\n this.open = null;\n this.testTo = '';\n this.busy = false;\n }\n\n /*\n Lifecycle\n */\n connectedCallback(){\n super.connectedCallback();\n getAllCurrentUserPermissions().then(([, data]) => { this.canManage = (data?.permissions ?? []).includes('system:emails:manage'); });\n this.load();\n }\n\n /*\n Loading\n */\n load = async () => {\n this.loading = true;\n const [error, data] = await listEmails({ status: this.status || undefined, search: this.search || undefined, limit: PAGE_SIZE, offset: this.offset });\n this.loading = false;\n if(error){\n this.error = error.msg || 'The emails could not be loaded';\n return;\n }\n this.error = '';\n this.emails = data.emails;\n this.total = data.total;\n };\n\n filter = async changes => {\n Object.assign(this, changes, { offset: 0 });\n await this.load();\n };\n\n page = async step => {\n this.offset = Math.max(0, this.offset + step * PAGE_SIZE);\n await this.load();\n };\n\n /*\n One email\n */\n show = async id => {\n const [error, found] = await getEmail(id);\n if(error){\n Toast.error(error.msg);\n return;\n }\n this.open = found;\n await this.updateComplete;\n this.renderRoot.querySelector('#emailDialog').open();\n };\n\n retry = async () => {\n this.busy = true;\n const [error, result] = await retryEmail(this.open.id);\n this.busy = false;\n if(error) Toast.error(error.msg);\n else if(result.sent) Toast.success('The email was sent');\n else Toast.error(result.error || 'The email could not be sent');\n this.renderRoot.querySelector('#emailDialog').close();\n await this.load();\n };\n\n /*\n Test email\n */\n openTest = () => this.renderRoot.querySelector('#testDialog').open();\n\n confirmTest = event => {\n event.keepDialogOpen = true;\n this.sendTest();\n };\n\n sendTest = async () => {\n this.busy = true;\n const [error, result] = await sendTestEmail(this.testTo.trim());\n this.busy = false;\n if(error){\n Toast.error(error.msg);\n return;\n }\n this.renderRoot.querySelector('#testDialog').close();\n if(result.sent) Toast.success(`A test email was sent to ${this.testTo.trim()}`);\n else Toast.error(result.error || 'The test email could not be sent');\n await this.load();\n };\n\n /*\n Rendering\n */\n renderDetail(){\n const mail = this.open;\n if(!mail) return nothing;\n return html`\n <k-dialog id=\"emailDialog\" style=\"--width: 60rem;\" cancel-text=\"Close\">\n <h2 slot=\"title\" class=\"h5 pyh px m0\">${mail.subject}</h2>\n <div class=\"p\">\n <p class=\"mb\"><strong class=${STATUS_CLASS[mail.status] ?? ''}>${mail.status}</strong>${mail.attempts ? html` · ${mail.attempts} ${mail.attempts === 1 ? 'try' : 'tries'}` : nothing}${mail.provider ? html` · sent by ${mail.provider}` : nothing}</p>\n <p class=\"mb\">To <strong>${mail.recipient}</strong>, from ${mail.sender}<br><span class=\"tc-muted\">${mail.type} · created ${when(mail.createdAt)}${mail.sentAt ? ` · sent ${when(mail.sentAt)}` : ''}</span></p>\n ${mail.lastError ? html`<p class=\"tc-danger mb\" role=\"alert\">${mail.lastError}</p>` : nothing}\n ${this.canManage && ['queued', 'failed'].includes(mail.status) ? html`<p class=\"mb\"><button class=\"primary\" ?disabled=${this.busy} @click=${this.retry}>${this.busy ? 'Trying…' : 'Try again'}</button></p>` : nothing}\n <iframe title=\"The email\" sandbox=\"\" .srcdoc=${mail.html} class=\"full b r\" height=\"420\"></iframe>\n </div>\n </k-dialog>\n `;\n }\n\n renderTest(){\n return html`\n <k-dialog id=\"testDialog\" confirm-text=${this.busy ? 'Sending…' : 'Send test email'} confirm-classes=\"primary ml\" cancel-text=\"Cancel\" .confirmAction=${this.confirmTest}>\n <h2 slot=\"title\" class=\"h5 pyh px m0\">Send a test email</h2>\n <div class=\"p\">\n <label class=\"mb d-b\">Send it to<input type=\"email\" .value=${this.testTo} @input=${event => { this.testTo = event.target.value; }}></label>\n <p class=\"tc-muted\">It goes out through the email extension that is installed, and shows in the log below.</p>\n </div>\n </k-dialog>\n `;\n }\n\n render(){\n const last = Math.min(this.offset + PAGE_SIZE, this.total);\n return html`\n <div class=\"d-f mb\">\n <label class=\"mr\">Status\n <select @change=${event => this.filter({ status: event.target.value })}>\n ${['', 'queued', 'sending', 'sent', 'failed'].map(status => html`<option value=${status} ?selected=${this.status === status}>${status || 'All'}</option>`)}\n </select>\n </label>\n <label class=\"mr\">Search\n <input type=\"search\" placeholder=\"Address or subject\" .value=${this.search} @change=${event => this.filter({ search: event.target.value })}>\n </label>\n ${this.canManage ? html`<button class=\"mb\" @click=${this.openTest}>Send a test email</button>` : nothing}\n </div>\n ${this.error ? html`<p class=\"tc-danger\" role=\"alert\">${this.error}</p>` : nothing}\n ${this.loading ? html`<k-spinner></k-spinner>` : this.emails.length ? html`\n <table class=\"mb\">\n <thead><tr><th>Status</th><th>To</th><th>Subject</th><th>Type</th><th>Created</th></tr></thead>\n <tbody>${this.emails.map(mail => html`<tr>\n <td><strong class=${STATUS_CLASS[mail.status] ?? ''}>${mail.status}</strong></td>\n <td>${mail.recipient}</td>\n <td><a href=\"#\" @click=${event => { event.preventDefault(); this.show(mail.id); }}>${mail.subject}</a>${mail.lastError ? html`<br><span class=\"tc-danger\">${mail.lastError}</span>` : nothing}</td>\n <td class=\"tc-muted\">${mail.type}</td>\n <td class=\"tc-muted\">${when(mail.createdAt)}</td>\n </tr>`)}</tbody>\n </table>\n <p class=\"d-f mb\"><button ?disabled=${this.offset === 0} @click=${() => this.page(-1)}>Newer</button><span class=\"tc-muted ph\">${this.offset + 1}–${last} of ${this.total}</span><button ?disabled=${last >= this.total} @click=${() => this.page(1)}>Older</button></p>\n ` : html`<p class=\"tc-muted\">No emails have been sent yet.</p>`}\n ${this.renderDetail()}\n ${this.renderTest()}\n `;\n }\n}\n\ncustomElements.define('k-admin-emails', Emails);\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\Emails.js","names":["ShadowComponent","html","nothing","Toast","listEmails","getEmail","retryEmail","sendTestEmail","getAllCurrentUserPermissions","STATUS_CLASS","sent","failed","sending","queued","when","value","Date","toLocaleString","fields","name","label","size","editable","calculator","mail","lastError","subject","formatter","Emails","static","status","state","search","loading","error","canManage","open","testTo","busy","constructor","super","this","connectedCallback","then","data","permissions","includes","pane","closest","active","load","addEventListener","detail","tab","async","updateComplete","$table","renderRoot","querySelector","query","undefined","limit","offset","msg","setData","records","emails","setupFetchRecords","total","start","count","fetchError","page","filter","changes","Object","assign","show","found","id","retry","result","success","close","openTest","confirmTest","event","keepDialogOpen","sendTest","trim","renderDetail","attempts","provider","recipient","sender","type","createdAt","sentAt","renderTest","target","render","map","record","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Spinner.js';\nimport '/kempo-ui/components/Dialog.js';\nimport '/kempo-ui/components/Table.js';\nimport '/kempo-ui/components/controls/TcFirstPage.js';\nimport '/kempo-ui/components/controls/TcPrevPage.js';\nimport '/kempo-ui/components/controls/TcNextPage.js';\nimport '/kempo-ui/components/controls/TcLastPage.js';\nimport '/kempo-ui/components/controls/TcPageSelect.js';\nimport '/kempo-ui/components/controls/TcPageSize.js';\nimport '/admin/components/tableControls/ViewEmail.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport { listEmails, getEmail, retryEmail, sendTestEmail, getAllCurrentUserPermissions } from '/kempo/sdk.js';\n\n/*\n The log of emails, served at /admin/components/Emails.js:\n\n <k-admin-emails></k-admin-emails>\n\n Every email kempo was asked to send is here, newest first, with how it went: `queued` (waiting for a sender or a retry), `sending`,\n `sent` or `failed`. Click one to read it, with what it said and the reason it did not go. Someone who can manage emails can try a\n waiting or failed one again and send a test email, which is the quickest way to see that an email extension is set up.\n*/\nconst PAGE_SIZE = 25;\nconst STATUS_CLASS = { sent: 'tc-success', failed: 'tc-danger', sending: 'tc-warning', queued: 'tc-muted' };\n\nconst when = value => (value ? new Date(value).toLocaleString() : '');\n\nconst fields = [\n { name: 'status', label: 'Status', size: 110, editable: false },\n { name: 'recipient', label: 'To', size: 240, editable: false },\n { name: 'subject', label: 'Subject', editable: false, calculator: mail => mail.lastError ? `${mail.subject} — ${mail.lastError}` : mail.subject },\n { name: 'type', label: 'Type', size: 140, editable: false },\n { name: 'createdAt', label: 'Created', size: 190, editable: false, formatter: when },\n];\n\nexport default class Emails extends ShadowComponent {\n static properties = {\n status: { state: true },\n search: { state: true },\n loading: { state: true },\n error: { state: true },\n canManage: { state: true },\n open: { state: true },\n testTo: { state: true },\n busy: { state: true },\n };\n\n constructor(){\n super();\n this.status = '';\n this.search = '';\n this.loading = true;\n this.error = '';\n this.canManage = false;\n this.open = null;\n this.testTo = '';\n this.busy = false;\n }\n\n /*\n Lifecycle\n */\n connectedCallback(){\n super.connectedCallback();\n getAllCurrentUserPermissions().then(([, data]) => { this.canManage = (data?.permissions ?? []).includes('system:emails:manage'); });\nconst pane = this.closest('k-tab-content');\n // Loads when its tab is first opened, so a tab nobody opens makes no requests.\n if(!pane || pane.active) this.load();\n else this.closest('k-tabs')?.addEventListener('tabfirstshown', ({ detail }) => {\n if(detail.tab === pane.name) this.load();\n });\n }\n\n /*\n Loading\n */\n load = async () => {\n await this.updateComplete;\n const $table = this.renderRoot.querySelector('#emails');\n const query = { status: this.status || undefined, search: this.search || undefined };\n this.loading = true;\n const [error, data] = await listEmails({ ...query, limit: PAGE_SIZE, offset: 0 });\n this.loading = false;\n if(error){\n this.error = error.msg || 'The emails could not be loaded';\n return;\n }\n this.error = '';\n $table.setData({ records: data.emails, fields });\n $table.setupFetchRecords(data.total, async (start, count) => {\n const [fetchError, page] = await listEmails({ ...query, limit: count, offset: start });\n return fetchError ? [] : page.emails;\n });\n };\n\n filter = changes => {\n Object.assign(this, changes);\n this.load();\n };\n\n /*\n One email\n */\n show = async id => {\n const [error, found] = await getEmail(id);\n if(error){\n Toast.error(error.msg);\n return;\n }\n this.open = found;\n await this.updateComplete;\n this.renderRoot.querySelector('#emailDialog').open();\n };\n\n retry = async () => {\n this.busy = true;\n const [error, result] = await retryEmail(this.open.id);\n this.busy = false;\n if(error) Toast.error(error.msg);\n else if(result.sent) Toast.success('The email was sent');\n else Toast.error(result.error || 'The email could not be sent');\n this.renderRoot.querySelector('#emailDialog').close();\n await this.load();\n };\n\n /*\n Test email\n */\n openTest = () => this.renderRoot.querySelector('#testDialog').open();\n\n confirmTest = event => {\n event.keepDialogOpen = true;\n this.sendTest();\n };\n\n sendTest = async () => {\n this.busy = true;\n const [error, result] = await sendTestEmail(this.testTo.trim());\n this.busy = false;\n if(error){\n Toast.error(error.msg);\n return;\n }\n this.renderRoot.querySelector('#testDialog').close();\n if(result.sent) Toast.success(`A test email was sent to ${this.testTo.trim()}`);\n else Toast.error(result.error || 'The test email could not be sent');\n await this.load();\n };\n\n /*\n Rendering\n */\n renderDetail(){\n const mail = this.open;\n if(!mail) return nothing;\n return html`\n <k-dialog id=\"emailDialog\" style=\"--width: 60rem;\" cancel-text=\"Close\">\n <h2 slot=\"title\" class=\"h5 pyh px m0\">${mail.subject}</h2>\n <div class=\"p\">\n <p class=\"mb\"><strong class=${STATUS_CLASS[mail.status] ?? ''}>${mail.status}</strong>${mail.attempts ? html` · ${mail.attempts} ${mail.attempts === 1 ? 'try' : 'tries'}` : nothing}${mail.provider ? html` · sent by ${mail.provider}` : nothing}</p>\n <p class=\"mb\">To <strong>${mail.recipient}</strong>, from ${mail.sender}<br><span class=\"tc-muted\">${mail.type} · created ${when(mail.createdAt)}${mail.sentAt ? ` · sent ${when(mail.sentAt)}` : ''}</span></p>\n ${mail.lastError ? html`<p class=\"tc-danger mb\" role=\"alert\">${mail.lastError}</p>` : nothing}\n ${this.canManage && ['queued', 'failed'].includes(mail.status) ? html`<p class=\"mb\"><button class=\"primary\" ?disabled=${this.busy} @click=${this.retry}>${this.busy ? 'Trying…' : 'Try again'}</button></p>` : nothing}\n <iframe title=\"The email\" sandbox=\"\" .srcdoc=${mail.html} class=\"full b r\" height=\"420\"></iframe>\n </div>\n </k-dialog>\n `;\n }\n\n renderTest(){\n return html`\n <k-dialog id=\"testDialog\" confirm-text=${this.busy ? 'Sending…' : 'Send test email'} confirm-classes=\"primary ml\" cancel-text=\"Cancel\" .confirmAction=${this.confirmTest}>\n <h2 slot=\"title\" class=\"h5 pyh px m0\">Send a test email</h2>\n <div class=\"p\">\n <label class=\"mb d-b\">Send it to<input type=\"email\" .value=${this.testTo} @input=${event => { this.testTo = event.target.value; }}></label>\n <p class=\"tc-muted\">It goes out through the email extension that is installed, and shows in the log below.</p>\n </div>\n </k-dialog>\n `;\n }\n\n render(){\n return html`\n <div class=\"d-f mb\">\n <label class=\"mr\">Status\n <select @change=${event => this.filter({ status: event.target.value })}>\n ${['', 'queued', 'sending', 'sent', 'failed'].map(status => html`<option value=${status} ?selected=${this.status === status}>${status || 'All'}</option>`)}\n </select>\n </label>\n <label class=\"mr\">Search\n <input type=\"search\" placeholder=\"Address or subject\" .value=${this.search} @change=${event => this.filter({ search: event.target.value })}>\n </label>\n ${this.canManage ? html`<button class=\"mb\" @click=${this.openTest}>Send a test email</button>` : nothing}\n </div>\n ${this.error ? html`<p class=\"tc-danger\" role=\"alert\">${this.error}</p>` : nothing}\n ${this.loading ? html`<k-spinner></k-spinner>` : nothing}\n <k-table id=\"emails\" enable-pages page-size=\"25\" page-size-options=\"[10,25,50]\" placeholder=\"No emails have been sent yet.\" @emailView=${event => this.show(event.detail.record.id)}>\n <admin-view-email slot=\"before\"></admin-view-email>\n <div class=\"d-f full\" style=\"align-items: center;\">\n <div>\n <kc-tc-first-page></kc-tc-first-page>\n <kc-tc-prev-page></kc-tc-prev-page>\n <kc-tc-next-page></kc-tc-next-page>\n <kc-tc-last-page></kc-tc-last-page>\n </div>\n <div class=\"flex\"></div>\n <div><kc-tc-page-select></kc-tc-page-select></div>\n <div class=\"flex\"></div>\n <div><kc-tc-page-size></kc-tc-page-size></div>\n </div>\n </k-table>\n ${this.renderDetail()}\n ${this.renderTest()}\n `;\n }\n}\n\ncustomElements.define('k-admin-emails', Emails);\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing as s}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Spinner.js";import"/kempo-ui/components/Dialog.js";import"/kempo-ui/components/Icon.js";import a from"/kempo-ui/components/Toast.js";import{getUpdateStatus as n,checkForUpdate as i,prepareUpdate as r,applyUpdate as o,rollbackUpdate as
|
|
1
|
+
import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing as s}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Spinner.js";import"/kempo-ui/components/Dialog.js";import"/kempo-ui/components/Icon.js";import"/kempo-ui/components/Table.js";import"/admin/components/tableControls/UpdateItem.js";import"/admin/components/tableControls/UpdateSelected.js";import a from"/kempo-ui/components/Toast.js";import{getUpdateStatus as n,checkForUpdate as i,prepareUpdate as r,applyUpdate as o,rollbackUpdate as l,discardUpdate as c}from"/kempo/sdk.js";const d="kempo-after-update",h=e=>e?new Date(e).toLocaleString():"never",p=e=>new Promise(t=>setTimeout(t,e)),m=(e,t,s)=>`${e} ${1===e?t:s}`,u=[{name:"name",label:"Name",editable:!1,calculator:e=>`${e.name}${e.needs?` · needs Kempo ${e.needs}`:""}${e.major?" · breaking release":""}`},{name:"current",label:"Current",size:140,editable:!1,calculator:e=>e.from},{name:"next",label:"New",size:140,editable:!1,calculator:e=>e.to}];export default class g extends e{static properties={status:{state:!0},error:{state:!0},checking:{state:!0},phase:{state:!0},stalled:{state:!0},action:{state:!0},pending:{state:!0}};constructor(){super(),this.status=null,this.error="",this.checking=!1,this.phase="idle",this.stalled=!1,this.action="update",this.pending=[]}connectedCallback(){super.connectedCallback(),this.load()}load=async()=>{const[e,t]=await n();e?this.error=e.msg||"The update status could not be loaded":(this.error="",this.status=t,this.reportOutcome(),t.switching&&!this.recheck&&(this.recheck=setTimeout(()=>{this.recheck=null,this.load()},5e3)))};updatable=e=>[...e.updateAvailable?[{key:"kempo",name:"Kempo",from:e.runningVersion,to:e.latestVersion,major:e.major}]:[],...(e.extensionUpdates?.updates??[]).map(e=>({key:e.name,name:e.name,from:e.installedVersion,to:e.latestVersion,major:e.major,needs:e.kempoRequired}))];async renderTable(){const e=this.renderRoot.querySelector("#updates");if(!e||!this.status)return;const t=this.updatable(this.status),s=t.map(e=>`${e.key}:${e.from}>${e.to}`).join("|");e.dataset.signature===s&&e.records?.length||(e.dataset.signature=s,e.setData({records:t,fields:u}),await e.updateComplete,e.selectAllOnPage())}updated(e){(e.has("status")||e.has("phase"))&&this.renderTable()}selectedItems=()=>this.renderRoot.querySelector("#updates")?.getSelectedRecords()??[];needsKempo=e=>e.some(e=>{return e.needs&&(t=this.status.runningVersion,s=e.needs,t.split(".").map(Number).reduce((e,t,a)=>e||t-Number(s.split(".")[a]),0)<0);var t,s})&&!e.some(e=>"kempo"===e.key);updateItems=e=>{e.length?this.needsKempo(e)?a.error("An extension that needs a newer Kempo has to be updated together with Kempo"):"release"===this.status.mode?this.prepare(e):this.openConfirm("update",e):a.error("Tick at least one update first")};reportOutcome=()=>{try{const e=JSON.parse(sessionStorage.getItem(d)||"null");if(!e)return;sessionStorage.removeItem(d);this.status.lastUpdate&&!this.status.lastUpdate.ok&&this.status.lastUpdate.at>e.at?a.error("The update did not work, so the site went back to how it was"):a.success(e.message)}catch{}};check=async()=>{this.checking=!0;const[e,t]=await i();if(this.checking=!1,e)return void a.error(e.msg);this.status=t;const s=t.checkError||t.extensionUpdates?.checkError;s?a.error(s):this.updatable(t).length||t.staged||a.success("Everything is up to date")};selection=e=>({version:e.some(e=>"kempo"===e.key)?this.status.latestVersion:null,extensions:e.filter(e=>"kempo"!==e.key).map(e=>e.key)});prepare=async e=>{this.phase="preparing";const[t]=await r(this.selection(e));this.phase="idle",t?a.error(t.msg):await this.load()};discard=async()=>{const[e]=await c();e?a.error(e.msg):await this.load()};openConfirm=(e,t=[])=>{this.action=e,this.pending=t,this.renderRoot.querySelector("#confirmDialog").open()};confirmed=async()=>{const{staged:e,bootId:t,mode:s,goBack:n}=this.status;let i,r,c;if(this.stalled=!1,"back"===this.action)this.phase="going-back",[i,r]=await l({data:Boolean(n?.restoresData)}),c="The site is back on what it ran before the last update";else if("release"===s)this.phase="switching",[i,r]=await o({confirm:{forced:e.plan.files.forced.length>0,breaking:e.plan.breaking}}),c=`Updated: ${e.packages.join(", ")}`;else{this.phase="installing";const{version:e,extensions:t}=this.selection(this.pending);[i,r]=await o({version:e,extensions:t}),c=`Updated: ${this.pending.map(e=>`${e.name} ${e.to}`).join(", ")}`}if(i)return this.phase="idle",void a.error(i.msg);if(this.phase="restarting",await this.waitForServer(t)){try{sessionStorage.setItem(d,JSON.stringify({message:c,at:new Date(Date.now()-5e3).toISOString(),to:r.to}))}catch{}window.location.reload()}else this.stalled=!0};waitForServer=async e=>{const t=Date.now();let s=!1;for(;Date.now()-t<6e5;){await p(2e3);try{const t=await fetch("/kempo/api/updates?offline=true",{credentials:"same-origin",cache:"no-store"});if(t.ok){if((await t.json()).bootId!==e)return!0}else if(t.status>=500)s=!0;else if(s)return!0}catch{s=!0}}return!1};renderBusy(){const[e,s]={preparing:["Downloading the updates…","Your site keeps working while this happens. It can take a few minutes."],installing:["Installing the updates…","This can take a minute."],switching:["Switching to the updates…","This can take a minute."],"going-back":["Going back…","This can take a minute."],restarting:["Restarting the server…","This page reloads by itself when the site is back, and updated extensions are brought up to date first."]}[this.phase];return t`
|
|
2
2
|
<div class="b r p mb" role="status">
|
|
3
3
|
<p class="d-f mb"><k-spinner class="mr"></k-spinner><strong>${e}</strong></p>
|
|
4
4
|
<p class="tc-muted">${s} Please keep this page open.</p>
|
|
@@ -9,16 +9,16 @@ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing
|
|
|
9
9
|
<p class="mb">Check that the server process is running; if it is managed by pm2, systemd or Docker, check that it restarts when it exits.</p>
|
|
10
10
|
<button class="primary" @click=${()=>window.location.reload()}>Reload this page</button>
|
|
11
11
|
</div>
|
|
12
|
-
`}renderRestartNote(e){return"respawn"===e.restart?t`The site is unavailable for a few seconds while the server restarts.`:t`The server restarts, which takes a few seconds, and the site is unavailable until it is back.`}renderPlan(e){const{update:a,add:n,edited:i,forced:r}=e.files,o=e.upgrades.filter(e=>e.breaking),
|
|
12
|
+
`}renderRestartNote(e){return"respawn"===e.restart?t`The site is unavailable for a few seconds while the server restarts.`:t`The server restarts, which takes a few seconds, and the site is unavailable until it is back.`}renderPlan(e){const{update:a,add:n,edited:i,forced:r}=e.files,o=e.upgrades.filter(e=>e.breaking),l=e.extensions.filter(e=>e.breaking),c=!(a.length||n.length||i.length||r.length||e.upgrades.length||e.extensions.length),d={permissions:["permission","permissions"],settings:["setting","settings"],groups:["group","groups"],widgets:["dashboard widget","dashboard widgets"],jobs:["scheduled job","scheduled jobs"]},h=e=>Object.entries(e.adds).filter(([,e])=>e).map(([e,t])=>m(t,...d[e])).join(", ");return t`
|
|
13
13
|
<ul class="mb">
|
|
14
|
-
${
|
|
15
|
-
${e.extensions.map(e=>t`<li>${e.name} ${e.from} <k-icon name="arrow" direction="right"></k-icon> ${e.to}${
|
|
14
|
+
${c?t`<li>Nothing changes except the code.</li>`:s}
|
|
15
|
+
${e.extensions.map(e=>t`<li>${e.name} ${e.from} <k-icon name="arrow" direction="right"></k-icon> ${e.to}${h(e)?t` <span class="tc-muted">adds ${h(e)}</span>`:s}</li>`)}
|
|
16
16
|
${a.length?t`<li>${m(a.length,"starter file","starter files")} you have not edited will be updated.</li>`:s}
|
|
17
17
|
${n.length?t`<li>${m(n.length,"new file","new files")} will be added.</li>`:s}
|
|
18
18
|
${i.length?t`<li>${m(i.length,"file","files")} you have edited will stay as you made ${1===i.length?"it":"them"}, even though Kempo has a newer version: <span class="tc-muted">${i.join(", ")}</span></li>`:s}
|
|
19
19
|
${r.length?t`<li class="tc-warning"><k-icon name="warning"></k-icon> ${m(r.length,"file","files")} you have edited must be replaced for security reasons. Your version is saved first: <span class="tc-muted">${r.join(", ")}</span></li>`:s}
|
|
20
20
|
${o.length?t`<li class="tc-warning"><k-icon name="warning"></k-icon> Kempo changes your data${o.some(e=>e.description)?t` (${o.map(e=>e.description).filter(Boolean).join("; ")})`:s}.</li>`:s}
|
|
21
|
-
${
|
|
21
|
+
${l.length?t`<li class="tc-warning"><k-icon name="warning"></k-icon> ${l.map(e=>e.name).join(", ")} ${1===l.length?"is a breaking update that":"are breaking updates that"} may change your data.</li>`:s}
|
|
22
22
|
${e.breaking?t`<li class="tc-warning"><k-icon name="warning"></k-icon> A backup of the database is taken first. Going back later restores it, which loses anything entered after the update.</li>`:s}
|
|
23
23
|
</ul>
|
|
24
24
|
`}renderStaged(e){const{staged:a}=e;return t`
|
|
@@ -31,23 +31,18 @@ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing
|
|
|
31
31
|
<button class="primary mr" ?disabled=${Boolean(e.switching)} @click=${()=>this.openConfirm("update")}>Switch now</button>
|
|
32
32
|
<button @click=${this.discard}>Not now</button>
|
|
33
33
|
</div>
|
|
34
|
-
`}
|
|
35
|
-
<label class="d-f mb">
|
|
36
|
-
<input type="checkbox" class="mr" ?checked=${this.chosen.has(e.key)&&!n} ?disabled=${n||"idle"!==this.phase} @change=${()=>this.toggle(e.key)}>
|
|
37
|
-
<span><strong>${e.name}</strong> ${e.from} <k-icon name="arrow" direction="right"></k-icon> ${e.to}${e.major?t` <span class="tc-warning">breaking release</span>`:s}
|
|
38
|
-
${n?t`<small class="d-b tc-muted">Needs Kempo ${e.needs} or newer: tick Kempo as well.</small>`:s}</span>
|
|
39
|
-
</label>
|
|
40
|
-
`}renderAvailable(e){const a=this.updatable(e),n=this.chosenItems(),i=n.filter(e=>e.major);return t`
|
|
34
|
+
`}renderAvailable(e){const a=this.updatable(e),n=a.filter(e=>e.major);return t`
|
|
41
35
|
<div class="b r p mb">
|
|
42
36
|
<h2 class="h4 mb">${m(a.length,"update is","updates are")} available</h2>
|
|
43
|
-
<
|
|
44
|
-
|
|
37
|
+
<k-table id="updates" enable-selection placeholder="No updates." @updateItem=${e=>this.updateItems([e.detail.record])} @updateSelected=${()=>this.updateItems(this.selectedItems())}>
|
|
38
|
+
<admin-update-item slot="after"></admin-update-item>
|
|
39
|
+
<admin-update-selected slot="bottom">${"release"===e.mode?"Get selected ready":"Update selected"}</admin-update-selected>
|
|
40
|
+
</k-table>
|
|
41
|
+
${n.length?t`<p class="tc-warning mb" role="alert"><k-icon name="warning"></k-icon> ${n.map(e=>e.name).join(", ")} ${1===n.length?"is a breaking release":"are breaking releases"} and may change how things work. Read ${1===n.length?"its":"their"} release notes before updating.</p>`:s}
|
|
45
42
|
${e.canUpdate?"release"===e.mode?t`
|
|
46
|
-
<p class="tc-muted
|
|
47
|
-
<button class="primary" ?disabled=${!n.length} @click=${this.prepare}>Get ${m(n.length,"update","updates")} ready</button>
|
|
43
|
+
<p class="tc-muted">Getting them ready downloads them without touching your site. You choose when to switch.</p>
|
|
48
44
|
`:t`
|
|
49
|
-
<p class="tc-muted
|
|
50
|
-
<button class="primary" ?disabled=${!n.length} @click=${()=>this.openConfirm("update")}>Update ${m(n.length,"item","items")}</button>
|
|
45
|
+
<p class="tc-muted">${this.renderRestartNote(e)}</p>
|
|
51
46
|
`:t`<p class="mb">${e.reason} Update where it is installed, then restart the server.</p>`}
|
|
52
47
|
</div>
|
|
53
48
|
`}renderCurrent(){return t`
|
|
@@ -58,7 +53,7 @@ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing
|
|
|
58
53
|
`}renderLast(e){const a=e.lastUpdate;return!a||a.ok?s:t`
|
|
59
54
|
<div class="b r p mb" role="alert">
|
|
60
55
|
<h2 class="h5 mb tc-danger">The last update did not work</h2>
|
|
61
|
-
<p>The update${a.packages?.length?` of ${a.packages.join(", ")}`:""} on ${
|
|
56
|
+
<p>The update${a.packages?.length?` of ${a.packages.join(", ")}`:""} on ${h(a.at)} did not stay running (${a.reason}), so the site went back to how it was.${a.breaking?" Your data was restored from the backup taken before it.":""}</p>
|
|
62
57
|
</div>
|
|
63
58
|
`}renderGoBack(e){const a=e.goBack;return a?e.switching?t`<p class="tc-muted mb">The last switch is still being checked, which takes about a minute. Going back is available after that.</p>`:a.available?t`<p class="mb"><button @click=${()=>this.openConfirm("back")}>Go back</button> <span class="tc-muted">Puts the site back on what it ran before the last update${a.packages.length?` (${a.packages.join(", ")})`:""}.${a.restoresData?" That update changed your data, so this also restores the database from before it.":""}</span></p>`:s:s}renderDatabase(e){const{applied:a=[],failed:n=[]}=e.database??{},{upgraded:i=[],failed:r=[]}=e.extensionsRun??{};return t`
|
|
64
59
|
${n.length?t`
|
|
@@ -87,7 +82,7 @@ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing
|
|
|
87
82
|
</ul>
|
|
88
83
|
</div>
|
|
89
84
|
`:s}
|
|
90
|
-
`}renderDialog(e){const a="back"===this.action,n=e.staged?.plan.files.forced??[],i=!a&&(e.staged?.plan.breaking??!1),r="release"===e.mode?e.staged?.packages??[]:this.
|
|
85
|
+
`}renderDialog(e){const a="back"===this.action,n=e.staged?.plan.files.forced??[],i=!a&&(e.staged?.plan.breaking??!1),r="release"===e.mode?e.staged?.packages??[]:this.pending.map(e=>`${e.name} ${e.to}`);return t`
|
|
91
86
|
<k-dialog id="confirmDialog" confirm-text=${a?"Go back and restart":"Update and restart"} confirm-classes="primary ml" cancel-text="Cancel" .confirmAction=${this.confirmed}>
|
|
92
87
|
<h2 slot="title" class="h5 pyh px m0">${a?"Go back":"Update"}</h2>
|
|
93
88
|
<div class="p">
|
|
@@ -108,11 +103,11 @@ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing
|
|
|
108
103
|
${e.checkError||e.extensionUpdates?.checkError?t`<p class="tc-danger mb" role="alert">The last check failed: ${e.checkError||e.extensionUpdates.checkError}</p>`:s}
|
|
109
104
|
<p class="d-f mb">
|
|
110
105
|
<button ?disabled=${this.checking} @click=${this.check}>${this.checking?"Checking…":"Check now"}</button>
|
|
111
|
-
<span class="tc-muted ph">Last checked ${
|
|
106
|
+
<span class="tc-muted ph">Last checked ${h(e.checkedAt)}</span>
|
|
112
107
|
</p>
|
|
113
108
|
${this.renderGoBack(e)}
|
|
114
109
|
<p class="tc-muted">Automatic checks can be turned off with <strong>update_check_enabled</strong> on the System tab of <a href="/admin/settings">Settings</a>. Installing or removing an extension is done from <a href="/admin/extensions">Extensions</a>.</p>
|
|
115
110
|
`:s}
|
|
116
111
|
${this.renderDialog(e)}
|
|
117
|
-
`}}customElements.define("k-admin-updates",
|
|
112
|
+
`}}customElements.define("k-admin-updates",g);
|
|
118
113
|
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\Updates.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src\\admin\\components\\Updates.js","names":["ShadowComponent","html","nothing","Toast","getUpdateStatus","checkForUpdate","prepareUpdate","applyUpdate","rollbackUpdate","discardUpdate","AFTER_RELOAD_KEY","when","value","Date","toLocaleString","sleep","ms","Promise","resolve","setTimeout","count","n","one","many","older","a","b","split","map","Number","reduce","result","part","index","Updates","static","status","state","error","checking","phase","stalled","action","chosen","constructor","super","this","connectedCallback","load","async","msg","selectNew","reportOutcome","switching","recheck","updatable","updateAvailable","key","name","from","runningVersion","to","latestVersion","major","extensionUpdates","updates","update","installedVersion","needs","kempoRequired","seen","Set","has","add","blocked","item","Boolean","chosenItems","filter","toggle","next","delete","expected","JSON","parse","sessionStorage","getItem","removeItem","lastUpdate","ok","at","success","message","check","problem","checkError","length","staged","selection","items","version","some","extensions","prepare","discard","openConfirm","renderRoot","querySelector","open","confirmed","bootId","mode","goBack","data","restoresData","confirm","forced","plan","files","breaking","packages","join","waitForServer","setItem","stringify","now","toISOString","window","location","reload","started","wasDown","res","fetch","credentials","cache","json","oldBootId","renderBusy","title","note","preparing","installing","restarting","renderStalled","renderRestartNote","restart","renderPlan","edited","breakingUpgrades","upgrades","upgrade","breakingExtensions","extension","noChanges","labels","permissions","settings","groups","widgets","jobs","adds","Object","entries","what","description","renderStaged","renderRow","renderAvailable","selected","majors","canUpdate","reason","renderCurrent","renderLast","last","renderGoBack","back","available","renderDatabase","applied","failed","database","upgraded","extensionFailed","extensionsRun","change","renderDialog","render","checkedAt","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Spinner.js';\nimport '/kempo-ui/components/Dialog.js';\nimport '/kempo-ui/components/Icon.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport { getUpdateStatus, checkForUpdate, prepareUpdate, applyUpdate, rollbackUpdate, discardUpdate } from '/kempo/sdk.js';\n\n/*\n The update screen, served at /admin/components/Updates.js:\n\n <k-admin-updates></k-admin-updates>\n\n Lists what is newer on npm than what the site runs: Kempo itself and each installed extension, which the server looks up\n every 12 hours (and when \"Check now\" is pressed). Each has a box; what is ticked is updated together.\n\n A site that runs from a Docker volume updates in two steps. \"Get ready\" downloads the new versions next to the running site,\n which keeps working, and shows what switching to them would change: starter files that will update, files you edited that\n stay, files that must be replaced anyway, what each extension adds, and any change to your data. \"Switch\" asks for your word\n where that is needed, then the server restarts on the new versions. A version that does not work puts the old ones back by\n itself, and \"Go back\" does it by hand. Any other site installs the update in place and restarts.\n\n While the server is gone this screen asks it, every couple of seconds, whether it is back, then reloads the page and says how\n it went. A server that does not come back in ten minutes is reported rather than waited on forever.\n*/\nconst POLL_MS = 2000;\nconst GIVE_UP_MS = 10 * 60 * 1000;\nconst AFTER_RELOAD_KEY = 'kempo-after-update';\n\nconst when = value => (value ? new Date(value).toLocaleString() : 'never');\n\nconst sleep = ms => new Promise(resolve => setTimeout(resolve, ms));\n\nconst count = (n, one, many) => `${n} ${n === 1 ? one : many}`;\n\nconst older = (a, b) => a.split('.').map(Number).reduce((result, part, index) => result || (part - Number(b.split('.')[index])), 0) < 0;\n\nexport default class Updates extends ShadowComponent {\n static properties = {\n status: { state: true },\n error: { state: true },\n checking: { state: true },\n phase: { state: true },\n stalled: { state: true },\n action: { state: true },\n chosen: { state: true },\n };\n\n constructor(){\n super();\n this.status = null;\n this.error = '';\n this.checking = false;\n this.phase = 'idle';\n this.stalled = false;\n this.action = 'update';\n this.chosen = null;\n }\n\n /*\n Lifecycle\n */\n connectedCallback(){\n super.connectedCallback();\n this.load();\n }\n\n /*\n Loading\n */\n load = async () => {\n const [error, status] = await getUpdateStatus();\n if(error){\n this.error = error.msg || 'The update status could not be loaded';\n return;\n }\n this.error = '';\n this.status = status;\n this.selectNew(status);\n this.reportOutcome();\n /* A switch that is still being checked blocks the buttons below for about a minute: look again until it is done. */\n if(status.switching && !this.recheck){\n this.recheck = setTimeout(() => {\n this.recheck = null;\n this.load();\n }, 5000);\n }\n };\n\n /* Everything that can be updated, as { key, name, from, to, major, needs }: Kempo first, then the extensions. */\n updatable = status => [\n ...(status.updateAvailable ? [{ key: 'kempo', name: 'Kempo', from: status.runningVersion, to: status.latestVersion, major: status.major }] : []),\n ...(status.extensionUpdates?.updates ?? []).map(update => ({ key: update.name, name: update.name, from: update.installedVersion, to: update.latestVersion, major: update.major, needs: update.kempoRequired })),\n ];\n\n /* Everything that has just become available is ticked, once; what the person unticks stays unticked. */\n selectNew = status => {\n this.seen ??= new Set();\n const chosen = new Set(this.chosen ?? []);\n for(const { key } of this.updatable(status)){\n if(!this.seen.has(key)){\n this.seen.add(key);\n chosen.add(key);\n }\n }\n this.chosen = chosen;\n };\n\n /* An extension that needs a newer Kempo than the one running can only be chosen together with the Kempo update that provides it. */\n blocked = (item, status) => Boolean(item.needs) && older(status.runningVersion, item.needs) && !(this.chosen.has('kempo') && status.updateAvailable && !older(status.latestVersion, item.needs));\n\n chosenItems = () => this.updatable(this.status).filter(item => this.chosen.has(item.key) && !this.blocked(item, this.status));\n\n toggle = key => {\n const next = new Set(this.chosen);\n if(next.has(key)) next.delete(key);\n else next.add(key);\n this.chosen = next;\n };\n\n /* After the page reloads from an update: say whether the version that was asked for is the one now running. */\n reportOutcome = () => {\n try {\n const expected = JSON.parse(sessionStorage.getItem(AFTER_RELOAD_KEY) || 'null');\n if(!expected) return;\n sessionStorage.removeItem(AFTER_RELOAD_KEY);\n const failed = this.status.lastUpdate && !this.status.lastUpdate.ok && this.status.lastUpdate.at > expected.at;\n if(failed) Toast.error(`The update did not work, so the site went back to how it was`);\n else Toast.success(expected.message);\n } catch {\n /* storage can be blocked; the page has still reloaded */\n }\n };\n\n check = async () => {\n this.checking = true;\n const [error, status] = await checkForUpdate();\n this.checking = false;\n if(error){\n Toast.error(error.msg);\n return;\n }\n this.status = status;\n this.selectNew(status);\n const problem = status.checkError || status.extensionUpdates?.checkError;\n if(problem) Toast.error(problem);\n else if(!this.updatable(status).length && !status.staged) Toast.success('Everything is up to date');\n };\n\n /*\n Getting an update ready (Docker volume sites), or throwing it away\n */\n selection = () => {\n const items = this.chosenItems();\n return { version: items.some(item => item.key === 'kempo') ? this.status.latestVersion : null, extensions: items.filter(item => item.key !== 'kempo').map(item => item.key) };\n };\n\n prepare = async () => {\n this.phase = 'preparing';\n const [error] = await prepareUpdate(this.selection());\n this.phase = 'idle';\n if(error){\n Toast.error(error.msg);\n return;\n }\n await this.load();\n };\n\n discard = async () => {\n const [error] = await discardUpdate();\n if(error){\n Toast.error(error.msg);\n return;\n }\n await this.load();\n };\n\n /*\n Switching, updating in place and going back\n */\n openConfirm = action => {\n this.action = action;\n this.renderRoot.querySelector('#confirmDialog').open();\n };\n\n confirmed = async () => {\n const { staged, bootId, mode, goBack } = this.status;\n this.stalled = false;\n let error;\n let result;\n let message;\n\n if(this.action === 'back'){\n this.phase = 'going-back';\n [error, result] = await rollbackUpdate({ data: Boolean(goBack?.restoresData) });\n message = 'The site is back on what it ran before the last update';\n } else if(mode === 'release'){\n this.phase = 'switching';\n [error, result] = await applyUpdate({ confirm: { forced: staged.plan.files.forced.length > 0, breaking: staged.plan.breaking } });\n message = `Updated: ${staged.packages.join(', ')}`;\n } else {\n this.phase = 'installing';\n const { version, extensions } = this.selection();\n [error, result] = await applyUpdate({ version, extensions });\n message = `Updated: ${this.chosenItems().map(item => `${item.name} ${item.to}`).join(', ')}`;\n }\n\n if(error){\n this.phase = 'idle';\n Toast.error(error.msg);\n return;\n }\n this.phase = 'restarting';\n if(await this.waitForServer(bootId)){\n try {\n sessionStorage.setItem(AFTER_RELOAD_KEY, JSON.stringify({ message, at: new Date(Date.now() - 5000).toISOString(), to: result.to }));\n } catch {\n /* the reload still shows what is running */\n }\n window.location.reload();\n return;\n }\n this.stalled = true;\n };\n\n /*\n True once a server that is not the one asked about answers. The old one may still answer for a moment after\n the request that restarts it, so a reply only counts if it carries a different boot id, or if the server\n has been seen gone first (any reply short of a server error then comes from the new one, even a sign-in\n redirect that has no boot id).\n */\n waitForServer = async oldBootId => {\n const started = Date.now();\n let wasDown = false;\n while(Date.now() - started < GIVE_UP_MS){\n await sleep(POLL_MS);\n try {\n const res = await fetch('/kempo/api/updates?offline=true', { credentials: 'same-origin', cache: 'no-store' });\n if(res.ok){\n if((await res.json()).bootId !== oldBootId) return true;\n } else if(res.status >= 500){\n wasDown = true;\n } else if(wasDown){\n return true;\n }\n } catch {\n wasDown = true;\n }\n }\n return false;\n };\n\n /*\n Rendering\n */\n renderBusy(){\n const [title, note] = {\n preparing: ['Downloading the updates…', 'Your site keeps working while this happens. It can take a few minutes.'],\n installing: ['Installing the updates…', 'This can take a minute.'],\n switching: ['Switching to the updates…', 'This can take a minute.'],\n 'going-back': ['Going back…', 'This can take a minute.'],\n restarting: ['Restarting the server…', 'This page reloads by itself when the site is back, and updated extensions are brought up to date first.'],\n }[this.phase];\n return html`\n <div class=\"b r p mb\" role=\"status\">\n <p class=\"d-f mb\"><k-spinner class=\"mr\"></k-spinner><strong>${title}</strong></p>\n <p class=\"tc-muted\">${note} Please keep this page open.</p>\n </div>\n `;\n }\n\n renderStalled(){\n return html`\n <div class=\"b r p mb\" role=\"alert\">\n <p class=\"tc-danger mb\"><strong>The server did not come back within ten minutes.</strong></p>\n <p class=\"mb\">Check that the server process is running; if it is managed by pm2, systemd or Docker, check that it restarts when it exits.</p>\n <button class=\"primary\" @click=${() => window.location.reload()}>Reload this page</button>\n </div>\n `;\n }\n\n renderRestartNote(status){\n return status.restart === 'respawn'\n ? html`The site is unavailable for a few seconds while the server restarts.`\n : html`The server restarts, which takes a few seconds, and the site is unavailable until it is back.`;\n }\n\n /* What switching to the prepared versions would change. */\n renderPlan(plan){\n const { update, add, edited, forced } = plan.files;\n const breakingUpgrades = plan.upgrades.filter(upgrade => upgrade.breaking);\n const breakingExtensions = plan.extensions.filter(extension => extension.breaking);\n const noChanges = !update.length && !add.length && !edited.length && !forced.length && !plan.upgrades.length && !plan.extensions.length;\n const labels = { permissions: ['permission', 'permissions'], settings: ['setting', 'settings'], groups: ['group', 'groups'], widgets: ['dashboard widget', 'dashboard widgets'], jobs: ['scheduled job', 'scheduled jobs'] };\n const adds = extension => Object.entries(extension.adds).filter(([, n]) => n).map(([what, n]) => count(n, ...labels[what])).join(', ');\n return html`\n <ul class=\"mb\">\n ${noChanges ? html`<li>Nothing changes except the code.</li>` : nothing}\n ${plan.extensions.map(extension => html`<li>${extension.name} ${extension.from} <k-icon name=\"arrow\" direction=\"right\"></k-icon> ${extension.to}${adds(extension) ? html` <span class=\"tc-muted\">adds ${adds(extension)}</span>` : nothing}</li>`)}\n ${update.length ? html`<li>${count(update.length, 'starter file', 'starter files')} you have not edited will be updated.</li>` : nothing}\n ${add.length ? html`<li>${count(add.length, 'new file', 'new files')} will be added.</li>` : nothing}\n ${edited.length ? html`<li>${count(edited.length, 'file', 'files')} you have edited will stay as you made ${edited.length === 1 ? 'it' : 'them'}, even though Kempo has a newer version: <span class=\"tc-muted\">${edited.join(', ')}</span></li>` : nothing}\n ${forced.length ? html`<li class=\"tc-warning\"><k-icon name=\"warning\"></k-icon> ${count(forced.length, 'file', 'files')} you have edited must be replaced for security reasons. Your version is saved first: <span class=\"tc-muted\">${forced.join(', ')}</span></li>` : nothing}\n ${breakingUpgrades.length ? html`<li class=\"tc-warning\"><k-icon name=\"warning\"></k-icon> Kempo changes your data${breakingUpgrades.some(upgrade => upgrade.description) ? html` (${breakingUpgrades.map(upgrade => upgrade.description).filter(Boolean).join('; ')})` : nothing}.</li>` : nothing}\n ${breakingExtensions.length ? html`<li class=\"tc-warning\"><k-icon name=\"warning\"></k-icon> ${breakingExtensions.map(extension => extension.name).join(', ')} ${breakingExtensions.length === 1 ? 'is a breaking update that' : 'are breaking updates that'} may change your data.</li>` : nothing}\n ${plan.breaking ? html`<li class=\"tc-warning\"><k-icon name=\"warning\"></k-icon> A backup of the database is taken first. Going back later restores it, which loses anything entered after the update.</li>` : nothing}\n </ul>\n `;\n }\n\n renderStaged(status){\n const { staged } = status;\n return html`\n <div class=\"b r p mb\">\n <h2 class=\"h4 mb\">${count(staged.packages.length, 'update is', 'updates are')} ready</h2>\n <p class=\"tc-muted mb\">${staged.packages.join(', ')}. ${staged.packages.length === 1 ? 'It is' : 'They are'} downloaded next to your running site, which has not changed. Switching will:</p>\n ${this.renderPlan(staged.plan)}\n <p class=\"tc-muted mb\">${this.renderRestartNote(status)}</p>\n ${status.switching ? html`<p class=\"tc-muted mb\">The last switch is still being checked, which takes about a minute after the restart. You can switch when that is done.</p>` : nothing}\n <button class=\"primary mr\" ?disabled=${Boolean(status.switching)} @click=${() => this.openConfirm('update')}>Switch now</button>\n <button @click=${this.discard}>Not now</button>\n </div>\n `;\n }\n\n renderRow(item, status){\n const blocked = this.blocked(item, status);\n return html`\n <label class=\"d-f mb\">\n <input type=\"checkbox\" class=\"mr\" ?checked=${this.chosen.has(item.key) && !blocked} ?disabled=${blocked || this.phase !== 'idle'} @change=${() => this.toggle(item.key)}>\n <span><strong>${item.name}</strong> ${item.from} <k-icon name=\"arrow\" direction=\"right\"></k-icon> ${item.to}${item.major ? html` <span class=\"tc-warning\">breaking release</span>` : nothing}\n ${blocked ? html`<small class=\"d-b tc-muted\">Needs Kempo ${item.needs} or newer: tick Kempo as well.</small>` : nothing}</span>\n </label>\n `;\n }\n\n renderAvailable(status){\n const items = this.updatable(status);\n const selected = this.chosenItems();\n const majors = selected.filter(item => item.major);\n return html`\n <div class=\"b r p mb\">\n <h2 class=\"h4 mb\">${count(items.length, 'update is', 'updates are')} available</h2>\n <div class=\"mb\">${items.map(item => this.renderRow(item, status))}</div>\n ${majors.length ? html`<p class=\"tc-warning mb\" role=\"alert\"><k-icon name=\"warning\"></k-icon> ${majors.map(item => item.name).join(', ')} ${majors.length === 1 ? 'is a breaking release' : 'are breaking releases'} and may change how things work. Read ${majors.length === 1 ? 'its' : 'their'} release notes before updating.</p>` : nothing}\n ${status.canUpdate ? (status.mode === 'release' ? html`\n <p class=\"tc-muted mb\">Getting them ready downloads them without touching your site. You choose when to switch.</p>\n <button class=\"primary\" ?disabled=${!selected.length} @click=${this.prepare}>Get ${count(selected.length, 'update', 'updates')} ready</button>\n ` : html`\n <p class=\"tc-muted mb\">${this.renderRestartNote(status)}</p>\n <button class=\"primary\" ?disabled=${!selected.length} @click=${() => this.openConfirm('update')}>Update ${count(selected.length, 'item', 'items')}</button>\n `) : html`<p class=\"mb\">${status.reason} Update where it is installed, then restart the server.</p>`}\n </div>\n `;\n }\n\n renderCurrent(){\n return html`\n <div class=\"b r p mb\">\n <h2 class=\"h4 mb\"><k-icon name=\"check\" class=\"tc-success\"></k-icon> Everything is up to date</h2>\n <p class=\"tc-muted\">This site runs Kempo ${this.status.runningVersion}${this.status.latestVersion ? ', the newest version' : ''}, and its extensions are on their newest versions.</p>\n </div>\n `;\n }\n\n renderLast(status){\n const last = status.lastUpdate;\n if(!last || last.ok) return nothing;\n return html`\n <div class=\"b r p mb\" role=\"alert\">\n <h2 class=\"h5 mb tc-danger\">The last update did not work</h2>\n <p>The update${last.packages?.length ? ` of ${last.packages.join(', ')}` : ''} on ${when(last.at)} did not stay running (${last.reason}), so the site went back to how it was.${last.breaking ? ' Your data was restored from the backup taken before it.' : ''}</p>\n </div>\n `;\n }\n\n renderGoBack(status){\n const back = status.goBack;\n if(!back) return nothing;\n if(status.switching) return html`<p class=\"tc-muted mb\">The last switch is still being checked, which takes about a minute. Going back is available after that.</p>`;\n if(!back.available) return nothing;\n return html`<p class=\"mb\"><button @click=${() => this.openConfirm('back')}>Go back</button> <span class=\"tc-muted\">Puts the site back on what it ran before the last update${back.packages.length ? ` (${back.packages.join(', ')})` : ''}.${back.restoresData ? ' That update changed your data, so this also restores the database from before it.' : ''}</span></p>`;\n }\n\n renderDatabase(status){\n const { applied = [], failed = [] } = status.database ?? {};\n const { upgraded = [], failed: extensionFailed = [] } = status.extensionsRun ?? {};\n return html`\n ${failed.length ? html`\n <div class=\"b r p mb\" role=\"alert\">\n <h2 class=\"h5 mb tc-danger\">The database needs attention</h2>\n <p class=\"mb\">Kempo started, but these changes to its tables were not applied. Anything that depends on them may not work until they are.</p>\n <ul class=\"mb\">\n ${failed.map(({ change, reason }) => html`<li>${change}: <span class=\"tc-danger\">${reason}</span></li>`)}\n </ul>\n <p class=\"tc-muted\">Kempo only ever adds to its tables on its own. It never drops or changes anything that holds data, so these need someone who can look at the database.</p>\n </div>\n ` : nothing}\n ${extensionFailed.length ? html`\n <div class=\"b r p mb\" role=\"alert\">\n <h2 class=\"h5 mb tc-danger\">An extension could not be brought up to date</h2>\n <ul class=\"mb\">${extensionFailed.map(({ name, from, to, reason }) => html`<li>${name} ${from} <k-icon name=\"arrow\" direction=\"right\"></k-icon> ${to}: <span class=\"tc-danger\">${reason}</span></li>`)}</ul>\n <p class=\"tc-muted\">It is tried again the next time the server starts.</p>\n </div>\n ` : nothing}\n ${applied.length || upgraded.length ? html`\n <div class=\"b r p mb\">\n <h2 class=\"h5 mb\">Brought up to date when the server last started</h2>\n <ul>\n ${upgraded.map(({ name, from, to }) => html`<li>${name} ${from} <k-icon name=\"arrow\" direction=\"right\"></k-icon> ${to}</li>`)}\n ${applied.map(change => html`<li>${change}</li>`)}\n </ul>\n </div>\n ` : nothing}\n `;\n }\n\n renderDialog(status){\n const back = this.action === 'back';\n const forced = status.staged?.plan.files.forced ?? [];\n const breaking = !back && (status.staged?.plan.breaking ?? false);\n const items = status.mode === 'release' ? (status.staged?.packages ?? []) : this.chosen ? this.chosenItems().map(item => `${item.name} ${item.to}`) : [];\n return html`\n <k-dialog id=\"confirmDialog\" confirm-text=${back ? 'Go back and restart' : 'Update and restart'} confirm-classes=\"primary ml\" cancel-text=\"Cancel\" .confirmAction=${this.confirmed}>\n <h2 slot=\"title\" class=\"h5 pyh px m0\">${back ? 'Go back' : 'Update'}</h2>\n <div class=\"p\">\n <p class=\"mb\">${back ? 'This puts the site back on what it ran before the last update.' : `This updates ${items.join(', ')}.`} ${this.renderRestartNote(status)}</p>\n ${forced.length ? html`<p class=\"tc-warning mb\"><k-icon name=\"warning\"></k-icon> Your edits to ${forced.join(', ')} will be replaced. Your version is saved first.</p>` : nothing}\n ${breaking ? html`<p class=\"tc-warning mb\"><k-icon name=\"warning\"></k-icon> This update changes your data. A backup is taken first, and going back later restores it, which loses anything entered after the update.</p>` : nothing}\n ${back && status.goBack?.restoresData ? html`<p class=\"tc-warning mb\"><k-icon name=\"warning\"></k-icon> The update changed your data, so going back restores the database from before it. Anything entered since is lost.</p>` : nothing}\n <p class=\"tc-muted\">Anyone using the site meanwhile will not be able to reach it. This page reloads when the server is back.</p>\n </div>\n </k-dialog>\n `;\n }\n\n render(){\n const status = this.status;\n if(this.error) return html`<p class=\"tc-danger\" role=\"alert\">${this.error}</p>`;\n if(!status) return html`<k-spinner></k-spinner>`;\n const items = this.updatable(status);\n return html`\n ${this.phase !== 'idle' ? this.renderBusy() : nothing}\n ${this.stalled ? this.renderStalled() : nothing}\n ${this.phase === 'idle' ? html`\n ${this.renderLast(status)}\n ${this.renderDatabase(status)}\n ${status.staged ? this.renderStaged(status) : (items.length ? this.renderAvailable(status) : this.renderCurrent())}\n ${status.checkError || status.extensionUpdates?.checkError ? html`<p class=\"tc-danger mb\" role=\"alert\">The last check failed: ${status.checkError || status.extensionUpdates.checkError}</p>` : nothing}\n <p class=\"d-f mb\">\n <button ?disabled=${this.checking} @click=${this.check}>${this.checking ? 'Checking…' : 'Check now'}</button>\n <span class=\"tc-muted ph\">Last checked ${when(status.checkedAt)}</span>\n </p>\n ${this.renderGoBack(status)}\n <p class=\"tc-muted\">Automatic checks can be turned off with <strong>update_check_enabled</strong> on the System tab of <a href=\"/admin/settings\">Settings</a>. Installing or removing an extension is done from <a href=\"/admin/extensions\">Extensions</a>.</p>\n ` : nothing}\n ${this.renderDialog(status)}\n `;\n }\n}\n\ncustomElements.define('k-admin-updates', Updates);\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\Updates.js","names":["ShadowComponent","html","nothing","Toast","getUpdateStatus","checkForUpdate","prepareUpdate","applyUpdate","rollbackUpdate","discardUpdate","AFTER_RELOAD_KEY","when","value","Date","toLocaleString","sleep","ms","Promise","resolve","setTimeout","count","n","one","many","UPDATE_FIELDS","name","label","editable","calculator","item","needs","major","size","from","to","Updates","static","status","state","error","checking","phase","stalled","action","pending","constructor","super","this","connectedCallback","load","async","msg","reportOutcome","switching","recheck","updatable","updateAvailable","key","runningVersion","latestVersion","extensionUpdates","updates","map","update","installedVersion","kempoRequired","renderTable","$table","renderRoot","querySelector","items","signature","join","dataset","records","length","setData","fields","updateComplete","selectAllOnPage","updated","changed","has","selectedItems","getSelectedRecords","needsKempo","some","a","b","split","Number","reduce","result","part","index","updateItems","mode","prepare","openConfirm","expected","JSON","parse","sessionStorage","getItem","removeItem","lastUpdate","ok","at","success","message","check","problem","checkError","staged","selection","version","extensions","filter","discard","open","confirmed","bootId","goBack","data","Boolean","restoresData","confirm","forced","plan","files","breaking","packages","waitForServer","setItem","stringify","now","toISOString","window","location","reload","started","wasDown","res","fetch","credentials","cache","json","oldBootId","renderBusy","title","note","preparing","installing","restarting","renderStalled","renderRestartNote","restart","renderPlan","add","edited","breakingUpgrades","upgrades","upgrade","breakingExtensions","extension","noChanges","labels","permissions","settings","groups","widgets","jobs","adds","Object","entries","what","description","renderStaged","renderAvailable","majors","event","detail","record","canUpdate","reason","renderCurrent","renderLast","last","renderGoBack","back","available","renderDatabase","applied","failed","database","upgraded","extensionFailed","extensionsRun","change","renderDialog","render","checkedAt","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Spinner.js';\nimport '/kempo-ui/components/Dialog.js';\nimport '/kempo-ui/components/Icon.js';\nimport '/kempo-ui/components/Table.js';\nimport '/admin/components/tableControls/UpdateItem.js';\nimport '/admin/components/tableControls/UpdateSelected.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport { getUpdateStatus, checkForUpdate, prepareUpdate, applyUpdate, rollbackUpdate, discardUpdate } from '/kempo/sdk.js';\n\n/*\n The update screen, served at /admin/components/Updates.js:\n\n <k-admin-updates></k-admin-updates>\n\n Lists what is newer on npm than what the site runs: Kempo itself and each installed extension, which the server looks up\n every 12 hours (and when \"Check now\" is pressed). Each has a box; what is ticked is updated together.\n\n A site that runs from a Docker volume updates in two steps. \"Get ready\" downloads the new versions next to the running site,\n which keeps working, and shows what switching to them would change: starter files that will update, files you edited that\n stay, files that must be replaced anyway, what each extension adds, and any change to your data. \"Switch\" asks for your word\n where that is needed, then the server restarts on the new versions. A version that does not work puts the old ones back by\n itself, and \"Go back\" does it by hand. Any other site installs the update in place and restarts.\n\n While the server is gone this screen asks it, every couple of seconds, whether it is back, then reloads the page and says how\n it went. A server that does not come back in ten minutes is reported rather than waited on forever.\n*/\nconst POLL_MS = 2000;\nconst GIVE_UP_MS = 10 * 60 * 1000;\nconst AFTER_RELOAD_KEY = 'kempo-after-update';\n\nconst when = value => (value ? new Date(value).toLocaleString() : 'never');\n\nconst sleep = ms => new Promise(resolve => setTimeout(resolve, ms));\n\nconst count = (n, one, many) => `${n} ${n === 1 ? one : many}`;\n\nconst older = (a, b) => a.split('.').map(Number).reduce((result, part, index) => result || (part - Number(b.split('.')[index])), 0) < 0;\n\nconst UPDATE_FIELDS = [\n { name: 'name', label: 'Name', editable: false, calculator: item => `${item.name}${item.needs ? ` · needs Kempo ${item.needs}` : ''}${item.major ? ' · breaking release' : ''}` },\n { name: 'current', label: 'Current', size: 140, editable: false, calculator: item => item.from },\n { name: 'next', label: 'New', size: 140, editable: false, calculator: item => item.to },\n];\n\nexport default class Updates extends ShadowComponent {\n static properties = {\n status: { state: true },\n error: { state: true },\n checking: { state: true },\n phase: { state: true },\n stalled: { state: true },\n action: { state: true },\n pending: { state: true },\n };\n\n constructor(){\n super();\n this.status = null;\n this.error = '';\n this.checking = false;\n this.phase = 'idle';\n this.stalled = false;\n this.action = 'update';\n this.pending = [];\n }\n\n /*\n Lifecycle\n */\n connectedCallback(){\n super.connectedCallback();\n this.load();\n }\n\n /*\n Loading\n */\n load = async () => {\n const [error, status] = await getUpdateStatus();\n if(error){\n this.error = error.msg || 'The update status could not be loaded';\n return;\n }\n this.error = '';\n this.status = status;\n this.reportOutcome();\n /* A switch that is still being checked blocks the buttons below for about a minute: look again until it is done. */\n if(status.switching && !this.recheck){\n this.recheck = setTimeout(() => {\n this.recheck = null;\n this.load();\n }, 5000);\n }\n };\n\n /* Everything that can be updated, as { key, name, from, to, major, needs }: Kempo first, then the extensions. */\n updatable = status => [\n ...(status.updateAvailable ? [{ key: 'kempo', name: 'Kempo', from: status.runningVersion, to: status.latestVersion, major: status.major }] : []),\n ...(status.extensionUpdates?.updates ?? []).map(update => ({ key: update.name, name: update.name, from: update.installedVersion, to: update.latestVersion, major: update.major, needs: update.kempoRequired })),\n ];\n\n /* Refills the table when its list changes. A new list ticks everything on the page; otherwise the person's ticks stay. */\n async renderTable(){\n const $table = this.renderRoot.querySelector('#updates');\n if(!$table || !this.status) return;\n const items = this.updatable(this.status);\n const signature = items.map(item => `${item.key}:${item.from}>${item.to}`).join('|');\n if($table.dataset.signature === signature && $table.records?.length) return;\n $table.dataset.signature = signature;\n $table.setData({ records: items, fields: UPDATE_FIELDS });\n await $table.updateComplete;\n $table.selectAllOnPage();\n }\n\n updated(changed){\n if(changed.has('status') || changed.has('phase')) this.renderTable();\n }\n\n /* What is ticked in the table. */\n selectedItems = () => this.renderRoot.querySelector('#updates')?.getSelectedRecords() ?? [];\n\n /* An extension that needs a newer Kempo than the one running has to go with the Kempo update that provides it. */\n needsKempo = items => items.some(item => item.needs && older(this.status.runningVersion, item.needs)) && !items.some(item => item.key === 'kempo');\n\n /* One row's update, or every ticked one. A Docker site gets them ready; any other site confirms first. */\n updateItems = items => {\n if(!items.length){\n Toast.error('Tick at least one update first');\n return;\n }\n if(this.needsKempo(items)){\n Toast.error('An extension that needs a newer Kempo has to be updated together with Kempo');\n return;\n }\n if(this.status.mode === 'release') this.prepare(items);\n else this.openConfirm('update', items);\n };\n\n /* After the page reloads from an update: say whether the version that was asked for is the one now running. */\n reportOutcome = () => {\n try {\n const expected = JSON.parse(sessionStorage.getItem(AFTER_RELOAD_KEY) || 'null');\n if(!expected) return;\n sessionStorage.removeItem(AFTER_RELOAD_KEY);\n const failed = this.status.lastUpdate && !this.status.lastUpdate.ok && this.status.lastUpdate.at > expected.at;\n if(failed) Toast.error(`The update did not work, so the site went back to how it was`);\n else Toast.success(expected.message);\n } catch {\n /* storage can be blocked; the page has still reloaded */\n }\n };\n\n check = async () => {\n this.checking = true;\n const [error, status] = await checkForUpdate();\n this.checking = false;\n if(error){\n Toast.error(error.msg);\n return;\n }\n this.status = status;\n const problem = status.checkError || status.extensionUpdates?.checkError;\n if(problem) Toast.error(problem);\n else if(!this.updatable(status).length && !status.staged) Toast.success('Everything is up to date');\n };\n\n /*\n Getting an update ready (Docker volume sites), or throwing it away\n */\n selection = items => ({ version: items.some(item => item.key === 'kempo') ? this.status.latestVersion : null, extensions: items.filter(item => item.key !== 'kempo').map(item => item.key) });\n\n prepare = async items => {\n this.phase = 'preparing';\n const [error] = await prepareUpdate(this.selection(items));\n this.phase = 'idle';\n if(error){\n Toast.error(error.msg);\n return;\n }\n await this.load();\n };\n\n discard = async () => {\n const [error] = await discardUpdate();\n if(error){\n Toast.error(error.msg);\n return;\n }\n await this.load();\n };\n\n /*\n Switching, updating in place and going back\n */\n openConfirm = (action, items = []) => {\n this.action = action;\n this.pending = items;\n this.renderRoot.querySelector('#confirmDialog').open();\n };\n\n confirmed = async () => {\n const { staged, bootId, mode, goBack } = this.status;\n this.stalled = false;\n let error;\n let result;\n let message;\n\n if(this.action === 'back'){\n this.phase = 'going-back';\n [error, result] = await rollbackUpdate({ data: Boolean(goBack?.restoresData) });\n message = 'The site is back on what it ran before the last update';\n } else if(mode === 'release'){\n this.phase = 'switching';\n [error, result] = await applyUpdate({ confirm: { forced: staged.plan.files.forced.length > 0, breaking: staged.plan.breaking } });\n message = `Updated: ${staged.packages.join(', ')}`;\n } else {\n this.phase = 'installing';\n const { version, extensions } = this.selection(this.pending);\n [error, result] = await applyUpdate({ version, extensions });\n message = `Updated: ${this.pending.map(item => `${item.name} ${item.to}`).join(', ')}`;\n }\n\n if(error){\n this.phase = 'idle';\n Toast.error(error.msg);\n return;\n }\n this.phase = 'restarting';\n if(await this.waitForServer(bootId)){\n try {\n sessionStorage.setItem(AFTER_RELOAD_KEY, JSON.stringify({ message, at: new Date(Date.now() - 5000).toISOString(), to: result.to }));\n } catch {\n /* the reload still shows what is running */\n }\n window.location.reload();\n return;\n }\n this.stalled = true;\n };\n\n /*\n True once a server that is not the one asked about answers. The old one may still answer for a moment after\n the request that restarts it, so a reply only counts if it carries a different boot id, or if the server\n has been seen gone first (any reply short of a server error then comes from the new one, even a sign-in\n redirect that has no boot id).\n */\n waitForServer = async oldBootId => {\n const started = Date.now();\n let wasDown = false;\n while(Date.now() - started < GIVE_UP_MS){\n await sleep(POLL_MS);\n try {\n const res = await fetch('/kempo/api/updates?offline=true', { credentials: 'same-origin', cache: 'no-store' });\n if(res.ok){\n if((await res.json()).bootId !== oldBootId) return true;\n } else if(res.status >= 500){\n wasDown = true;\n } else if(wasDown){\n return true;\n }\n } catch {\n wasDown = true;\n }\n }\n return false;\n };\n\n /*\n Rendering\n */\n renderBusy(){\n const [title, note] = {\n preparing: ['Downloading the updates…', 'Your site keeps working while this happens. It can take a few minutes.'],\n installing: ['Installing the updates…', 'This can take a minute.'],\n switching: ['Switching to the updates…', 'This can take a minute.'],\n 'going-back': ['Going back…', 'This can take a minute.'],\n restarting: ['Restarting the server…', 'This page reloads by itself when the site is back, and updated extensions are brought up to date first.'],\n }[this.phase];\n return html`\n <div class=\"b r p mb\" role=\"status\">\n <p class=\"d-f mb\"><k-spinner class=\"mr\"></k-spinner><strong>${title}</strong></p>\n <p class=\"tc-muted\">${note} Please keep this page open.</p>\n </div>\n `;\n }\n\n renderStalled(){\n return html`\n <div class=\"b r p mb\" role=\"alert\">\n <p class=\"tc-danger mb\"><strong>The server did not come back within ten minutes.</strong></p>\n <p class=\"mb\">Check that the server process is running; if it is managed by pm2, systemd or Docker, check that it restarts when it exits.</p>\n <button class=\"primary\" @click=${() => window.location.reload()}>Reload this page</button>\n </div>\n `;\n }\n\n renderRestartNote(status){\n return status.restart === 'respawn'\n ? html`The site is unavailable for a few seconds while the server restarts.`\n : html`The server restarts, which takes a few seconds, and the site is unavailable until it is back.`;\n }\n\n /* What switching to the prepared versions would change. */\n renderPlan(plan){\n const { update, add, edited, forced } = plan.files;\n const breakingUpgrades = plan.upgrades.filter(upgrade => upgrade.breaking);\n const breakingExtensions = plan.extensions.filter(extension => extension.breaking);\n const noChanges = !update.length && !add.length && !edited.length && !forced.length && !plan.upgrades.length && !plan.extensions.length;\n const labels = { permissions: ['permission', 'permissions'], settings: ['setting', 'settings'], groups: ['group', 'groups'], widgets: ['dashboard widget', 'dashboard widgets'], jobs: ['scheduled job', 'scheduled jobs'] };\n const adds = extension => Object.entries(extension.adds).filter(([, n]) => n).map(([what, n]) => count(n, ...labels[what])).join(', ');\n return html`\n <ul class=\"mb\">\n ${noChanges ? html`<li>Nothing changes except the code.</li>` : nothing}\n ${plan.extensions.map(extension => html`<li>${extension.name} ${extension.from} <k-icon name=\"arrow\" direction=\"right\"></k-icon> ${extension.to}${adds(extension) ? html` <span class=\"tc-muted\">adds ${adds(extension)}</span>` : nothing}</li>`)}\n ${update.length ? html`<li>${count(update.length, 'starter file', 'starter files')} you have not edited will be updated.</li>` : nothing}\n ${add.length ? html`<li>${count(add.length, 'new file', 'new files')} will be added.</li>` : nothing}\n ${edited.length ? html`<li>${count(edited.length, 'file', 'files')} you have edited will stay as you made ${edited.length === 1 ? 'it' : 'them'}, even though Kempo has a newer version: <span class=\"tc-muted\">${edited.join(', ')}</span></li>` : nothing}\n ${forced.length ? html`<li class=\"tc-warning\"><k-icon name=\"warning\"></k-icon> ${count(forced.length, 'file', 'files')} you have edited must be replaced for security reasons. Your version is saved first: <span class=\"tc-muted\">${forced.join(', ')}</span></li>` : nothing}\n ${breakingUpgrades.length ? html`<li class=\"tc-warning\"><k-icon name=\"warning\"></k-icon> Kempo changes your data${breakingUpgrades.some(upgrade => upgrade.description) ? html` (${breakingUpgrades.map(upgrade => upgrade.description).filter(Boolean).join('; ')})` : nothing}.</li>` : nothing}\n ${breakingExtensions.length ? html`<li class=\"tc-warning\"><k-icon name=\"warning\"></k-icon> ${breakingExtensions.map(extension => extension.name).join(', ')} ${breakingExtensions.length === 1 ? 'is a breaking update that' : 'are breaking updates that'} may change your data.</li>` : nothing}\n ${plan.breaking ? html`<li class=\"tc-warning\"><k-icon name=\"warning\"></k-icon> A backup of the database is taken first. Going back later restores it, which loses anything entered after the update.</li>` : nothing}\n </ul>\n `;\n }\n\n renderStaged(status){\n const { staged } = status;\n return html`\n <div class=\"b r p mb\">\n <h2 class=\"h4 mb\">${count(staged.packages.length, 'update is', 'updates are')} ready</h2>\n <p class=\"tc-muted mb\">${staged.packages.join(', ')}. ${staged.packages.length === 1 ? 'It is' : 'They are'} downloaded next to your running site, which has not changed. Switching will:</p>\n ${this.renderPlan(staged.plan)}\n <p class=\"tc-muted mb\">${this.renderRestartNote(status)}</p>\n ${status.switching ? html`<p class=\"tc-muted mb\">The last switch is still being checked, which takes about a minute after the restart. You can switch when that is done.</p>` : nothing}\n <button class=\"primary mr\" ?disabled=${Boolean(status.switching)} @click=${() => this.openConfirm('update')}>Switch now</button>\n <button @click=${this.discard}>Not now</button>\n </div>\n `;\n }\n\n renderAvailable(status){\n const items = this.updatable(status);\n const majors = items.filter(item => item.major);\n return html`\n <div class=\"b r p mb\">\n <h2 class=\"h4 mb\">${count(items.length, 'update is', 'updates are')} available</h2>\n <k-table id=\"updates\" enable-selection placeholder=\"No updates.\" @updateItem=${event => this.updateItems([event.detail.record])} @updateSelected=${() => this.updateItems(this.selectedItems())}>\n <admin-update-item slot=\"after\"></admin-update-item>\n <admin-update-selected slot=\"bottom\">${status.mode === 'release' ? 'Get selected ready' : 'Update selected'}</admin-update-selected>\n </k-table>\n ${majors.length ? html`<p class=\"tc-warning mb\" role=\"alert\"><k-icon name=\"warning\"></k-icon> ${majors.map(item => item.name).join(', ')} ${majors.length === 1 ? 'is a breaking release' : 'are breaking releases'} and may change how things work. Read ${majors.length === 1 ? 'its' : 'their'} release notes before updating.</p>` : nothing}\n ${status.canUpdate ? (status.mode === 'release' ? html`\n <p class=\"tc-muted\">Getting them ready downloads them without touching your site. You choose when to switch.</p>\n ` : html`\n <p class=\"tc-muted\">${this.renderRestartNote(status)}</p>\n `) : html`<p class=\"mb\">${status.reason} Update where it is installed, then restart the server.</p>`}\n </div>\n `;\n }\n\n renderCurrent(){\n return html`\n <div class=\"b r p mb\">\n <h2 class=\"h4 mb\"><k-icon name=\"check\" class=\"tc-success\"></k-icon> Everything is up to date</h2>\n <p class=\"tc-muted\">This site runs Kempo ${this.status.runningVersion}${this.status.latestVersion ? ', the newest version' : ''}, and its extensions are on their newest versions.</p>\n </div>\n `;\n }\n\n renderLast(status){\n const last = status.lastUpdate;\n if(!last || last.ok) return nothing;\n return html`\n <div class=\"b r p mb\" role=\"alert\">\n <h2 class=\"h5 mb tc-danger\">The last update did not work</h2>\n <p>The update${last.packages?.length ? ` of ${last.packages.join(', ')}` : ''} on ${when(last.at)} did not stay running (${last.reason}), so the site went back to how it was.${last.breaking ? ' Your data was restored from the backup taken before it.' : ''}</p>\n </div>\n `;\n }\n\n renderGoBack(status){\n const back = status.goBack;\n if(!back) return nothing;\n if(status.switching) return html`<p class=\"tc-muted mb\">The last switch is still being checked, which takes about a minute. Going back is available after that.</p>`;\n if(!back.available) return nothing;\n return html`<p class=\"mb\"><button @click=${() => this.openConfirm('back')}>Go back</button> <span class=\"tc-muted\">Puts the site back on what it ran before the last update${back.packages.length ? ` (${back.packages.join(', ')})` : ''}.${back.restoresData ? ' That update changed your data, so this also restores the database from before it.' : ''}</span></p>`;\n }\n\n renderDatabase(status){\n const { applied = [], failed = [] } = status.database ?? {};\n const { upgraded = [], failed: extensionFailed = [] } = status.extensionsRun ?? {};\n return html`\n ${failed.length ? html`\n <div class=\"b r p mb\" role=\"alert\">\n <h2 class=\"h5 mb tc-danger\">The database needs attention</h2>\n <p class=\"mb\">Kempo started, but these changes to its tables were not applied. Anything that depends on them may not work until they are.</p>\n <ul class=\"mb\">\n ${failed.map(({ change, reason }) => html`<li>${change}: <span class=\"tc-danger\">${reason}</span></li>`)}\n </ul>\n <p class=\"tc-muted\">Kempo only ever adds to its tables on its own. It never drops or changes anything that holds data, so these need someone who can look at the database.</p>\n </div>\n ` : nothing}\n ${extensionFailed.length ? html`\n <div class=\"b r p mb\" role=\"alert\">\n <h2 class=\"h5 mb tc-danger\">An extension could not be brought up to date</h2>\n <ul class=\"mb\">${extensionFailed.map(({ name, from, to, reason }) => html`<li>${name} ${from} <k-icon name=\"arrow\" direction=\"right\"></k-icon> ${to}: <span class=\"tc-danger\">${reason}</span></li>`)}</ul>\n <p class=\"tc-muted\">It is tried again the next time the server starts.</p>\n </div>\n ` : nothing}\n ${applied.length || upgraded.length ? html`\n <div class=\"b r p mb\">\n <h2 class=\"h5 mb\">Brought up to date when the server last started</h2>\n <ul>\n ${upgraded.map(({ name, from, to }) => html`<li>${name} ${from} <k-icon name=\"arrow\" direction=\"right\"></k-icon> ${to}</li>`)}\n ${applied.map(change => html`<li>${change}</li>`)}\n </ul>\n </div>\n ` : nothing}\n `;\n }\n\n renderDialog(status){\n const back = this.action === 'back';\n const forced = status.staged?.plan.files.forced ?? [];\n const breaking = !back && (status.staged?.plan.breaking ?? false);\n const items = status.mode === 'release' ? (status.staged?.packages ?? []) : this.pending.map(item => `${item.name} ${item.to}`);\n return html`\n <k-dialog id=\"confirmDialog\" confirm-text=${back ? 'Go back and restart' : 'Update and restart'} confirm-classes=\"primary ml\" cancel-text=\"Cancel\" .confirmAction=${this.confirmed}>\n <h2 slot=\"title\" class=\"h5 pyh px m0\">${back ? 'Go back' : 'Update'}</h2>\n <div class=\"p\">\n <p class=\"mb\">${back ? 'This puts the site back on what it ran before the last update.' : `This updates ${items.join(', ')}.`} ${this.renderRestartNote(status)}</p>\n ${forced.length ? html`<p class=\"tc-warning mb\"><k-icon name=\"warning\"></k-icon> Your edits to ${forced.join(', ')} will be replaced. Your version is saved first.</p>` : nothing}\n ${breaking ? html`<p class=\"tc-warning mb\"><k-icon name=\"warning\"></k-icon> This update changes your data. A backup is taken first, and going back later restores it, which loses anything entered after the update.</p>` : nothing}\n ${back && status.goBack?.restoresData ? html`<p class=\"tc-warning mb\"><k-icon name=\"warning\"></k-icon> The update changed your data, so going back restores the database from before it. Anything entered since is lost.</p>` : nothing}\n <p class=\"tc-muted\">Anyone using the site meanwhile will not be able to reach it. This page reloads when the server is back.</p>\n </div>\n </k-dialog>\n `;\n }\n\n render(){\n const status = this.status;\n if(this.error) return html`<p class=\"tc-danger\" role=\"alert\">${this.error}</p>`;\n if(!status) return html`<k-spinner></k-spinner>`;\n const items = this.updatable(status);\n return html`\n ${this.phase !== 'idle' ? this.renderBusy() : nothing}\n ${this.stalled ? this.renderStalled() : nothing}\n ${this.phase === 'idle' ? html`\n ${this.renderLast(status)}\n ${this.renderDatabase(status)}\n ${status.staged ? this.renderStaged(status) : (items.length ? this.renderAvailable(status) : this.renderCurrent())}\n ${status.checkError || status.extensionUpdates?.checkError ? html`<p class=\"tc-danger mb\" role=\"alert\">The last check failed: ${status.checkError || status.extensionUpdates.checkError}</p>` : nothing}\n <p class=\"d-f mb\">\n <button ?disabled=${this.checking} @click=${this.check}>${this.checking ? 'Checking…' : 'Check now'}</button>\n <span class=\"tc-muted ph\">Last checked ${when(status.checkedAt)}</span>\n </p>\n ${this.renderGoBack(status)}\n <p class=\"tc-muted\">Automatic checks can be turned off with <strong>update_check_enabled</strong> on the System tab of <a href=\"/admin/settings\">Settings</a>. Installing or removing an extension is done from <a href=\"/admin/extensions\">Extensions</a>.</p>\n ` : nothing}\n ${this.renderDialog(status)}\n `;\n }\n}\n\ncustomElements.define('k-admin-updates', Updates);\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing as s}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Spinner.js";import"/kempo-ui/components/Icon.js";import i from"/kempo-ui/components/Toast.js";import n from"/kempo-ui/components/Dialog.js";import{listSettings as a,setSetting as r,deleteSetting as o,getAllCurrentUserPermissions as l}from"/kempo/sdk.js";import"./Setting.js";import c from"./humanize.js";const u={system:"system:settings:update",custom:"system:custom-settings:manage"};let h=null;const d={string:"",number:0,boolean:!1,json:{},secret:""},m=e=>{if(null===e.value||void 0===e.value||"secret"===e.type)return e;try{if("number"===e.type)return{...e,value:Number(e.value)};if("boolean"===e.type)return{...e,value:!0===e.value||"true"===e.value};if("json"===e.type)return{...e,value:"string"==typeof e.value?JSON.parse(e.value):e.value}}catch{return e}return e};export default class p extends e{static properties={owner:{type:String},heading:{type:String},settings:{state:!0},changes:{state:!0},canEdit:{state:!0},loading:{state:!0},busy:{state:!0},error:{state:!0}};constructor(){super(),this.owner="",this.heading="",this.settings=[],this.changes={},this.canEdit=!1,this.loading=!0,this.busy=!1,this.error=""}connectedCallback(){super.connectedCallback(),this.load()}shortName=e=>e.name.slice(this.owner.length+1);load=async()=>{const[[e,t],s]=await Promise.all([a({owner:this.owner,limit:1e3}),h??=l().then(([,e])=>new Set(e?.permissions??[]))]);this.canEdit=s.has(u[this.owner]??"system:extensions:manage"),this.loading=!1,e?this.error=e.msg||"The settings could not be loaded":(this.error="",this.settings=t.settings.map(m),this.changes={})};record=e=>{const{name:t,value:s,valid:i}=e.detail,n=this.settings.find(e=>this.shortName(e)===t),a=n&&("secret"===n.type?""===s:JSON.stringify(s)===JSON.stringify(n.value)),{[t]:r,...o}=this.changes;this.changes=a&&i?o:{...o,[t]:{value:s,valid:i}}};get dirty(){return Object.keys(this.changes).length>0}get blocked(){return Object.values(this.changes).some(e=>!e.valid)}save=async()=>{this.busy=!0;let e=0;for(const[t,s]of Object.entries(this.changes)){const n=this.settings.find(e=>this.shortName(e)===t),[a]=await r(this.owner,t,{value:s.value,type:n.type,isPublic:n.isPublic,description:n.description});a&&(e++,i.error(`${c(t)}: ${a.msg||"could not be saved"}`))}this.busy=!1,e||i.success("Settings saved"),await this.load()};create=()=>{const e=n.create(t`
|
|
1
|
+
import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing as s}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Spinner.js";import"/kempo-ui/components/Icon.js";import i from"/kempo-ui/components/Toast.js";import n from"/kempo-ui/components/Dialog.js";import{listSettings as a,setSetting as r,deleteSetting as o,getAllCurrentUserPermissions as l}from"/kempo/sdk.js";import"./Setting.js";import c from"./humanize.js";const u={system:"system:settings:update",custom:"system:custom-settings:manage"};let h=null;const d={string:"",number:0,boolean:!1,json:{},secret:""},m=e=>{if(null===e.value||void 0===e.value||"secret"===e.type)return e;try{if("number"===e.type)return{...e,value:Number(e.value)};if("boolean"===e.type)return{...e,value:!0===e.value||"true"===e.value};if("json"===e.type)return{...e,value:"string"==typeof e.value?JSON.parse(e.value):e.value}}catch{return e}return e};export default class p extends e{static properties={owner:{type:String},heading:{type:String},settings:{state:!0},changes:{state:!0},canEdit:{state:!0},loading:{state:!0},busy:{state:!0},error:{state:!0}};constructor(){super(),this.owner="",this.heading="",this.settings=[],this.changes={},this.canEdit=!1,this.loading=!0,this.busy=!1,this.error=""}connectedCallback(){super.connectedCallback();const e=this.closest("k-tab-content");!e||e.active?this.load():this.closest("k-tabs")?.addEventListener("tabfirstshown",({detail:t})=>{t.tab===e.name&&this.load()})}shortName=e=>e.name.slice(this.owner.length+1);load=async()=>{const[[e,t],s]=await Promise.all([a({owner:this.owner,limit:1e3}),h??=l().then(([,e])=>new Set(e?.permissions??[]))]);this.canEdit=s.has(u[this.owner]??"system:extensions:manage"),this.loading=!1,e?this.error=e.msg||"The settings could not be loaded":(this.error="",this.settings=t.settings.map(m),this.changes={})};record=e=>{const{name:t,value:s,valid:i}=e.detail,n=this.settings.find(e=>this.shortName(e)===t),a=n&&("secret"===n.type?""===s:JSON.stringify(s)===JSON.stringify(n.value)),{[t]:r,...o}=this.changes;this.changes=a&&i?o:{...o,[t]:{value:s,valid:i}}};get dirty(){return Object.keys(this.changes).length>0}get blocked(){return Object.values(this.changes).some(e=>!e.valid)}save=async()=>{this.busy=!0;let e=0;for(const[t,s]of Object.entries(this.changes)){const n=this.settings.find(e=>this.shortName(e)===t),[a]=await r(this.owner,t,{value:s.value,type:n.type,isPublic:n.isPublic,description:n.description});a&&(e++,i.error(`${c(t)}: ${a.msg||"could not be saved"}`))}this.busy=!1,e||i.success("Settings saved"),await this.load()};create=()=>{const e=n.create(t`
|
|
2
2
|
<div class="p">
|
|
3
3
|
<label class="mb d-b">Name<input type="text" id="newName" placeholder="my_setting" required></label>
|
|
4
4
|
<label class="mb d-b">Type
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src\\admin\\components\\settings\\SettingsPanel.js","names":["ShadowComponent","html","nothing","Toast","Dialog","listSettings","setSetting","deleteSetting","getAllCurrentUserPermissions","humanize","PERMISSIONS","system","custom","permissionsPromise","DEFAULTS","string","number","boolean","json","secret","typed","record","value","undefined","type","Number","JSON","parse","SettingsPanel","static","owner","String","heading","settings","state","changes","canEdit","loading","busy","error","constructor","super","this","connectedCallback","load","shortName","name","slice","length","async","data","permissions","Promise","all","limit","then","Set","has","msg","map","event","valid","detail","find","entry","unchanged","stringify","removed","rest","dirty","Object","keys","blocked","values","some","change","save","failed","entries","isPublic","description","success","create","dialog","title","confirmText","cancelText","confirmAction","read","id","querySelector","trim","includes","text","isFinite","Error","toLowerCase","coerce","checked","addEventListener","target","remove","confirm","confirmed","confirmClasses","cancelClasses","renderSetting","hasOwn","editor","render","firstUpdated","renderRoot","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Spinner.js';\nimport '/kempo-ui/components/Icon.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport Dialog from '/kempo-ui/components/Dialog.js';\nimport { listSettings, setSetting, deleteSetting, getAllCurrentUserPermissions } from '/kempo/sdk.js';\nimport './Setting.js';\nimport humanize from './humanize.js';\n\n/*\n The settings of one owner, each with the editor for its type, and a Save button for what was changed. Served at\n /admin/components/settings/SettingsPanel.js:\n\n <k-settings-panel owner=\"system\"></k-settings-panel>\n <k-settings-panel owner=\"my-extension\" heading=\"My Extension\"></k-settings-panel>\n\n `owner` is `system`, `custom` or the name of an extension. The Custom panel also creates and deletes settings. An extension drops\n one of these in its own settings tab and needs no editor of its own. Whether the person can change them depends on who owns\n them (`system:settings:update`, `system:custom-settings:manage`, `system:extensions:manage`); without it the fields are read-only.\n Saving sends only what was changed, each as its own setting.\n*/\nconst PERMISSIONS = { system: 'system:settings:update', custom: 'system:custom-settings:manage' };\nconst EXTENSION_PERMISSION = 'system:extensions:manage';\n\nlet permissionsPromise = null;\nconst myPermissions = () => (permissionsPromise ??= getAllCurrentUserPermissions().then(([, data]) => new Set(data?.permissions ?? [])));\n\nconst DEFAULTS = { string: '', number: 0, boolean: false, json: {}, secret: '' };\n\nconst coerce = (text, type) => {\n if(type === 'number'){\n const number = Number(text);\n if(String(text).trim() === '' || !Number.isFinite(number)) throw new Error('number');\n return number;\n }\n if(type === 'boolean') return String(text).toLowerCase() === 'true';\n if(type === 'json') return JSON.parse(String(text || '{}'));\n return String(text);\n};\n\n/*\n The settings API gives each value as it is stored, as text (`'true'`, `'7'`, a JSON string). The editors take real values, so the value\n is turned into the type the setting has. A secret is left as the mask the API sends: it only says that one is saved.\n*/\nconst typed = record => {\n if(record.value === null || record.value === undefined || record.type === 'secret') return record;\n try {\n if(record.type === 'number') return { ...record, value: Number(record.value) };\n if(record.type === 'boolean') return { ...record, value: record.value === true || record.value === 'true' };\n if(record.type === 'json') return { ...record, value: typeof record.value === 'string' ? JSON.parse(record.value) : record.value };\n } catch {\n return record;\n }\n return record;\n};\n\nexport default class SettingsPanel extends ShadowComponent {\n static properties = {\n owner: { type: String },\n heading: { type: String },\n settings: { state: true },\n changes: { state: true },\n canEdit: { state: true },\n loading: { state: true },\n busy: { state: true },\n error: { state: true },\n };\n\n constructor(){\n super();\n this.owner = '';\n this.heading = '';\n this.settings = [];\n this.changes = {};\n this.canEdit = false;\n this.loading = true;\n this.busy = false;\n this.error = '';\n }\n\n /*\n Lifecycle\n */\n connectedCallback(){\n super.connectedCallback();\n this.load();\n }\n\n /*\n Loading and saving\n */\n shortName = record => record.name.slice(this.owner.length + 1);\n\n load = async () => {\n const [[error, data], permissions] = await Promise.all([listSettings({ owner: this.owner, limit: 1000 }), myPermissions()]);\n this.canEdit = permissions.has(PERMISSIONS[this.owner] ?? EXTENSION_PERMISSION);\n this.loading = false;\n if(error){\n this.error = error.msg || 'The settings could not be loaded';\n return;\n }\n this.error = '';\n this.settings = data.settings.map(typed);\n this.changes = {};\n };\n\n record = event => {\n const { name, value, valid } = event.detail;\n const record = this.settings.find(entry => this.shortName(entry) === name);\n const unchanged = record && (record.type === 'secret' ? value === '' : JSON.stringify(value) === JSON.stringify(record.value));\n const { [name]: removed, ...rest } = this.changes;\n this.changes = unchanged && valid ? rest : { ...rest, [name]: { value, valid } };\n };\n\n get dirty(){\n return Object.keys(this.changes).length > 0;\n }\n\n get blocked(){\n return Object.values(this.changes).some(change => !change.valid);\n }\n\n save = async () => {\n this.busy = true;\n let failed = 0;\n for(const [name, change] of Object.entries(this.changes)){\n const record = this.settings.find(entry => this.shortName(entry) === name);\n const [error] = await setSetting(this.owner, name, { value: change.value, type: record.type, isPublic: record.isPublic, description: record.description });\n if(error){\n failed++;\n Toast.error(`${humanize(name)}: ${error.msg || 'could not be saved'}`);\n }\n }\n this.busy = false;\n if(!failed) Toast.success('Settings saved');\n await this.load();\n };\n\n /*\n Custom settings: create and delete\n */\n create = () => {\n const dialog = Dialog.create(html`\n <div class=\"p\">\n <label class=\"mb d-b\">Name<input type=\"text\" id=\"newName\" placeholder=\"my_setting\" required></label>\n <label class=\"mb d-b\">Type\n <select id=\"newType\">\n <option value=\"string\">String</option>\n <option value=\"number\">Number</option>\n <option value=\"boolean\">Boolean (true or false)</option>\n <option value=\"json\">JSON</option>\n <option value=\"secret\">Secret</option>\n </select>\n </label>\n <label class=\"mb d-b\">Value<input type=\"text\" id=\"newValue\"></label>\n <label class=\"mb d-b\">Description<input type=\"text\" id=\"newDescription\"></label>\n <label class=\"mb d-b\"><input type=\"checkbox\" id=\"newPublic\"> Public (anyone can read it, without signing in)</label>\n </div>\n `, {\n title: 'Create a setting',\n confirmText: 'Create',\n cancelText: 'Cancel',\n confirmAction: async () => {\n const read = id => dialog.querySelector(id);\n const name = read('#newName').value.trim();\n const type = read('#newType').value;\n if(!name){ Toast.error('Give the setting a name'); return; }\n if(name.includes(':')){ Toast.error('The name cannot contain \":\"'); return; }\n let value;\n try {\n value = read('#newValue').value === '' ? DEFAULTS[type] : coerce(read('#newValue').value, type);\n } catch {\n Toast.error(`That is not a valid ${type} value`);\n return;\n }\n const [error] = await setSetting('custom', name, { value, type, isPublic: type !== 'secret' && read('#newPublic').checked, description: read('#newDescription').value.trim() });\n if(error){ Toast.error(error.msg || 'The setting could not be created'); return; }\n Toast.success('Setting created');\n await this.load();\n },\n });\n dialog.querySelector('#newType').addEventListener('change', event => {\n dialog.querySelector('#newValue').type = event.target.value === 'secret' ? 'password' : 'text';\n });\n };\n\n remove = record => {\n Dialog.confirm(`Delete ${humanize(this.shortName(record))}? This cannot be undone.`, async confirmed => {\n if(!confirmed) return;\n const [error] = await deleteSetting(this.owner, this.shortName(record));\n if(error){ Toast.error(error.msg || 'The setting could not be deleted'); return; }\n Toast.success('Setting deleted');\n await this.load();\n }, { title: 'Delete setting', confirmText: 'Delete', confirmClasses: 'danger ml', cancelText: 'Cancel', cancelClasses: '' });\n };\n\n /*\n Rendering\n */\n renderSetting(record){\n const name = this.shortName(record);\n const value = Object.hasOwn(this.changes, name) && this.changes[name].valid ? this.changes[name].value : record.value;\n const editor = html`<k-setting .setting=${{ name, value, type: record.type, description: record.description }} ?disabled=${!this.canEdit}></k-setting>`;\n if(this.owner !== 'custom' || !this.canEdit) return editor;\n return html`<div class=\"d-f\"><div class=\"flex\">${editor}</div><div><button class=\"danger\" aria-label=${`Delete ${humanize(name)}`} title=\"Delete\" @click=${() => this.remove(record)}><k-icon name=\"delete\"></k-icon></button></div></div>`;\n }\n\n render(){\n if(this.loading) return html`<k-spinner></k-spinner>`;\n if(this.error) return html`<p class=\"tc-danger\" role=\"alert\">${this.error}</p>`;\n return html`\n ${this.heading ? html`<h2 class=\"h4\">${this.heading}</h2>` : nothing}\n ${this.owner === 'custom' && this.canEdit ? html`<p class=\"mb\"><button @click=${this.create}><k-icon name=\"add\"></k-icon> Create a setting</button></p>` : nothing}\n ${this.canEdit ? nothing : html`<p class=\"tc-muted mb\">You can see these settings but not change them.</p>`}\n ${this.settings.length ? this.settings.map(record => this.renderSetting(record)) : html`<p class=\"tc-muted mb\">${this.owner === 'custom' ? 'There are no custom settings yet.' : 'There are no settings here.'}</p>`}\n ${this.canEdit && this.settings.length ? html`<button class=\"primary\" ?disabled=${!this.dirty || this.blocked || this.busy} @click=${this.save}>${this.busy ? 'Saving…' : 'Save settings'}</button>` : nothing}\n `;\n }\n\n firstUpdated(){\n this.renderRoot.addEventListener('setting-change', this.record);\n }\n}\n\ncustomElements.define('k-settings-panel', SettingsPanel);\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\SettingsPanel.js","names":["ShadowComponent","html","nothing","Toast","Dialog","listSettings","setSetting","deleteSetting","getAllCurrentUserPermissions","humanize","PERMISSIONS","system","custom","permissionsPromise","DEFAULTS","string","number","boolean","json","secret","typed","record","value","undefined","type","Number","JSON","parse","SettingsPanel","static","owner","String","heading","settings","state","changes","canEdit","loading","busy","error","constructor","super","this","connectedCallback","pane","closest","active","load","addEventListener","detail","tab","name","shortName","slice","length","async","data","permissions","Promise","all","limit","then","Set","has","msg","map","event","valid","find","entry","unchanged","stringify","removed","rest","dirty","Object","keys","blocked","values","some","change","save","failed","entries","isPublic","description","success","create","dialog","title","confirmText","cancelText","confirmAction","read","id","querySelector","trim","includes","text","isFinite","Error","toLowerCase","coerce","checked","target","remove","confirm","confirmed","confirmClasses","cancelClasses","renderSetting","hasOwn","editor","render","firstUpdated","renderRoot","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Spinner.js';\nimport '/kempo-ui/components/Icon.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport Dialog from '/kempo-ui/components/Dialog.js';\nimport { listSettings, setSetting, deleteSetting, getAllCurrentUserPermissions } from '/kempo/sdk.js';\nimport './Setting.js';\nimport humanize from './humanize.js';\n\n/*\n The settings of one owner, each with the editor for its type, and a Save button for what was changed. Served at\n /admin/components/settings/SettingsPanel.js:\n\n <k-settings-panel owner=\"system\"></k-settings-panel>\n <k-settings-panel owner=\"my-extension\" heading=\"My Extension\"></k-settings-panel>\n\n `owner` is `system`, `custom` or the name of an extension. The Custom panel also creates and deletes settings. An extension drops\n one of these in its own settings tab and needs no editor of its own. Whether the person can change them depends on who owns\n them (`system:settings:update`, `system:custom-settings:manage`, `system:extensions:manage`); without it the fields are read-only.\n Saving sends only what was changed, each as its own setting.\n*/\nconst PERMISSIONS = { system: 'system:settings:update', custom: 'system:custom-settings:manage' };\nconst EXTENSION_PERMISSION = 'system:extensions:manage';\n\nlet permissionsPromise = null;\nconst myPermissions = () => (permissionsPromise ??= getAllCurrentUserPermissions().then(([, data]) => new Set(data?.permissions ?? [])));\n\nconst DEFAULTS = { string: '', number: 0, boolean: false, json: {}, secret: '' };\n\nconst coerce = (text, type) => {\n if(type === 'number'){\n const number = Number(text);\n if(String(text).trim() === '' || !Number.isFinite(number)) throw new Error('number');\n return number;\n }\n if(type === 'boolean') return String(text).toLowerCase() === 'true';\n if(type === 'json') return JSON.parse(String(text || '{}'));\n return String(text);\n};\n\n/*\n The settings API gives each value as it is stored, as text (`'true'`, `'7'`, a JSON string). The editors take real values, so the value\n is turned into the type the setting has. A secret is left as the mask the API sends: it only says that one is saved.\n*/\nconst typed = record => {\n if(record.value === null || record.value === undefined || record.type === 'secret') return record;\n try {\n if(record.type === 'number') return { ...record, value: Number(record.value) };\n if(record.type === 'boolean') return { ...record, value: record.value === true || record.value === 'true' };\n if(record.type === 'json') return { ...record, value: typeof record.value === 'string' ? JSON.parse(record.value) : record.value };\n } catch {\n return record;\n }\n return record;\n};\n\nexport default class SettingsPanel extends ShadowComponent {\n static properties = {\n owner: { type: String },\n heading: { type: String },\n settings: { state: true },\n changes: { state: true },\n canEdit: { state: true },\n loading: { state: true },\n busy: { state: true },\n error: { state: true },\n };\n\n constructor(){\n super();\n this.owner = '';\n this.heading = '';\n this.settings = [];\n this.changes = {};\n this.canEdit = false;\n this.loading = true;\n this.busy = false;\n this.error = '';\n }\n\n /*\n Lifecycle\n */\n connectedCallback(){\n super.connectedCallback();\n const pane = this.closest('k-tab-content');\n // Loads when its tab is first opened, so a tab nobody opens makes no requests.\n if(!pane || pane.active) this.load();\n else this.closest('k-tabs')?.addEventListener('tabfirstshown', ({ detail }) => {\n if(detail.tab === pane.name) this.load();\n });\n }\n\n /*\n Loading and saving\n */\n shortName = record => record.name.slice(this.owner.length + 1);\n\n load = async () => {\n const [[error, data], permissions] = await Promise.all([listSettings({ owner: this.owner, limit: 1000 }), myPermissions()]);\n this.canEdit = permissions.has(PERMISSIONS[this.owner] ?? EXTENSION_PERMISSION);\n this.loading = false;\n if(error){\n this.error = error.msg || 'The settings could not be loaded';\n return;\n }\n this.error = '';\n this.settings = data.settings.map(typed);\n this.changes = {};\n };\n\n record = event => {\n const { name, value, valid } = event.detail;\n const record = this.settings.find(entry => this.shortName(entry) === name);\n const unchanged = record && (record.type === 'secret' ? value === '' : JSON.stringify(value) === JSON.stringify(record.value));\n const { [name]: removed, ...rest } = this.changes;\n this.changes = unchanged && valid ? rest : { ...rest, [name]: { value, valid } };\n };\n\n get dirty(){\n return Object.keys(this.changes).length > 0;\n }\n\n get blocked(){\n return Object.values(this.changes).some(change => !change.valid);\n }\n\n save = async () => {\n this.busy = true;\n let failed = 0;\n for(const [name, change] of Object.entries(this.changes)){\n const record = this.settings.find(entry => this.shortName(entry) === name);\n const [error] = await setSetting(this.owner, name, { value: change.value, type: record.type, isPublic: record.isPublic, description: record.description });\n if(error){\n failed++;\n Toast.error(`${humanize(name)}: ${error.msg || 'could not be saved'}`);\n }\n }\n this.busy = false;\n if(!failed) Toast.success('Settings saved');\n await this.load();\n };\n\n /*\n Custom settings: create and delete\n */\n create = () => {\n const dialog = Dialog.create(html`\n <div class=\"p\">\n <label class=\"mb d-b\">Name<input type=\"text\" id=\"newName\" placeholder=\"my_setting\" required></label>\n <label class=\"mb d-b\">Type\n <select id=\"newType\">\n <option value=\"string\">String</option>\n <option value=\"number\">Number</option>\n <option value=\"boolean\">Boolean (true or false)</option>\n <option value=\"json\">JSON</option>\n <option value=\"secret\">Secret</option>\n </select>\n </label>\n <label class=\"mb d-b\">Value<input type=\"text\" id=\"newValue\"></label>\n <label class=\"mb d-b\">Description<input type=\"text\" id=\"newDescription\"></label>\n <label class=\"mb d-b\"><input type=\"checkbox\" id=\"newPublic\"> Public (anyone can read it, without signing in)</label>\n </div>\n `, {\n title: 'Create a setting',\n confirmText: 'Create',\n cancelText: 'Cancel',\n confirmAction: async () => {\n const read = id => dialog.querySelector(id);\n const name = read('#newName').value.trim();\n const type = read('#newType').value;\n if(!name){ Toast.error('Give the setting a name'); return; }\n if(name.includes(':')){ Toast.error('The name cannot contain \":\"'); return; }\n let value;\n try {\n value = read('#newValue').value === '' ? DEFAULTS[type] : coerce(read('#newValue').value, type);\n } catch {\n Toast.error(`That is not a valid ${type} value`);\n return;\n }\n const [error] = await setSetting('custom', name, { value, type, isPublic: type !== 'secret' && read('#newPublic').checked, description: read('#newDescription').value.trim() });\n if(error){ Toast.error(error.msg || 'The setting could not be created'); return; }\n Toast.success('Setting created');\n await this.load();\n },\n });\n dialog.querySelector('#newType').addEventListener('change', event => {\n dialog.querySelector('#newValue').type = event.target.value === 'secret' ? 'password' : 'text';\n });\n };\n\n remove = record => {\n Dialog.confirm(`Delete ${humanize(this.shortName(record))}? This cannot be undone.`, async confirmed => {\n if(!confirmed) return;\n const [error] = await deleteSetting(this.owner, this.shortName(record));\n if(error){ Toast.error(error.msg || 'The setting could not be deleted'); return; }\n Toast.success('Setting deleted');\n await this.load();\n }, { title: 'Delete setting', confirmText: 'Delete', confirmClasses: 'danger ml', cancelText: 'Cancel', cancelClasses: '' });\n };\n\n /*\n Rendering\n */\n renderSetting(record){\n const name = this.shortName(record);\n const value = Object.hasOwn(this.changes, name) && this.changes[name].valid ? this.changes[name].value : record.value;\n const editor = html`<k-setting .setting=${{ name, value, type: record.type, description: record.description }} ?disabled=${!this.canEdit}></k-setting>`;\n if(this.owner !== 'custom' || !this.canEdit) return editor;\n return html`<div class=\"d-f\"><div class=\"flex\">${editor}</div><div><button class=\"danger\" aria-label=${`Delete ${humanize(name)}`} title=\"Delete\" @click=${() => this.remove(record)}><k-icon name=\"delete\"></k-icon></button></div></div>`;\n }\n\n render(){\n if(this.loading) return html`<k-spinner></k-spinner>`;\n if(this.error) return html`<p class=\"tc-danger\" role=\"alert\">${this.error}</p>`;\n return html`\n ${this.heading ? html`<h2 class=\"h4\">${this.heading}</h2>` : nothing}\n ${this.owner === 'custom' && this.canEdit ? html`<p class=\"mb\"><button @click=${this.create}><k-icon name=\"add\"></k-icon> Create a setting</button></p>` : nothing}\n ${this.canEdit ? nothing : html`<p class=\"tc-muted mb\">You can see these settings but not change them.</p>`}\n ${this.settings.length ? this.settings.map(record => this.renderSetting(record)) : html`<p class=\"tc-muted mb\">${this.owner === 'custom' ? 'There are no custom settings yet.' : 'There are no settings here.'}</p>`}\n ${this.canEdit && this.settings.length ? html`<button class=\"primary\" ?disabled=${!this.dirty || this.blocked || this.busy} @click=${this.save}>${this.busy ? 'Saving…' : 'Save settings'}</button>` : nothing}\n `;\n }\n\n firstUpdated(){\n this.renderRoot.addEventListener('setting-change', this.record);\n }\n}\n\ncustomElements.define('k-settings-panel', SettingsPanel);\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import t from"../AdminTableControl.js";import{html as e}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Icon.js";export default class i extends t{connectedCallback(){super.connectedCallback(),this.hasAttribute("title")||(this.title="Edit")}handleAction(){this.record&&this.table.dispatchEvent(new CustomEvent("editTemplate",{detail:{record:this.record}}))}render(){return e`<slot><k-icon name="edit"></k-icon></slot>`}}customElements.define("admin-edit-email-template",i);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\tableControls\EditEmailTemplate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\tableControls\\EditEmailTemplate.js","names":["AdminTableControl","html","EditEmailTemplate","connectedCallback","super","this","hasAttribute","title","handleAction","record","table","dispatchEvent","CustomEvent","detail","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 one email template in the editor dialog. The dialog belongs to the list, so this only reports the\n choice, as an `editTemplate` event on the table: { record }.\n*/\nexport default class EditEmailTemplate extends AdminTableControl {\n connectedCallback(){\n super.connectedCallback();\n if(!this.hasAttribute('title')) this.title = 'Edit';\n }\n\n handleAction(){\n if(!this.record) return;\n this.table.dispatchEvent(new CustomEvent('editTemplate', { detail: { record: this.record } }));\n }\n\n render(){ return html`<slot><k-icon name=\"edit\"></k-icon></slot>`; }\n}\n\ncustomElements.define('admin-edit-email-template', EditEmailTemplate);\n"],"mappings":"OAAOA,MAAuB,yCACrBC,MAAY,iCACd,8CAMQ,MAAMC,UAA0BF,EAC7C,iBAAAG,GACEC,MAAMD,oBACFE,KAAKC,aAAa,WAAUD,KAAKE,MAAQ,OAC/C,CAEA,YAAAC,GACMH,KAAKI,QACTJ,KAAKK,MAAMC,cAAc,IAAIC,YAAY,eAAgB,CAAEC,OAAQ,CAAEJ,OAAQJ,KAAKI,UACpF,CAEA,MAAAK,GAAU,OAAOb,CAAI,4CAA8C,EAGrEc,eAAeC,OAAO,4BAA6Bd","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"../AdminTableControl.js";import{html as t}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Icon.js";export default class o extends e{connectedCallback(){super.connectedCallback(),this.hasAttribute("title")||(this.title="Update")}handleAction(){this.record&&this.table.dispatchEvent(new CustomEvent("updateItem",{detail:{record:this.record}}))}render(){return t`<slot><k-icon name="deployed_code_update"></k-icon></slot>`}}customElements.define("admin-update-item",o);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\tableControls\UpdateItem.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\tableControls\\UpdateItem.js","names":["AdminTableControl","html","UpdateItem","connectedCallback","super","this","hasAttribute","title","handleAction","record","table","dispatchEvent","CustomEvent","detail","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 Updates one row on its own. The page owns the confirmation and the request, so this only reports the\n choice, as an `updateItem` event on the table: { record }.\n*/\nexport default class UpdateItem extends AdminTableControl {\n connectedCallback(){\n super.connectedCallback();\n if(!this.hasAttribute('title')) this.title = 'Update';\n }\n\n handleAction(){\n if(!this.record) return;\n this.table.dispatchEvent(new CustomEvent('updateItem', { detail: { record: this.record } }));\n }\n\n render(){ return html`<slot><k-icon name=\"deployed_code_update\"></k-icon></slot>`; }\n}\n\ncustomElements.define('admin-update-item', UpdateItem);\n"],"mappings":"OAAOA,MAAuB,yCACrBC,MAAY,iCACd,8CAMQ,MAAMC,UAAmBF,EACtC,iBAAAG,GACEC,MAAMD,oBACFE,KAAKC,aAAa,WAAUD,KAAKE,MAAQ,SAC/C,CAEA,YAAAC,GACMH,KAAKI,QACTJ,KAAKK,MAAMC,cAAc,IAAIC,YAAY,aAAc,CAAEC,OAAQ,CAAEJ,OAAQJ,KAAKI,UAClF,CAEA,MAAAK,GAAU,OAAOb,CAAI,4DAA8D,EAGrFc,eAAeC,OAAO,oBAAqBd","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"../AdminTableControl.js";import{html as t}from"/kempo-ui/lit-all.min.js";export default class l extends e{handleAction(){this.table.dispatchEvent(new CustomEvent("updateSelected"))}render(){return t`<slot>Update selected</slot>`}}customElements.define("admin-update-selected",l);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\tableControls\UpdateSelected.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\tableControls\\UpdateSelected.js","names":["AdminTableControl","html","UpdateSelected","handleAction","this","table","dispatchEvent","CustomEvent","render","customElements","define"],"sources":["0"],"sourcesContent":["import AdminTableControl from '../AdminTableControl.js';\nimport { html } from '/kempo-ui/lit-all.min.js';\n\n/*\n Updates every ticked row together. It sits in the table's bottom strip, not in a row, so it reports the\n choice as an `updateSelected` event on the table; the page reads what is ticked.\n*/\nexport default class UpdateSelected extends AdminTableControl {\n handleAction(){\n this.table.dispatchEvent(new CustomEvent('updateSelected'));\n }\n\n render(){ return html`<slot>Update selected</slot>`; }\n}\n\ncustomElements.define('admin-update-selected', UpdateSelected);\n"],"mappings":"OAAOA,MAAuB,yCACrBC,MAAY,0CAMN,MAAMC,UAAuBF,EAC1C,YAAAG,GACEC,KAAKC,MAAMC,cAAc,IAAIC,YAAY,kBAC3C,CAEA,MAAAC,GAAU,OAAOP,CAAI,8BAAgC,EAGvDQ,eAAeC,OAAO,wBAAyBR","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"../AdminTableControl.js";import{html as t}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Icon.js";export default class i extends e{connectedCallback(){super.connectedCallback(),this.hasAttribute("title")||(this.title="View")}handleAction(){this.record&&this.table.dispatchEvent(new CustomEvent("emailView",{detail:{record:this.record}}))}render(){return t`<slot><k-icon name="visibility"></k-icon></slot>`}}customElements.define("admin-view-email",i);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\tableControls\ViewEmail.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\tableControls\\ViewEmail.js","names":["AdminTableControl","html","ViewEmail","connectedCallback","super","this","hasAttribute","title","handleAction","record","table","dispatchEvent","CustomEvent","detail","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 one email from the log. The dialog that shows it belongs to the log, so this only reports the\n choice, as an `emailView` event on the table: { record }.\n*/\nexport default class ViewEmail extends AdminTableControl {\n connectedCallback(){\n super.connectedCallback();\n if(!this.hasAttribute('title')) this.title = 'View';\n }\n\n handleAction(){\n if(!this.record) return;\n this.table.dispatchEvent(new CustomEvent('emailView', { detail: { record: this.record } }));\n }\n\n render(){ return html`<slot><k-icon name=\"visibility\"></k-icon></slot>`; }\n}\n\ncustomElements.define('admin-view-email', ViewEmail);\n"],"mappings":"OAAOA,MAAuB,yCACrBC,MAAY,iCACd,8CAMQ,MAAMC,UAAkBF,EACrC,iBAAAG,GACEC,MAAMD,oBACFE,KAAKC,aAAa,WAAUD,KAAKE,MAAQ,OAC/C,CAEA,YAAAC,GACMH,KAAKI,QACTJ,KAAKK,MAAMC,cAAc,IAAIC,YAAY,YAAa,CAAEC,OAAQ,CAAEJ,OAAQJ,KAAKI,UACjF,CAEA,MAAAK,GAAU,OAAOb,CAAI,kDAAoD,EAG3Ec,eAAeC,OAAO,mBAAoBd","ignoreList":[]}
|