kempo 4.2.10 → 4.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app-public/.config.json +2 -5
- package/bin/cli.js +35 -14
- package/dist/admin/accounts/groups/[name]/index.html +518 -0
- package/dist/admin/accounts/groups/index.html +378 -0
- package/dist/admin/accounts/permissions/index.html +397 -0
- package/dist/admin/accounts/users/[id]/index.html +541 -0
- package/dist/admin/accounts/users/index.html +396 -0
- package/dist/admin/content/fragments/edit/index.html +132 -0
- package/dist/admin/content/fragments/index.html +375 -0
- package/dist/admin/content/globals/edit/index.html +132 -0
- package/dist/admin/content/globals/index.html +358 -0
- package/dist/admin/content/pages/edit/index.html +132 -0
- package/dist/admin/content/pages/index.html +509 -0
- package/dist/admin/content/templates/edit/index.html +132 -0
- package/dist/admin/content/templates/index.html +429 -0
- package/dist/admin/extensions/index.html +222 -0
- package/dist/admin/index.html +130 -0
- package/dist/admin/index.page.html +1 -0
- package/dist/admin/settings/[owner]/[name]/index.html +449 -0
- package/dist/admin/settings/index.html +410 -0
- package/middleware/{extension-scope-router.js → kempo.js} +40 -25
- package/package.json +3 -3
- package/middleware/kempo-auth.js +0 -26
|
@@ -0,0 +1,509 @@
|
|
|
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>Pages - 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
|
|
126
|
+
type="module"
|
|
127
|
+
src="/kempo-ui/components/Table.js"
|
|
128
|
+
></script>
|
|
129
|
+
<script
|
|
130
|
+
type="module"
|
|
131
|
+
src="/kempo-ui/components/tableControls/FirstPage.js"
|
|
132
|
+
></script>
|
|
133
|
+
<script
|
|
134
|
+
type="module"
|
|
135
|
+
src="/kempo-ui/components/tableControls/PrevPage.js"
|
|
136
|
+
></script>
|
|
137
|
+
<script
|
|
138
|
+
type="module"
|
|
139
|
+
src="/kempo-ui/components/tableControls/NextPage.js"
|
|
140
|
+
></script>
|
|
141
|
+
<script
|
|
142
|
+
type="module"
|
|
143
|
+
src="/kempo-ui/components/tableControls/LastPage.js"
|
|
144
|
+
></script>
|
|
145
|
+
<script
|
|
146
|
+
type="module"
|
|
147
|
+
src="/kempo-ui/components/tableControls/PageSelect.js"
|
|
148
|
+
></script>
|
|
149
|
+
<script
|
|
150
|
+
type="module"
|
|
151
|
+
src="/kempo-ui/components/tableControls/PageSize.js"
|
|
152
|
+
></script>
|
|
153
|
+
<script
|
|
154
|
+
type="module"
|
|
155
|
+
src="/kempo-ui/components/tableControls/DeleteRecord.js"
|
|
156
|
+
></script>
|
|
157
|
+
<script
|
|
158
|
+
type="module"
|
|
159
|
+
src="/kempo-ui/components/tableControls/DeleteSelected.js"
|
|
160
|
+
></script>
|
|
161
|
+
<script
|
|
162
|
+
type="module"
|
|
163
|
+
src="/kempo-ui/components/Icon.js"
|
|
164
|
+
></script>
|
|
165
|
+
<script
|
|
166
|
+
type="module"
|
|
167
|
+
src="/admin/components/TableRowLink.js"
|
|
168
|
+
></script>
|
|
169
|
+
<script
|
|
170
|
+
type="module"
|
|
171
|
+
src="/kempo/components/Permission.js"
|
|
172
|
+
></script>
|
|
173
|
+
|
|
174
|
+
<div
|
|
175
|
+
class="d-f mb"
|
|
176
|
+
style="align-items: center; gap: var(--spacer);"
|
|
177
|
+
>
|
|
178
|
+
<h1 class="flex mb0">Pages</h1>
|
|
179
|
+
<k-permission has="system:pages:create">
|
|
180
|
+
<button
|
|
181
|
+
id="createPageBtn"
|
|
182
|
+
class="primary"
|
|
183
|
+
>Create Page</button>
|
|
184
|
+
</k-permission>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<h2>Custom Pages</h2>
|
|
188
|
+
<k-table
|
|
189
|
+
id="customTable"
|
|
190
|
+
enable-pages
|
|
191
|
+
page-size="25"
|
|
192
|
+
page-size-options="[10,25,50,100]"
|
|
193
|
+
placeholder="No custom pages have been created yet."
|
|
194
|
+
>
|
|
195
|
+
<admin-table-row-link
|
|
196
|
+
slot="before"
|
|
197
|
+
url-pattern="/admin/content/pages/edit?path={editPath}"
|
|
198
|
+
field-name="editPath"
|
|
199
|
+
></admin-table-row-link>
|
|
200
|
+
<div
|
|
201
|
+
slot="top"
|
|
202
|
+
class="d-f full"
|
|
203
|
+
style="align-items: center;"
|
|
204
|
+
>
|
|
205
|
+
<div>
|
|
206
|
+
<k-permission has="system:pages:delete">
|
|
207
|
+
<k-tc-delete-selected></k-tc-delete-selected>
|
|
208
|
+
</k-permission>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
<div
|
|
212
|
+
class="d-f full"
|
|
213
|
+
style="align-items: center;"
|
|
214
|
+
>
|
|
215
|
+
<div>
|
|
216
|
+
<k-tc-first-page></k-tc-first-page>
|
|
217
|
+
<k-tc-prev-page></k-tc-prev-page>
|
|
218
|
+
<k-tc-next-page></k-tc-next-page>
|
|
219
|
+
<k-tc-last-page></k-tc-last-page>
|
|
220
|
+
</div>
|
|
221
|
+
<div class="flex"></div>
|
|
222
|
+
<div><k-tc-page-select></k-tc-page-select></div>
|
|
223
|
+
<div class="flex"></div>
|
|
224
|
+
<div><k-tc-page-size></k-tc-page-size></div>
|
|
225
|
+
</div>
|
|
226
|
+
</k-table>
|
|
227
|
+
|
|
228
|
+
<h2>Extension Pages</h2>
|
|
229
|
+
<k-table
|
|
230
|
+
id="extensionTable"
|
|
231
|
+
enable-pages
|
|
232
|
+
page-size="25"
|
|
233
|
+
page-size-options="[10,25,50,100]"
|
|
234
|
+
placeholder="No extension pages installed."
|
|
235
|
+
>
|
|
236
|
+
<admin-table-row-link
|
|
237
|
+
slot="before"
|
|
238
|
+
url-pattern="/admin/pages/edit?path={editPath}"
|
|
239
|
+
field-name="editPath"
|
|
240
|
+
></admin-table-row-link>
|
|
241
|
+
<div
|
|
242
|
+
class="d-f full"
|
|
243
|
+
style="align-items: center;"
|
|
244
|
+
>
|
|
245
|
+
<div>
|
|
246
|
+
<k-tc-first-page></k-tc-first-page>
|
|
247
|
+
<k-tc-prev-page></k-tc-prev-page>
|
|
248
|
+
<k-tc-next-page></k-tc-next-page>
|
|
249
|
+
<k-tc-last-page></k-tc-last-page>
|
|
250
|
+
</div>
|
|
251
|
+
<div class="flex"></div>
|
|
252
|
+
<div><k-tc-page-select></k-tc-page-select></div>
|
|
253
|
+
<div class="flex"></div>
|
|
254
|
+
<div><k-tc-page-size></k-tc-page-size></div>
|
|
255
|
+
</div>
|
|
256
|
+
</k-table>
|
|
257
|
+
|
|
258
|
+
<h2>System Pages</h2>
|
|
259
|
+
<k-table
|
|
260
|
+
id="systemTable"
|
|
261
|
+
enable-pages
|
|
262
|
+
page-size="25"
|
|
263
|
+
page-size-options="[10,25,50,100]"
|
|
264
|
+
placeholder="No system pages found."
|
|
265
|
+
>
|
|
266
|
+
<admin-table-row-link
|
|
267
|
+
slot="before"
|
|
268
|
+
url-pattern="/admin/pages/edit?path={editPath}"
|
|
269
|
+
field-name="editPath"
|
|
270
|
+
></admin-table-row-link>
|
|
271
|
+
<div
|
|
272
|
+
class="d-f full"
|
|
273
|
+
style="align-items: center;"
|
|
274
|
+
>
|
|
275
|
+
<div>
|
|
276
|
+
<k-tc-first-page></k-tc-first-page>
|
|
277
|
+
<k-tc-prev-page></k-tc-prev-page>
|
|
278
|
+
<k-tc-next-page></k-tc-next-page>
|
|
279
|
+
<k-tc-last-page></k-tc-last-page>
|
|
280
|
+
</div>
|
|
281
|
+
<div class="flex"></div>
|
|
282
|
+
<div><k-tc-page-select></k-tc-page-select></div>
|
|
283
|
+
<div class="flex"></div>
|
|
284
|
+
<div><k-tc-page-size></k-tc-page-size></div>
|
|
285
|
+
</div>
|
|
286
|
+
</k-table>
|
|
287
|
+
|
|
288
|
+
<script type="module">
|
|
289
|
+
import { listPages, listTemplates, listDirectories, createPage, deletePages, getAllCurrentUserPermissions } from '/kempo/sdk.js';
|
|
290
|
+
import { html } from '/kempo-ui/lit-all.min.js';
|
|
291
|
+
import Dialog from '/kempo-ui/components/Dialog.js';
|
|
292
|
+
import Toast from '/kempo-ui/components/Toast.js';
|
|
293
|
+
|
|
294
|
+
await window.customElements.whenDefined('k-table');
|
|
295
|
+
|
|
296
|
+
const [, permData] = await getAllCurrentUserPermissions();
|
|
297
|
+
const myPerms = new Set(permData?.permissions ?? []);
|
|
298
|
+
|
|
299
|
+
const fields = [
|
|
300
|
+
{ name: 'lockedBadge', label: '', size: 40, formatter: v => v },
|
|
301
|
+
{ name: 'name', label: 'Name', size: 200 },
|
|
302
|
+
{ name: 'url', label: 'URL', size: 250 },
|
|
303
|
+
{ name: 'author', label: 'Author', size: 150 },
|
|
304
|
+
{ name: 'description', label: 'Description', size: 300 },
|
|
305
|
+
{ name: 'updatedAt', label: 'Updated', size: 190, formatter: v => v ? new Date(v).toLocaleString() : '' },
|
|
306
|
+
{ name: 'createdAt', label: 'Created', size: 190, formatter: v => v ? new Date(v).toLocaleString() : '' }
|
|
307
|
+
];
|
|
308
|
+
|
|
309
|
+
const extensionFields = [
|
|
310
|
+
{ name: 'owner', label: 'Extension', size: 120 },
|
|
311
|
+
...fields
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
const $customTable = document.getElementById('customTable');
|
|
315
|
+
const $extensionTable = document.getElementById('extensionTable');
|
|
316
|
+
const $systemTable = document.getElementById('systemTable');
|
|
317
|
+
|
|
318
|
+
const loadPages = async () => {
|
|
319
|
+
const [error, data] = await listPages();
|
|
320
|
+
if(error){
|
|
321
|
+
Toast.error(error.msg || 'Failed to load pages');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const toEditPath = p => ({ ...p, editPath: p.file.replace(/\.page\.html$/, ''), lockedBadge: p.locked ? '<k-icon name="lock"></k-icon>' : '' });
|
|
325
|
+
const sortPages = pages => pages.slice().sort((a, b) => {
|
|
326
|
+
const aPath = a.file.replace(/\.page\.html$/, '');
|
|
327
|
+
const bPath = b.file.replace(/\.page\.html$/, '');
|
|
328
|
+
const aDir = aPath.includes('/') ? aPath.split('/').slice(0, -1).join('/') : '';
|
|
329
|
+
const bDir = bPath.includes('/') ? bPath.split('/').slice(0, -1).join('/') : '';
|
|
330
|
+
if(aPath === 'index' || aPath === '') return -1;
|
|
331
|
+
if(bPath === 'index' || bPath === '') return 1;
|
|
332
|
+
if(aDir !== bDir){
|
|
333
|
+
if(!aDir) return -1;
|
|
334
|
+
if(!bDir) return 1;
|
|
335
|
+
return aDir.localeCompare(bDir);
|
|
336
|
+
}
|
|
337
|
+
return aPath.localeCompare(bPath);
|
|
338
|
+
});
|
|
339
|
+
const custom = sortPages(data.pages.filter(p => p.owner === 'custom').map(toEditPath));
|
|
340
|
+
const extension = sortPages(data.pages.filter(p => p.owner !== 'custom' && p.owner !== 'system').map(toEditPath));
|
|
341
|
+
const system = sortPages(data.pages.filter(p => p.owner === 'system').map(toEditPath));
|
|
342
|
+
$customTable.setData({ records: custom, fields });
|
|
343
|
+
$extensionTable.setData({ records: extension, fields: extensionFields });
|
|
344
|
+
$systemTable.setData({ records: system, fields });
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
if(myPerms.has('system:pages:delete')){
|
|
348
|
+
$customTable.setAttribute('request-delete', '');
|
|
349
|
+
$customTable.setAttribute('enable-selection', '');
|
|
350
|
+
const del = document.createElement('k-tc-delete-record');
|
|
351
|
+
del.setAttribute('slot', 'after');
|
|
352
|
+
$customTable.appendChild(del);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
await loadPages();
|
|
356
|
+
|
|
357
|
+
$customTable.addEventListener('requestDelete', ({ detail: { records, approve, reject } }) => {
|
|
358
|
+
const locked = records.filter(r => r.locked);
|
|
359
|
+
if(locked.length){
|
|
360
|
+
Toast.error(locked.length === 1 ? `"${locked[0].title || locked[0].file}" is locked and cannot be deleted.` : `${locked.length} selected pages are locked and cannot be deleted.`);
|
|
361
|
+
reject();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const count = records.length;
|
|
365
|
+
const msg = count === 1
|
|
366
|
+
? `Delete "${records[0].title || records[0].file}"? This action cannot be undone.`
|
|
367
|
+
: `Delete ${count} pages? This action cannot be undone.`;
|
|
368
|
+
Dialog.confirm(msg, async (confirmed) => {
|
|
369
|
+
if(!confirmed){ reject(); return; }
|
|
370
|
+
const files = records.map(r => r.file);
|
|
371
|
+
const [error] = await deletePages(files);
|
|
372
|
+
if(error){
|
|
373
|
+
reject();
|
|
374
|
+
Toast.error(error.msg || 'Failed to delete');
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
approve();
|
|
378
|
+
Toast.success(count === 1 ? 'Page deleted' : `${count} pages deleted`);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
/*
|
|
383
|
+
Create Page Dialog
|
|
384
|
+
*/
|
|
385
|
+
|
|
386
|
+
const slugify = name => name
|
|
387
|
+
.toLowerCase()
|
|
388
|
+
.trim()
|
|
389
|
+
.replace(/[^\w\s-]/g, '')
|
|
390
|
+
.replace(/[\s_]+/g, '-')
|
|
391
|
+
.replace(/-+/g, '-')
|
|
392
|
+
.replace(/^-|-$/g, '');
|
|
393
|
+
|
|
394
|
+
document.getElementById('createPageBtn')?.addEventListener('click', async () => {
|
|
395
|
+
const [[tplError, tplData], [dirError, dirData]] = await Promise.all([
|
|
396
|
+
listTemplates(),
|
|
397
|
+
listDirectories()
|
|
398
|
+
]);
|
|
399
|
+
if(tplError){
|
|
400
|
+
Toast.error(tplError.msg || 'Failed to load templates');
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if(dirError){
|
|
404
|
+
Toast.error(dirError.msg || 'Failed to load directories');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const directories = dirData.directories;
|
|
409
|
+
|
|
410
|
+
const templateOptions = tplData.templates
|
|
411
|
+
.map(t => html`<option value="${t.name}" ?selected=${t.name === 'default'}>${t.name} (${t.directory || '/'})</option>`);
|
|
412
|
+
|
|
413
|
+
const isValidDir = p => /^\/[a-zA-Z0-9_\-\[\]\/]*$/.test(p) && !p.includes('..') && !p.includes('//');
|
|
414
|
+
const dirToInternal = p => { const t = (p || '/').replace(/^\//, '').replace(/\/$/, ''); return t || '.'; };
|
|
415
|
+
|
|
416
|
+
const openLearnMore = () => Dialog.alert(html`
|
|
417
|
+
<div class="p">
|
|
418
|
+
<p>Directory paths define where a page lives in the URL structure.</p>
|
|
419
|
+
<p><strong>Examples:</strong></p>
|
|
420
|
+
<ul>
|
|
421
|
+
<li><code>/</code> — root, e.g. <code>/about</code></li>
|
|
422
|
+
<li><code>/blog</code> — e.g. <code>/blog/my-post</code></li>
|
|
423
|
+
<li><code>/blog/[slug]</code> — dynamic segment; <code>[slug]</code> matches any value</li>
|
|
424
|
+
</ul>
|
|
425
|
+
<p>Dynamic segments like <code>[token]</code> allow a single page to handle many URLs. The segment name is available to the page as a URL parameter.</p>
|
|
426
|
+
<p>You can nest segments: <code>/store/[category]/[id]</code></p>
|
|
427
|
+
</div>
|
|
428
|
+
`, null, { title: 'About Directory Paths' });
|
|
429
|
+
|
|
430
|
+
const $dialog = Dialog.create(html`
|
|
431
|
+
<div class="p">
|
|
432
|
+
<div class="mb">
|
|
433
|
+
<label class="d-b mb-sm">Location</label>
|
|
434
|
+
<input type="text" id="dlg-directory" class="full" list="dlg-dir-list" value="/" placeholder="/">
|
|
435
|
+
<datalist id="dlg-dir-list">
|
|
436
|
+
${directories.map(d => html`<option value="${d === '.' ? '/' : '/' + d}">`)}
|
|
437
|
+
</datalist>
|
|
438
|
+
<p id="dlg-dir-error" class="mt-sm tc-danger d-n"><small>Invalid path — use format: /path/to/directory</small></p>
|
|
439
|
+
<p class="mt-sm muted"><small>Supports dynamic segments e.g. <code>/blog/[slug]</code>. <button class="link" @click="${openLearnMore}">Learn more</button></small></p>
|
|
440
|
+
</div>
|
|
441
|
+
<div class="mb">
|
|
442
|
+
<label class="d-b mb-sm">Page Name</label>
|
|
443
|
+
<input type="text" id="dlg-name" class="full" placeholder="My New Page" required>
|
|
444
|
+
<p class="mt-sm muted"><small id="dlg-slug"></small></p>
|
|
445
|
+
</div>
|
|
446
|
+
<div>
|
|
447
|
+
<label class="d-b mb-sm">Template</label>
|
|
448
|
+
<select id="dlg-template" class="full">
|
|
449
|
+
${templateOptions}
|
|
450
|
+
</select>
|
|
451
|
+
</div>
|
|
452
|
+
</div>
|
|
453
|
+
`, {
|
|
454
|
+
title: 'Create Page',
|
|
455
|
+
confirmText: 'Create',
|
|
456
|
+
confirmAction: async () => {
|
|
457
|
+
const name = $dialog.querySelector('#dlg-name').value.trim();
|
|
458
|
+
const dirInput = $dialog.querySelector('#dlg-directory').value.trim() || '/';
|
|
459
|
+
const template = $dialog.querySelector('#dlg-template').value;
|
|
460
|
+
|
|
461
|
+
if(!name){
|
|
462
|
+
Toast.error('Page name is required');
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if(!isValidDir(dirInput)){
|
|
467
|
+
Toast.error('Invalid directory path');
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const directory = dirToInternal(dirInput);
|
|
472
|
+
|
|
473
|
+
const [error, data] = await createPage({
|
|
474
|
+
directory: directory === '.' ? '' : directory,
|
|
475
|
+
name,
|
|
476
|
+
template
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
if(error){
|
|
480
|
+
Toast.error(error.msg || 'Failed to create page');
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
Toast.success('Page created — redirecting to editor...');
|
|
485
|
+
setTimeout(() => {
|
|
486
|
+
window.location.href = `/admin/pages/edit?path=${encodeURIComponent(data.file.replace(/\.page\.html$/, ''))}`;
|
|
487
|
+
}, 1500);
|
|
488
|
+
},
|
|
489
|
+
cancelText: 'Cancel'
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const updateSlug = () => {
|
|
493
|
+
const slug = slugify($dialog.querySelector('#dlg-name').value);
|
|
494
|
+
const dir = dirToInternal($dialog.querySelector('#dlg-directory').value);
|
|
495
|
+
const prefix = dir === '.' ? '/' : '/' + dir + '/';
|
|
496
|
+
$dialog.querySelector('#dlg-slug').textContent = slug ? prefix + slug : '';
|
|
497
|
+
};
|
|
498
|
+
$dialog.querySelector('#dlg-directory').addEventListener('input', e => {
|
|
499
|
+
$dialog.querySelector('#dlg-dir-error').classList.toggle('d-n', isValidDir(e.target.value || '/'));
|
|
500
|
+
updateSlug();
|
|
501
|
+
});
|
|
502
|
+
$dialog.querySelector('#dlg-name').addEventListener('input', updateSlug);
|
|
503
|
+
});
|
|
504
|
+
</script>
|
|
505
|
+
|
|
506
|
+
</k-main>
|
|
507
|
+
|
|
508
|
+
</body>
|
|
509
|
+
</html>
|
|
@@ -0,0 +1,132 @@
|
|
|
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>Edit Template - Admin</title>
|
|
17
|
+
<link rel="stylesheet" href="/kempo-css/kempo.min.css">
|
|
18
|
+
<script src="/admin/init.js"></script>
|
|
19
|
+
|
|
20
|
+
<script type="module" src="/admin/components/TemplateEditor.js"></script>
|
|
21
|
+
|
|
22
|
+
</head>
|
|
23
|
+
<body>
|
|
24
|
+
<!--
|
|
25
|
+
locked: true
|
|
26
|
+
-->
|
|
27
|
+
|
|
28
|
+
<k-aside
|
|
29
|
+
main="push"
|
|
30
|
+
state="expanded"
|
|
31
|
+
style="--aside_padding: 0;"
|
|
32
|
+
persistent-id="admin-nav"
|
|
33
|
+
>
|
|
34
|
+
<k-aside-toggle>
|
|
35
|
+
<h3 class="m0">Kempo Admin</h3>
|
|
36
|
+
</k-aside-toggle>
|
|
37
|
+
<k-aside-item
|
|
38
|
+
icon="preview"
|
|
39
|
+
href="/"
|
|
40
|
+
no-expand
|
|
41
|
+
>Site</k-aside-item>
|
|
42
|
+
<k-aside-item
|
|
43
|
+
icon="cards"
|
|
44
|
+
href="/admin"
|
|
45
|
+
no-expand
|
|
46
|
+
>Dashboard</k-aside-item>
|
|
47
|
+
<k-aside-menu
|
|
48
|
+
icon="manage-accounts"
|
|
49
|
+
label="Accounts"
|
|
50
|
+
>
|
|
51
|
+
<k-aside-item
|
|
52
|
+
icon="person"
|
|
53
|
+
href="/admin/accounts/users"
|
|
54
|
+
no-expand
|
|
55
|
+
>Users</k-aside-item>
|
|
56
|
+
<k-aside-item
|
|
57
|
+
icon="group"
|
|
58
|
+
href="/admin/accounts/groups"
|
|
59
|
+
no-expand
|
|
60
|
+
>Groups</k-aside-item>
|
|
61
|
+
<k-aside-item
|
|
62
|
+
icon="lock"
|
|
63
|
+
href="/admin/accounts/permissions"
|
|
64
|
+
no-expand
|
|
65
|
+
>Permissions</k-aside-item>
|
|
66
|
+
</k-aside-menu>
|
|
67
|
+
<k-aside-menu
|
|
68
|
+
icon="cards_stack"
|
|
69
|
+
label="Content"
|
|
70
|
+
>
|
|
71
|
+
<k-aside-item
|
|
72
|
+
icon="browse"
|
|
73
|
+
href="/admin/content/templates"
|
|
74
|
+
>Templates</k-aside-item>
|
|
75
|
+
<k-aside-item
|
|
76
|
+
icon="docs"
|
|
77
|
+
href="/admin/content/pages"
|
|
78
|
+
>Pages</k-aside-item>
|
|
79
|
+
<k-aside-item
|
|
80
|
+
icon="content"
|
|
81
|
+
href="/admin/content/fragments"
|
|
82
|
+
>Fragments</k-aside-item>
|
|
83
|
+
<k-aside-item
|
|
84
|
+
icon="globe"
|
|
85
|
+
href="/admin/content/globals"
|
|
86
|
+
>Global Content</k-aside-item>
|
|
87
|
+
</k-aside-menu>
|
|
88
|
+
<k-aside-item
|
|
89
|
+
icon="settings"
|
|
90
|
+
href="/admin/settings"
|
|
91
|
+
no-expand
|
|
92
|
+
>Settings</k-aside-item>
|
|
93
|
+
<k-aside-item
|
|
94
|
+
icon="extension"
|
|
95
|
+
href="/admin/extensions"
|
|
96
|
+
no-expand
|
|
97
|
+
>Extensions</k-aside-item>
|
|
98
|
+
<hr>
|
|
99
|
+
<k-aside-spacer></k-aside-spacer>
|
|
100
|
+
<k-theme-switcher
|
|
101
|
+
labels
|
|
102
|
+
style="--padding:var(--spacer_h);margin:var(--spacer_h) auto"
|
|
103
|
+
></k-theme-switcher>
|
|
104
|
+
</k-aside>
|
|
105
|
+
<script type="module">
|
|
106
|
+
import '/kempo-ui/components/ThemeSwitcher.js';
|
|
107
|
+
const items = document.querySelectorAll('k-aside-item');
|
|
108
|
+
const currentPath = window.location.pathname;
|
|
109
|
+
|
|
110
|
+
items.forEach(item => {
|
|
111
|
+
const href = item.getAttribute('href');
|
|
112
|
+
if (!href) return;
|
|
113
|
+
const isExact = href === '/admin' || href === '/';
|
|
114
|
+
const match = isExact
|
|
115
|
+
? (currentPath === href || currentPath === href + '/')
|
|
116
|
+
: (currentPath === href || currentPath.startsWith(href + '/'));
|
|
117
|
+
if (match) {
|
|
118
|
+
item.setAttribute('active', '');
|
|
119
|
+
const menu = item.closest('k-aside-menu');
|
|
120
|
+
if (menu) menu.setAttribute('open', '');
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
<k-main>
|
|
126
|
+
|
|
127
|
+
<admin-template-editor></admin-template-editor>
|
|
128
|
+
|
|
129
|
+
</k-main>
|
|
130
|
+
|
|
131
|
+
</body>
|
|
132
|
+
</html>
|