kempo 4.2.9 → 4.2.11

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.
@@ -0,0 +1,378 @@
1
+ <!--
2
+ owner: system
3
+ locked: true
4
+ name:
5
+ author:
6
+ created:
7
+ updated:
8
+ -->
9
+ <!DOCTYPE html>
10
+ <html lang="en">
11
+ <head>
12
+ <meta charset="UTF-8">
13
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
14
+ <link rel="icon" type="image/png" sizes="48x48" href="/media/icon48.png">
15
+ <link rel="manifest" href="/manifest.json">
16
+ <title>Groups - Admin</title>
17
+ <link rel="stylesheet" href="/kempo-css/kempo.min.css">
18
+ <script src="/admin/init.js"></script>
19
+
20
+ </head>
21
+ <body>
22
+ <!--
23
+ locked: true
24
+ -->
25
+
26
+ <k-aside
27
+ main="push"
28
+ state="expanded"
29
+ style="--aside_padding: 0;"
30
+ persistent-id="admin-nav"
31
+ >
32
+ <k-aside-toggle>
33
+ <h3 class="m0">Kempo Admin</h3>
34
+ </k-aside-toggle>
35
+ <k-aside-item
36
+ icon="preview"
37
+ href="/"
38
+ no-expand
39
+ >Site</k-aside-item>
40
+ <k-aside-item
41
+ icon="cards"
42
+ href="/admin"
43
+ no-expand
44
+ >Dashboard</k-aside-item>
45
+ <k-aside-menu
46
+ icon="manage-accounts"
47
+ label="Accounts"
48
+ >
49
+ <k-aside-item
50
+ icon="person"
51
+ href="/admin/accounts/users"
52
+ no-expand
53
+ >Users</k-aside-item>
54
+ <k-aside-item
55
+ icon="group"
56
+ href="/admin/accounts/groups"
57
+ no-expand
58
+ >Groups</k-aside-item>
59
+ <k-aside-item
60
+ icon="lock"
61
+ href="/admin/accounts/permissions"
62
+ no-expand
63
+ >Permissions</k-aside-item>
64
+ </k-aside-menu>
65
+ <k-aside-menu
66
+ icon="cards_stack"
67
+ label="Content"
68
+ >
69
+ <k-aside-item
70
+ icon="browse"
71
+ href="/admin/content/templates"
72
+ >Templates</k-aside-item>
73
+ <k-aside-item
74
+ icon="docs"
75
+ href="/admin/content/pages"
76
+ >Pages</k-aside-item>
77
+ <k-aside-item
78
+ icon="content"
79
+ href="/admin/content/fragments"
80
+ >Fragments</k-aside-item>
81
+ <k-aside-item
82
+ icon="globe"
83
+ href="/admin/content/globals"
84
+ >Global Content</k-aside-item>
85
+ </k-aside-menu>
86
+ <k-aside-item
87
+ icon="settings"
88
+ href="/admin/settings"
89
+ no-expand
90
+ >Settings</k-aside-item>
91
+ <k-aside-item
92
+ icon="extension"
93
+ href="/admin/extensions"
94
+ no-expand
95
+ >Extensions</k-aside-item>
96
+ <hr>
97
+ <k-aside-spacer></k-aside-spacer>
98
+ <k-theme-switcher
99
+ labels
100
+ style="--padding:var(--spacer_h);margin:var(--spacer_h) auto"
101
+ ></k-theme-switcher>
102
+ </k-aside>
103
+ <script type="module">
104
+ import '/kempo-ui/components/ThemeSwitcher.js';
105
+ const items = document.querySelectorAll('k-aside-item');
106
+ const currentPath = window.location.pathname;
107
+
108
+ items.forEach(item => {
109
+ const href = item.getAttribute('href');
110
+ if (!href) return;
111
+ const isExact = href === '/admin' || href === '/';
112
+ const match = isExact
113
+ ? (currentPath === href || currentPath === href + '/')
114
+ : (currentPath === href || currentPath.startsWith(href + '/'));
115
+ if (match) {
116
+ item.setAttribute('active', '');
117
+ const menu = item.closest('k-aside-menu');
118
+ if (menu) menu.setAttribute('open', '');
119
+ }
120
+ });
121
+ </script>
122
+
123
+ <k-main>
124
+
125
+ <script type="module" src="/kempo-ui/components/Table.js"></script>
126
+ <script type="module" src="/kempo-ui/components/tableControls/FirstPage.js"></script>
127
+ <script type="module" src="/kempo-ui/components/tableControls/PrevPage.js"></script>
128
+ <script type="module" src="/kempo-ui/components/tableControls/NextPage.js"></script>
129
+ <script type="module" src="/kempo-ui/components/tableControls/LastPage.js"></script>
130
+ <script type="module" src="/kempo-ui/components/tableControls/PageSelect.js"></script>
131
+ <script type="module" src="/kempo-ui/components/tableControls/PageSize.js"></script>
132
+ <script type="module" src="/kempo-ui/components/tableControls/Edit.js"></script>
133
+ <script type="module" src="/kempo-ui/components/tableControls/DeleteRecord.js"></script>
134
+ <script type="module" src="/kempo-ui/components/tableControls/DeleteSelected.js"></script>
135
+ <script type="module" src="/admin/components/TableRowLink.js"></script>
136
+ <script type="module" src="/kempo/components/Permission.js"></script>
137
+
138
+ <div class="d-f mb" style="align-items: center; gap: var(--spacer);">
139
+ <h1 class="flex mb0">Groups</h1>
140
+ <k-permission has="system:group:create">
141
+ <button id="createGroupBtn" class="primary">Create Group</button>
142
+ </k-permission>
143
+ </div>
144
+
145
+ <h2>Custom Groups</h2>
146
+ <k-table id="customTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No custom groups have been created yet.">
147
+ <admin-table-row-link slot="before" url-pattern="/admin/accounts/groups/{name}" field-name="name"></admin-table-row-link>
148
+ <div slot="top" class="d-f full" style="align-items: center;">
149
+ <div>
150
+ <k-permission has="system:group:delete">
151
+ <k-tc-delete-selected></k-tc-delete-selected>
152
+ </k-permission>
153
+ </div>
154
+ </div>
155
+ <div class="d-f full" style="align-items: center;">
156
+ <div>
157
+ <k-tc-first-page></k-tc-first-page>
158
+ <k-tc-prev-page></k-tc-prev-page>
159
+ <k-tc-next-page></k-tc-next-page>
160
+ <k-tc-last-page></k-tc-last-page>
161
+ </div>
162
+ <div class="flex"></div>
163
+ <div><k-tc-page-select></k-tc-page-select></div>
164
+ <div class="flex"></div>
165
+ <div><k-tc-page-size></k-tc-page-size></div>
166
+ </div>
167
+ </k-table>
168
+
169
+ <h2>Extension Groups</h2>
170
+ <k-table id="extensionTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No extension groups found.">
171
+ <admin-table-row-link slot="before" url-pattern="/admin/accounts/groups/{name}" field-name="name"></admin-table-row-link>
172
+ <div class="d-f full" style="align-items: center;">
173
+ <div>
174
+ <k-tc-first-page></k-tc-first-page>
175
+ <k-tc-prev-page></k-tc-prev-page>
176
+ <k-tc-next-page></k-tc-next-page>
177
+ <k-tc-last-page></k-tc-last-page>
178
+ </div>
179
+ <div class="flex"></div>
180
+ <div><k-tc-page-select></k-tc-page-select></div>
181
+ <div class="flex"></div>
182
+ <div><k-tc-page-size></k-tc-page-size></div>
183
+ </div>
184
+ </k-table>
185
+
186
+ <h2>System Groups</h2>
187
+ <k-table id="systemTable" enable-pages page-size="10" page-size-options="[10,25,50]" placeholder="No system groups found.">
188
+ <admin-table-row-link slot="before" url-pattern="/admin/accounts/groups/{name}" field-name="name"></admin-table-row-link>
189
+ <div class="d-f full" style="align-items: center;">
190
+ <div>
191
+ <k-tc-first-page></k-tc-first-page>
192
+ <k-tc-prev-page></k-tc-prev-page>
193
+ <k-tc-next-page></k-tc-next-page>
194
+ <k-tc-last-page></k-tc-last-page>
195
+ </div>
196
+ <div class="flex"></div>
197
+ <div><k-tc-page-select></k-tc-page-select></div>
198
+ <div class="flex"></div>
199
+ <div><k-tc-page-size></k-tc-page-size></div>
200
+ </div>
201
+ </k-table>
202
+
203
+ <script type="module">
204
+ import { listGroups, createGroup, updateGroup, deleteGroups, getAllCurrentUserPermissions } from '/kempo/sdk.js';
205
+ import { html } from '/kempo-ui/lit-all.min.js';
206
+ import Toast from '/kempo-ui/components/Toast.js';
207
+ import Dialog from '/kempo-ui/components/Dialog.js';
208
+
209
+ await window.customElements.whenDefined('k-table');
210
+
211
+ const [, permData] = await getAllCurrentUserPermissions();
212
+ const myPerms = new Set(permData?.permissions ?? []);
213
+ const canEdit = myPerms.has('system:group:update');
214
+ const canDelete = myPerms.has('system:group:delete');
215
+
216
+ const fields = [
217
+ {
218
+ name: 'groupName',
219
+ label: 'Name',
220
+ size: 300,
221
+ editable: false,
222
+ calculator: r => r.name.includes(':') ? r.name.split(':').slice(1).join(':') : r.name
223
+ },
224
+ { name: 'description', label: 'Description', editable: canEdit },
225
+ {
226
+ name: 'createdAt',
227
+ label: 'Created',
228
+ formatter: v => new Date(v).toLocaleString(),
229
+ size: 208,
230
+ editable: false
231
+ }
232
+ ];
233
+
234
+ const customFields = [
235
+ { name: 'groupName', label: 'Name', size: 300, editable: canEdit },
236
+ { name: 'description', label: 'Description', editable: canEdit },
237
+ {
238
+ name: 'createdAt',
239
+ label: 'Created',
240
+ formatter: v => new Date(v).toLocaleString(),
241
+ size: 208,
242
+ editable: false
243
+ }
244
+ ];
245
+
246
+ const addGroupName = r => ({ ...r, groupName: r.name.includes(':') ? r.name.split(':').slice(1).join(':') : r.name });
247
+
248
+ const setupTable = ($table, owner, loadFn) => {
249
+ if(canEdit && owner !== 'system') { // Prevent editor control for system groups
250
+ $table.setAttribute('request-edit', '');
251
+ const edit = document.createElement('k-tc-edit');
252
+ edit.setAttribute('slot', 'after');
253
+ $table.appendChild(edit);
254
+ }
255
+
256
+ if(canDelete && owner === 'custom'){
257
+ $table.setAttribute('request-delete', '');
258
+ $table.setAttribute('enable-selection', '');
259
+ const del = document.createElement('k-tc-delete-record');
260
+ del.setAttribute('slot', 'after');
261
+ $table.appendChild(del);
262
+ }
263
+
264
+ $table.addEventListener('requestSave', async ({ detail: { record, newData, approve, reject } }) => {
265
+ const patch = { name: record.name, description: newData.description };
266
+ if(owner === 'custom' && newData.groupName !== undefined && newData.groupName !== record.groupName){
267
+ patch.newName = `custom:${newData.groupName}`;
268
+ }
269
+ const [error] = await updateGroup(patch);
270
+ if(error){
271
+ reject();
272
+ Toast.error(error.msg || 'Failed to update group');
273
+ return;
274
+ }
275
+ approve();
276
+ if(patch.newName) loadFn();
277
+ Toast.success('Group updated');
278
+ });
279
+
280
+ if(owner === 'custom'){
281
+ $table.addEventListener('requestDelete', ({ detail: { records, approve, reject } }) => {
282
+ const displayName = r => r.name.includes(':') ? r.name.split(':').slice(1).join(':') : r.name;
283
+ const count = records.length;
284
+ const msg = count === 1
285
+ ? `<div class="p"><p>Delete "${displayName(records[0])}"? This action cannot be undone.</p></div>`
286
+ : `<div class="p"><p>Delete ${count} groups? This action cannot be undone.</p></div>`;
287
+ Dialog.confirm(msg, async (confirmed) => {
288
+ if(!confirmed){ reject(); return; }
289
+ const [error] = await deleteGroups(records.map(r => r.name));
290
+ if(error){
291
+ reject();
292
+ Toast.error(error.msg || 'Failed to delete');
293
+ return;
294
+ }
295
+ approve();
296
+ Toast.success(count === 1 ? 'Group deleted' : `${count} groups deleted`);
297
+ loadFn();
298
+ });
299
+ });
300
+ }
301
+ };
302
+
303
+ const loadTable = async ($table, owner) => {
304
+ const [error, data] = await listGroups({ owner });
305
+ if(error){
306
+ Toast.error(error.msg || 'Failed to load groups');
307
+ return;
308
+ }
309
+ const isCustom = owner === 'custom';
310
+ const records = isCustom ? data.groups.map(addGroupName) : data.groups;
311
+ $table.setData({ records, fields: isCustom ? customFields : fields });
312
+ $table.setupFetchRecords(data.total, async (start, count) => {
313
+ const [err, d] = await listGroups({ limit: count, offset: start, owner });
314
+ if(err) return [];
315
+ return isCustom ? d.groups.map(addGroupName) : d.groups;
316
+ });
317
+ };
318
+
319
+ const $customTable = document.getElementById('customTable');
320
+ const $extensionTable = document.getElementById('extensionTable');
321
+ const $systemTable = document.getElementById('systemTable');
322
+
323
+ setupTable($customTable, 'custom', () => loadTable($customTable, 'custom'));
324
+ setupTable($extensionTable, 'extension', () => loadTable($extensionTable, 'extension'));
325
+ setupTable($systemTable, 'system', () => loadTable($systemTable, 'system'));
326
+
327
+ await Promise.all([
328
+ loadTable($customTable, 'custom'),
329
+ loadTable($extensionTable, 'extension'),
330
+ loadTable($systemTable, 'system')
331
+ ]);
332
+
333
+ if(myPerms.has('system:group:create')){
334
+ document.getElementById('createGroupBtn').addEventListener('click', () => {
335
+ const $dialog = Dialog.create(html`
336
+ <div class="p">
337
+ <div class="mb">
338
+ <label class="d-b mb-sm">Name</label>
339
+ <input type="text" id="dlg-name" class="full" required>
340
+ </div>
341
+ <div>
342
+ <label class="d-b mb-sm">Description</label>
343
+ <input type="text" id="dlg-description" class="full">
344
+ </div>
345
+ </div>
346
+ `, {
347
+ title: 'Create Group',
348
+ confirmText: 'Create',
349
+ confirmAction: async () => {
350
+ const name = $dialog.querySelector('#dlg-name').value.trim();
351
+ const description = $dialog.querySelector('#dlg-description').value.trim();
352
+ if(!name){
353
+ Toast.error('Group name is required');
354
+ return;
355
+ }
356
+ if(name.includes(':')){
357
+ Toast.error('Group name cannot contain ":"');
358
+ return;
359
+ }
360
+ const [error] = await createGroup({ name, description });
361
+ if(error){
362
+ Toast.error(error.msg || 'Failed to create group');
363
+ return;
364
+ }
365
+ Toast.success('Group created');
366
+ loadTable($customTable, 'custom');
367
+ },
368
+ cancelText: 'Cancel'
369
+ });
370
+ });
371
+ }
372
+ </script>
373
+
374
+
375
+ </k-main>
376
+
377
+ </body>
378
+ </html>