kempo 4.7.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/bin/cli.js +15 -33
- package/dist/admin/components/EmailTemplates.js +25 -0
- package/dist/admin/components/EmailTemplates.js.map +1 -0
- package/dist/admin/components/Emails.js +49 -0
- package/dist/admin/components/Emails.js.map +1 -0
- package/dist/admin/components/settings/AdminSettings.js +7 -0
- package/dist/admin/components/settings/AdminSettings.js.map +1 -0
- package/dist/admin/components/settings/BooleanSetting.js +2 -0
- package/dist/admin/components/settings/BooleanSetting.js.map +1 -0
- package/dist/admin/components/settings/JsonSetting.js +2 -0
- package/dist/admin/components/settings/JsonSetting.js.map +1 -0
- package/dist/admin/components/settings/NumberSetting.js +2 -0
- package/dist/admin/components/settings/NumberSetting.js.map +1 -0
- package/dist/admin/components/settings/SecretSetting.js +2 -0
- package/dist/admin/components/settings/SecretSetting.js.map +1 -0
- package/dist/admin/components/settings/Setting.js +2 -0
- package/dist/admin/components/settings/Setting.js.map +1 -0
- package/dist/admin/components/settings/SettingEditor.js +2 -0
- package/dist/admin/components/settings/SettingEditor.js.map +1 -0
- package/dist/admin/components/settings/SettingsPanel.js +24 -0
- package/dist/admin/components/settings/SettingsPanel.js.map +1 -0
- package/dist/admin/components/settings/StringSetting.js +2 -0
- package/dist/admin/components/settings/StringSetting.js.map +1 -0
- package/dist/admin/components/settings/humanize.js +2 -0
- package/dist/admin/components/settings/humanize.js.map +1 -0
- package/dist/admin/emails/index.page.html +14 -0
- package/dist/admin/icons/mail.svg +1 -0
- package/dist/admin/nav.fragment.html +6 -0
- package/dist/admin/settings/[owner]/[name]/index.page.html +1 -1
- package/dist/admin/settings/index.page.html +6 -289
- package/dist/kempo/api/email-templates/GET.js +2 -0
- package/dist/kempo/api/email-templates/GET.js.map +1 -0
- package/dist/kempo/api/email-templates/file/DELETE.js +2 -0
- package/dist/kempo/api/email-templates/file/DELETE.js.map +1 -0
- package/dist/kempo/api/email-templates/file/GET.js +2 -0
- package/dist/kempo/api/email-templates/file/GET.js.map +1 -0
- package/dist/kempo/api/email-templates/file/PUT.js +2 -0
- package/dist/kempo/api/email-templates/file/PUT.js.map +1 -0
- package/dist/kempo/api/email-templates/preview/GET.js +2 -0
- package/dist/kempo/api/email-templates/preview/GET.js.map +1 -0
- package/dist/kempo/api/emails/GET.js +2 -0
- package/dist/kempo/api/emails/GET.js.map +1 -0
- package/dist/kempo/api/emails/[id]/GET.js +2 -0
- package/dist/kempo/api/emails/[id]/GET.js.map +1 -0
- package/dist/kempo/api/emails/[id]/retry/POST.js +2 -0
- package/dist/kempo/api/emails/[id]/retry/POST.js.map +1 -0
- package/dist/kempo/api/emails/test/POST.js +2 -0
- package/dist/kempo/api/emails/test/POST.js.map +1 -0
- package/dist/kempo/api/settings/[owner]/[name]/POST.js +1 -1
- package/dist/kempo/api/settings/[owner]/[name]/POST.js.map +1 -1
- package/dist/kempo/sdk.js +1 -1
- package/dist/kempo/sdk.js.map +1 -1
- package/middleware/kempo.js +21 -0
- package/package.json +2 -3
- package/server/db/schema.js +32 -0
- package/server/sdk.js +5 -0
- package/server/utils/email/defaultSender.js +2 -0
- package/server/utils/email/dispatchEmail.js +56 -0
- package/server/utils/email/emailSettings.js +21 -0
- package/server/utils/email/emailTemplatePaths.js +36 -0
- package/server/utils/email/getEmail.js +14 -0
- package/server/utils/email/getSenderEmail.js +11 -0
- package/server/utils/email/getSenderFrom.js +18 -0
- package/server/utils/email/isValidSender.js +4 -0
- package/server/utils/email/listEmailTemplates.js +23 -0
- package/server/utils/email/listEmails.js +35 -0
- package/server/utils/email/maintainEmails.js +46 -0
- package/server/utils/email/pruneEmails.js +29 -0
- package/server/utils/email/readEmailTemplate.js +28 -0
- package/server/utils/email/resetEmailTemplate.js +15 -0
- package/server/utils/email/retryEmail.js +21 -0
- package/server/utils/email/saveEmailTemplate.js +38 -0
- package/server/utils/email/sendEmail.js +22 -32
- package/server/utils/email/sendEmailFromTemplate.js +2 -2
- package/server/utils/hooks/triggerHook.js +17 -4
- package/server/utils/settings/listSettings.js +10 -5
- package/server/utils/settings/setSetting.js +4 -0
- package/server/utils/settings/settingRules.js +21 -0
- package/server/utils/settings/systemSettings.js +40 -0
- package/server/utils/setup/seedDatabase.js +5 -60
- package/server/utils/setup/systemPermissions.js +75 -0
package/README.md
CHANGED
|
@@ -372,7 +372,9 @@ Extensions can declare `jobs` in `kempo-config.json` and kempo runs them on a ti
|
|
|
372
372
|
|
|
373
373
|
## Email
|
|
374
374
|
|
|
375
|
-
Kempo
|
|
375
|
+
Kempo does not send email itself. It renders the email, saves it in a log (**Admin > Emails** shows every one, with whether it was sent and why not), and hands it to an **email extension**: `kempo-resend` sends through Resend, `kempo-smtp` through any SMTP server (a Hostinger or Google mailbox, Amazon SES, a Postfix of your own). Install one from Admin > Extensions and enter its settings on the Settings screen; with none installed, emails are logged and fail with "No email extension is installed".
|
|
376
|
+
|
|
377
|
+
The address emails are sent from is the **Email from** setting (`system:email_from`); until it is set, `EMAIL_FROM` from `.env` is used, and it must be on a domain your email service allows. A failed email is tried again (`email_max_retries`, 1 by default) and the log is kept for `email_retention_days` (30 by default; `email_retention_by_type` sets it per kind of email). See [docs/extensions/creating-extensions.md](docs/extensions/creating-extensions.md) to write an email extension.
|
|
376
378
|
|
|
377
379
|
Built-in email templates (in `templates/emails/`):
|
|
378
380
|
- `verify-email.html` — Email verification link
|
package/bin/cli.js
CHANGED
|
@@ -267,40 +267,20 @@ if(adminSetupChoice === 'browser'){
|
|
|
267
267
|
adminPassword = adminAnswers.adminPassword || generatedAdminPassword;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
let emailApiKey = '';
|
|
271
270
|
let emailFrom = 'noreply@example.com';
|
|
272
|
-
let emailServiceSkipped = false;
|
|
273
271
|
|
|
272
|
+
/*
|
|
273
|
+
Kempo does not send email itself: an email extension (kempo-resend, kempo-smtp...) does, and is installed from the admin once the
|
|
274
|
+
site is running. Only the address emails are sent from is asked here; it can be changed later on the admin Settings screen.
|
|
275
|
+
*/
|
|
274
276
|
if(!useDefaults){
|
|
275
|
-
const {
|
|
276
|
-
type: '
|
|
277
|
-
name: '
|
|
278
|
-
message: '
|
|
279
|
-
|
|
280
|
-
{ name: 'Resend (recommended — https://resend.com)', value: 'resend' },
|
|
281
|
-
{ name: 'Skip for now', value: 'skip' }
|
|
282
|
-
]
|
|
277
|
+
const { fromEmail } = await inquirer.prompt([{
|
|
278
|
+
type: 'input',
|
|
279
|
+
name: 'fromEmail',
|
|
280
|
+
message: 'The address emails are sent from:',
|
|
281
|
+
default: 'noreply@example.com'
|
|
283
282
|
}]);
|
|
284
|
-
|
|
285
|
-
if(emailService === 'resend'){
|
|
286
|
-
const emailAnswers = await inquirer.prompt([
|
|
287
|
-
{
|
|
288
|
-
type: 'input',
|
|
289
|
-
name: 'apiKey',
|
|
290
|
-
message: 'Resend API key:'
|
|
291
|
-
},
|
|
292
|
-
{
|
|
293
|
-
type: 'input',
|
|
294
|
-
name: 'fromEmail',
|
|
295
|
-
message: 'From email address:',
|
|
296
|
-
default: 'noreply@example.com'
|
|
297
|
-
}
|
|
298
|
-
]);
|
|
299
|
-
emailApiKey = emailAnswers.apiKey;
|
|
300
|
-
emailFrom = emailAnswers.fromEmail;
|
|
301
|
-
} else {
|
|
302
|
-
emailServiceSkipped = true;
|
|
303
|
-
}
|
|
283
|
+
emailFrom = fromEmail;
|
|
304
284
|
}
|
|
305
285
|
|
|
306
286
|
/*
|
|
@@ -393,8 +373,7 @@ writeFileSync(join(projectDir, '.env'),
|
|
|
393
373
|
`# Database
|
|
394
374
|
DATABASE_URL=${databaseUrl}
|
|
395
375
|
|
|
396
|
-
# Email
|
|
397
|
-
RESEND_API_KEY=${emailApiKey || 'your_resend_api_key_here'}
|
|
376
|
+
# Email: the address emails are sent from (also a setting on the admin Settings screen). Sending needs an email extension such as kempo-resend or kempo-smtp.
|
|
398
377
|
EMAIL_FROM=${emailFrom}
|
|
399
378
|
|
|
400
379
|
# Encrypts the API credentials saved in the admin. Back this up with your database: if it is lost they cannot be recovered.
|
|
@@ -524,7 +503,10 @@ ${adminSetupChoice === 'browser' ? `Create your admin user in the browser:
|
|
|
524
503
|
` : `Admin credentials:
|
|
525
504
|
Email: ${adminEmail}
|
|
526
505
|
Password: ${adminPassword}
|
|
527
|
-
`}
|
|
506
|
+
`}
|
|
507
|
+
Note: Kempo does not send email itself. Install an email extension (kempo-resend or kempo-smtp)
|
|
508
|
+
from Admin > Extensions when you want the site to send email.
|
|
509
|
+
|
|
528
510
|
Next steps:
|
|
529
511
|
npm run dev
|
|
530
512
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import t from"/kempo-ui/components/ShadowComponent.js";import{html as e,nothing as i}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Spinner.js";import"/kempo-ui/components/Icon.js";import"/kempo-ui/components/Dialog.js";import"/kempo-ui/components/CodeEditor.js";import s from"/kempo-ui/components/Toast.js";import{listEmailTemplates as a,getEmailTemplate as o,saveEmailTemplate as r,resetEmailTemplate as n}from"/kempo/sdk.js";export default class l extends t{static properties={owner:{type:String},previewUrl:{type:String,attribute:"preview-url"},templates:{state:!0},error:{state:!0},editing:{state:!0},saving:{state:!0}};constructor(){super(),this.owner="",this.previewUrl="/kempo/api/email-templates/preview",this.templates=null,this.error="",this.editing=null,this.saving=!1}connectedCallback(){super.connectedCallback(),this.load()}load=async()=>{const[t,e]=await a(this.owner);t?this.error=t.msg:this.templates=e.templates};edit=async t=>{const[e,i]=await o(t);e?s.error(e.msg):(this.editing=i,await this.updateComplete,this.renderRoot.querySelector("#emailDialog").open())};confirmSave=t=>{t.keepDialogOpen=!0,this.save()};save=async()=>{this.saving=!0;const[t]=await r({template:this.editing.template,source:this.renderRoot.querySelector("#emailEditor").value});this.saving=!1,t?s.error(t.msg):(this.renderRoot.querySelector("#emailDialog").close(),s.success("Email template saved"),await this.load())};reset=async()=>{this.saving=!0;const[t]=await n(this.editing.template);this.saving=!1,t?s.error(t.msg):(this.renderRoot.querySelector("#emailDialog").close(),s.success("Email template reset to the default"),await this.load())};renderDialog(){const t=this.editing;return t?e`
|
|
2
|
+
<k-dialog id="emailDialog" style="--width: 60rem;" confirm-text=${this.saving?"Saving…":"Save"} confirm-classes="primary ml" cancel-text="Cancel" .confirmAction=${this.confirmSave}>
|
|
3
|
+
<h2 slot="title" class="h5 pyh px m0">${t.title||t.name}</h2>
|
|
4
|
+
<div class="p">
|
|
5
|
+
<p class="tc-muted mb">${t.template}. Saving keeps your copy in <strong>templates/emails</strong>, which is used from the next email and is not changed by updates. A template that does not render is not saved.</p>
|
|
6
|
+
<k-code-editor id="emailEditor" class="r b mb" language="html" .value=${t.source} style="height: 55vh; min-height: 300px;"></k-code-editor>
|
|
7
|
+
${t.overridden?e`<button type="button" class="danger" ?disabled=${this.saving} @click=${this.reset}>Reset to default</button>`:i}
|
|
8
|
+
</div>
|
|
9
|
+
</k-dialog>
|
|
10
|
+
`:i}render(){return this.error?e`<p class="tc-danger" role="alert">${this.error}</p>`:this.templates?this.templates.length?e`
|
|
11
|
+
<table class="mb">
|
|
12
|
+
<thead><tr><th>Email</th><th>Template</th><th>Status</th><th></th></tr></thead>
|
|
13
|
+
<tbody>${this.templates.map(t=>e`<tr>
|
|
14
|
+
<td>${t.title||t.name}</td>
|
|
15
|
+
<td class="tc-muted">${t.template}</td>
|
|
16
|
+
<td>${t.overridden?e`<span class="tc-warning">Customized</span>`:e`<span class="tc-muted">Default</span>`}</td>
|
|
17
|
+
<td class="ta-right">
|
|
18
|
+
<a class="btn" href=${`${this.previewUrl}?template=${encodeURIComponent(t.template)}`} target="email-preview">Preview</a>
|
|
19
|
+
<button aria-label="Edit" title="Edit" @click=${()=>this.edit(t.template)}><k-icon name="edit"></k-icon></button>
|
|
20
|
+
</td>
|
|
21
|
+
</tr>`)}</tbody>
|
|
22
|
+
</table>
|
|
23
|
+
${this.renderDialog()}
|
|
24
|
+
`:e`<p class="tc-muted">There are no email templates here.</p>`:e`<k-spinner></k-spinner>`}}customElements.define("k-email-templates",l);
|
|
25
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\EmailTemplates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\EmailTemplates.js","names":["ShadowComponent","html","nothing","Toast","listEmailTemplates","getEmailTemplate","saveEmailTemplate","resetEmailTemplate","EmailTemplates","static","owner","type","String","previewUrl","attribute","templates","state","error","editing","saving","constructor","super","this","connectedCallback","load","async","data","msg","edit","found","template","updateComplete","renderRoot","querySelector","open","confirmSave","event","keepDialogOpen","save","source","value","close","success","reset","renderDialog","title","name","overridden","render","length","map","encodeURIComponent","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 '/kempo-ui/components/Dialog.js';\nimport '/kempo-ui/components/CodeEditor.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport { listEmailTemplates, getEmailTemplate, saveEmailTemplate, resetEmailTemplate } from '/kempo/sdk.js';\n\n/*\n The email templates of one extension, to edit, served at /admin/components/EmailTemplates.js:\n\n <script type=\"module\" src=\"/admin/components/EmailTemplates.js\"></script>\n <k-email-templates owner=\"kempo-commerce\"></k-email-templates>\n\n Without `owner` it lists kempo's own templates (verify-email, reset-password). Each row opens a dialog with the template in\n a code editor. Saving writes the site's own copy to `templates/emails/<owner>/<name>.page.html`, which is used from the next\n email on and survives updates of the extension; **Reset to default** removes it. The packaged template is never changed.\n\n **Preview** opens the template as it would be sent, in one named tab that each preview replaces. By default that is kempo's\n preview, which draws it with no data; an extension that can do better (its own orders as the data) serves its own and gives\n its address as `preview-url`:\n\n <k-email-templates owner=\"kempo-commerce\" preview-url=\"/commerce/api/emails/preview\"></k-email-templates>\n\n The preview is the saved template, not what is being typed in the editor.\n An extension puts this on its settings page; it needs no code of its own.\n*/\nexport default class EmailTemplates extends ShadowComponent {\n static properties = {\n owner: { type: String },\n previewUrl: { type: String, attribute: 'preview-url' },\n templates: { state: true },\n error: { state: true },\n editing: { state: true },\n saving: { state: true },\n };\n\n constructor(){\n super();\n this.owner = '';\n this.previewUrl = '/kempo/api/email-templates/preview';\n this.templates = null;\n this.error = '';\n this.editing = null;\n this.saving = false;\n }\n\n /*\n Lifecycle\n */\n connectedCallback(){\n super.connectedCallback();\n this.load();\n }\n\n /*\n Actions\n */\n load = async () => {\n const [error, data] = await listEmailTemplates(this.owner);\n if(error){\n this.error = error.msg;\n return;\n }\n this.templates = data.templates;\n };\n\n edit = async template => {\n const [error, found] = await getEmailTemplate(template);\n if(error){\n Toast.error(error.msg);\n return;\n }\n this.editing = found;\n await this.updateComplete;\n this.renderRoot.querySelector('#emailDialog').open();\n };\n\n /* The dialog's Save button: it stays open until the template is saved, so a template that does not render is not lost. */\n confirmSave = event => {\n event.keepDialogOpen = true;\n this.save();\n };\n\n save = async () => {\n this.saving = true;\n const [error] = await saveEmailTemplate({ template: this.editing.template, source: this.renderRoot.querySelector('#emailEditor').value });\n this.saving = false;\n if(error){\n Toast.error(error.msg);\n return;\n }\n this.renderRoot.querySelector('#emailDialog').close();\n Toast.success('Email template saved');\n await this.load();\n };\n\n reset = async () => {\n this.saving = true;\n const [error] = await resetEmailTemplate(this.editing.template);\n this.saving = false;\n if(error){\n Toast.error(error.msg);\n return;\n }\n this.renderRoot.querySelector('#emailDialog').close();\n Toast.success('Email template reset to the default');\n await this.load();\n };\n\n /*\n Rendering\n */\n renderDialog(){\n const template = this.editing;\n if(!template) return nothing;\n return html`\n <k-dialog id=\"emailDialog\" style=\"--width: 60rem;\" confirm-text=${this.saving ? 'Saving…' : 'Save'} confirm-classes=\"primary ml\" cancel-text=\"Cancel\" .confirmAction=${this.confirmSave}>\n <h2 slot=\"title\" class=\"h5 pyh px m0\">${template.title || template.name}</h2>\n <div class=\"p\">\n <p class=\"tc-muted mb\">${template.template}. Saving keeps your copy in <strong>templates/emails</strong>, which is used from the next email and is not changed by updates. A template that does not render is not saved.</p>\n <k-code-editor id=\"emailEditor\" class=\"r b mb\" language=\"html\" .value=${template.source} style=\"height: 55vh; min-height: 300px;\"></k-code-editor>\n ${template.overridden ? html`<button type=\"button\" class=\"danger\" ?disabled=${this.saving} @click=${this.reset}>Reset to default</button>` : nothing}\n </div>\n </k-dialog>\n `;\n }\n\n render(){\n if(this.error) return html`<p class=\"tc-danger\" role=\"alert\">${this.error}</p>`;\n if(!this.templates) return html`<k-spinner></k-spinner>`;\n if(!this.templates.length) return html`<p class=\"tc-muted\">There are no email templates here.</p>`;\n return html`\n <table class=\"mb\">\n <thead><tr><th>Email</th><th>Template</th><th>Status</th><th></th></tr></thead>\n <tbody>${this.templates.map(template => html`<tr>\n <td>${template.title || template.name}</td>\n <td class=\"tc-muted\">${template.template}</td>\n <td>${template.overridden ? html`<span class=\"tc-warning\">Customized</span>` : html`<span class=\"tc-muted\">Default</span>`}</td>\n <td class=\"ta-right\">\n <a class=\"btn\" href=${`${this.previewUrl}?template=${encodeURIComponent(template.template)}`} target=\"email-preview\">Preview</a>\n <button aria-label=\"Edit\" title=\"Edit\" @click=${() => this.edit(template.template)}><k-icon name=\"edit\"></k-icon></button>\n </td>\n </tr>`)}</tbody>\n </table>\n ${this.renderDialog()}\n `;\n }\n}\n\ncustomElements.define('k-email-templates', EmailTemplates);\n"],"mappings":"OAAOA,MAAqB,yDACnBC,aAAMC,MAAe,iCACvB,wCACA,qCACA,uCACA,4CACAC,MAAW,6DACTC,sBAAoBC,uBAAkBC,wBAAmBC,MAA0B,+BAqB7E,MAAMC,UAAuBR,EAC1CS,kBAAoB,CAClBC,MAAO,CAAEC,KAAMC,QACfC,WAAY,CAAEF,KAAMC,OAAQE,UAAW,eACvCC,UAAW,CAAEC,OAAO,GACpBC,MAAO,CAAED,OAAO,GAChBE,QAAS,CAAEF,OAAO,GAClBG,OAAQ,CAAEH,OAAO,IAGnB,WAAAI,GACEC,QACAC,KAAKZ,MAAQ,GACbY,KAAKT,WAAa,qCAClBS,KAAKP,UAAY,KACjBO,KAAKL,MAAQ,GACbK,KAAKJ,QAAU,KACfI,KAAKH,QAAS,CAChB,CAKA,iBAAAI,GACEF,MAAME,oBACND,KAAKE,MACP,CAKAA,KAAOC,UACL,MAAOR,EAAOS,SAActB,EAAmBkB,KAAKZ,OACjDO,EACDK,KAAKL,MAAQA,EAAMU,IAGrBL,KAAKP,UAAYW,EAAKX,WAGxBa,KAAOH,UACL,MAAOR,EAAOY,SAAexB,EAAiByB,GAC3Cb,EACDd,EAAMc,MAAMA,EAAMU,MAGpBL,KAAKJ,QAAUW,QACTP,KAAKS,eACXT,KAAKU,WAAWC,cAAc,gBAAgBC,SAIhDC,YAAcC,IACZA,EAAMC,gBAAiB,EACvBf,KAAKgB,QAGPA,KAAOb,UACLH,KAAKH,QAAS,EACd,MAAOF,SAAeX,EAAkB,CAAEwB,SAAUR,KAAKJ,QAAQY,SAAUS,OAAQjB,KAAKU,WAAWC,cAAc,gBAAgBO,QACjIlB,KAAKH,QAAS,EACXF,EACDd,EAAMc,MAAMA,EAAMU,MAGpBL,KAAKU,WAAWC,cAAc,gBAAgBQ,QAC9CtC,EAAMuC,QAAQ,8BACRpB,KAAKE,SAGbmB,MAAQlB,UACNH,KAAKH,QAAS,EACd,MAAOF,SAAeV,EAAmBe,KAAKJ,QAAQY,UACtDR,KAAKH,QAAS,EACXF,EACDd,EAAMc,MAAMA,EAAMU,MAGpBL,KAAKU,WAAWC,cAAc,gBAAgBQ,QAC9CtC,EAAMuC,QAAQ,6CACRpB,KAAKE,SAMb,YAAAoB,GACE,MAAMd,EAAWR,KAAKJ,QACtB,OAAIY,EACG7B,CAAI;wEACyDqB,KAAKH,OAAS,UAAY,2EAA2EG,KAAKa;gDAClIL,EAASe,OAASf,EAASgB;;mCAExChB,EAASA;kFACsCA,EAASS;YAC/ET,EAASiB,WAAa9C,CAAI,kDAAkDqB,KAAKH,iBAAiBG,KAAKqB,kCAAoCzC;;;MAP9HA,CAWvB,CAEA,MAAA8C,GACE,OAAG1B,KAAKL,MAAchB,CAAI,qCAAqCqB,KAAKL,YAChEK,KAAKP,UACLO,KAAKP,UAAUkC,OACZhD,CAAI;;;iBAGEqB,KAAKP,UAAUmC,IAAIpB,GAAY7B,CAAI;gBACpC6B,EAASe,OAASf,EAASgB;iCACVhB,EAASA;gBAC1BA,EAASiB,WAAa9C,CAAI,6CAA+CA,CAAI;;kCAE3D,GAAGqB,KAAKT,uBAAuBsC,mBAAmBrB,EAASA;4DACjC,IAAMR,KAAKM,KAAKE,EAASA;;;;QAI7ER,KAAKsB;MAdyB3C,CAAI,6DADXA,CAAI,yBAiBjC,EAGFmD,eAAeC,OAAO,oBAAqB7C","ignoreList":[]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import t from"/kempo-ui/components/ShadowComponent.js";import{html as s,nothing as e}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Spinner.js";import"/kempo-ui/components/Dialog.js";import a from"/kempo-ui/components/Toast.js";import{listEmails as i,getEmail as o,retryEmail as r,sendTestEmail as l,getAllCurrentUserPermissions as n}from"/kempo/sdk.js";const c={sent:"tc-success",failed:"tc-danger",sending:"tc-warning",queued:"tc-muted"},h=t=>t?new Date(t).toLocaleString():"";export default class d extends t{static properties={emails:{state:!0},total:{state:!0},offset:{state:!0},status:{state:!0},search:{state:!0},loading:{state:!0},error:{state:!0},canManage:{state:!0},open:{state:!0},testTo:{state:!0},busy:{state:!0}};constructor(){super(),this.emails=[],this.total=0,this.offset=0,this.status="",this.search="",this.loading=!0,this.error="",this.canManage=!1,this.open=null,this.testTo="",this.busy=!1}connectedCallback(){super.connectedCallback(),n().then(([,t])=>{this.canManage=(t?.permissions??[]).includes("system:emails:manage")}),this.load()}load=async()=>{this.loading=!0;const[t,s]=await i({status:this.status||void 0,search:this.search||void 0,limit:25,offset:this.offset});this.loading=!1,t?this.error=t.msg||"The emails could not be loaded":(this.error="",this.emails=s.emails,this.total=s.total)};filter=async t=>{Object.assign(this,t,{offset:0}),await this.load()};page=async t=>{this.offset=Math.max(0,this.offset+25*t),await this.load()};show=async t=>{const[s,e]=await o(t);s?a.error(s.msg):(this.open=e,await this.updateComplete,this.renderRoot.querySelector("#emailDialog").open())};retry=async()=>{this.busy=!0;const[t,s]=await r(this.open.id);this.busy=!1,t?a.error(t.msg):s.sent?a.success("The email was sent"):a.error(s.error||"The email could not be sent"),this.renderRoot.querySelector("#emailDialog").close(),await this.load()};openTest=()=>this.renderRoot.querySelector("#testDialog").open();confirmTest=t=>{t.keepDialogOpen=!0,this.sendTest()};sendTest=async()=>{this.busy=!0;const[t,s]=await l(this.testTo.trim());this.busy=!1,t?a.error(t.msg):(this.renderRoot.querySelector("#testDialog").close(),s.sent?a.success(`A test email was sent to ${this.testTo.trim()}`):a.error(s.error||"The test email could not be sent"),await this.load())};renderDetail(){const t=this.open;return t?s`
|
|
2
|
+
<k-dialog id="emailDialog" style="--width: 60rem;" cancel-text="Close">
|
|
3
|
+
<h2 slot="title" class="h5 pyh px m0">${t.subject}</h2>
|
|
4
|
+
<div class="p">
|
|
5
|
+
<p class="mb"><strong class=${c[t.status]??""}>${t.status}</strong>${t.attempts?s` · ${t.attempts} ${1===t.attempts?"try":"tries"}`:e}${t.provider?s` · sent by ${t.provider}`:e}</p>
|
|
6
|
+
<p class="mb">To <strong>${t.recipient}</strong>, from ${t.sender}<br><span class="tc-muted">${t.type} · created ${h(t.createdAt)}${t.sentAt?` · sent ${h(t.sentAt)}`:""}</span></p>
|
|
7
|
+
${t.lastError?s`<p class="tc-danger mb" role="alert">${t.lastError}</p>`:e}
|
|
8
|
+
${this.canManage&&["queued","failed"].includes(t.status)?s`<p class="mb"><button class="primary" ?disabled=${this.busy} @click=${this.retry}>${this.busy?"Trying…":"Try again"}</button></p>`:e}
|
|
9
|
+
<iframe title="The email" sandbox="" .srcdoc=${t.html} class="full b r" height="420"></iframe>
|
|
10
|
+
</div>
|
|
11
|
+
</k-dialog>
|
|
12
|
+
`:e}renderTest(){return s`
|
|
13
|
+
<k-dialog id="testDialog" confirm-text=${this.busy?"Sending…":"Send test email"} confirm-classes="primary ml" cancel-text="Cancel" .confirmAction=${this.confirmTest}>
|
|
14
|
+
<h2 slot="title" class="h5 pyh px m0">Send a test email</h2>
|
|
15
|
+
<div class="p">
|
|
16
|
+
<label class="mb d-b">Send it to<input type="email" .value=${this.testTo} @input=${t=>{this.testTo=t.target.value}}></label>
|
|
17
|
+
<p class="tc-muted">It goes out through the email extension that is installed, and shows in the log below.</p>
|
|
18
|
+
</div>
|
|
19
|
+
</k-dialog>
|
|
20
|
+
`}render(){const t=Math.min(this.offset+25,this.total);return s`
|
|
21
|
+
<div class="d-f mb">
|
|
22
|
+
<label class="mr">Status
|
|
23
|
+
<select @change=${t=>this.filter({status:t.target.value})}>
|
|
24
|
+
${["","queued","sending","sent","failed"].map(t=>s`<option value=${t} ?selected=${this.status===t}>${t||"All"}</option>`)}
|
|
25
|
+
</select>
|
|
26
|
+
</label>
|
|
27
|
+
<label class="mr">Search
|
|
28
|
+
<input type="search" placeholder="Address or subject" .value=${this.search} @change=${t=>this.filter({search:t.target.value})}>
|
|
29
|
+
</label>
|
|
30
|
+
${this.canManage?s`<button class="mb" @click=${this.openTest}>Send a test email</button>`:e}
|
|
31
|
+
</div>
|
|
32
|
+
${this.error?s`<p class="tc-danger" role="alert">${this.error}</p>`:e}
|
|
33
|
+
${this.loading?s`<k-spinner></k-spinner>`:this.emails.length?s`
|
|
34
|
+
<table class="mb">
|
|
35
|
+
<thead><tr><th>Status</th><th>To</th><th>Subject</th><th>Type</th><th>Created</th></tr></thead>
|
|
36
|
+
<tbody>${this.emails.map(t=>s`<tr>
|
|
37
|
+
<td><strong class=${c[t.status]??""}>${t.status}</strong></td>
|
|
38
|
+
<td>${t.recipient}</td>
|
|
39
|
+
<td><a href="#" @click=${s=>{s.preventDefault(),this.show(t.id)}}>${t.subject}</a>${t.lastError?s`<br><span class="tc-danger">${t.lastError}</span>`:e}</td>
|
|
40
|
+
<td class="tc-muted">${t.type}</td>
|
|
41
|
+
<td class="tc-muted">${h(t.createdAt)}</td>
|
|
42
|
+
</tr>`)}</tbody>
|
|
43
|
+
</table>
|
|
44
|
+
<p class="d-f mb"><button ?disabled=${0===this.offset} @click=${()=>this.page(-1)}>Newer</button><span class="tc-muted ph">${this.offset+1}–${t} of ${this.total}</span><button ?disabled=${t>=this.total} @click=${()=>this.page(1)}>Older</button></p>
|
|
45
|
+
`:s`<p class="tc-muted">No emails have been sent yet.</p>`}
|
|
46
|
+
${this.renderDetail()}
|
|
47
|
+
${this.renderTest()}
|
|
48
|
+
`}}customElements.define("k-admin-emails",d);
|
|
49
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\Emails.js.map
|
|
@@ -0,0 +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":[]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import t from"/kempo-ui/components/ShadowComponent.js";import{html as e}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Tabs.js";import"./SettingsPanel.js";import s from"./humanize.js";export default class n extends t{static properties={extensionTabs:{state:!0}};constructor(){super(),this.extensionTabs=[]}connectedCallback(){super.connectedCallback(),this.extensionTabs=[...this.querySelectorAll(":scope > k-settings-tab")].map((t,e)=>{const n=t.getAttribute("name")||t.getAttribute("label")||`tab-${e}`;return t.slot=`tab-${n}`,t.style.display="block",{id:n,slotName:`tab-${n}`,label:t.getAttribute("label")||s(n),position:Number(t.getAttribute("position"))||500,extension:!0}})}render(){const t=[{id:"custom",label:"Custom",position:100,content:()=>e`<k-settings-panel owner="custom"></k-settings-panel>`},{id:"system",label:"System",position:200,content:()=>e`<k-settings-panel owner="system"></k-settings-panel>`},...this.extensionTabs].sort((t,e)=>t.position-e.position);return e`
|
|
2
|
+
<k-tabs>
|
|
3
|
+
${t.map(t=>e`<k-tab for=${t.id}>${t.label}</k-tab>`)}
|
|
4
|
+
${t.map(t=>e`<k-tab-content name=${t.id} class="pt">${t.extension?e`<slot name=${t.slotName}></slot>`:t.content()}</k-tab-content>`)}
|
|
5
|
+
</k-tabs>
|
|
6
|
+
`}}customElements.define("k-admin-settings",n);
|
|
7
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\AdminSettings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\AdminSettings.js","names":["ShadowComponent","html","humanize","AdminSettings","static","extensionTabs","state","constructor","super","this","connectedCallback","querySelectorAll","map","$tab","index","name","getAttribute","slot","style","display","id","slotName","label","position","Number","extension","render","tabs","content","sort","a","b","tab","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Tabs.js';\nimport './SettingsPanel.js';\nimport humanize from './humanize.js';\n\n/*\n The admin Settings page, served at /admin/components/settings/AdminSettings.js. Tabs: **Custom** (the site's own settings) and **System**\n (kempo's), then a tab for each extension that chooses to add one. An extension that does not add a tab has nothing here: it manages\n its own settings wherever it likes.\n\n **An extension adds a tab** by filling the `admin-settings-tabs` location of the page with a `k-settings-tab` (and loads what the tab\n uses from the `admin-settings-scripts` location). `position` orders the tabs (Custom is 100, System 200, a tab with none 500):\n\n <k-settings-tab name=\"shipping\" label=\"Shipping\" position=\"500\">\n <k-settings-panel owner=\"kempo-shipping\"></k-settings-panel>\n </k-settings-tab>\n\n The tab holds anything: the generic panel above, a few of the extension's settings with their editors, or a screen of its own.\n*/\nconst DEFAULT_TAB_POSITION = 500;\n\nexport default class AdminSettings extends ShadowComponent {\n static properties = {\n extensionTabs: { state: true },\n };\n\n constructor(){\n super();\n this.extensionTabs = [];\n }\n\n /*\n Lifecycle\n */\n connectedCallback(){\n super.connectedCallback();\n this.extensionTabs = [...this.querySelectorAll(':scope > k-settings-tab')].map(($tab, index) => {\n const name = $tab.getAttribute('name') || $tab.getAttribute('label') || `tab-${index}`;\n $tab.slot = `tab-${name}`;\n $tab.style.display = 'block';\n return {\n id: name,\n slotName: `tab-${name}`,\n label: $tab.getAttribute('label') || humanize(name),\n position: Number($tab.getAttribute('position')) || DEFAULT_TAB_POSITION,\n extension: true,\n };\n });\n }\n\n /*\n Rendering\n */\n render(){\n const tabs = [\n { id: 'custom', label: 'Custom', position: 100, content: () => html`<k-settings-panel owner=\"custom\"></k-settings-panel>` },\n { id: 'system', label: 'System', position: 200, content: () => html`<k-settings-panel owner=\"system\"></k-settings-panel>` },\n ...this.extensionTabs,\n ].sort((a, b) => a.position - b.position);\n\n return html`\n <k-tabs>\n ${tabs.map(tab => html`<k-tab for=${tab.id}>${tab.label}</k-tab>`)}\n ${tabs.map(tab => html`<k-tab-content name=${tab.id} class=\"pt\">${tab.extension ? html`<slot name=${tab.slotName}></slot>` : tab.content()}</k-tab-content>`)}\n </k-tabs>\n `;\n }\n}\n\ncustomElements.define('k-admin-settings', AdminSettings);\n"],"mappings":"OAAOA,MAAqB,yDACnBC,MAAY,iCACd,qCACA,4BACAC,MAAc,+BAkBN,MAAMC,UAAsBH,EACzCI,kBAAoB,CAClBC,cAAe,CAAEC,OAAO,IAG1B,WAAAC,GACEC,QACAC,KAAKJ,cAAgB,EACvB,CAKA,iBAAAK,GACEF,MAAME,oBACND,KAAKJ,cAAgB,IAAII,KAAKE,iBAAiB,4BAA4BC,IAAI,CAACC,EAAMC,KACpF,MAAMC,EAAOF,EAAKG,aAAa,SAAWH,EAAKG,aAAa,UAAY,OAAOF,IAG/E,OAFAD,EAAKI,KAAO,OAAOF,IACnBF,EAAKK,MAAMC,QAAU,QACd,CACLC,GAAIL,EACJM,SAAU,OAAON,IACjBO,MAAOT,EAAKG,aAAa,UAAYd,EAASa,GAC9CQ,SAAUC,OAAOX,EAAKG,aAAa,cAzBd,IA0BrBS,WAAW,IAGjB,CAKA,MAAAC,GACE,MAAMC,EAAO,CACX,CAAEP,GAAI,SAAUE,MAAO,SAAUC,SAAU,IAAKK,QAAS,IAAM3B,CAAI,wDACnE,CAAEmB,GAAI,SAAUE,MAAO,SAAUC,SAAU,IAAKK,QAAS,IAAM3B,CAAI,2DAChEQ,KAAKJ,eACRwB,KAAK,CAACC,EAAGC,IAAMD,EAAEP,SAAWQ,EAAER,UAEhC,OAAOtB,CAAI;;UAEL0B,EAAKf,IAAIoB,GAAO/B,CAAI,cAAc+B,EAAIZ,MAAMY,EAAIV;UAChDK,EAAKf,IAAIoB,GAAO/B,CAAI,uBAAuB+B,EAAIZ,iBAAiBY,EAAIP,UAAYxB,CAAI,cAAc+B,EAAIX,mBAAqBW,EAAIJ;;KAGvI,EAGFK,eAAeC,OAAO,mBAAoB/B","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{html as e}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Toggle.js";import t from"./SettingEditor.js";export default class i extends t{render(){return e`<div class="mb"><k-toggle ?disabled=${this.disabled} .value=${!0===this.value||"true"===this.value} @change=${e=>this.announce(!0===e.target.value)}>${this.heading}</k-toggle>${this.renderHint()}</div>`}}customElements.define("k-setting-boolean",i);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\BooleanSetting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\BooleanSetting.js","names":["html","SettingEditor","BooleanSetting","render","this","disabled","value","event","announce","target","heading","renderHint","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport '/kempo-ui/components/Toggle.js';\nimport SettingEditor from './SettingEditor.js';\n\n/* A boolean setting: a toggle with the name beside it and the description under it. */\nexport default class BooleanSetting extends SettingEditor {\n render(){\n return html`<div class=\"mb\"><k-toggle ?disabled=${this.disabled} .value=${this.value === true || this.value === 'true'} @change=${event => this.announce(event.target.value === true)}>${this.heading}</k-toggle>${this.renderHint()}</div>`;\n }\n}\n\ncustomElements.define('k-setting-boolean', BooleanSetting);\n"],"mappings":"eAASA,MAAY,iCACd,wCACAC,MAAmB,oCAGX,MAAMC,UAAuBD,EAC1C,MAAAE,GACE,OAAOH,CAAI,uCAAuCI,KAAKC,oBAAkC,IAAfD,KAAKE,OAAiC,SAAfF,KAAKE,iBAA4BC,GAASH,KAAKI,UAAgC,IAAvBD,EAAME,OAAOH,UAAmBF,KAAKM,qBAAqBN,KAAKO,oBAC1N,EAGFC,eAAeC,OAAO,oBAAqBX","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{html as t}from"/kempo-ui/lit-all.min.js";import e from"./SettingEditor.js";export default class i extends e{text="";willUpdate(t){if(!t.has("value"))return;let e;try{e=JSON.parse(this.text)}catch{e=void 0}void 0!==e&&JSON.stringify(e)===JSON.stringify(this.value)||(this.text=JSON.stringify(this.value??null,null,2))}change=t=>{this.text=t.target.value;try{this.announce(JSON.parse(this.text))}catch{this.announce(this.value,"That is not valid JSON")}};renderControl(){return t`<textarea class="ff-mono" rows="8" ?disabled=${this.disabled} .value=${this.text} @input=${this.change}></textarea>`}}customElements.define("k-setting-json",i);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\JsonSetting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\JsonSetting.js","names":["html","SettingEditor","JsonSetting","text","willUpdate","changed","has","typed","JSON","parse","this","undefined","stringify","value","change","event","target","announce","renderControl","disabled","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport SettingEditor from './SettingEditor.js';\n\n/*\n A JSON setting: the formatted JSON in a box. Text that is not JSON is reported as a problem and is not saved. What is typed is\n kept as typed (it is only formatted again when the value is set from outside), so the cursor does not jump while editing.\n*/\nexport default class JsonSetting extends SettingEditor {\n text = '';\n\n willUpdate(changed){\n if(!changed.has('value')) return;\n let typed;\n try { typed = JSON.parse(this.text); } catch { typed = undefined; }\n if(typed === undefined || JSON.stringify(typed) !== JSON.stringify(this.value)) this.text = JSON.stringify(this.value ?? null, null, 2);\n }\n\n change = event => {\n this.text = event.target.value;\n try {\n this.announce(JSON.parse(this.text));\n } catch {\n this.announce(this.value, 'That is not valid JSON');\n }\n };\n\n renderControl(){\n return html`<textarea class=\"ff-mono\" rows=\"8\" ?disabled=${this.disabled} .value=${this.text} @input=${this.change}></textarea>`;\n }\n}\n\ncustomElements.define('k-setting-json', JsonSetting);\n"],"mappings":"eAASA,MAAY,kCACdC,MAAmB,oCAMX,MAAMC,UAAoBD,EACvCE,KAAO,GAEP,UAAAC,CAAWC,GACT,IAAIA,EAAQC,IAAI,SAAU,OAC1B,IAAIC,EACJ,IAAMA,EAAQC,KAAKC,MAAMC,KAAKP,KAAO,CAAE,MAAQI,OAAQI,CAAW,MACrDA,IAAVJ,GAAuBC,KAAKI,UAAUL,KAAWC,KAAKI,UAAUF,KAAKG,SAAQH,KAAKP,KAAOK,KAAKI,UAAUF,KAAKG,OAAS,KAAM,KAAM,GACvI,CAEAC,OAASC,IACPL,KAAKP,KAAOY,EAAMC,OAAOH,MACzB,IACEH,KAAKO,SAAST,KAAKC,MAAMC,KAAKP,MAChC,CAAE,MACAO,KAAKO,SAASP,KAAKG,MAAO,yBAC5B,GAGF,aAAAK,GACE,OAAOlB,CAAI,gDAAgDU,KAAKS,mBAAmBT,KAAKP,eAAeO,KAAKI,oBAC9G,EAGFM,eAAeC,OAAO,iBAAkBnB","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{html as e}from"/kempo-ui/lit-all.min.js";import t from"./SettingEditor.js";export default class n extends t{change=e=>{const t=e.target.value,n=Number(t);""!==t.trim()&&Number.isFinite(n)?this.announce(n):this.announce(this.value,"Enter a number")};renderControl(){return e`<input type="number" step="any" ?disabled=${this.disabled} .value=${String(this.value??0)} @input=${this.change}>`}}customElements.define("k-setting-number",n);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\NumberSetting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\NumberSetting.js","names":["html","SettingEditor","NumberSetting","change","event","text","target","value","number","Number","trim","isFinite","this","announce","renderControl","disabled","String","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport SettingEditor from './SettingEditor.js';\n\n/* A number setting: a number field. An empty or non-numeric entry is reported as a problem instead of being saved as NaN. */\nexport default class NumberSetting extends SettingEditor {\n change = event => {\n const text = event.target.value;\n const number = Number(text);\n if(text.trim() === '' || !Number.isFinite(number)) this.announce(this.value, 'Enter a number');\n else this.announce(number);\n };\n\n renderControl(){\n return html`<input type=\"number\" step=\"any\" ?disabled=${this.disabled} .value=${String(this.value ?? 0)} @input=${this.change}>`;\n }\n}\n\ncustomElements.define('k-setting-number', NumberSetting);\n"],"mappings":"eAASA,MAAY,kCACdC,MAAmB,oCAGX,MAAMC,UAAsBD,EACzCE,OAASC,IACP,MAAMC,EAAOD,EAAME,OAAOC,MACpBC,EAASC,OAAOJ,GACH,KAAhBA,EAAKK,QAAkBD,OAAOE,SAASH,GACrCI,KAAKC,SAASL,GADgCI,KAAKC,SAASD,KAAKL,MAAO,mBAI/E,aAAAO,GACE,OAAOd,CAAI,6CAA6CY,KAAKG,mBAAmBC,OAAOJ,KAAKL,OAAS,aAAaK,KAAKT,SACzH,EAGFc,eAAeC,OAAO,mBAAoBhB","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{html as e}from"/kempo-ui/lit-all.min.js";import t from"./SettingEditor.js";export default class s extends t{static properties={...t.properties,saved:{type:Boolean}};constructor(){super(),this.saved=!0,this.value=""}renderHint(){return e`${super.renderHint()}<small class="tc-muted d-b">${this.saved?"A value is saved. It is stored encrypted and never shown. Type a new one to replace it.":"Not set yet. It is stored encrypted and never shown again."}</small>`}renderControl(){return e`<input type="password" autocomplete="new-password" ?disabled=${this.disabled} .value=${this.value??""} @input=${e=>this.announce(e.target.value)}>`}}customElements.define("k-setting-secret",s);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\SecretSetting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\SecretSetting.js","names":["html","SettingEditor","SecretSetting","static","properties","saved","type","Boolean","constructor","super","this","value","renderHint","renderControl","disabled","event","announce","target","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport SettingEditor from './SettingEditor.js';\n\n/*\n A secret setting (an API key, a signing secret). It is replace-only: the saved value is never sent to the browser, so the field starts\n empty and an empty field means \"keep what is saved\". Typing a new value announces it, and clearing the field again takes it back.\n*/\nexport default class SecretSetting extends SettingEditor {\n static properties = { ...SettingEditor.properties, saved: { type: Boolean } };\n\n constructor(){\n super();\n this.saved = true;\n this.value = '';\n }\n\n renderHint(){\n return html`${super.renderHint()}<small class=\"tc-muted d-b\">${this.saved ? 'A value is saved. It is stored encrypted and never shown. Type a new one to replace it.' : 'Not set yet. It is stored encrypted and never shown again.'}</small>`;\n }\n\n renderControl(){\n return html`<input type=\"password\" autocomplete=\"new-password\" ?disabled=${this.disabled} .value=${this.value ?? ''} @input=${event => this.announce(event.target.value)}>`;\n }\n}\n\ncustomElements.define('k-setting-secret', SecretSetting);\n"],"mappings":"eAASA,MAAY,kCACdC,MAAmB,oCAMX,MAAMC,UAAsBD,EACzCE,kBAAoB,IAAKF,EAAcG,WAAYC,MAAO,CAAEC,KAAMC,UAElE,WAAAC,GACEC,QACAC,KAAKL,OAAQ,EACbK,KAAKC,MAAQ,EACf,CAEA,UAAAC,GACE,OAAOZ,CAAI,GAAGS,MAAMG,2CAA2CF,KAAKL,MAAQ,0FAA4F,sEAC1K,CAEA,aAAAQ,GACE,OAAOb,CAAI,gEAAgEU,KAAKI,mBAAmBJ,KAAKC,OAAS,aAAaI,GAASL,KAAKM,SAASD,EAAME,OAAON,SACpK,EAGFO,eAAeC,OAAO,mBAAoBjB","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"/kempo-ui/components/ShadowComponent.js";import{html as t}from"/kempo-ui/lit-all.min.js";import"./StringSetting.js";import"./NumberSetting.js";import"./BooleanSetting.js";import"./JsonSetting.js";import"./SecretSetting.js";export default class s extends e{static properties={setting:{attribute:!1},disabled:{type:Boolean}};constructor(){super(),this.setting=null,this.disabled=!1}render(){const{name:e,value:s,type:i,description:n}=this.setting??{};if(!e)return null;switch(i){case"number":return t`<k-setting-number name=${e} description=${n??""} ?disabled=${this.disabled} .value=${s}></k-setting-number>`;case"boolean":return t`<k-setting-boolean name=${e} description=${n??""} ?disabled=${this.disabled} .value=${s}></k-setting-boolean>`;case"json":return t`<k-setting-json name=${e} description=${n??""} ?disabled=${this.disabled} .value=${s}></k-setting-json>`;case"secret":return t`<k-setting-secret name=${e} description=${n??""} ?disabled=${this.disabled} .saved=${null!=s} .value=${""}></k-setting-secret>`;default:return t`<k-setting-string name=${e} description=${n??""} ?disabled=${this.disabled} .value=${s??""}></k-setting-string>`}}}customElements.define("k-setting",s);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\Setting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\Setting.js","names":["ShadowComponent","html","Setting","static","setting","attribute","disabled","type","Boolean","constructor","super","this","render","name","value","description","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html } from '/kempo-ui/lit-all.min.js';\nimport './StringSetting.js';\nimport './NumberSetting.js';\nimport './BooleanSetting.js';\nimport './JsonSetting.js';\nimport './SecretSetting.js';\n\n/*\n The editor for a setting, chosen by its type, served at /admin/components/settings/Setting.js:\n\n <k-setting .setting=${{ name: 'site_name', value: 'My Site', type: 'string', description: 'The name of the website' }}></k-setting>\n\n `name` is the setting's own name without its owner (`site_name`, not `system:site_name`); the readable title is made from it. An\n unknown type is edited as a string. The editor's `setting-change` event bubbles out of this element unchanged.\n*/\nexport default class Setting extends ShadowComponent {\n static properties = {\n setting: { attribute: false },\n disabled: { type: Boolean },\n };\n\n constructor(){\n super();\n this.setting = null;\n this.disabled = false;\n }\n\n render(){\n const { name, value, type, description } = this.setting ?? {};\n if(!name) return null;\n switch(type){\n case 'number': return html`<k-setting-number name=${name} description=${description ?? ''} ?disabled=${this.disabled} .value=${value}></k-setting-number>`;\n case 'boolean': return html`<k-setting-boolean name=${name} description=${description ?? ''} ?disabled=${this.disabled} .value=${value}></k-setting-boolean>`;\n case 'json': return html`<k-setting-json name=${name} description=${description ?? ''} ?disabled=${this.disabled} .value=${value}></k-setting-json>`;\n case 'secret': return html`<k-setting-secret name=${name} description=${description ?? ''} ?disabled=${this.disabled} .saved=${value !== null && value !== undefined} .value=${''}></k-setting-secret>`;\n default: return html`<k-setting-string name=${name} description=${description ?? ''} ?disabled=${this.disabled} .value=${value ?? ''}></k-setting-string>`;\n }\n }\n}\n\ncustomElements.define('k-setting', Setting);\n"],"mappings":"OAAOA,MAAqB,yDACnBC,MAAY,iCACd,2BACA,2BACA,4BACA,yBACA,oCAUQ,MAAMC,UAAgBF,EACnCG,kBAAoB,CAClBC,QAAS,CAAEC,WAAW,GACtBC,SAAU,CAAEC,KAAMC,UAGpB,WAAAC,GACEC,QACAC,KAAKP,QAAU,KACfO,KAAKL,UAAW,CAClB,CAEA,MAAAM,GACE,MAAMC,KAAEA,EAAIC,MAAEA,EAAKP,KAAEA,EAAIQ,YAAEA,GAAgBJ,KAAKP,SAAW,CAAC,EAC5D,IAAIS,EAAM,OAAO,KACjB,OAAON,GACL,IAAK,SAAU,OAAON,CAAI,0BAA0BY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,wBAC/H,IAAK,UAAW,OAAOb,CAAI,2BAA2BY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,yBACjI,IAAK,OAAQ,OAAOb,CAAI,wBAAwBY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,sBAC3H,IAAK,SAAU,OAAOb,CAAI,0BAA0BY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,kBAAgD,yBAC/K,QAAS,OAAOb,CAAI,0BAA0BY,iBAAoBE,GAAe,gBAAgBJ,KAAKL,mBAAmBQ,GAAS,yBAEtI,EAGFE,eAAeC,OAAO,YAAaf","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing as r}from"/kempo-ui/lit-all.min.js";import s from"./humanize.js";export default class i extends e{static properties={name:{type:String},label:{type:String},description:{type:String},value:{attribute:!1},disabled:{type:Boolean},problem:{state:!0}};constructor(){super(),this.name="",this.label="",this.description="",this.value="",this.disabled=!1,this.problem=""}get heading(){return this.label||s(this.name)}announce=(e,t="")=>{this.problem=t,this.dispatchEvent(new CustomEvent("setting-change",{detail:{name:this.name,value:e,valid:!t},bubbles:!0,composed:!0}))};renderControl(){return r}renderHint(){return this.description?t`<small class="tc-muted d-b">${this.description}</small>`:r}renderProblem(){return this.problem?t`<small class="tc-danger d-b" role="alert">${this.problem}</small>`:r}render(){return t`<label class="mb d-b">${this.heading}${this.renderHint()}${this.renderControl()}</label>${this.renderProblem()}`}}
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\SettingEditor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\SettingEditor.js","names":["ShadowComponent","html","nothing","humanize","SettingEditor","static","name","type","String","label","description","value","attribute","disabled","Boolean","problem","state","constructor","super","this","heading","announce","dispatchEvent","CustomEvent","detail","valid","bubbles","composed","renderControl","renderHint","renderProblem","render"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing } from '/kempo-ui/lit-all.min.js';\nimport humanize from './humanize.js';\n\n/*\n The base of the editors, one per setting type (StringSetting, NumberSetting, BooleanSetting, JsonSetting, SecretSetting). It draws\n the readable name, the description as a hint under it, and the problem if the value cannot be saved, and leaves the control itself\n to `renderControl()`.\n\n <k-setting-string name=\"site_name\" description=\"The name of the website\" .value=${'My Site'}></k-setting-string>\n\n A change is announced as a bubbling `setting-change` event with `{ name, value, valid }`; `value` is already the right type (a number\n is a Number, JSON is parsed). Use <k-setting> to get the editor for a type without choosing one.\n*/\nexport default class SettingEditor extends ShadowComponent {\n static properties = {\n name: { type: String },\n label: { type: String },\n description: { type: String },\n value: { attribute: false },\n disabled: { type: Boolean },\n problem: { state: true },\n };\n\n constructor(){\n super();\n this.name = '';\n this.label = '';\n this.description = '';\n this.value = '';\n this.disabled = false;\n this.problem = '';\n }\n\n /*\n Public\n */\n get heading(){\n return this.label || humanize(this.name);\n }\n\n /* Tell whoever is listening what the value is now. A `problem` sentence means it cannot be saved yet. */\n announce = (value, problem = '') => {\n this.problem = problem;\n this.dispatchEvent(new CustomEvent('setting-change', { detail: { name: this.name, value, valid: !problem }, bubbles: true, composed: true }));\n };\n\n /*\n Rendering\n */\n renderControl(){\n return nothing;\n }\n\n renderHint(){\n return this.description ? html`<small class=\"tc-muted d-b\">${this.description}</small>` : nothing;\n }\n\n renderProblem(){\n return this.problem ? html`<small class=\"tc-danger d-b\" role=\"alert\">${this.problem}</small>` : nothing;\n }\n\n render(){\n return html`<label class=\"mb d-b\">${this.heading}${this.renderHint()}${this.renderControl()}</label>${this.renderProblem()}`;\n }\n}\n"],"mappings":"OAAOA,MAAqB,yDACnBC,aAAMC,MAAe,kCACvBC,MAAc,+BAYN,MAAMC,UAAsBJ,EACzCK,kBAAoB,CAClBC,KAAM,CAAEC,KAAMC,QACdC,MAAO,CAAEF,KAAMC,QACfE,YAAa,CAAEH,KAAMC,QACrBG,MAAO,CAAEC,WAAW,GACpBC,SAAU,CAAEN,KAAMO,SAClBC,QAAS,CAAEC,OAAO,IAGpB,WAAAC,GACEC,QACAC,KAAKb,KAAO,GACZa,KAAKV,MAAQ,GACbU,KAAKT,YAAc,GACnBS,KAAKR,MAAQ,GACbQ,KAAKN,UAAW,EAChBM,KAAKJ,QAAU,EACjB,CAKA,WAAIK,GACF,OAAOD,KAAKV,OAASN,EAASgB,KAAKb,KACrC,CAGAe,SAAW,CAACV,EAAOI,EAAU,MAC3BI,KAAKJ,QAAUA,EACfI,KAAKG,cAAc,IAAIC,YAAY,iBAAkB,CAAEC,OAAQ,CAAElB,KAAMa,KAAKb,KAAMK,QAAOc,OAAQV,GAAWW,SAAS,EAAMC,UAAU,MAMvI,aAAAC,GACE,OAAO1B,CACT,CAEA,UAAA2B,GACE,OAAOV,KAAKT,YAAcT,CAAI,+BAA+BkB,KAAKT,sBAAwBR,CAC5F,CAEA,aAAA4B,GACE,OAAOX,KAAKJ,QAAUd,CAAI,6CAA6CkB,KAAKJ,kBAAoBb,CAClG,CAEA,MAAA6B,GACE,OAAO9B,CAAI,yBAAyBkB,KAAKC,UAAUD,KAAKU,eAAeV,KAAKS,0BAA0BT,KAAKW,iBAC7G","ignoreList":[]}
|
|
@@ -0,0 +1,24 @@
|
|
|
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`
|
|
2
|
+
<div class="p">
|
|
3
|
+
<label class="mb d-b">Name<input type="text" id="newName" placeholder="my_setting" required></label>
|
|
4
|
+
<label class="mb d-b">Type
|
|
5
|
+
<select id="newType">
|
|
6
|
+
<option value="string">String</option>
|
|
7
|
+
<option value="number">Number</option>
|
|
8
|
+
<option value="boolean">Boolean (true or false)</option>
|
|
9
|
+
<option value="json">JSON</option>
|
|
10
|
+
<option value="secret">Secret</option>
|
|
11
|
+
</select>
|
|
12
|
+
</label>
|
|
13
|
+
<label class="mb d-b">Value<input type="text" id="newValue"></label>
|
|
14
|
+
<label class="mb d-b">Description<input type="text" id="newDescription"></label>
|
|
15
|
+
<label class="mb d-b"><input type="checkbox" id="newPublic"> Public (anyone can read it, without signing in)</label>
|
|
16
|
+
</div>
|
|
17
|
+
`,{title:"Create a setting",confirmText:"Create",cancelText:"Cancel",confirmAction:async()=>{const t=t=>e.querySelector(t),s=t("#newName").value.trim(),n=t("#newType").value;if(!s)return void i.error("Give the setting a name");if(s.includes(":"))return void i.error('The name cannot contain ":"');let a;try{a=""===t("#newValue").value?d[n]:((e,t)=>{if("number"===t){const t=Number(e);if(""===String(e).trim()||!Number.isFinite(t))throw new Error("number");return t}return"boolean"===t?"true"===String(e).toLowerCase():"json"===t?JSON.parse(String(e||"{}")):String(e)})(t("#newValue").value,n)}catch{return void i.error(`That is not a valid ${n} value`)}const[o]=await r("custom",s,{value:a,type:n,isPublic:"secret"!==n&&t("#newPublic").checked,description:t("#newDescription").value.trim()});o?i.error(o.msg||"The setting could not be created"):(i.success("Setting created"),await this.load())}});e.querySelector("#newType").addEventListener("change",t=>{e.querySelector("#newValue").type="secret"===t.target.value?"password":"text"})};remove=e=>{n.confirm(`Delete ${c(this.shortName(e))}? This cannot be undone.`,async t=>{if(!t)return;const[s]=await o(this.owner,this.shortName(e));s?i.error(s.msg||"The setting could not be deleted"):(i.success("Setting deleted"),await this.load())},{title:"Delete setting",confirmText:"Delete",confirmClasses:"danger ml",cancelText:"Cancel",cancelClasses:""})};renderSetting(e){const s=this.shortName(e),i=Object.hasOwn(this.changes,s)&&this.changes[s].valid?this.changes[s].value:e.value,n=t`<k-setting .setting=${{name:s,value:i,type:e.type,description:e.description}} ?disabled=${!this.canEdit}></k-setting>`;return"custom"===this.owner&&this.canEdit?t`<div class="d-f"><div class="flex">${n}</div><div><button class="danger" aria-label=${`Delete ${c(s)}`} title="Delete" @click=${()=>this.remove(e)}><k-icon name="delete"></k-icon></button></div></div>`:n}render(){return this.loading?t`<k-spinner></k-spinner>`:this.error?t`<p class="tc-danger" role="alert">${this.error}</p>`:t`
|
|
18
|
+
${this.heading?t`<h2 class="h4">${this.heading}</h2>`:s}
|
|
19
|
+
${"custom"===this.owner&&this.canEdit?t`<p class="mb"><button @click=${this.create}><k-icon name="add"></k-icon> Create a setting</button></p>`:s}
|
|
20
|
+
${this.canEdit?s:t`<p class="tc-muted mb">You can see these settings but not change them.</p>`}
|
|
21
|
+
${this.settings.length?this.settings.map(e=>this.renderSetting(e)):t`<p class="tc-muted mb">${"custom"===this.owner?"There are no custom settings yet.":"There are no settings here."}</p>`}
|
|
22
|
+
${this.canEdit&&this.settings.length?t`<button class="primary" ?disabled=${!this.dirty||this.blocked||this.busy} @click=${this.save}>${this.busy?"Saving…":"Save settings"}</button>`:s}
|
|
23
|
+
`}firstUpdated(){this.renderRoot.addEventListener("setting-change",this.record)}}customElements.define("k-settings-panel",p);
|
|
24
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\SettingsPanel.js.map
|
|
@@ -0,0 +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":"OAAOA,MAAqB,yDACnBC,aAAMC,MAAe,iCACvB,wCACA,sCACAC,MAAW,uCACXC,MAAY,wDACVC,gBAAcC,mBAAYC,kCAAeC,MAAoC,sBAC/E,sBACAC,MAAc,gBAcrB,MAAMC,EAAc,CAAEC,OAAQ,yBAA0BC,OAAQ,iCAGhE,IAAIC,EAAqB,KACzB,MAEMC,EAAW,CAAEC,OAAQ,GAAIC,OAAQ,EAAGC,SAAS,EAAOC,KAAM,CAAC,EAAGC,OAAQ,IAiBtEC,EAAQC,IACZ,GAAoB,OAAjBA,EAAOC,YAAmCC,IAAjBF,EAAOC,OAAuC,WAAhBD,EAAOG,KAAmB,OAAOH,EAC3F,IACE,GAAmB,WAAhBA,EAAOG,KAAmB,MAAO,IAAKH,EAAQC,MAAOG,OAAOJ,EAAOC,QACtE,GAAmB,YAAhBD,EAAOG,KAAoB,MAAO,IAAKH,EAAQC,OAAwB,IAAjBD,EAAOC,OAAmC,SAAjBD,EAAOC,OACzF,GAAmB,SAAhBD,EAAOG,KAAiB,MAAO,IAAKH,EAAQC,MAA+B,iBAAjBD,EAAOC,MAAqBI,KAAKC,MAAMN,EAAOC,OAASD,EAAOC,MAC7H,CAAE,MACA,OAAOD,CACT,CACA,OAAOA,kBAGM,MAAMO,UAAsB5B,EACzC6B,kBAAoB,CAClBC,MAAO,CAAEN,KAAMO,QACfC,QAAS,CAAER,KAAMO,QACjBE,SAAU,CAAEC,OAAO,GACnBC,QAAS,CAAED,OAAO,GAClBE,QAAS,CAAEF,OAAO,GAClBG,QAAS,CAAEH,OAAO,GAClBI,KAAM,CAAEJ,OAAO,GACfK,MAAO,CAAEL,OAAO,IAGlB,WAAAM,GACEC,QACAC,KAAKZ,MAAQ,GACbY,KAAKV,QAAU,GACfU,KAAKT,SAAW,GAChBS,KAAKP,QAAU,CAAC,EAChBO,KAAKN,SAAU,EACfM,KAAKL,SAAU,EACfK,KAAKJ,MAAO,EACZI,KAAKH,MAAQ,EACf,CAKA,iBAAAI,GACEF,MAAME,oBACND,KAAKE,MACP,CAKAC,UAAYxB,GAAUA,EAAOyB,KAAKC,MAAML,KAAKZ,MAAMkB,OAAS,GAE5DJ,KAAOK,UACL,OAAQV,EAAOW,GAAOC,SAAqBC,QAAQC,IAAI,CAAChD,EAAa,CAAEyB,MAAOY,KAAKZ,MAAOwB,MAAO,MArExEzC,IAAuBL,IAA+B+C,KAAK,EAAE,CAAEL,KAAU,IAAIM,IAAIN,GAAMC,aAAe,OAsE/HT,KAAKN,QAAUe,EAAYM,IAAI/C,EAAYgC,KAAKZ,QAzEvB,4BA0EzBY,KAAKL,SAAU,EACZE,EACDG,KAAKH,MAAQA,EAAMmB,KAAO,oCAG5BhB,KAAKH,MAAQ,GACbG,KAAKT,SAAWiB,EAAKjB,SAAS0B,IAAIvC,GAClCsB,KAAKP,QAAU,CAAC,IAGlBd,OAASuC,IACP,MAAMd,KAAEA,EAAIxB,MAAEA,EAAKuC,MAAEA,GAAUD,EAAME,OAC/BzC,EAASqB,KAAKT,SAAS8B,KAAKC,GAAStB,KAAKG,UAAUmB,KAAWlB,GAC/DmB,EAAY5C,IAA2B,WAAhBA,EAAOG,KAA8B,KAAVF,EAAeI,KAAKwC,UAAU5C,KAAWI,KAAKwC,UAAU7C,EAAOC,SAC/GwB,CAACA,GAAOqB,KAAYC,GAAS1B,KAAKP,QAC1CO,KAAKP,QAAU8B,GAAaJ,EAAQO,EAAO,IAAKA,EAAMtB,CAACA,GAAO,CAAExB,QAAOuC,WAGzE,SAAIQ,GACF,OAAOC,OAAOC,KAAK7B,KAAKP,SAASa,OAAS,CAC5C,CAEA,WAAIwB,GACF,OAAOF,OAAOG,OAAO/B,KAAKP,SAASuC,KAAKC,IAAWA,EAAOd,MAC5D,CAEAe,KAAO3B,UACLP,KAAKJ,MAAO,EACZ,IAAIuC,EAAS,EACb,IAAI,MAAO/B,EAAM6B,KAAWL,OAAOQ,QAAQpC,KAAKP,SAAS,CACvD,MAAMd,EAASqB,KAAKT,SAAS8B,KAAKC,GAAStB,KAAKG,UAAUmB,KAAWlB,IAC9DP,SAAejC,EAAWoC,KAAKZ,MAAOgB,EAAM,CAAExB,MAAOqD,EAAOrD,MAAOE,KAAMH,EAAOG,KAAMuD,SAAU1D,EAAO0D,SAAUC,YAAa3D,EAAO2D,cACzIzC,IACDsC,IACA1E,EAAMoC,MAAM,GAAG9B,EAASqC,OAAUP,EAAMmB,KAAO,wBAEnD,CACAhB,KAAKJ,MAAO,EACRuC,GAAQ1E,EAAM8E,QAAQ,wBACpBvC,KAAKE,QAMbsC,OAAS,KACP,MAAMC,EAAS/E,EAAO8E,OAAOjF,CAAI;;;;;;;;;;;;;;;;MAgB9B,CACDmF,MAAO,mBACPC,YAAa,SACbC,WAAY,SACZC,cAAetC,UACb,MAAMuC,EAAOC,GAAMN,EAAOO,cAAcD,GAClC3C,EAAO0C,EAAK,YAAYlE,MAAMqE,OAC9BnE,EAAOgE,EAAK,YAAYlE,MAC9B,IAAIwB,EAA+C,YAAxC3C,EAAMoC,MAAM,2BACvB,GAAGO,EAAK8C,SAAS,KAAmD,YAA5CzF,EAAMoC,MAAM,+BACpC,IAAIjB,EACJ,IACEA,EAAoC,KAA5BkE,EAAK,aAAalE,MAAeR,EAASU,GA7I7C,EAACqE,EAAMrE,KACpB,GAAY,WAATA,EAAkB,CACnB,MAAMR,EAASS,OAAOoE,GACtB,GAA2B,KAAxB9D,OAAO8D,GAAMF,SAAkBlE,OAAOqE,SAAS9E,GAAS,MAAM,IAAI+E,MAAM,UAC3E,OAAO/E,CACT,CACA,MAAY,YAATQ,EAA0D,SAA/BO,OAAO8D,GAAMG,cAC/B,SAATxE,EAAwBE,KAAKC,MAAMI,OAAO8D,GAAQ,OAC9C9D,OAAO8D,IAqIoDI,CAAOT,EAAK,aAAalE,MAAOE,EAC5F,CAAE,MAEA,YADArB,EAAMoC,MAAM,uBAAuBf,UAErC,CACA,MAAOe,SAAejC,EAAW,SAAUwC,EAAM,CAAExB,QAAOE,OAAMuD,SAAmB,WAATvD,GAAqBgE,EAAK,cAAcU,QAASlB,YAAaQ,EAAK,mBAAmBlE,MAAMqE,SACnKpD,EAAQpC,EAAMoC,MAAMA,EAAMmB,KAAO,qCACpCvD,EAAM8E,QAAQ,yBACRvC,KAAKE,WAGfuC,EAAOO,cAAc,YAAYS,iBAAiB,SAAUvC,IAC1DuB,EAAOO,cAAc,aAAalE,KAA8B,WAAvBoC,EAAMwC,OAAO9E,MAAqB,WAAa,UAI5F+E,OAAShF,IACPjB,EAAOkG,QAAQ,UAAU7F,EAASiC,KAAKG,UAAUxB,8BAAoC4B,UACnF,IAAIsD,EAAW,OACf,MAAOhE,SAAehC,EAAcmC,KAAKZ,MAAOY,KAAKG,UAAUxB,IAC5DkB,EAAQpC,EAAMoC,MAAMA,EAAMmB,KAAO,qCACpCvD,EAAM8E,QAAQ,yBACRvC,KAAKE,SACV,CAAEwC,MAAO,iBAAkBC,YAAa,SAAUmB,eAAgB,YAAalB,WAAY,SAAUmB,cAAe,MAMzH,aAAAC,CAAcrF,GACZ,MAAMyB,EAAOJ,KAAKG,UAAUxB,GACtBC,EAAQgD,OAAOqC,OAAOjE,KAAKP,QAASW,IAASJ,KAAKP,QAAQW,GAAMe,MAAQnB,KAAKP,QAAQW,GAAMxB,MAAQD,EAAOC,MAC1GsF,EAAS3G,CAAI,uBAAuB,CAAE6C,OAAMxB,QAAOE,KAAMH,EAAOG,KAAMwD,YAAa3D,EAAO2D,2BAA4BtC,KAAKN,uBACjI,MAAkB,WAAfM,KAAKZ,OAAuBY,KAAKN,QAC7BnC,CAAI,sCAAsC2G,iDAAsD,UAAUnG,EAASqC,8BAAiC,IAAMJ,KAAK2D,OAAOhF,0DADzHuF,CAEtD,CAEA,MAAAC,GACE,OAAGnE,KAAKL,QAAgBpC,CAAI,0BACzByC,KAAKH,MAActC,CAAI,qCAAqCyC,KAAKH,YAC7DtC,CAAI;QACPyC,KAAKV,QAAU/B,CAAI,kBAAkByC,KAAKV,eAAiB9B;QAC5C,WAAfwC,KAAKZ,OAAsBY,KAAKN,QAAUnC,CAAI,gCAAgCyC,KAAKwC,oEAAsEhF;QACzJwC,KAAKN,QAAUlC,EAAUD,CAAI;QAC7ByC,KAAKT,SAASe,OAASN,KAAKT,SAAS0B,IAAItC,GAAUqB,KAAKgE,cAAcrF,IAAWpB,CAAI,0BAAyC,WAAfyC,KAAKZ,MAAqB,oCAAsC;QAC/KY,KAAKN,SAAWM,KAAKT,SAASe,OAAS/C,CAAI,sCAAsCyC,KAAK2B,OAAS3B,KAAK8B,SAAW9B,KAAKJ,eAAeI,KAAKkC,QAAQlC,KAAKJ,KAAO,UAAY,2BAA6BpC;KAE3M,CAEA,YAAA4G,GACEpE,KAAKqE,WAAWZ,iBAAiB,iBAAkBzD,KAAKrB,OAC1D,EAGF2F,eAAeC,OAAO,mBAAoBrF","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{html as t}from"/kempo-ui/lit-all.min.js";import e from"./SettingEditor.js";export default class i extends e{renderControl(){const e=String(this.value??"");return e.length>120||e.includes("\n")?t`<textarea rows="4" ?disabled=${this.disabled} .value=${e} @input=${t=>this.announce(t.target.value)}></textarea>`:t`<input type="text" ?disabled=${this.disabled} .value=${e} @input=${t=>this.announce(t.target.value)}>`}}customElements.define("k-setting-string",i);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\StringSetting.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\StringSetting.js","names":["html","SettingEditor","StringSetting","renderControl","text","String","this","value","length","includes","disabled","event","announce","target","customElements","define"],"sources":["0"],"sourcesContent":["import { html } from '/kempo-ui/lit-all.min.js';\nimport SettingEditor from './SettingEditor.js';\n\n/* A string setting: a text field, or a larger box for one that is long or has several lines. */\nexport default class StringSetting extends SettingEditor {\n renderControl(){\n const text = String(this.value ?? '');\n if(text.length > 120 || text.includes('\\n')) return html`<textarea rows=\"4\" ?disabled=${this.disabled} .value=${text} @input=${event => this.announce(event.target.value)}></textarea>`;\n return html`<input type=\"text\" ?disabled=${this.disabled} .value=${text} @input=${event => this.announce(event.target.value)}>`;\n }\n}\n\ncustomElements.define('k-setting-string', StringSetting);\n"],"mappings":"eAASA,MAAY,kCACdC,MAAmB,oCAGX,MAAMC,UAAsBD,EACzC,aAAAE,GACE,MAAMC,EAAOC,OAAOC,KAAKC,OAAS,IAClC,OAAGH,EAAKI,OAAS,KAAOJ,EAAKK,SAAS,MAAcT,CAAI,gCAAgCM,KAAKI,mBAAmBN,YAAeO,GAASL,KAAKM,SAASD,EAAME,OAAON,qBAC5JP,CAAI,gCAAgCM,KAAKI,mBAAmBN,YAAeO,GAASL,KAAKM,SAASD,EAAME,OAAON,SACxH,EAGFO,eAAeC,OAAO,mBAAoBb","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
const s=new Set(["api","css","html","id","ip","js","json","seo","smtp","sms","sql","ssl","tls","ui","uri","url","utc","uuid","xml"]);export default e=>String(e??"").replace(/([a-z0-9])([A-Z])/g,"$1 $2").split(/[\s_\-.:]+/).filter(Boolean).map(e=>s.has(e.toLowerCase())?e.toUpperCase():e[0].toUpperCase()+e.slice(1)).join(" ");
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\settings\humanize.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\settings\\humanize.js","names":["ACRONYMS","Set","name","String","replace","split","filter","Boolean","map","word","has","toLowerCase","toUpperCase","slice","join"],"sources":["0"],"sourcesContent":["/*\n A readable name for a setting, made from its name, so nobody has to write one:\n\n session_duration_days -> Session Duration Days\n verification_url -> Verification URL\n shippoCacheMinutes -> Shippo Cache Minutes\n\n Words are split on `_`, `-`, `.`, `:` and spaces and where a lowercase letter meets a capital. Each gets a capital, and the\n well-known abbreviations are written in capitals.\n*/\nconst ACRONYMS = new Set(['api', 'css', 'html', 'id', 'ip', 'js', 'json', 'seo', 'smtp', 'sms', 'sql', 'ssl', 'tls', 'ui', 'uri', 'url', 'utc', 'uuid', 'xml']);\n\nexport default name => String(name ?? '')\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .split(/[\\s_\\-.:]+/)\n .filter(Boolean)\n .map(word => (ACRONYMS.has(word.toLowerCase()) ? word.toUpperCase() : word[0].toUpperCase() + word.slice(1)))\n .join(' ');\n"],"mappings":"AAUA,MAAMA,EAAW,IAAIC,IAAI,CAAC,MAAO,MAAO,OAAQ,KAAM,KAAM,KAAM,OAAQ,MAAO,OAAQ,MAAO,MAAO,MAAO,MAAO,KAAM,MAAO,MAAO,MAAO,OAAQ,uBAEzIC,GAAQC,OAAOD,GAAQ,IACnCE,QAAQ,qBAAsB,SAC9BC,MAAM,cACNC,OAAOC,SACPC,IAAIC,GAAST,EAASU,IAAID,EAAKE,eAAiBF,EAAKG,cAAgBH,EAAK,GAAGG,cAAgBH,EAAKI,MAAM,IACxGC,KAAK","ignoreList":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
locked: true
|
|
3
|
+
-->
|
|
4
|
+
<page title="Emails - Admin">
|
|
5
|
+
<content>
|
|
6
|
+
<script type="module" src="/admin/components/Emails.js"></script>
|
|
7
|
+
|
|
8
|
+
<h1>Emails</h1>
|
|
9
|
+
<p class="tc-muted mb">Every email the site was asked to send, and what happened to it. Kempo does not send email itself: an email extension (such as kempo-resend or kempo-smtp) does. How long this log is kept, and how many times a failed email is tried again, are on the System tab of <a href="/admin/settings">Settings</a>.</p>
|
|
10
|
+
|
|
11
|
+
<k-admin-emails></k-admin-emails>
|
|
12
|
+
|
|
13
|
+
</content>
|
|
14
|
+
</page>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path fill="currentColor" d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm320-280L160-640v400h640v-400L480-440Zm0-80 320-200H160l320 200ZM160-640v-80 480-400Z"/></svg>
|
|
@@ -84,6 +84,12 @@
|
|
|
84
84
|
group="Settings"
|
|
85
85
|
no-expand
|
|
86
86
|
>Extensions</k-aside-item>
|
|
87
|
+
<k-aside-item
|
|
88
|
+
icon="mail"
|
|
89
|
+
href="/admin/emails"
|
|
90
|
+
group="Settings"
|
|
91
|
+
no-expand
|
|
92
|
+
>Emails</k-aside-item>
|
|
87
93
|
<location name="admin-nav-extensions"></location>
|
|
88
94
|
<k-aside-item
|
|
89
95
|
icon="sensors"
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
? myPerms.has('system:settings:update')
|
|
89
89
|
: owner === 'custom'
|
|
90
90
|
? myPerms.has('system:custom-settings:manage')
|
|
91
|
-
: myPerms.has('system:
|
|
91
|
+
: myPerms.has('system:extensions:manage');
|
|
92
92
|
const canEditMetadata = owner === 'custom' && myPerms.has('system:custom-settings:manage');
|
|
93
93
|
const canDelete = owner === 'custom' && myPerms.has('system:custom-settings:manage');
|
|
94
94
|
|