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,130 @@
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>Admin Dashboard</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
+ <h1>Dashboard</h1>
126
+
127
+ </k-main>
128
+
129
+ </body>
130
+ </html>
@@ -1,4 +1,5 @@
1
1
  <!--
2
2
  locked: true
3
+ template: default
3
4
  -->
4
5
  <page title="Admin Dashboard"><content><h1>Dashboard</h1></content></page>
@@ -0,0 +1,449 @@
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>Setting - 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/Icon.js"></script>
126
+ <script type="module" src="/kempo-ui/components/Toggle.js"></script>
127
+ <script type="module" src="/admin/components/Monaco.js"></script>
128
+ <script type="module" src="/kempo/components/Permission.js"></script>
129
+
130
+ <div class="mb">
131
+ <a href="/admin/settings/" class="no-link">
132
+ <button class="mbh"><k-icon name="arrow" direction="left"></k-icon> Settings</button>
133
+ </a>
134
+ <div class="d-f mb0" style="align-items: center; justify-content: space-between;">
135
+ <div>
136
+ <h1 class="mb0" id="pageTitle">Setting</h1>
137
+ <p class="tc-muted mb0" id="settingDescription"></p>
138
+ </div>
139
+ <div class="d-f" style="gap: var(--spacer);">
140
+ <button id="saveBtn" class="primary" disabled>Save</button>
141
+ <button id="deleteBtn" class="danger" style="display: none;">Delete</button>
142
+ </div>
143
+ </div>
144
+ </div>
145
+
146
+ <div id="loading">Loading setting...</div>
147
+ <div id="notFound" style="display: none;">
148
+ <p>Setting not found.</p>
149
+ <a href="/admin/settings/">Back to Settings</a>
150
+ </div>
151
+ <div id="content" style="display: none;">
152
+
153
+ <div class="card">
154
+ <h2>Value</h2>
155
+ <div id="editorContainer" class="mb"></div>
156
+ </div>
157
+
158
+ <div class="card">
159
+ <h2>Metadata</h2>
160
+ <div class="mb">
161
+ <label class="d-b mbh">Type</label>
162
+ <select id="typeSelect" class="full">
163
+ <option value="string">String</option>
164
+ <option value="number">Number</option>
165
+ <option value="boolean">Boolean</option>
166
+ <option value="json">JSON</option>
167
+ </select>
168
+ </div>
169
+ <div class="mb d-f" style="align-items: center; gap: var(--spacer_h);">
170
+ <k-toggle id="publicToggle"></k-toggle>
171
+ <label>Public (accessible without authentication)</label>
172
+ </div>
173
+ <div class="mb">
174
+ <label class="d-b mbh">Description</label>
175
+ <textarea id="descriptionInput" class="full" rows="3"></textarea>
176
+ </div>
177
+ <div class="mb d-f" style="gap: var(--spacer);">
178
+ <div><span class="tc-muted">Created:</span> <span id="createdAt"></span></div>
179
+ <div><span class="tc-muted">Updated:</span> <span id="updatedAt"></span></div>
180
+ </div>
181
+ </div>
182
+
183
+ </div>
184
+
185
+ <script type="module">
186
+ import { getSetting, setSetting, deleteSetting, getAllCurrentUserPermissions } from '/kempo/sdk.js';
187
+ import Toast from '/kempo-ui/components/Toast.js';
188
+ import Dialog from '/kempo-ui/components/Dialog.js';
189
+
190
+ const pathParts = window.location.pathname.split('/admin/settings/')[1]?.replace(/\/$/, '').split('/') || [];
191
+ const owner = decodeURIComponent(pathParts[0] || '');
192
+ const settingName = decodeURIComponent(pathParts[1] || '');
193
+
194
+ if(!owner || !settingName){
195
+ document.getElementById('loading').style.display = 'none';
196
+ document.getElementById('notFound').style.display = '';
197
+ }
198
+
199
+ /*
200
+ Permission checks
201
+ */
202
+
203
+ const [, permResult] = await getAllCurrentUserPermissions();
204
+ const myPerms = new Set(permResult?.permissions ?? []);
205
+ const canEditValue = owner === 'system'
206
+ ? myPerms.has('system:settings:update')
207
+ : owner === 'custom'
208
+ ? myPerms.has('system:custom-settings:manage')
209
+ : myPerms.has('system:extension:configure');
210
+ const canEditMetadata = owner === 'custom' && myPerms.has('system:custom-settings:manage');
211
+ const canDelete = owner === 'custom' && myPerms.has('system:custom-settings:manage');
212
+
213
+ /*
214
+ Load setting
215
+ */
216
+
217
+ const [error, settingData] = await getSetting(owner, settingName);
218
+
219
+ document.getElementById('loading').style.display = 'none';
220
+
221
+ if(error){
222
+ document.getElementById('notFound').style.display = '';
223
+ } else {
224
+ document.getElementById('content').style.display = '';
225
+ document.getElementById('pageTitle').textContent = settingName;
226
+ document.title = `${settingName} - Settings - Admin`;
227
+ document.getElementById('settingDescription').textContent = settingData.description || '';
228
+
229
+ const $typeSelect = document.getElementById('typeSelect');
230
+ const $publicToggle = document.getElementById('publicToggle');
231
+ const $descriptionInput = document.getElementById('descriptionInput');
232
+ const $editorContainer = document.getElementById('editorContainer');
233
+ const $saveBtn = document.getElementById('saveBtn');
234
+ const $deleteBtn = document.getElementById('deleteBtn');
235
+
236
+ $typeSelect.value = settingData.type;
237
+ $descriptionInput.value = settingData.description || '';
238
+ document.getElementById('createdAt').textContent = new Date(settingData.createdAt).toLocaleString();
239
+ document.getElementById('updatedAt').textContent = new Date(settingData.updatedAt).toLocaleString();
240
+
241
+ await customElements.whenDefined('k-toggle');
242
+ $publicToggle.value = settingData.isPublic;
243
+
244
+ /*
245
+ Value editors
246
+ */
247
+
248
+ let currentType = settingData.type;
249
+
250
+ const getValueFromEditor = () => {
251
+ switch(currentType){
252
+ case 'string':
253
+ return $editorContainer.querySelector('textarea').value;
254
+ case 'number':
255
+ return Number($editorContainer.querySelector('input').value);
256
+ case 'boolean':
257
+ return $editorContainer.querySelector('k-toggle').value;
258
+ case 'json': {
259
+ const monacoEl = $editorContainer.querySelector('admin-monaco');
260
+ if(monacoEl) return JSON.parse(monacoEl.value);
261
+ return settingData.value;
262
+ }
263
+ }
264
+ };
265
+
266
+ const renderStringEditor = value => {
267
+ $editorContainer.innerHTML = '';
268
+ const textarea = document.createElement('textarea');
269
+ textarea.className = 'full ff-mono';
270
+ textarea.rows = 8;
271
+ textarea.value = value ?? '';
272
+ if(!canEditValue) textarea.readOnly = true;
273
+ $editorContainer.appendChild(textarea);
274
+ };
275
+
276
+ const renderNumberEditor = value => {
277
+ $editorContainer.innerHTML = '';
278
+ const input = document.createElement('input');
279
+ input.type = 'number';
280
+ input.className = 'full';
281
+ input.value = value ?? 0;
282
+ input.step = 'any';
283
+ if(!canEditValue) input.readOnly = true;
284
+ $editorContainer.appendChild(input);
285
+ };
286
+
287
+ const renderBooleanEditor = async value => {
288
+ $editorContainer.innerHTML = '';
289
+ const wrapper = document.createElement('div');
290
+ wrapper.className = 'd-f';
291
+ wrapper.style.alignItems = 'center';
292
+ wrapper.style.gap = 'var(--spacer_h)';
293
+ const ToggleClass = customElements.get('k-toggle');
294
+ const toggle = new ToggleClass();
295
+ const label = document.createElement('span');
296
+ label.textContent = value ? 'true' : 'false';
297
+ wrapper.appendChild(toggle);
298
+ wrapper.appendChild(label);
299
+ $editorContainer.appendChild(wrapper);
300
+ await toggle.updateComplete;
301
+ toggle.value = Boolean(value);
302
+ if(!canEditValue) toggle.setAttribute('disabled', '');
303
+ toggle.addEventListener('change', () => {
304
+ label.textContent = toggle.value ? 'true' : 'false';
305
+ });
306
+ };
307
+
308
+ const renderJsonEditor = value => {
309
+ $editorContainer.innerHTML = '';
310
+ const editor = document.createElement('admin-monaco');
311
+ editor.language = 'json';
312
+ editor.readOnly = !canEditValue;
313
+ editor.value = typeof value === 'string' ? value : JSON.stringify(value, null, 2);
314
+ $editorContainer.appendChild(editor);
315
+ };
316
+
317
+ const renderEditor = async (type, value) => {
318
+ currentType = type;
319
+ switch(type){
320
+ case 'string': renderStringEditor(String(value ?? '')); break;
321
+ case 'number': renderNumberEditor(value); break;
322
+ case 'boolean': await renderBooleanEditor(value); break;
323
+ case 'json': renderJsonEditor(value); break;
324
+ default: renderStringEditor(String(value ?? ''));
325
+ }
326
+ };
327
+
328
+ await renderEditor(settingData.type, settingData.value);
329
+
330
+ /*
331
+ Type change
332
+ */
333
+
334
+ $typeSelect.addEventListener('change', async () => {
335
+ const newType = $typeSelect.value;
336
+ let converted;
337
+ try { converted = getValueFromEditor(); }
338
+ catch(e){ converted = settingData.value; }
339
+
340
+ switch(newType){
341
+ case 'string':
342
+ converted = typeof converted === 'object'
343
+ ? JSON.stringify(converted, null, 2)
344
+ : String(converted ?? '');
345
+ break;
346
+ case 'number':
347
+ converted = Number(converted) || 0;
348
+ break;
349
+ case 'boolean':
350
+ converted = converted === 'true' || converted === true || converted === 1;
351
+ break;
352
+ case 'json':
353
+ if(typeof converted === 'string'){
354
+ try { converted = JSON.parse(converted); }
355
+ catch(e){ /* keep as string */ }
356
+ }
357
+ break;
358
+ }
359
+ await renderEditor(newType, converted);
360
+ });
361
+
362
+ /*
363
+ Enable/disable editing
364
+ */
365
+
366
+ if(canEditValue){
367
+ $saveBtn.disabled = false;
368
+ } else {
369
+ $typeSelect.disabled = true;
370
+ $descriptionInput.readOnly = true;
371
+ }
372
+
373
+ if(!canEditMetadata){
374
+ $typeSelect.disabled = true;
375
+ $descriptionInput.readOnly = true;
376
+ $publicToggle.setAttribute('disabled', '');
377
+ }
378
+
379
+ if(canDelete){
380
+ $deleteBtn.style.display = '';
381
+ }
382
+
383
+ /*
384
+ Save
385
+ */
386
+
387
+ const save = async () => {
388
+ if($saveBtn.disabled) return;
389
+ let value;
390
+ try {
391
+ value = getValueFromEditor();
392
+ } catch(e){
393
+ Toast.error(`Invalid ${currentType} value: ${e.message}`);
394
+ return;
395
+ }
396
+
397
+ $saveBtn.disabled = true;
398
+ const [saveError] = await setSetting(owner, settingName, {
399
+ value,
400
+ type: $typeSelect.value,
401
+ isPublic: $publicToggle.value,
402
+ description: $descriptionInput.value.trim()
403
+ });
404
+ $saveBtn.disabled = !canEditValue;
405
+
406
+ if(saveError){
407
+ Toast.error(saveError.msg || 'Failed to save setting');
408
+ return;
409
+ }
410
+ Toast.success('Setting saved');
411
+ document.getElementById('updatedAt').textContent = new Date().toLocaleString();
412
+ };
413
+
414
+ $saveBtn.addEventListener('click', save);
415
+
416
+ document.addEventListener('keydown', e => {
417
+ if((e.ctrlKey || e.metaKey) && e.key === 's'){
418
+ e.preventDefault();
419
+ save();
420
+ }
421
+ });
422
+
423
+ /*
424
+ Delete
425
+ */
426
+
427
+ $deleteBtn.addEventListener('click', () => {
428
+ Dialog.confirm(
429
+ `<div class="p"><p>Delete "${settingName}"? This action cannot be undone.</p></div>`,
430
+ async confirmed => {
431
+ if(!confirmed) return;
432
+ const [deleteError] = await deleteSetting(owner, settingName);
433
+ if(deleteError){
434
+ Toast.error(deleteError.msg || 'Failed to delete setting');
435
+ return;
436
+ }
437
+ Toast.success('Setting deleted');
438
+ window.location.href = '/admin/settings/';
439
+ }
440
+ );
441
+ });
442
+ }
443
+ </script>
444
+
445
+
446
+ </k-main>
447
+
448
+ </body>
449
+ </html>