kempo 4.9.6 → 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.
Files changed (27) hide show
  1. package/dist/admin/accounts/groups/[name]/index.page.html +5 -5
  2. package/dist/admin/accounts/groups/index.page.html +12 -10
  3. package/dist/admin/accounts/permissions/index.page.html +1 -1
  4. package/dist/admin/accounts/users/[id]/index.page.html +6 -5
  5. package/dist/admin/accounts/users/index.page.html +7 -9
  6. package/dist/admin/components/EmailTemplates.js +11 -19
  7. package/dist/admin/components/EmailTemplates.js.map +1 -1
  8. package/dist/admin/components/Emails.js +32 -29
  9. package/dist/admin/components/Emails.js.map +1 -1
  10. package/dist/admin/components/Updates.js +17 -22
  11. package/dist/admin/components/Updates.js.map +1 -1
  12. package/dist/admin/components/tableControls/EditEmailTemplate.js +2 -0
  13. package/dist/admin/components/tableControls/EditEmailTemplate.js.map +1 -0
  14. package/dist/admin/components/tableControls/UpdateItem.js +2 -0
  15. package/dist/admin/components/tableControls/UpdateItem.js.map +1 -0
  16. package/dist/admin/components/tableControls/UpdateSelected.js +2 -0
  17. package/dist/admin/components/tableControls/UpdateSelected.js.map +1 -0
  18. package/dist/admin/components/tableControls/ViewEmail.js +2 -0
  19. package/dist/admin/components/tableControls/ViewEmail.js.map +1 -0
  20. package/dist/admin/content/fragments/index.page.html +12 -12
  21. package/dist/admin/content/globals/index.page.html +12 -11
  22. package/dist/admin/content/pages/index.page.html +11 -23
  23. package/dist/admin/content/templates/index.page.html +13 -25
  24. package/dist/admin/extensions/index.page.html +10 -9
  25. package/package.json +2 -2
  26. package/dist/admin/components/tableControls/ExtensionDetails.js +0 -2
  27. package/dist/admin/components/tableControls/ExtensionDetails.js.map +0 -1
@@ -15,7 +15,7 @@
15
15
  <script type="module" src="/kempo-ui/components/controls/TcDeleteSelected.js"></script>
16
16
  <script type="module" src="/admin/components/tableControls/AddMember.js"></script>
17
17
  <script type="module" src="/admin/components/tableControls/AddPermission.js"></script>
18
- <script type="module" src="/admin/components/TableRowLink.js"></script>
18
+ <script type="module" src="/kempo-ui/components/controls/TcOpen.js"></script>
19
19
 
20
20
  <div class="mb d-f" style="align-items: flex-start; justify-content: space-between;">
21
21
  <div>
@@ -63,8 +63,8 @@
63
63
  <div class="card">
64
64
  <h2>Members</h2>
65
65
  <p id="usersMembersMsg" style="display:none">All users are automatically members of this group.</p>
66
- <k-table id="membersTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="This group has no members.">
67
- <admin-table-row-link slot="before" url-pattern="/admin/accounts/users/{id}" field-name="id"></admin-table-row-link>
66
+ <k-table id="membersTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="This group has no members." open-on-click>
67
+ <kc-tc-open slot="before"></kc-tc-open>
68
68
  <kc-tc-delete-selected id="tcDeleteSelected" slot="top" style="display:none;"></kc-tc-delete-selected>
69
69
  <admin-add-member id="tcAddMember" slot="top" style="display:none;"></admin-add-member>
70
70
  <div class="d-f full" style="align-items: center;">
@@ -276,7 +276,7 @@
276
276
  return;
277
277
  }
