kempo 4.9.5 → 4.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) 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 +81 -63
  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/settings/SettingsPanel.js +1 -1
  13. package/dist/admin/components/settings/SettingsPanel.js.map +1 -1
  14. package/dist/admin/components/tableControls/EditEmailTemplate.js +2 -0
  15. package/dist/admin/components/tableControls/EditEmailTemplate.js.map +1 -0
  16. package/dist/admin/components/tableControls/UpdateItem.js +2 -0
  17. package/dist/admin/components/tableControls/UpdateItem.js.map +1 -0
  18. package/dist/admin/components/tableControls/UpdateSelected.js +2 -0
  19. package/dist/admin/components/tableControls/UpdateSelected.js.map +1 -0
  20. package/dist/admin/components/tableControls/ViewEmail.js +2 -0
  21. package/dist/admin/components/tableControls/ViewEmail.js.map +1 -0
  22. package/dist/admin/content/fragments/index.page.html +12 -12
  23. package/dist/admin/content/globals/index.page.html +12 -11
  24. package/dist/admin/content/pages/index.page.html +11 -23
  25. package/dist/admin/content/templates/index.page.html +13 -25
  26. package/dist/admin/emails/index.page.html +1 -1
  27. package/dist/admin/extensions/index.page.html +10 -9
  28. package/dist/admin/settings/index.page.html +13 -3
  29. package/middleware/kempo.js +1 -0
  30. package/package.json +2 -2
  31. package/server/utils/sessions/pruneExpiredSessions.js +32 -0
  32. package/server/utils/settings/settingRules.js +1 -0
  33. package/server/utils/settings/systemSettings.js +1 -0
  34. package/dist/admin/components/settings/AdminSettings.js +0 -7
  35. package/dist/admin/components/settings/AdminSettings.js.map +0 -1
  36. package/dist/admin/components/tableControls/ExtensionDetails.js +0 -2
  37. 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',
@@ -5,6 +5,7 @@
5
5
  <content>
6
6
  <script type="module" src="/kempo-ui/components/Toggle.js"></script>
7
7
  <script type="module" src="/kempo-ui/components/Icon.js"></script>
8
+ <script type="module" src="/kempo-ui/components/Tabs.js"></script>
8
9
  <script type="module" src="/kempo-ui/components/Table.js"></script>
9
10
  <script type="module" src="/kempo-ui/components/controls/TcFirstPage.js"></script>
10
11
  <script type="module" src="/kempo-ui/components/controls/TcPrevPage.js"></script>
@@ -17,7 +18,7 @@
17
18
  <script type="module" src="/admin/components/GroupDeleteSelected.js"></script>
18
19
  <script type="module" src="/admin/components/AddGroupControl.js"></script>
19
20
  <script type="module" src="/admin/components/ClearExpiredSessionsControl.js"></script>
20
- <script type="module" src="/admin/components/TableRowLink.js"></script>
21
+ <script type="module" src="/kempo-ui/components/controls/TcOpen.js"></script>
21
22
  <div class="d-f">
22
23
  <div>
23
24
  <a href="/admin/accounts/users" class="no-link mb">
@@ -38,69 +39,71 @@
38
39
  <a href="/admin/accounts/users">Back to Users</a>
39
40
  </div>
40
41
  <div id="content" style="display: none;">
41
-
42
- <!-- User Information -->
43
- <div class="card">
44
- <h2>Information</h2>
45
- <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
46
- <label for="userName" style="min-width: 120px;"><strong>Name</strong></label>
47
- <input type="text" id="userName" style="max-width: 400px;">
48
- </div>
49
- <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
50
- <label for="userEmail" style="min-width: 120px;"><strong>Email</strong></label>
51
- <input type="email" id="userEmail" style="max-width: 400px;">
52
- </div>
53
- <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
54
- <label style="min-width: 120px;"><strong>Verified</strong></label>
55
- <k-toggle id="emailVerified"></k-toggle>
56
- </div>
57
- <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
58
- <label style="min-width: 120px;"><strong>Created</strong></label>
59
- <span id="userCreatedAt"></span>
60
- </div>
61
- </div>
62
-
63
- <!-- Groups -->
64
- <div class="card">
65
- <h2>Groups</h2>
66
- <k-table id="groupsTable" request-delete placeholder="Not a member of any groups.">
67
- <admin-table-row-link slot="before" url-pattern="/admin/accounts/groups/{name}" field-name="name"></admin-table-row-link>
68
- <kc-tc-delete-record slot="after"></kc-tc-delete-record>
69
- <div slot="top" id="groupsTableTop" class="d-f full" style="display:none; align-items: center;">
70
- <div><admin-group-delete-selected></admin-group-delete-selected></div>
71
- <div class="flex"></div>
72
- <div><admin-add-group-control id="addGroupBtn"></admin-add-group-control></div>
42
+ <k-tabs id="userTabs" active="information" hash-routing>
43
+ <k-tab for="information">Information</k-tab>
44
+ <k-tab for="groups">Groups</k-tab>
45
+ <k-tab for="sessions">Sessions</k-tab>
46
+
47
+ <k-tab-content name="information">
48
+ <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
49
+ <label for="userName" style="min-width: 120px;"><strong>Name</strong></label>
50
+ <input type="text" id="userName" style="max-width: 400px;">
73
51
  </div>