278
278
  $membersTable.setData({
279
- records: data.members,
279
+ records: data.members.map(m => ({ ...m, _url: `/admin/accounts/users/${m.id}` })),
280
280
  fields: [
281
281
  { name: 'name', label: 'Name' },
282
282
  { name: 'email', label: 'Email' },
@@ -290,7 +290,7 @@
290
290
  $membersTable.setupFetchRecords(data.total, async (start, count) => {
291
291
  const [err, d] = await getGroupMembers(groupName, { limit: count, offset: start });
292
292
  if(err) return [];
293
- return d.members;
293
+ return d.members.map(m => ({ ...m, _url: `/admin/accounts/users/${m.id}` }));
294
294
  });
295
295
  };
296
296
 
@@ -13,7 +13,7 @@
13
13
  <script type="module" src="/kempo-ui/components/controls/TcEdit.js"></script>
14
14
  <script type="module" src="/kempo-ui/components/controls/TcDeleteRecord.js"></script>
15
15
  <script type="module" src="/kempo-ui/components/controls/TcDeleteSelected.js"></script>
16
- <script type="module" src="/admin/components/TableRowLink.js"></script>
16
+ <script type="module" src="/kempo-ui/components/controls/TcOpen.js"></script>
17
17
  <script type="module" src="/kempo/components/Permission.js"></script>
18
18
 
19
19
  <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
@@ -24,8 +24,8 @@
24
24
  </div>
25
25
 
26
26
  <h2>Custom Groups</h2>
27
- <k-table id="customTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No custom groups have been created yet.">
28
- <admin-table-row-link slot="before" url-pattern="/admin/accounts/groups/{name}" field-name="name"></admin-table-row-link>
27
+ <k-table id="customTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No custom groups have been created yet." open-on-click>
28
+ <kc-tc-open slot="before"></kc-tc-open>
29
29
  <div slot="top" class="d-f full" style="align-items: center;">
30
30
  <div>
31
31
  <k-permission has="system:group:delete">
@@ -48,8 +48,8 @@
48
48
  </k-table>
49
49
 
50
50
  <h2>Extension Groups</h2>
51
- <k-table id="extensionTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No extension groups found.">
52
- <admin-table-row-link slot="before" url-pattern="/admin/accounts/groups/{name}" field-name="name"></admin-table-row-link>
51
+ <k-table id="extensionTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No extension groups found." open-on-click>
52
+ <kc-tc-open slot="before"></kc-tc-open>
53
53
  <div class="d-f full" style="align-items: center;">
54
54
  <div>
55
55
  <kc-tc-first-page></kc-tc-first-page>
@@ -65,8 +65,8 @@
65
65
  </k-table>
66
66
 
67
67
  <h2>System Groups</h2>
68
- <k-table id="systemTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No system groups found.">
69
- <admin-table-row-link slot="before" url-pattern="/admin/accounts/groups/{name}" field-name="name"></admin-table-row-link>
68
+ <k-table id="systemTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No system groups found." open-on-click>
69
+ <kc-tc-open slot="before"></kc-tc-open>
70
70
  <div class="d-f full" style="align-items: center;">
71
71
  <div>
72
72
  <kc-tc-first-page></kc-tc-first-page>
@@ -126,6 +126,8 @@
126
126
 
127
127
  const addGroupName = r => ({ ...r, groupName: r.name.includes(':') ? r.name.split(':').slice(1).join(':') : r.name });
128
128
 
129
+ const withUrl = r => ({ ...r, _url: `/admin/accounts/groups/${r.name}` });
130
+
129
131
  const setupTable = ($table, owner, loadFn) => {
130
132
  if(canEdit && owner !== 'system') { // Prevent editor control for system groups
131
133
  $table.setAttribute('request-edit', '');
@@ -188,12 +190,12 @@
188
190
  return;
189
191
  }
190
192
  const isCustom = owner === 'custom';
191
- const records = isCustom ? data.groups.map(addGroupName) : data.groups;
192
- $table.setData({ records, fields: isCustom ? customFields : fields });
193
+ const toRecord = isCustom ? r => withUrl(addGroupName(r)) : withUrl;
194
+ $table.setData({ records: data.groups.map(toRecord), fields: isCustom ? customFields : fields });
193
195
  $table.setupFetchRecords(data.total, async (start, count) => {
194
196
  const [err, d] = await listGroups({ limit: count, offset: start, owner });
195
197
  if(err) return [];
196
- return isCustom ? d.groups.map(addGroupName) : d.groups;
198
+ return d.groups.map(toRecord);
197
199
  });
198
200
  };
199
201
 
@@ -111,7 +111,7 @@
111
111
  return parts.length >= 3 ? parts.slice(2).join(':') : '';
112
112
  }
113
113
  },
114
- { name: 'description', label: 'Description', size: 360, editable: canManage },
114
+ { name: 'description', label: 'Description', editable: canManage },
115
115
  {
116
116
  name: 'createdAt',
117
117
  label: 'Created',
@@ -18,7 +18,7 @@
18
18
  <script type="module" src="/admin/components/GroupDeleteSelected.js"></script>
19
19
  <script type="module" src="/admin/components/AddGroupControl.js"></script>
20
20
  <script type="module" src="/admin/components/ClearExpiredSessionsControl.js"></script>
21
- <script type="module" src="/admin/components/TableRowLink.js"></script>
21
+ <script type="module" src="/kempo-ui/components/controls/TcOpen.js"></script>
22
22
  <div class="d-f">
23
23
  <div>
24
24
  <a href="/admin/accounts/users" class="no-link mb">
@@ -64,8 +64,8 @@
64
64
  </k-tab-content>
65
65
 
66
66
  <k-tab-content name="groups">
67
- <k-table id="groupsTable" request-delete placeholder="Not a member of any groups.">
68
- <admin-table-row-link slot="before" url-pattern="/admin/accounts/groups/{name}" field-name="name"></admin-table-row-link>
67
+ <k-table id="groupsTable" request-delete placeholder="Not a member of any groups." open-on-click>
68
+ <kc-tc-open slot="before"></kc-tc-open>
69
69
  <kc-tc-delete-record slot="after"></kc-tc-delete-record>
70
70
  <div slot="top" id="groupsTableTop" class="d-f full" style="display:none; align-items: center;">
71
71
  <div><admin-group-delete-selected></admin-group-delete-selected></div>
@@ -263,6 +263,7 @@
263
263
 
264
264
  await customElements.whenDefined('k-table');
265
265
  const $groupsTable = document.getElementById('groupsTable');
266
+ const toRecord = g => ({ ...g, _url: `/admin/accounts/groups/${g.name}` });
266
267
 
267
268
  if(canEditGroups){
268
269
  $groupsTable.setAttribute('enable-selection', '');
@@ -311,13 +312,13 @@
311
312
  return;
312
313
  }
313
314
  $groupsTable.setData({
314
- records: data.groups,
315
+ records: data.groups.map(toRecord),
315
316
  fields: [{ name: 'name', label: 'Name' }]
316
317
  });
317
318
  $groupsTable.setupFetchRecords(data.total, async (start, count) => {
318
319
  const [err, d] = await getUserGroups(userid, { limit: count, offset: start });
319
320
  if(err) return [];
320
- return d.groups;
321
+ return d.groups.map(toRecord);
321
322
  });
322
323
  };
323
324
 
@@ -49,7 +49,7 @@
49
49
  ></script>
50
50
  <script
51
51
  type="module"
52
- src="/admin/components/TableRowLink.js"
52
+ src="/kempo-ui/components/controls/TcOpen.js"
53
53
  ></script>
54
54
  <script
55
55
  type="module"
@@ -74,12 +74,9 @@
74
74
  page-size-options="[10,25,50]"
75
75
  placeholder="No users found."
76
76
  filtered-placeholder="No users match your search."
77
+ open-on-click
77
78
  >