74
- </k-table>
75
- </div>
76
-
77
- <!-- Sessions -->
78
- <div class="card">
79
- <h2>Sessions</h2>
80
- <k-table id="sessionsTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No active sessions.">
81
- <kc-tc-delete-record slot="after"></kc-tc-delete-record>
82
- <div slot="top" class="d-f full" style="align-items: center;">
83
- <div><kc-tc-delete-selected></kc-tc-delete-selected></div>
84
- <div class="flex"></div>
85
- <div><admin-clear-expired-sessions id="clearExpiredBtn" style="display:none;"></admin-clear-expired-sessions></div>
52
+ <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
53
+ <label for="userEmail" style="min-width: 120px;"><strong>Email</strong></label>
54
+ <input type="email" id="userEmail" style="max-width: 400px;">
86
55
  </div>
87
- <div class="d-f full" style="align-items: center;">
88
- <div>
89
- <kc-tc-first-page></kc-tc-first-page>
90
- <kc-tc-prev-page></kc-tc-prev-page>
91
- <kc-tc-next-page></kc-tc-next-page>
92
- <kc-tc-last-page></kc-tc-last-page>
93
- </div>
94
- <div class="flex"></div>
95
- <div><kc-tc-page-select></kc-tc-page-select></div>
96
- <div class="flex"></div>
97
- <div><kc-tc-page-size></kc-tc-page-size></div>
56
+ <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
57
+ <label style="min-width: 120px;"><strong>Verified</strong></label>
58
+ <k-toggle id="emailVerified"></k-toggle>
98
59
  </div>
99
- </k-table>
100
- </div>
60
+ <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
61
+ <label style="min-width: 120px;"><strong>Created</strong></label>
62
+ <span id="userCreatedAt"></span>
63
+ </div>
64
+ </k-tab-content>
65
+
66
+ <k-tab-content name="groups">
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
+ <kc-tc-delete-record slot="after"></kc-tc-delete-record>
70
+ <div slot="top" id="groupsTableTop" class="d-f full" style="display:none; align-items: center;">
71
+ <div><admin-group-delete-selected></admin-group-delete-selected></div>
72
+ <div class="flex"></div>
73
+ <div><admin-add-group-control id="addGroupBtn"></admin-add-group-control></div>
74
+ </div>
75
+ </k-table>
76
+ </k-tab-content>
77
+
78
+ <k-tab-content name="sessions">
79
+ <k-table id="sessionsTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No active sessions.">
80
+ <kc-tc-delete-record slot="after"></kc-tc-delete-record>
81
+ <div slot="top" class="d-f full" style="align-items: center;">
82
+ <div><kc-tc-delete-selected></kc-tc-delete-selected></div>
83
+ <div class="flex"></div>
84
+ <div><admin-clear-expired-sessions id="clearExpiredBtn" style="display:none;"></admin-clear-expired-sessions></div>
85
+ </div>
86
+ <div class="d-f full" style="align-items: center;">
87
+ <div>
88
+ <kc-tc-first-page></kc-tc-first-page>
89
+ <kc-tc-prev-page></kc-tc-prev-page>
90
+ <kc-tc-next-page></kc-tc-next-page>
91
+ <kc-tc-last-page></kc-tc-last-page>
92
+ </div>
93
+ <div class="flex"></div>
94
+ <div><kc-tc-page-select></kc-tc-page-select></div>
95
+ <div class="flex"></div>
96
+ <div><kc-tc-page-size></kc-tc-page-size></div>
97
+ </div>
98
+ </k-table>
99
+ </k-tab-content>
101
100
 
101
+ <location name="admin-user-tabs" />
102
+ </k-tabs>
102
103
  </div>
103
104
 
105
+ <location name="admin-user-scripts" />
106
+
104
107
  <script type="module">
105
108
  import {
106
109
  getSession, getUser, updateUser, deleteUsers,
@@ -260,6 +263,7 @@
260
263
 
261
264
  await customElements.whenDefined('k-table');
262
265
  const $groupsTable = document.getElementById('groupsTable');
266
+ const toRecord = g => ({ ...g, _url: `/admin/accounts/groups/${g.name}` });
263
267
 
264
268
  if(canEditGroups){
265
269
  $groupsTable.setAttribute('enable-selection', '');
@@ -308,13 +312,13 @@
308
312
  return;
309
313
  }
310
314
  $groupsTable.setData({
311
- records: data.groups,
315
+ records: data.groups.map(toRecord),
312
316
  fields: [{ name: 'name', label: 'Name' }]
313
317
  });
314
318
  $groupsTable.setupFetchRecords(data.total, async (start, count) => {
315
319
  const [err, d] = await getUserGroups(userid, { limit: count, offset: start });
316
320
  if(err) return [];
317
- return d.groups;
321
+ return d.groups.map(toRecord);
318
322
  });
319
323
  };
320
324
 
@@ -347,8 +351,6 @@
347
351
  });
348
352
  });
349
353
 
350
- loadGroups();
351
-
352
354
  /*
353
355
  Sessions
354
356
  */
@@ -420,7 +422,23 @@
420
422
  });
421
423
  };
422
424
 
423
- loadSessions();
425
+ /*
426
+ Each tab loads its data the first time it is opened, so a visit that never leaves Information makes no queries.
427
+ */
428
+
429
+ const $userTabs = document.getElementById('userTabs');
430
+ const shownTabs = new Set();
431
+ const loaders = { groups: loadGroups, sessions: loadSessions };
432
+
433
+ $userTabs.addEventListener('tabfirstshown', ({ detail }) => {
434
+ shownTabs.add(detail.tab);
435
+ loaders[detail.tab]?.();
436
+ });
437
+
438
+ // The tab the address opened on was chosen before this listener existed, so it is loaded here.
439
+ await $userTabs.updateComplete;
440
+ const opening = $userTabs.active;
441
+ if(loaders[opening] && !shownTabs.has(opening)) loaders[opening]();
424
442
  }
425
443
  </script>
426
444
 
@@ -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 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`
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>`:i}
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
- `: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>
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","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":[]}
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")}),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