78
- <admin-table-row-link
79
- slot="before"
80
- url-pattern="/admin/accounts/users/{id}"
81
- field-name="id"
82
- ></admin-table-row-link>
79
+ <kc-tc-open slot="before"></kc-tc-open>
83
80
  <div
84
81
  slot="top"
85
82
  class="d-f full"
@@ -118,6 +115,7 @@
118
115
  import Dialog from '/kempo-ui/components/Dialog.js';
119
116
  await window.customElements.whenDefined('k-table');
120
117
  const $table = document.getElementById('usersTable');
118
+ const withUrl = u => ({ ...u, _url: `/admin/accounts/users/${u.id}` });
121
119
 
122
120
  const [, permData] = await getAllCurrentUserPermissions();
123
121
  const myPerms = new Set(permData?.permissions ?? []);
@@ -141,9 +139,9 @@
141
139
  const [error, data] = await getUsers({ limit: 10, offset: 0 });
142
140
  if (error) return;
143
141
  $table.setData({
144
- records: data.users,
142
+ records: data.users.map(withUrl),
145
143
  fields: [
146
- { name: 'name', label: 'Name', size: 352, editable: true },
144
+ { name: 'name', label: 'Name', editable: true },
147
145
  { name: 'email', label: 'Email', size: 352, editable: true },
148
146
  {
149
147
  name: 'emailVerified',
@@ -170,7 +168,7 @@
170
168
  $table.setupFetchRecords(data.total, async (start, count) => {
171
169
  const [err, d] = await getUsers({ limit: count, offset: start });
172
170
  if (err) return [];
173
- return d.users;
171
+ return d.users.map(withUrl);
174
172
  });
175
173
  };
176
174
 
@@ -1,25 +1,17 @@
1
- import t from"/kempo-ui/components/ShadowComponent.js";import{html as e,nothing as s}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 i 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();const t=this.closest("k-tab-content");!t||t.active?this.load():this.closest("k-tabs")?.addEventListener("tabfirstshown",({detail:e})=>{e.tab===t.name&&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,s]=await o(t);e?i.error(e.msg):(this.editing=s,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?i.error(t.msg):(this.renderRoot.querySelector("#emailDialog").close(),i.success("Email template saved"),await this.load())};reset=async()=>{this.saving=!0;const[t]=await n(this.editing.template);this.saving=!1,t?i.error(t.msg):(this.renderRoot.querySelector("#emailDialog").close(),i.success("Email template reset to the default"),await this.load())};renderDialog(){const t=this.editing;return t?e`
1
+ import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,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"/kempo-ui/components/Table.js";import"/kempo-ui/components/controls/TcOpen.js";import"/admin/components/tableControls/EditEmailTemplate.js";import s from"/kempo-ui/components/Toast.js";import{listEmailTemplates as a,getEmailTemplate as o,saveEmailTemplate as l,resetEmailTemplate as r}from"/kempo/sdk.js";const n=[{name:"title",label:"Email",editable:!1,calculator:e=>e.title||e.name},{name:"template",label:"Template",size:260,editable:!1,calculator:e=>e.template},{name:"status",label:"Status",size:130,editable:!1,calculator:e=>e.overridden?"Customized":"Default"}];export default class m extends e{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();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()})}load=async()=>{const[e,t]=await a(this.owner);e?this.error=e.msg:this.templates=t.templates};edit=async e=>{const[t,i]=await o(e);t?s.error(t.msg):(this.editing=i,await this.updateComplete,this.renderRoot.querySelector("#emailDialog").open())};confirmSave=e=>{e.keepDialogOpen=!0,this.save()};save=async()=>{this.saving=!0;const[e]=await l({template:this.editing.template,source:this.renderRoot.querySelector("#emailEditor").value});this.saving=!1,e?s.error(e.msg):(this.renderRoot.querySelector("#emailDialog").close(),s.success("Email template saved"),await this.load())};reset=async()=>{this.saving=!0;const[e]=await r(this.editing.template);this.saving=!1,e?s.error(e.msg):(this.renderRoot.querySelector("#emailDialog").close(),s.success("Email template reset to the default"),await this.load())};renderDialog(){const e=this.editing;return e?t`
2
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>
3
+ <h2 slot="title" class="h5 pyh px m0">${e.title||e.name}</h2>
4
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>`:s}
5
+ <p class="tc-muted mb">${e.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=${e.source} style="height: 55vh; min-height: 300px;"></k-code-editor>
7
+ ${e.overridden?t`<button type="button" class="danger" ?disabled=${this.saving} @click=${this.reset}>Reset to default</button>`:i}
8
8
  </div>
9
9
  </k-dialog>
10
- `:s}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>
10
+ `:i}updated(e){if(!e.has("templates")&&!e.has("previewUrl"))return;const t=this.renderRoot.querySelector("k-table");t&&this.templates&&t.setData({records:this.templates.map(e=>({...e,_url:`${this.previewUrl}?template=${encodeURIComponent(e.template)}`})),fields:n})}render(){return this.error?t`<p class="tc-danger" role="alert">${this.error}</p>`:this.templates?this.templates.length?t`
11
+ <k-table class="mb" placeholder="There are no email templates here." @editTemplate=${e=>this.edit(e.detail.record.template)}>
12
+ <kc-tc-open slot="after" target="_blank" title="Preview"></kc-tc-open>
13
+ <admin-edit-email-template slot="after"></admin-edit-email-template>
14
+ </k-table>
23
15
  ${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);
16
+ `:t`<p class="tc-muted">There are no email templates here.</p>`:t`<k-spinner></k-spinner>`}}customElements.define("k-email-templates",m);
25
17
  //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\EmailTemplates.js.map
@@ -1 +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","pane","closest","active","load","addEventListener","detail","tab","name","async","data","msg","edit","found","template","updateComplete","renderRoot","querySelector","open","confirmSave","event","keepDialogOpen","save","source","value","close","success","reset","renderDialog","title","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();\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 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,oBACV,MAAMC,EAAOF,KAAKG,QAAQ,kBAElBD,GAAQA,EAAKE,OAAQJ,KAAKK,OACzBL,KAAKG,QAAQ,WAAWG,iBAAiB,gBAAiB,EAAGC,aAC7DA,EAAOC,MAAQN,EAAKO,MAAMT,KAAKK,QAEtC,CAKAA,KAAOK,UACL,MAAOf,EAAOgB,SAAc7B,EAAmBkB,KAAKZ,OACjDO,EACDK,KAAKL,MAAQA,EAAMiB,IAGrBZ,KAAKP,UAAYkB,EAAKlB,WAGxBoB,KAAOH,UACL,MAAOf,EAAOmB,SAAe/B,EAAiBgC,GAC3CpB,EACDd,EAAMc,MAAMA,EAAMiB,MAGpBZ,KAAKJ,QAAUkB,QACTd,KAAKgB,eACXhB,KAAKiB,WAAWC,cAAc,gBAAgBC,SAIhDC,YAAcC,IACZA,EAAMC,gBAAiB,EACvBtB,KAAKuB,QAGPA,KAAOb,UACLV,KAAKH,QAAS,EACd,MAAOF,SAAeX,EAAkB,CAAE+B,SAAUf,KAAKJ,QAAQmB,SAAUS,OAAQxB,KAAKiB,WAAWC,cAAc,gBAAgBO,QACjIzB,KAAKH,QAAS,EACXF,EACDd,EAAMc,MAAMA,EAAMiB,MAGpBZ,KAAKiB,WAAWC,cAAc,gBAAgBQ,QAC9C7C,EAAM8C,QAAQ,8BACR3B,KAAKK,SAGbuB,MAAQlB,UACNV,KAAKH,QAAS,EACd,MAAOF,SAAeV,EAAmBe,KAAKJ,QAAQmB,UACtDf,KAAKH,QAAS,EACXF,EACDd,EAAMc,MAAMA,EAAMiB,MAGpBZ,KAAKiB,WAAWC,cAAc,gBAAgBQ,QAC9C7C,EAAM8C,QAAQ,6CACR3B,KAAKK,SAMb,YAAAwB,GACE,MAAMd,EAAWf,KAAKJ,QACtB,OAAImB,EACGpC,CAAI;wEACyDqB,KAAKH,OAAS,UAAY,2EAA2EG,KAAKoB;gDAClIL,EAASe,OAASf,EAASN;;mCAExCM,EAASA;kFACsCA,EAASS;YAC/ET,EAASgB,WAAapD,CAAI,kDAAkDqB,KAAKH,iBAAiBG,KAAK4B,kCAAoChD;;;MAP9HA,CAWvB,CAEA,MAAAoD,GACE,OAAGhC,KAAKL,MAAchB,CAAI,qCAAqCqB,KAAKL,YAChEK,KAAKP,UACLO,KAAKP,UAAUwC,OACZtD,CAAI;;;iBAGEqB,KAAKP,UAAUyC,IAAInB,GAAYpC,CAAI;gBACpCoC,EAASe,OAASf,EAASN;iCACVM,EAASA;gBAC1BA,EAASgB,WAAapD,CAAI,6CAA+CA,CAAI;;kCAE3D,GAAGqB,KAAKT,uBAAuB4C,mBAAmBpB,EAASA;4DACjC,IAAMf,KAAKa,KAAKE,EAASA;;;;QAI7Ef,KAAK6B;MAdyBlD,CAAI,6DADXA,CAAI,yBAiBjC,EAGFyD,eAAeC,OAAO,oBAAqBnD","ignoreList":[]}
1
+ {"version":3,"file":"src\\admin\\components\\EmailTemplates.js","names":["ShadowComponent","html","nothing","Toast","listEmailTemplates","getEmailTemplate","saveEmailTemplate","resetEmailTemplate","fields","name","label","editable","calculator","template","title","size","overridden","EmailTemplates","static","owner","type","String","previewUrl","attribute","templates","state","error","editing","saving","constructor","super","this","connectedCallback","pane","closest","active","load","addEventListener","detail","tab","async","data","msg","edit","found","updateComplete","renderRoot","querySelector","open","confirmSave","event","keepDialogOpen","save","source","value","close","success","reset","renderDialog","updated","changed","has","$table","setData","records","map","_url","encodeURIComponent","render","length","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/Icon.js';\nimport '/kempo-ui/components/Dialog.js';\nimport '/kempo-ui/components/CodeEditor.js';\nimport '/kempo-ui/components/Table.js';\nimport '/kempo-ui/components/controls/TcOpen.js';\nimport '/admin/components/tableControls/EditEmailTemplate.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport { listEmailTemplates, getEmailTemplate, saveEmailTemplate, resetEmailTemplate } from '/kempo/sdk.js';\n\nconst fields = [\n { name: 'title', label: 'Email', editable: false, calculator: template => template.title || template.name },\n { name: 'template', label: 'Template', size: 260, editable: false, calculator: template => template.template },\n { name: 'status', label: 'Status', size: 130, editable: false, calculator: template => template.overridden ? 'Customized' : 'Default' },\n];\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();\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 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 /* Refills the table whenever the list or the preview address changes. */\n updated(changed){\n if(!changed.has('templates') && !changed.has('previewUrl')) return;\n const $table = this.renderRoot.querySelector('k-table');\n if(!$table || !this.templates) return;\n $table.setData({\n records: this.templates.map(template => ({ ...template, _url: `${this.previewUrl}?template=${encodeURIComponent(template.template)}` })),\n fields,\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 <k-table class=\"mb\" placeholder=\"There are no email templates here.\" @editTemplate=${event => this.edit(event.detail.record.template)}>\n <kc-tc-open slot=\"after\" target=\"_blank\" title=\"Preview\"></kc-tc-open>\n <admin-edit-email-template slot=\"after\"></admin-edit-email-template>\n </k-table>\n ${this.renderDialog()}\n `;\n }\n}\n\ncustomElements.define('k-email-templates', EmailTemplates);\n"],"mappings":"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","ignoreList":[]}
@@ -1,49 +1,52 @@
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")});const t=this.closest("k-tab-content");!t||t.active?this.load():this.closest("k-tabs")?.addEventListener("tabfirstshown",({detail:s})=>{s.tab===t.name&&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`
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/Table.js";import"/kempo-ui/components/controls/TcFirstPage.js";import"/kempo-ui/components/controls/TcPrevPage.js";import"/kempo-ui/components/controls/TcNextPage.js";import"/kempo-ui/components/controls/TcLastPage.js";import"/kempo-ui/components/controls/TcPageSelect.js";import"/kempo-ui/components/controls/TcPageSize.js";import"/admin/components/tableControls/ViewEmail.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"},m=e=>e?new Date(e).toLocaleString():"",d=[{name:"status",label:"Status",size:110,editable:!1},{name:"recipient",label:"To",size:240,editable:!1},{name:"subject",label:"Subject",editable:!1,calculator:e=>e.lastError?`${e.subject} — ${e.lastError}`:e.subject},{name:"type",label:"Type",size:140,editable:!1},{name:"createdAt",label:"Created",size:190,editable:!1,formatter:m}];export default class p extends e{static properties={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.status="",this.search="",this.loading=!0,this.error="",this.canManage=!1,this.open=null,this.testTo="",this.busy=!1}connectedCallback(){super.connectedCallback(),n().then(([,e])=>{this.canManage=(e?.permissions??[]).includes("system:emails:manage")});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()})}load=async()=>{await this.updateComplete;const e=this.renderRoot.querySelector("#emails"),t={status:this.status||void 0,search:this.search||void 0};this.loading=!0;const[s,a]=await i({...t,limit:25,offset:0});this.loading=!1,s?this.error=s.msg||"The emails could not be loaded":(this.error="",e.setData({records:a.emails,fields:d}),e.setupFetchRecords(a.total,async(e,s)=>{const[a,o]=await i({...t,limit:s,offset:e});return a?[]:o.emails}))};filter=e=>{Object.assign(this,e),this.load()};show=async e=>{const[t,s]=await o(e);t?a.error(t.msg):(this.open=s,await this.updateComplete,this.renderRoot.querySelector("#emailDialog").open())};retry=async()=>{this.busy=!0;const[e,t]=await r(this.open.id);this.busy=!1,e?a.error(e.msg):t.sent?a.success("The email was sent"):a.error(t.error||"The email could not be sent"),this.renderRoot.querySelector("#emailDialog").close(),await this.load()};openTest=()=>this.renderRoot.querySelector("#testDialog").open();confirmTest=e=>{e.keepDialogOpen=!0,this.sendTest()};sendTest=async()=>{this.busy=!0;const[e,t]=await l(this.testTo.trim());this.busy=!1,e?a.error(e.msg):(this.renderRoot.querySelector("#testDialog").close(),t.sent?a.success(`A test email was sent to ${this.testTo.trim()}`):a.error(t.error||"The test email could not be sent"),await this.load())};renderDetail(){const e=this.open;return e?t`
2
2
  <k-dialog id="emailDialog" style="--width: 60rem;" cancel-text="Close">
3
- <h2 slot="title" class="h5 pyh px m0">${t.subject}</h2>
3
+ <h2 slot="title" class="h5 pyh px m0">${e.subject}</h2>
4
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>
5
+ <p class="mb"><strong class=${c[e.status]??""}>${e.status}</strong>${e.attempts?t` · ${e.attempts} ${1===e.attempts?"try":"tries"}`:s}${e.provider?t` · sent by ${e.provider}`:s}</p>
6
+ <p class="mb">To <strong>${e.recipient}</strong>, from ${e.sender}<br><span class="tc-muted">${e.type} · created ${m(e.createdAt)}${e.sentAt?` · sent ${m(e.sentAt)}`:""}</span></p>
7
+ ${e.lastError?t`<p class="tc-danger mb" role="alert">${e.lastError}</p>`:s}
8
+ ${this.canManage&&["queued","failed"].includes(e.status)?t`<p class="mb"><button class="primary" ?disabled=${this.busy} @click=${this.retry}>${this.busy?"Trying…":"Try again"}</button></p>`:s}
9
+ <iframe title="The email" sandbox="" .srcdoc=${e.html} class="full b r" height="420"></iframe>
10
10
  </div>
11
11
  </k-dialog>
12
- `:e}renderTest(){return s`
12
+ `:s}renderTest(){return t`
13
13
  <k-dialog id="testDialog" confirm-text=${this.busy?"Sending…":"Send test email"} confirm-classes="primary ml" cancel-text="Cancel" .confirmAction=${this.confirmTest}>
14
14
  <h2 slot="title" class="h5 pyh px m0">Send a test email</h2>
15
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>
16
+ <label class="mb d-b">Send it to<input type="email" .value=${this.testTo} @input=${e=>{this.testTo=e.target.value}}></label>
17
17
  <p class="tc-muted">It goes out through the email extension that is installed, and shows in the log below.</p>
18
18
  </div>
19
19
  </k-dialog>
20
- `}render(){const t=Math.min(this.offset+25,this.total);return s`
20
+ `}render(){return t`
21
21
  <div class="d-f mb">
22
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>`)}
23
+ <select @change=${e=>this.filter({status:e.target.value})}>
24
+ ${["","queued","sending","sent","failed"].map(e=>t`<option value=${e} ?selected=${this.status===e}>${e||"All"}</option>`)}
25
25
  </select>
26
26
  </label>
27
27
  <label class="mr">Search
28
- <input type="search" placeholder="Address or subject" .value=${this.search} @change=${t=>this.filter({search:t.target.value})}>
28
+ <input type="search" placeholder="Address or subject" .value=${this.search} @change=${e=>this.filter({search:e.target.value})}>
29
29
  </label>
30
- ${this.canManage?s`<button class="mb" @click=${this.openTest}>Send a test email</button>`:e}
30
+ ${this.canManage?t`<button class="mb" @click=${this.openTest}>Send a test email</button>`:s}
31
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>`}
32
+ ${this.error?t`<p class="tc-danger" role="alert">${this.error}</p>`:s}
33
+ ${this.loading?t`<k-spinner></k-spinner>`:s}
34
+ <k-table id="emails" enable-pages page-size="25" page-size-options="[10,25,50]" placeholder="No emails have been sent yet." @emailView=${e=>this.show(e.detail.record.id)}>
35
+ <admin-view-email slot="before"></admin-view-email>
36
+ <div class="d-f full" style="align-items: center;">
37
+ <div>
38
+ <kc-tc-first-page></kc-tc-first-page>
39
+ <kc-tc-prev-page></kc-tc-prev-page>
40
+ <kc-tc-next-page></kc-tc-next-page>
41
+ <kc-tc-last-page></kc-tc-last-page>
42
+ </div>
43
+ <div class="flex"></div>
44
+ <div><kc-tc-page-select></kc-tc-page-select></div>
45
+ <div class="flex"></div>
46
+ <div><kc-tc-page-size></kc-tc-page-size></div>
47
+ </div>
48
+ </k-table>
46
49
  ${this.renderDetail()}
47
50
  ${this.renderTest()}
48
- `}}customElements.define("k-admin-emails",d);
51
+ `}}customElements.define("k-admin-emails",p);
49
52
  //# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\Emails.js.map
@@ -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","pane","closest","active","load","addEventListener","detail","tab","name","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'); });\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 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":[]}