kempo 4.6.2 → 4.7.0
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/README.md +5 -0
- package/app-public/account/index.page.html +2 -0
- package/app-public/nav.fragment.html +2 -1
- package/app-templates/emails/email.template.html +14 -0
- package/app-templates/emails/reset-password.page.html +19 -0
- package/app-templates/emails/test.page.html +21 -0
- package/app-templates/emails/verify-email.page.html +18 -0
- package/bin/cli.js +3 -0
- package/dist/admin/components/Dashboard.js +2 -2
- package/dist/admin/components/Dashboard.js.map +1 -1
- package/dist/admin/components/Menus.js +103 -0
- package/dist/admin/components/Menus.js.map +1 -0
- package/dist/admin/content/menus/index.page.html +11 -0
- package/dist/admin/nav.fragment.html +6 -0
- package/dist/kempo/api/fragments/DELETE.js +1 -1
- package/dist/kempo/api/fragments/DELETE.js.map +1 -1
- package/dist/kempo/api/fragments/GET.js +1 -1
- package/dist/kempo/api/fragments/GET.js.map +1 -1
- package/dist/kempo/api/fragments/POST.js +1 -1
- package/dist/kempo/api/fragments/POST.js.map +1 -1
- package/dist/kempo/api/fragments/disable/PUT.js +1 -1
- package/dist/kempo/api/fragments/disable/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/enable/PUT.js +1 -1
- package/dist/kempo/api/fragments/enable/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/file/GET.js +1 -1
- package/dist/kempo/api/fragments/file/GET.js.map +1 -1
- package/dist/kempo/api/fragments/file/PATCH.js +1 -1
- package/dist/kempo/api/fragments/file/PATCH.js.map +1 -1
- package/dist/kempo/api/fragments/file/PUT.js +1 -1
- package/dist/kempo/api/fragments/file/PUT.js.map +1 -1
- package/dist/kempo/api/fragments/lock/PUT.js +1 -1
- package/dist/kempo/api/fragments/lock/PUT.js.map +1 -1
- package/dist/kempo/api/globals/DELETE.js +1 -1
- package/dist/kempo/api/globals/DELETE.js.map +1 -1
- package/dist/kempo/api/globals/GET.js +1 -1
- package/dist/kempo/api/globals/GET.js.map +1 -1
- package/dist/kempo/api/globals/POST.js +1 -1
- package/dist/kempo/api/globals/POST.js.map +1 -1
- package/dist/kempo/api/globals/disable/PUT.js +1 -1
- package/dist/kempo/api/globals/disable/PUT.js.map +1 -1
- package/dist/kempo/api/globals/enable/PUT.js +1 -1
- package/dist/kempo/api/globals/enable/PUT.js.map +1 -1
- package/dist/kempo/api/globals/entry/GET.js +1 -1
- package/dist/kempo/api/globals/entry/GET.js.map +1 -1
- package/dist/kempo/api/globals/entry/PUT.js +1 -1
- package/dist/kempo/api/globals/entry/PUT.js.map +1 -1
- package/dist/kempo/api/globals/lock/PUT.js +1 -1
- package/dist/kempo/api/globals/lock/PUT.js.map +1 -1
- package/dist/kempo/api/menus/DELETE.js +2 -0
- package/dist/kempo/api/menus/DELETE.js.map +1 -0
- package/dist/kempo/api/menus/GET.js +2 -0
- package/dist/kempo/api/menus/GET.js.map +1 -0
- package/dist/kempo/api/menus/POST.js +2 -0
- package/dist/kempo/api/menus/POST.js.map +1 -0
- package/dist/kempo/api/menus/adopt/PUT.js +2 -0
- package/dist/kempo/api/menus/adopt/PUT.js.map +1 -0
- package/dist/kempo/api/menus/items/DELETE.js +2 -0
- package/dist/kempo/api/menus/items/DELETE.js.map +1 -0
- package/dist/kempo/api/menus/items/POST.js +2 -0
- package/dist/kempo/api/menus/items/POST.js.map +1 -0
- package/dist/kempo/api/menus/items/enabled/PUT.js +2 -0
- package/dist/kempo/api/menus/items/enabled/PUT.js.map +1 -0
- package/dist/kempo/api/menus/items/order/PUT.js +2 -0
- package/dist/kempo/api/menus/items/order/PUT.js.map +1 -0
- package/dist/kempo/api/menus/location/PUT.js +2 -0
- package/dist/kempo/api/menus/location/PUT.js.map +1 -0
- package/dist/kempo/api/pages/DELETE.js +1 -1
- package/dist/kempo/api/pages/DELETE.js.map +1 -1
- package/dist/kempo/api/pages/GET.js +1 -1
- package/dist/kempo/api/pages/GET.js.map +1 -1
- package/dist/kempo/api/pages/POST.js +1 -1
- package/dist/kempo/api/pages/POST.js.map +1 -1
- package/dist/kempo/api/pages/directories/GET.js +1 -1
- package/dist/kempo/api/pages/directories/GET.js.map +1 -1
- package/dist/kempo/api/pages/disable/PUT.js +1 -1
- package/dist/kempo/api/pages/disable/PUT.js.map +1 -1
- package/dist/kempo/api/pages/enable/PUT.js +1 -1
- package/dist/kempo/api/pages/enable/PUT.js.map +1 -1
- package/dist/kempo/api/pages/file/GET.js +1 -1
- package/dist/kempo/api/pages/file/GET.js.map +1 -1
- package/dist/kempo/api/pages/file/PATCH.js +1 -1
- package/dist/kempo/api/pages/file/PATCH.js.map +1 -1
- package/dist/kempo/api/pages/file/PUT.js +1 -1
- package/dist/kempo/api/pages/file/PUT.js.map +1 -1
- package/dist/kempo/api/pages/lock/PUT.js +1 -1
- package/dist/kempo/api/pages/lock/PUT.js.map +1 -1
- package/dist/kempo/api/pages/rescan/POST.js +1 -1
- package/dist/kempo/api/pages/rescan/POST.js.map +1 -1
- package/dist/kempo/api/pages/search/GET.js +1 -1
- package/dist/kempo/api/pages/search/GET.js.map +1 -1
- package/dist/kempo/api/pages/templates/GET.js +1 -1
- package/dist/kempo/api/pages/templates/GET.js.map +1 -1
- package/dist/kempo/api/templates/DELETE.js +1 -1
- package/dist/kempo/api/templates/DELETE.js.map +1 -1
- package/dist/kempo/api/templates/GET.js +1 -1
- package/dist/kempo/api/templates/GET.js.map +1 -1
- package/dist/kempo/api/templates/POST.js +1 -1
- package/dist/kempo/api/templates/POST.js.map +1 -1
- package/dist/kempo/api/templates/disable/PUT.js +1 -1
- package/dist/kempo/api/templates/disable/PUT.js.map +1 -1
- package/dist/kempo/api/templates/enable/PUT.js +1 -1
- package/dist/kempo/api/templates/enable/PUT.js.map +1 -1
- package/dist/kempo/api/templates/file/GET.js +1 -1
- package/dist/kempo/api/templates/file/GET.js.map +1 -1
- package/dist/kempo/api/templates/file/PATCH.js +1 -1
- package/dist/kempo/api/templates/file/PATCH.js.map +1 -1
- package/dist/kempo/api/templates/file/PUT.js +1 -1
- package/dist/kempo/api/templates/file/PUT.js.map +1 -1
- package/dist/kempo/api/templates/lock/PUT.js +1 -1
- package/dist/kempo/api/templates/lock/PUT.js.map +1 -1
- package/dist/kempo/components/DashboardWidget.js +4 -4
- package/dist/kempo/components/DashboardWidget.js.map +1 -1
- package/dist/kempo/icons/campaign.svg +1 -0
- package/dist/kempo/icons/fields.svg +1 -0
- package/dist/kempo/icons/open_in_new.svg +1 -0
- package/dist/kempo/sdk.js +1 -1
- package/dist/kempo/sdk.js.map +1 -1
- package/middleware/kempo.js +48 -12
- package/package.json +2 -2
- package/server/db/schema.js +24 -0
- package/server/sdk.js +21 -0
- package/server/utils/auth/requestPasswordReset.js +9 -3
- package/server/utils/auth/verifyEmail.js +10 -1
- package/server/utils/dashboard/coreWidgets.js +1 -0
- package/server/utils/dashboard/helpers.js +6 -0
- package/server/utils/email/renderEmailTemplate.js +63 -0
- package/server/utils/email/sendEmailFromTemplate.js +9 -27
- package/server/utils/extensions/aliases.js +71 -0
- package/server/utils/extensions/checkClaims.js +32 -0
- package/server/utils/extensions/claims.js +54 -0
- package/server/utils/extensions/createTablesFromSchema.js +13 -20
- package/server/utils/extensions/enableExtensionTree.js +8 -0
- package/server/utils/extensions/installExtension.js +32 -0
- package/server/utils/extensions/readKempoConfig.js +2 -2
- package/server/utils/extensions/scopeCache.js +14 -1
- package/server/utils/extensions/scopes.js +72 -0
- package/server/utils/extensions/uninstallExtensionTree.js +2 -1
- package/server/utils/extensions/updateExtension.js +35 -0
- package/server/utils/global-content/createGlobalContent.js +8 -18
- package/server/utils/global-content/deleteGlobalContent.js +1 -16
- package/server/utils/global-content/disableGlobalContent.js +1 -16
- package/server/utils/global-content/enableGlobalContent.js +1 -16
- package/server/utils/global-content/helpers.js +40 -6
- package/server/utils/global-content/listGlobalContent.js +2 -2
- package/server/utils/global-content/setGlobalContentLocked.js +1 -16
- package/server/utils/global-content/updateGlobalContent.js +1 -16
- package/server/utils/menus/adoptMenuItem.js +58 -0
- package/server/utils/menus/assignMenuLocation.js +47 -0
- package/server/utils/menus/deleteMenu.js +27 -0
- package/server/utils/menus/deleteMenuItems.js +27 -0
- package/server/utils/menus/findMenuLocations.js +41 -0
- package/server/utils/menus/findNavItems.js +74 -0
- package/server/utils/menus/findPages.js +73 -0
- package/server/utils/menus/listMenus.js +103 -0
- package/server/utils/menus/markup.js +55 -0
- package/server/utils/menus/reorderMenuItems.js +36 -0
- package/server/utils/menus/saveMenu.js +54 -0
- package/server/utils/menus/saveMenuItem.js +122 -0
- package/server/utils/menus/setMenuItemEnabled.js +31 -0
- package/server/utils/menus/store.js +90 -0
- package/server/utils/scheduler/helpers.js +36 -0
- package/server/utils/scheduler/listJobs.js +11 -0
- package/server/utils/scheduler/registerJobs.js +37 -0
- package/server/utils/scheduler/runJobs.js +90 -0
- package/server/utils/scheduler/scheduler.js +37 -0
- package/server/utils/scheduler/unregisterJobs.js +16 -0
- package/server/utils/settings/ensureEncryptionKey.js +132 -0
- package/server/utils/settings/secretCrypto.js +5 -1
- package/dist/kempo/icons/create_new_folder.svg +0 -1
- package/dist/kempo/icons/credit_card.svg +0 -1
- package/dist/kempo/icons/login.svg +0 -1
- package/dist/kempo/icons/payments.svg +0 -1
- package/dist/kempo/icons/toll.svg +0 -1
- package/dist/kempo/icons/wallet.svg +0 -1
package/README.md
CHANGED
|
@@ -179,6 +179,7 @@ Kempo provides these tables out of the box:
|
|
|
179
179
|
| `notification` | Notifications raised by core or extensions: title, message, level, actions, dedupe key |
|
|
180
180
|
| `notificationRecipient` | One row per person a notification was sent to, with their own read, handled and dismissed state |
|
|
181
181
|
| `dashboardWidget` | Widgets extensions add to the admin dashboard: element, module script, size limits, permission |
|
|
182
|
+
| `scheduledJob` | Recurring jobs extensions declare with `jobs`: interval, next run, and the lease that stops two processes running one |
|
|
182
183
|
|
|
183
184
|
## Frontend SDK
|
|
184
185
|
|
|
@@ -365,6 +366,10 @@ The admin home page is a grid of widgets one to four blocks wide and one to thre
|
|
|
365
366
|
|
|
366
367
|
Core and extensions can notify users with `createNotification`: to specific users, everyone holding a permission, or a group. Each person has their own read state and history, a notification can carry buttons that link somewhere or call an API as the signed-in user, and a `dedupeKey` keeps a repeating event to one notification. The admin has a bell with an unread count and a Notifications page; a public site can use `<k-notification-bell>`. Existing sites need `npx drizzle-kit push` to add the two new tables. See [docs/notifications.md](docs/notifications.md).
|
|
367
368
|
|
|
369
|
+
## Scheduled Jobs, Aliases and Email Templates for Extensions
|
|
370
|
+
|
|
371
|
+
Extensions can declare `jobs` in `kempo-config.json` and kempo runs them on a timer (several processes sharing a database run each once per interval), `aliases` to answer on a short address such as `/cart`, and ship their own email templates as `owner/name`. Existing sites need `npx drizzle-kit push` to add the `scheduledJob` table. See [docs/extensions/creating-extensions.md](docs/extensions/creating-extensions.md).
|
|
372
|
+
|
|
368
373
|
## Email
|
|
369
374
|
|
|
370
375
|
Kempo uses [Resend](https://resend.com/) for sending emails. Set `RESEND_API_KEY` and `EMAIL_FROM` in your `.env` file.
|
|
@@ -28,10 +28,12 @@
|
|
|
28
28
|
<div class="mb">
|
|
29
29
|
<strong>Member Since:</strong> <k-user-created-at></k-user-created-at>
|
|
30
30
|
</div>
|
|
31
|
+
<location name="account-details" label="After the Account Details" />
|
|
31
32
|
<div class="mt-lg">
|
|
32
33
|
<k-permission has="system:admin:access">
|
|
33
34
|
<a href="{{pathToRoot}}admin" class="btn primary">Admin Panel</a>
|
|
34
35
|
</k-permission>
|
|
36
|
+
<location name="account-links" label="Account Links" />
|
|
35
37
|
<button id="changePasswordBtn" class="btn">Change Password</button>
|
|
36
38
|
<k-logout-btn redirect="{{pathToRoot}}login" button-class="btn danger">Logout</k-logout-btn>
|
|
37
39
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
updated: 2026-
|
|
2
|
+
updated: 2026-10-16T23:11:11.700Z
|
|
3
3
|
-->
|
|
4
4
|
<fragment>
|
|
5
5
|
<nav
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
/>
|
|
20
20
|
<strong>Kempo</strong>
|
|
21
21
|
</a>
|
|
22
|
+
<location name="main-nav-links" label="Main menu" menu="true" />
|
|
22
23
|
<div style="flex: 1"></div>
|
|
23
24
|
<a
|
|
24
25
|
href="{{pathToRoot}}account/"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>{{title}}</title>
|
|
7
|
+
<location name="head" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<div style="max-width: 600px; margin: 20px auto; padding: 40px; border-radius: 8px;">
|
|
11
|
+
<location />
|
|
12
|
+
</div>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
owner: system
|
|
3
|
+
-->
|
|
4
|
+
<page template="email" title="Reset Your Password">
|
|
5
|
+
<content>
|
|
6
|
+
<h2>Reset Your Password</h2>
|
|
7
|
+
<if condition="name"><p>Hi {{name}},</p></if>
|
|
8
|
+
<if condition="!name"><p>Hello,</p></if>
|
|
9
|
+
<p>We received a request to reset your password. Click the button below to choose a new password:</p>
|
|
10
|
+
<div style="text-align: center; margin: 24px 0;">
|
|
11
|
+
<a href="{{resetLink}}" style="display: inline-block; padding: 14px 28px; background-color: rgb(51, 102, 255); color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 600;">Reset Password</a>
|
|
12
|
+
</div>
|
|
13
|
+
<p class="small" style="word-break: break-all; padding: 12px; border-radius: 4px;">Or copy and paste this link into your browser:<br><strong>{{resetLink}}</strong></p>
|
|
14
|
+
<p><strong>This link will expire in 1 hour.</strong></p>
|
|
15
|
+
<div style="margin-top: 32px; padding-top: 24px; border-top: 1px solid; font-size: 13px;">
|
|
16
|
+
<p>If you didn't request a password reset, you can safely ignore this email. Your password will not be changed.</p>
|
|
17
|
+
</div>
|
|
18
|
+
</content>
|
|
19
|
+
</page>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
owner: system
|
|
3
|
+
-->
|
|
4
|
+
<page template="email" title="Test Email">
|
|
5
|
+
<content>
|
|
6
|
+
<div style="background-color: #007bff; color: white; padding: 20px; text-align: center; border-radius: 5px;">
|
|
7
|
+
<h1>Test Email</h1>
|
|
8
|
+
</div>
|
|
9
|
+
<div style="padding: 20px; background-color: #f9f9f9; border-radius: 5px; margin-top: 20px;">
|
|
10
|
+
<p>Hello<if condition="name"> {{name}}</if>!</p>
|
|
11
|
+
<p>This is a test email from your Kempo application.</p>
|
|
12
|
+
<p>If you received this, the templating system is working correctly!</p>
|
|
13
|
+
<if condition="timestamp">
|
|
14
|
+
<p><strong>Sent at:</strong> {{timestamp}}</p>
|
|
15
|
+
</if>
|
|
16
|
+
</div>
|
|
17
|
+
<div style="margin-top: 20px; text-align: center; color: #666; font-size: 12px;">
|
|
18
|
+
<p>This is an automated test message from Kempo.</p>
|
|
19
|
+
</div>
|
|
20
|
+
</content>
|
|
21
|
+
</page>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
owner: system
|
|
3
|
+
-->
|
|
4
|
+
<page template="email" title="Verify Your Email">
|
|
5
|
+
<content>
|
|
6
|
+
<h2>Verify Your Email</h2>
|
|
7
|
+
<if condition="name"><p>Hi {{name}},</p></if>
|
|
8
|
+
<if condition="!name"><p>Hello,</p></if>
|
|
9
|
+
<p>Thank you for signing up! Please verify your email address by clicking the button below:</p>
|
|
10
|
+
<div style="text-align: center; margin: 24px 0;">
|
|
11
|
+
<a href="{{verificationLink}}" style="display: inline-block; padding: 14px 28px; background-color: rgb(51, 102, 255); color: #ffffff; text-decoration: none; border-radius: 6px; font-weight: 600;">Verify Email</a>
|
|
12
|
+
</div>
|
|
13
|
+
<p class="small" style="word-break: break-all; padding: 12px; border-radius: 4px;">Or copy and paste this link into your browser:<br><strong>{{verificationLink}}</strong></p>
|
|
14
|
+
<div style="margin-top: 32px; padding-top: 24px; border-top: 1px solid; font-size: 13px;">
|
|
15
|
+
<p>If you didn't create an account, you can safely ignore this email.</p>
|
|
16
|
+
</div>
|
|
17
|
+
</content>
|
|
18
|
+
</page>
|
package/bin/cli.js
CHANGED
|
@@ -397,6 +397,9 @@ DATABASE_URL=${databaseUrl}
|
|
|
397
397
|
RESEND_API_KEY=${emailApiKey || 'your_resend_api_key_here'}
|
|
398
398
|
EMAIL_FROM=${emailFrom}
|
|
399
399
|
|
|
400
|
+
# Encrypts the API credentials saved in the admin. Back this up with your database: if it is lost they cannot be recovered.
|
|
401
|
+
SETTINGS_ENCRYPTION_KEY=${crypto.randomBytes(32).toString('hex')}
|
|
402
|
+
|
|
400
403
|
# Server
|
|
401
404
|
PORT=9876
|
|
402
405
|
NODE_ENV=development
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import t from"/kempo-ui/components/LightComponent.js";import{html as e,nothing as i,repeat as s,keyed as a}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Icon.js";import"/kempo-ui/components/Dialog.js";import"/kempo-ui/components/Sortable.js";import{getDashboardWidgets as n}from"/kempo/sdk.js";import{resolveLayout as
|
|
1
|
+
import t from"/kempo-ui/components/LightComponent.js";import{html as e,nothing as i,repeat as s,keyed as a}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Icon.js";import"/kempo-ui/components/Dialog.js";import"/kempo-ui/components/Sortable.js";import{getDashboardWidgets as n}from"/kempo/sdk.js";import{resolveLayout as o,serializeLayout as l,reorder as r,readStored as d,writeStored as h,clearStored as c}from"./dashboardLayout.js";const m="min(32rem, 70vw)",u=(t,e)=>Array.from({length:e-t+1},(e,i)=>t+i);export default class p extends t{static properties={heading:{type:String},layout:{state:!0},draft:{state:!0},sortVersion:{state:!0},columns:{state:!0},loading:{state:!0},failure:{state:!0},failures:{state:!0}};#t=[];#e=[];#i=!1;#s=new Map;#a=new Set;#n=null;constructor(){super(),this.heading="",this.layout=[],this.draft=[],this.sortVersion=0,this.columns=4,this.loading=!0,this.failure="",this.failures={}}connectedCallback(){super.connectedCallback(),this.classList.add("d-b"),this.#n=new ResizeObserver(([t])=>{const{width:e}=t.contentRect,i=e>=880?4:e>=560?2:1;i!==this.columns&&(this.columns=i)}),this.#n.observe(this),this.load()}disconnectedCallback(){super.disconnectedCallback(),this.#n?.disconnect()}updated(t){super.updated(t),this.layout.forEach(({widget:t,visible:e,width:i,height:s})=>{const a=this.#s.get(t.id);e&&a&&(a.width=Math.min(i,this.columns),a.height=s)})}load=async()=>{const[t,e]=await n();this.loading=!1,t?this.failure=`Could not load the dashboard: ${t.msg}`:(this.failure="",this.#t=e.widgets,this.#e=e.unavailable??[],this.layout=o(this.#t,d()),this.mountVisible())};mount=async({widget:t})=>{if(!this.#s.has(t.id)&&!this.#a.has(t.id)){this.#a.add(t.id);try{if(await(import(t.module)),!customElements.get(t.element))throw new Error(`${t.module} did not define <${t.element}>`);const e=document.createElement(t.element);e.definition=t,this.#s.set(t.id,e)}catch(e){this.failures={...this.failures,[t.id]:`is not available: its script ${t.module} could not be loaded, which usually means the extension that provides it needs updating.`},console.error(`Dashboard widget ${t.id} failed to load:`,e)}this.#a.delete(t.id),this.requestUpdate()}};mountVisible=()=>this.layout.filter(({visible:t})=>t).forEach(this.mount);commit=t=>{const e=l(t,{stored:this.#i?null:d(),unavailable:this.#e});this.layout=t,JSON.stringify(e)===JSON.stringify(l(o(this.#t,null)))?c():h(e),t.filter(({visible:t})=>!t).forEach(({widget:t})=>{if(this.#s.delete(t.id),this.failures[t.id]){const{[t.id]:e,...i}=this.failures;this.failures=i}}),this.mountVisible()};openCustomize=()=>{this.#i=!1,this.draft=this.layout.map(t=>({...t})),this.sortVersion++,this.querySelector("k-dialog").open()};save=()=>this.commit(this.draft);resetDraft=()=>{this.#i=!0,this.draft=o(this.#t,null),this.sortVersion++};changeDraft=(t,e)=>{this.draft=this.draft.map(i=>i.widget.id===t?{...i,...e}:i)};handleSort=t=>{this.draft=r(this.draft,[...t.currentTarget.children].map(t=>t.dataset.widgetId)),this.sortVersion++};renderDraftRow=({widget:t,visible:s,width:a,height:n})=>e`
|
|
2
2
|
<k-sortable-item class="mbh" data-widget-id=${t.id}>
|
|
3
3
|
<div style="display: flex; flex-wrap: nowrap; align-items: center; gap: var(--spacer); width: ${m}; min-height: 3.5rem;">
|
|
4
4
|
<div class="flex" style="display: flex; flex-wrap: nowrap; align-items: center; gap: var(--spacer_h); min-width: 0;">
|
|
5
|
-
${t.icon?e`<k-icon class=${s?"":"tc-muted"} name=${t.icon}></k-icon>`:i}
|
|
5
|
+
${t.iconSrc?e`<k-icon class=${s?"":"tc-muted"} src=${t.iconSrc}></k-icon>`:t.icon?e`<k-icon class=${s?"":"tc-muted"} name=${t.icon}></k-icon>`:i}
|
|
6
6
|
<strong class=${s?"":"tc-muted"}>${t.title}</strong>
|
|
7
7
|
</div>
|
|
8
8
|
<select style="width: 4.5rem;" aria-label="${t.title} width" ?disabled=${!s} @change=${e=>this.changeDraft(t.id,{width:Number(e.target.value)})}>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src\\admin\\components\\Dashboard.js","names":["LightComponent","html","nothing","repeat","keyed","getDashboardWidgets","resolveLayout","serializeLayout","reorder","readStored","writeStored","clearStored","ROW_WIDTH","range","from","to","Array","length","_","index","Dashboard","static","heading","type","String","layout","state","draft","sortVersion","columns","loading","failure","failures","definitions","unavailable","resetting","elements","Map","pending","Set","observer","constructor","super","this","connectedCallback","classList","add","ResizeObserver","entry","width","contentRect","observe","load","disconnectedCallback","disconnect","updated","changed","forEach","widget","visible","height","element","get","id","Math","min","async","error","data","msg","widgets","mountVisible","mount","has","module","customElements","Error","document","createElement","definition","set","console","delete","requestUpdate","filter","commit","stored","JSON","stringify","dropped","rest","openCustomize","map","querySelector","open","save","resetDraft","changeDraft","changes","handleSort","event","currentTarget","children","item","dataset","widgetId","renderDraftRow","icon","title","Number","target","value","minWidth","maxWidth","minHeight","maxHeight","renderDraftHeader","renderCustomizeDialog","renderCell","renderLightDom","define"],"sources":["0"],"sourcesContent":["import LightComponent from '/kempo-ui/components/LightComponent.js';\r\nimport { html, nothing, repeat, keyed } from '/kempo-ui/lit-all.min.js';\r\nimport '/kempo-ui/components/Icon.js';\r\nimport '/kempo-ui/components/Dialog.js';\r\nimport '/kempo-ui/components/Sortable.js';\r\nimport { getDashboardWidgets } from '/kempo/sdk.js';\r\nimport { resolveLayout, serializeLayout, reorder, readStored, writeStored, clearStored } from './dashboardLayout.js';\r\n\r\n// Wide enough for the columns; narrower screens shrink it rather than scroll.\r\nconst ROW_WIDTH = 'min(32rem, 70vw)';\r\n\r\nconst range = (from, to) => Array.from({ length: to - from + 1 }, (_, index) => from + index);\r\n\r\n/*\r\n The admin dashboard: a grid four blocks wide of widgets one to four blocks wide and one to three tall.\r\n\r\n The server only says which widgets exist and their size bounds. Which of them are shown, in what order\r\n and at what size is the user's own and lives in this browser's localStorage, changed through the\r\n Customize dialog and applied only when it is saved. A widget's script is not imported, and so nothing\r\n it needs is fetched, until it is both on the user's dashboard and rendered; a widget the user hides is\r\n torn down and starts fresh when it is shown again.\r\n\r\n Attributes: `heading`, the title shown above the grid.\r\n*/\r\nexport default class Dashboard extends LightComponent {\r\n static properties = {\r\n heading: { type: String },\r\n layout: { state: true },\r\n draft: { state: true },\r\n sortVersion: { state: true },\r\n columns: { state: true },\r\n loading: { state: true },\r\n failure: { state: true },\r\n failures: { state: true }\r\n };\r\n\r\n #definitions = [];\r\n #unavailable = [];\r\n #resetting = false;\r\n #elements = new Map();\r\n #pending = new Set();\r\n #observer = null;\r\n\r\n constructor(){\r\n super();\r\n this.heading = '';\r\n this.layout = [];\r\n this.draft = [];\r\n this.sortVersion = 0;\r\n this.columns = 4;\r\n this.loading = true;\r\n this.failure = '';\r\n this.failures = {};\r\n }\r\n\r\n /*\r\n Lifecycle Callbacks\r\n */\r\n\r\n connectedCallback(){\r\n super.connectedCallback();\r\n this.classList.add('d-b');\r\n this.#observer = new ResizeObserver(([entry]) => {\r\n const { width } = entry.contentRect;\r\n const columns = width >= 880 ? 4 : width >= 560 ? 2 : 1;\r\n if(columns !== this.columns) this.columns = columns;\r\n });\r\n this.#observer.observe(this);\r\n this.load();\r\n }\r\n\r\n disconnectedCallback(){\r\n super.disconnectedCallback();\r\n this.#observer?.disconnect();\r\n }\r\n\r\n updated(changed){\r\n super.updated(changed);\r\n this.layout.forEach(({ widget, visible, width, height }) => {\r\n const element = this.#elements.get(widget.id);\r\n if(!visible || !element) return;\r\n element.width = Math.min(width, this.columns);\r\n element.height = height;\r\n });\r\n }\r\n\r\n /*\r\n Public Methods\r\n */\r\n\r\n load = async () => {\r\n const [error, data] = await getDashboardWidgets();\r\n this.loading = false;\r\n\r\n if(error){\r\n this.failure = `Could not load the dashboard: ${error.msg}`;\r\n return;\r\n }\r\n\r\n this.failure = '';\r\n this.#definitions = data.widgets;\r\n this.#unavailable = data.unavailable ?? [];\r\n this.layout = resolveLayout(this.#definitions, readStored());\r\n this.mountVisible();\r\n };\r\n\r\n /*\r\n Utility Functions\r\n */\r\n\r\n /*\r\n Imports a widget's module and creates its element, once. The element fetches its own data when it\r\n connects to the page, so nothing is requested for a widget that is hidden.\r\n */\r\n mount = async ({ widget }) => {\r\n if(this.#elements.has(widget.id) || this.#pending.has(widget.id)) return;\r\n this.#pending.add(widget.id);\r\n\r\n try {\r\n await import(widget.module);\r\n if(!customElements.get(widget.element)){\r\n throw new Error(`${widget.module} did not define <${widget.element}>`);\r\n }\r\n const element = document.createElement(widget.element);\r\n element.definition = widget;\r\n this.#elements.set(widget.id, element);\r\n } catch(error) {\r\n this.failures = { ...this.failures, [widget.id]: `is not available: its script ${widget.module} could not be loaded, which usually means the extension that provides it needs updating.` };\r\n console.error(`Dashboard widget ${widget.id} failed to load:`, error);\r\n }\r\n\r\n this.#pending.delete(widget.id);\r\n this.requestUpdate();\r\n };\r\n\r\n mountVisible = () => this.layout.filter(({ visible }) => visible).forEach(this.mount);\r\n\r\n /*\r\n Applies a layout, remembers it, and mounts anything newly shown. Widgets no longer shown are released\r\n here so showing them again loads fresh data. A layout identical to the defaults is not stored, so the\r\n user keeps following the defaults as they change.\r\n */\r\n commit = layout => {\r\n const stored = serializeLayout(layout, { stored: this.#resetting ? null : readStored(), unavailable: this.#unavailable });\r\n this.layout = layout;\r\n\r\n if(JSON.stringify(stored) === JSON.stringify(serializeLayout(resolveLayout(this.#definitions, null)))){\r\n clearStored();\r\n } else {\r\n writeStored(stored);\r\n }\r\n\r\n layout.filter(({ visible }) => !visible).forEach(({ widget }) => {\r\n this.#elements.delete(widget.id);\r\n if(this.failures[widget.id]){\r\n const { [widget.id]: dropped, ...rest } = this.failures;\r\n this.failures = rest;\r\n }\r\n });\r\n\r\n this.mountVisible();\r\n };\r\n\r\n /*\r\n Event Handlers\r\n */\r\n\r\n openCustomize = () => {\r\n this.#resetting = false;\r\n this.draft = this.layout.map(entry => ({ ...entry }));\r\n this.sortVersion++;\r\n this.querySelector('k-dialog').open();\r\n };\r\n\r\n save = () => this.commit(this.draft);\r\n\r\n resetDraft = () => {\r\n this.#resetting = true;\r\n this.draft = resolveLayout(this.#definitions, null);\r\n this.sortVersion++;\r\n };\r\n\r\n changeDraft = (id, changes) => {\r\n this.draft = this.draft.map(entry => entry.widget.id === id ? { ...entry, ...changes } : entry);\r\n };\r\n\r\n /*\r\n The sortable moves its own items in the DOM. The new order is read from there and the list is then\r\n rebuilt from the draft (the `keyed` version changes) so Lit never disagrees with the DOM.\r\n */\r\n handleSort = event => {\r\n this.draft = reorder(this.draft, [...event.currentTarget.children].map(item => item.dataset.widgetId));\r\n this.sortVersion++;\r\n };\r\n\r\n /*\r\n Rendering\r\n */\r\n\r\n /*\r\n The item sizes its content to fit, so the row sets its own width. Its drag handle is 3.5rem tall, so\r\n the row is the same height with its controls centered, and the columns have fixed widths that the\r\n header row above the list repeats. `.d-f` is avoided because it wraps.\r\n */\r\n renderDraftRow = ({ widget, visible, width, height }) => html`\r\n <k-sortable-item class=\"mbh\" data-widget-id=${widget.id}>\r\n <div style=\"display: flex; flex-wrap: nowrap; align-items: center; gap: var(--spacer); width: ${ROW_WIDTH}; min-height: 3.5rem;\">\r\n <div class=\"flex\" style=\"display: flex; flex-wrap: nowrap; align-items: center; gap: var(--spacer_h); min-width: 0;\">\r\n ${widget.icon ? html`<k-icon class=${visible ? '' : 'tc-muted'} name=${widget.icon}></k-icon>` : nothing}\r\n <strong class=${visible ? '' : 'tc-muted'}>${widget.title}</strong>\r\n </div>\r\n <select style=\"width: 4.5rem;\" aria-label=\"${widget.title} width\" ?disabled=${!visible} @change=${event => this.changeDraft(widget.id, { width: Number(event.target.value) })}>\r\n ${range(widget.minWidth, widget.maxWidth).map(value => html`<option value=${value} ?selected=${value === width}>${value}</option>`)}\r\n </select>\r\n <select style=\"width: 4.5rem;\" aria-label=\"${widget.title} height\" ?disabled=${!visible} @change=${event => this.changeDraft(widget.id, { height: Number(event.target.value) })}>\r\n ${range(widget.minHeight, widget.maxHeight).map(value => html`<option value=${value} ?selected=${value === height}>${value}</option>`)}\r\n </select>\r\n <button\r\n class=\"no-btn pq\"\r\n style=\"width: 2.5rem;\"\r\n title=${visible ? 'Hide' : 'Show'}\r\n aria-label=\"${visible ? 'Hide' : 'Show'} ${widget.title}\"\r\n @click=${() => this.changeDraft(widget.id, { visible: !visible })}\r\n ><k-icon name=${visible ? 'hide' : 'show'}></k-icon></button>\r\n </div>\r\n </k-sortable-item>\r\n `;\r\n\r\n /*\r\n Column titles for the rows. The left offset is the sortable item's border and drag handle, so the\r\n columns line up with the rows below.\r\n */\r\n renderDraftHeader = () => html`\r\n <div class=\"small tc-muted mbh\" style=\"display: flex; flex-wrap: nowrap; gap: var(--spacer); width: calc(${ROW_WIDTH} + 3.7rem); padding-left: 3.7rem;\">\r\n <span class=\"flex\">Widget</span>\r\n <span style=\"width: 4.5rem;\">Width</span>\r\n <span style=\"width: 4.5rem;\">Height</span>\r\n <span style=\"width: 2.5rem;\"></span>\r\n </div>\r\n `;\r\n\r\n renderCustomizeDialog = () => html`\r\n <k-dialog\r\n confirm-text=\"Save\"\r\n confirm-classes=\"primary ml\"\r\n cancel-text=\"Cancel\"\r\n .confirmAction=${this.save}\r\n >\r\n <h5 slot=\"title\" class=\"pyh px m0\">Customize dashboard</h5>\r\n <div class=\"p\">\r\n <p class=\"small tc-muted\">Drag to reorder. Hidden widgets are not loaded. Sizes are in blocks; the dashboard is four blocks wide.</p>\r\n ${this.renderDraftHeader()}\r\n ${keyed(this.sortVersion, html`\r\n <k-sortable @sort=${this.handleSort}>${this.draft.map(this.renderDraftRow)}</k-sortable>\r\n `)}\r\n <button class=\"mt\" @click=${this.resetDraft}><k-icon name=\"restart_alt\"></k-icon> Reset to defaults</button>\r\n </div>\r\n </k-dialog>\r\n `;\r\n\r\n /*\r\n The grid assigns the spans and row height, so they are set here as values rather than classes: they\r\n depend on each user's saved sizes and the width the grid currently has. A widget wider than the\r\n columns available is drawn as wide as the grid is; its saved width is kept for when there is room.\r\n */\r\n renderCell = ({ widget, width, height }) => {\r\n const element = this.#elements.get(widget.id);\r\n const failure = this.failures[widget.id];\r\n return html`\r\n <div\r\n data-widget-id=${widget.id}\r\n style=\"grid-column: span ${Math.min(width, this.columns)}; grid-row: span ${height}; min-width: 0;\"\r\n >\r\n <section class=\"b r\" style=\"height: 100%; min-height: 0; overflow: hidden;\">\r\n ${failure\r\n ? html`<p class=\"p tc-danger\" role=\"alert\"><strong>${widget.title}</strong> ${failure}</p>`\r\n : element ?? html`<p class=\"p tc-muted\"><strong>${widget.title}</strong> Loading…</p>`}\r\n </section>\r\n </div>\r\n `;\r\n };\r\n\r\n renderLightDom(){\r\n if(this.failure) return html`<p class=\"tc-danger\" role=\"alert\">${this.failure}</p>`;\r\n if(this.loading) return html`<p class=\"tc-muted\">Loading…</p>`;\r\n\r\n const visible = this.layout.filter(entry => entry.visible);\r\n\r\n return html`\r\n <div class=\"d-f mb\" style=\"align-items: center;\">\r\n <h1 class=\"flex m0\">${this.heading}</h1>\r\n <button @click=${this.openCustomize}><k-icon name=\"edit\"></k-icon> Customize</button>\r\n </div>\r\n ${visible.length ? nothing : html`<p class=\"tc-muted\">No widgets are shown. Choose Customize to add some.</p>`}\r\n <div class=\"d-g\" style=\"grid-template-columns: repeat(${this.columns}, minmax(0, 1fr)); grid-auto-rows: 14rem; grid-auto-flow: dense; gap: var(--spacer);\">\r\n ${repeat(visible, entry => entry.widget.id, this.renderCell)}\r\n </div>\r\n ${this.renderCustomizeDialog()}\r\n `;\r\n }\r\n}\r\n\r\ncustomElements.define('k-dashboard', Dashboard);\r\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\Dashboard.js","names":["LightComponent","html","nothing","repeat","keyed","getDashboardWidgets","resolveLayout","serializeLayout","reorder","readStored","writeStored","clearStored","ROW_WIDTH","range","from","to","Array","length","_","index","Dashboard","static","heading","type","String","layout","state","draft","sortVersion","columns","loading","failure","failures","definitions","unavailable","resetting","elements","Map","pending","Set","observer","constructor","super","this","connectedCallback","classList","add","ResizeObserver","entry","width","contentRect","observe","load","disconnectedCallback","disconnect","updated","changed","forEach","widget","visible","height","element","get","id","Math","min","async","error","data","msg","widgets","mountVisible","mount","has","module","customElements","Error","document","createElement","definition","set","console","delete","requestUpdate","filter","commit","stored","JSON","stringify","dropped","rest","openCustomize","map","querySelector","open","save","resetDraft","changeDraft","changes","handleSort","event","currentTarget","children","item","dataset","widgetId","renderDraftRow","iconSrc","icon","title","Number","target","value","minWidth","maxWidth","minHeight","maxHeight","renderDraftHeader","renderCustomizeDialog","renderCell","renderLightDom","define"],"sources":["0"],"sourcesContent":["import LightComponent from '/kempo-ui/components/LightComponent.js';\r\nimport { html, nothing, repeat, keyed } from '/kempo-ui/lit-all.min.js';\r\nimport '/kempo-ui/components/Icon.js';\r\nimport '/kempo-ui/components/Dialog.js';\r\nimport '/kempo-ui/components/Sortable.js';\r\nimport { getDashboardWidgets } from '/kempo/sdk.js';\r\nimport { resolveLayout, serializeLayout, reorder, readStored, writeStored, clearStored } from './dashboardLayout.js';\r\n\r\n// Wide enough for the columns; narrower screens shrink it rather than scroll.\r\nconst ROW_WIDTH = 'min(32rem, 70vw)';\r\n\r\nconst range = (from, to) => Array.from({ length: to - from + 1 }, (_, index) => from + index);\r\n\r\n/*\r\n The admin dashboard: a grid four blocks wide of widgets one to four blocks wide and one to three tall.\r\n\r\n The server only says which widgets exist and their size bounds. Which of them are shown, in what order\r\n and at what size is the user's own and lives in this browser's localStorage, changed through the\r\n Customize dialog and applied only when it is saved. A widget's script is not imported, and so nothing\r\n it needs is fetched, until it is both on the user's dashboard and rendered; a widget the user hides is\r\n torn down and starts fresh when it is shown again.\r\n\r\n Attributes: `heading`, the title shown above the grid.\r\n*/\r\nexport default class Dashboard extends LightComponent {\r\n static properties = {\r\n heading: { type: String },\r\n layout: { state: true },\r\n draft: { state: true },\r\n sortVersion: { state: true },\r\n columns: { state: true },\r\n loading: { state: true },\r\n failure: { state: true },\r\n failures: { state: true }\r\n };\r\n\r\n #definitions = [];\r\n #unavailable = [];\r\n #resetting = false;\r\n #elements = new Map();\r\n #pending = new Set();\r\n #observer = null;\r\n\r\n constructor(){\r\n super();\r\n this.heading = '';\r\n this.layout = [];\r\n this.draft = [];\r\n this.sortVersion = 0;\r\n this.columns = 4;\r\n this.loading = true;\r\n this.failure = '';\r\n this.failures = {};\r\n }\r\n\r\n /*\r\n Lifecycle Callbacks\r\n */\r\n\r\n connectedCallback(){\r\n super.connectedCallback();\r\n this.classList.add('d-b');\r\n this.#observer = new ResizeObserver(([entry]) => {\r\n const { width } = entry.contentRect;\r\n const columns = width >= 880 ? 4 : width >= 560 ? 2 : 1;\r\n if(columns !== this.columns) this.columns = columns;\r\n });\r\n this.#observer.observe(this);\r\n this.load();\r\n }\r\n\r\n disconnectedCallback(){\r\n super.disconnectedCallback();\r\n this.#observer?.disconnect();\r\n }\r\n\r\n updated(changed){\r\n super.updated(changed);\r\n this.layout.forEach(({ widget, visible, width, height }) => {\r\n const element = this.#elements.get(widget.id);\r\n if(!visible || !element) return;\r\n element.width = Math.min(width, this.columns);\r\n element.height = height;\r\n });\r\n }\r\n\r\n /*\r\n Public Methods\r\n */\r\n\r\n load = async () => {\r\n const [error, data] = await getDashboardWidgets();\r\n this.loading = false;\r\n\r\n if(error){\r\n this.failure = `Could not load the dashboard: ${error.msg}`;\r\n return;\r\n }\r\n\r\n this.failure = '';\r\n this.#definitions = data.widgets;\r\n this.#unavailable = data.unavailable ?? [];\r\n this.layout = resolveLayout(this.#definitions, readStored());\r\n this.mountVisible();\r\n };\r\n\r\n /*\r\n Utility Functions\r\n */\r\n\r\n /*\r\n Imports a widget's module and creates its element, once. The element fetches its own data when it\r\n connects to the page, so nothing is requested for a widget that is hidden.\r\n */\r\n mount = async ({ widget }) => {\r\n if(this.#elements.has(widget.id) || this.#pending.has(widget.id)) return;\r\n this.#pending.add(widget.id);\r\n\r\n try {\r\n await import(widget.module);\r\n if(!customElements.get(widget.element)){\r\n throw new Error(`${widget.module} did not define <${widget.element}>`);\r\n }\r\n const element = document.createElement(widget.element);\r\n element.definition = widget;\r\n this.#elements.set(widget.id, element);\r\n } catch(error) {\r\n this.failures = { ...this.failures, [widget.id]: `is not available: its script ${widget.module} could not be loaded, which usually means the extension that provides it needs updating.` };\r\n console.error(`Dashboard widget ${widget.id} failed to load:`, error);\r\n }\r\n\r\n this.#pending.delete(widget.id);\r\n this.requestUpdate();\r\n };\r\n\r\n mountVisible = () => this.layout.filter(({ visible }) => visible).forEach(this.mount);\r\n\r\n /*\r\n Applies a layout, remembers it, and mounts anything newly shown. Widgets no longer shown are released\r\n here so showing them again loads fresh data. A layout identical to the defaults is not stored, so the\r\n user keeps following the defaults as they change.\r\n */\r\n commit = layout => {\r\n const stored = serializeLayout(layout, { stored: this.#resetting ? null : readStored(), unavailable: this.#unavailable });\r\n this.layout = layout;\r\n\r\n if(JSON.stringify(stored) === JSON.stringify(serializeLayout(resolveLayout(this.#definitions, null)))){\r\n clearStored();\r\n } else {\r\n writeStored(stored);\r\n }\r\n\r\n layout.filter(({ visible }) => !visible).forEach(({ widget }) => {\r\n this.#elements.delete(widget.id);\r\n if(this.failures[widget.id]){\r\n const { [widget.id]: dropped, ...rest } = this.failures;\r\n this.failures = rest;\r\n }\r\n });\r\n\r\n this.mountVisible();\r\n };\r\n\r\n /*\r\n Event Handlers\r\n */\r\n\r\n openCustomize = () => {\r\n this.#resetting = false;\r\n this.draft = this.layout.map(entry => ({ ...entry }));\r\n this.sortVersion++;\r\n this.querySelector('k-dialog').open();\r\n };\r\n\r\n save = () => this.commit(this.draft);\r\n\r\n resetDraft = () => {\r\n this.#resetting = true;\r\n this.draft = resolveLayout(this.#definitions, null);\r\n this.sortVersion++;\r\n };\r\n\r\n changeDraft = (id, changes) => {\r\n this.draft = this.draft.map(entry => entry.widget.id === id ? { ...entry, ...changes } : entry);\r\n };\r\n\r\n /*\r\n The sortable moves its own items in the DOM. The new order is read from there and the list is then\r\n rebuilt from the draft (the `keyed` version changes) so Lit never disagrees with the DOM.\r\n */\r\n handleSort = event => {\r\n this.draft = reorder(this.draft, [...event.currentTarget.children].map(item => item.dataset.widgetId));\r\n this.sortVersion++;\r\n };\r\n\r\n /*\r\n Rendering\r\n */\r\n\r\n /*\r\n The item sizes its content to fit, so the row sets its own width. Its drag handle is 3.5rem tall, so\r\n the row is the same height with its controls centered, and the columns have fixed widths that the\r\n header row above the list repeats. `.d-f` is avoided because it wraps.\r\n */\r\n renderDraftRow = ({ widget, visible, width, height }) => html`\r\n <k-sortable-item class=\"mbh\" data-widget-id=${widget.id}>\r\n <div style=\"display: flex; flex-wrap: nowrap; align-items: center; gap: var(--spacer); width: ${ROW_WIDTH}; min-height: 3.5rem;\">\r\n <div class=\"flex\" style=\"display: flex; flex-wrap: nowrap; align-items: center; gap: var(--spacer_h); min-width: 0;\">\r\n ${widget.iconSrc ? html`<k-icon class=${visible ? '' : 'tc-muted'} src=${widget.iconSrc}></k-icon>` : widget.icon ? html`<k-icon class=${visible ? '' : 'tc-muted'} name=${widget.icon}></k-icon>` : nothing}\r\n <strong class=${visible ? '' : 'tc-muted'}>${widget.title}</strong>\r\n </div>\r\n <select style=\"width: 4.5rem;\" aria-label=\"${widget.title} width\" ?disabled=${!visible} @change=${event => this.changeDraft(widget.id, { width: Number(event.target.value) })}>\r\n ${range(widget.minWidth, widget.maxWidth).map(value => html`<option value=${value} ?selected=${value === width}>${value}</option>`)}\r\n </select>\r\n <select style=\"width: 4.5rem;\" aria-label=\"${widget.title} height\" ?disabled=${!visible} @change=${event => this.changeDraft(widget.id, { height: Number(event.target.value) })}>\r\n ${range(widget.minHeight, widget.maxHeight).map(value => html`<option value=${value} ?selected=${value === height}>${value}</option>`)}\r\n </select>\r\n <button\r\n class=\"no-btn pq\"\r\n style=\"width: 2.5rem;\"\r\n title=${visible ? 'Hide' : 'Show'}\r\n aria-label=\"${visible ? 'Hide' : 'Show'} ${widget.title}\"\r\n @click=${() => this.changeDraft(widget.id, { visible: !visible })}\r\n ><k-icon name=${visible ? 'hide' : 'show'}></k-icon></button>\r\n </div>\r\n </k-sortable-item>\r\n `;\r\n\r\n /*\r\n Column titles for the rows. The left offset is the sortable item's border and drag handle, so the\r\n columns line up with the rows below.\r\n */\r\n renderDraftHeader = () => html`\r\n <div class=\"small tc-muted mbh\" style=\"display: flex; flex-wrap: nowrap; gap: var(--spacer); width: calc(${ROW_WIDTH} + 3.7rem); padding-left: 3.7rem;\">\r\n <span class=\"flex\">Widget</span>\r\n <span style=\"width: 4.5rem;\">Width</span>\r\n <span style=\"width: 4.5rem;\">Height</span>\r\n <span style=\"width: 2.5rem;\"></span>\r\n </div>\r\n `;\r\n\r\n renderCustomizeDialog = () => html`\r\n <k-dialog\r\n confirm-text=\"Save\"\r\n confirm-classes=\"primary ml\"\r\n cancel-text=\"Cancel\"\r\n .confirmAction=${this.save}\r\n >\r\n <h5 slot=\"title\" class=\"pyh px m0\">Customize dashboard</h5>\r\n <div class=\"p\">\r\n <p class=\"small tc-muted\">Drag to reorder. Hidden widgets are not loaded. Sizes are in blocks; the dashboard is four blocks wide.</p>\r\n ${this.renderDraftHeader()}\r\n ${keyed(this.sortVersion, html`\r\n <k-sortable @sort=${this.handleSort}>${this.draft.map(this.renderDraftRow)}</k-sortable>\r\n `)}\r\n <button class=\"mt\" @click=${this.resetDraft}><k-icon name=\"restart_alt\"></k-icon> Reset to defaults</button>\r\n </div>\r\n </k-dialog>\r\n `;\r\n\r\n /*\r\n The grid assigns the spans and row height, so they are set here as values rather than classes: they\r\n depend on each user's saved sizes and the width the grid currently has. A widget wider than the\r\n columns available is drawn as wide as the grid is; its saved width is kept for when there is room.\r\n */\r\n renderCell = ({ widget, width, height }) => {\r\n const element = this.#elements.get(widget.id);\r\n const failure = this.failures[widget.id];\r\n return html`\r\n <div\r\n data-widget-id=${widget.id}\r\n style=\"grid-column: span ${Math.min(width, this.columns)}; grid-row: span ${height}; min-width: 0;\"\r\n >\r\n <section class=\"b r\" style=\"height: 100%; min-height: 0; overflow: hidden;\">\r\n ${failure\r\n ? html`<p class=\"p tc-danger\" role=\"alert\"><strong>${widget.title}</strong> ${failure}</p>`\r\n : element ?? html`<p class=\"p tc-muted\"><strong>${widget.title}</strong> Loading…</p>`}\r\n </section>\r\n </div>\r\n `;\r\n };\r\n\r\n renderLightDom(){\r\n if(this.failure) return html`<p class=\"tc-danger\" role=\"alert\">${this.failure}</p>`;\r\n if(this.loading) return html`<p class=\"tc-muted\">Loading…</p>`;\r\n\r\n const visible = this.layout.filter(entry => entry.visible);\r\n\r\n return html`\r\n <div class=\"d-f mb\" style=\"align-items: center;\">\r\n <h1 class=\"flex m0\">${this.heading}</h1>\r\n <button @click=${this.openCustomize}><k-icon name=\"edit\"></k-icon> Customize</button>\r\n </div>\r\n ${visible.length ? nothing : html`<p class=\"tc-muted\">No widgets are shown. Choose Customize to add some.</p>`}\r\n <div class=\"d-g\" style=\"grid-template-columns: repeat(${this.columns}, minmax(0, 1fr)); grid-auto-rows: 14rem; grid-auto-flow: dense; gap: var(--spacer);\">\r\n ${repeat(visible, entry => entry.widget.id, this.renderCell)}\r\n </div>\r\n ${this.renderCustomizeDialog()}\r\n `;\r\n }\r\n}\r\n\r\ncustomElements.define('k-dashboard', Dashboard);\r\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import e from"/kempo-ui/components/ShadowComponent.js";import{html as t,nothing as n,live as a}from"/kempo-ui/lit-all.min.js";import{listMenus as s,deleteMenu as i,assignMenuLocation as l,saveMenuItem as o,deleteMenuItems as d,setMenuItemEnabled as c,reorderMenuItems as m,adoptMenuItem as r,getAllCurrentUserPermissions as h}from"/kempo/sdk.js";import u from"/kempo-ui/components/Dialog.js";import p from"/kempo-ui/components/Toast.js";import"/kempo-ui/components/Icon.js";import"/kempo-ui/components/Sortable.js";import"/kempo-ui/components/Combobox.js";export default class b extends e{static properties={loading:{state:!0},failed:{state:!0},menus:{state:!0},locations:{state:!0},navItems:{state:!0},can:{state:!0}};constructor(){super(),this.loading=!0,this.failed=!1,this.menus=[],this.locations=[],this.navItems=[],this.can={create:!1,update:!1,delete:!1},this.pages=[]}async connectedCallback(){super.connectedCallback();const[,e]=await h(),t=new Set(e?.permissions??[]);this.can={create:t.has("system:globals:create"),update:t.has("system:globals:update"),delete:t.has("system:globals:delete")},await this.load()}load=async()=>{const[e,t]=await s();this.loading=!1,this.failed=Boolean(e),e?p.error(e.msg||"Failed to load menus"):(this.menus=t.menus,this.locations=t.locations,this.navItems=t.navItems,this.pages=t.pages??[])};run=async(e,t)=>{const[n]=await e;return n?(p.error(n.msg||"That did not work"),await this.load(),!1):(t&&p.success(t),await this.load(),!0)};get pageOptions(){return this.pages.map(e=>({value:e.url,label:`${e.title} (${e.url})`}))}iconOf=e=>e.navItem?this.navItems.find(t=>t.key===e.navItem)?.icon:e.icon;iconSrcOf=e=>e.navItem?this.navItems.find(t=>t.key===e.navItem)?.iconSrc:"";menuAt=e=>this.menus.find(t=>t.id===e.menuId);otherPlaces=e=>this.locations.filter(t=>t.menuId&&t.menuId===e.menuId&&t!==e);handleSort=async e=>{const t=e.currentTarget,n=[...t.querySelectorAll("k-sortable-item")].map(e=>e.dataset.id);await this.run(m({menuId:t.dataset.menu,ids:n}))};handleToggle=e=>this.run(c({id:e.id,enabled:e.disabled}));handleDeleteItem=e=>u.confirm(`Delete "${e.label}" from the menu?`,async t=>{t&&await this.run(d([e.id]),"Item deleted")});handleDeleteMenu=e=>u.confirm(`Delete the unused "${e.name}" menu and its ${e.items.length} item${1===e.items.length?"":"s"}? This cannot be undone.`,async t=>{t&&await this.run(i(e.id),"Menu deleted")});handleMenuChange=(e,t)=>this.run(l({location:e.location,menuId:t.target.value}));handleAdopt=(e,t)=>this.run(r({id:t.id,menuName:e.label}),`"${t.label}" can now be reordered and edited here`);showKind=(e,t)=>{const n=this.navItems.find(e=>e.key===t);e.querySelector("#menu-link-fields").hidden=Boolean(n);const a=e.querySelector("#menu-kind-description");a.hidden=!n,a.textContent=n?n.description||`${n.label} from ${n.extension}.`:""};openLinkEditor=(e,a)=>{const s=Boolean(a),i=u.create(t`
|
|
2
|
+
<div class="p">
|
|
3
|
+
${!s&&this.navItems.length?t`
|
|
4
|
+
<div class="mb">
|
|
5
|
+
<label class="d-b mbq" for="menu-kind">What to add<small class="d-b tc-muted">A link to a page or address, or something an installed extension offers, such as a cart.</small></label>
|
|
6
|
+
<select id="menu-kind" class="full" @change=${e=>this.showKind(i,e.target.value)}>
|
|
7
|
+
<option value="">A link</option>
|
|
8
|
+
${[...new Set(this.navItems.map(e=>e.extension))].map(e=>t`
|
|
9
|
+
<optgroup label=${"kempo"===e?"Built in":e}>
|
|
10
|
+
${this.navItems.filter(t=>t.extension===e).map(e=>t`<option value=${e.key}>${e.label}</option>`)}
|
|
11
|
+
</optgroup>
|
|
12
|
+
`)}
|
|
13
|
+
</select>
|
|
14
|
+
</div>
|
|
15
|
+
<p id="menu-kind-description" class="tc-muted mb" hidden></p>
|
|
16
|
+
`:n}
|
|
17
|
+
<div id="menu-link-fields">
|
|
18
|
+
<div class="mb">
|
|
19
|
+
<label class="d-b mbq" for="menu-label">Label</label>
|
|
20
|
+
<input type="text" id="menu-label" class="full" placeholder="About" .value=${a?.label??""}>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="mb">
|
|
23
|
+
<label class="d-b mbq" for="menu-href">Link<small class="d-b tc-muted">Choose one of your pages, or type any address.</small></label>
|
|
24
|
+
<div id="menu-href-wrap"></div>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="mb">
|
|
27
|
+
<label class="d-b mbq" for="menu-icon">Icon (optional)<small class="d-b tc-muted">The name of an icon, such as <strong>home</strong> or <strong>star</strong>.</small></label>
|
|
28
|
+
<input type="text" id="menu-icon" class="full" .value=${a?.icon??""}>
|
|
29
|
+
</div>
|
|
30
|
+
<label class="checkbox"><input type="checkbox" id="menu-new-tab" ?checked=${a?.newTab}> Open in a new tab</label>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
`,{title:s?"Edit link":`Add an item to ${e.label}`,confirmText:s?"Save":"Add item",cancelText:"Cancel",confirmAction:async()=>{const t=i.querySelector("#menu-href").value,n=i.querySelector("#menu-kind")?.value;await this.run(o(n?{location:e.location,navItem:n}:{id:a?.id,location:e.location,label:i.querySelector("#menu-label").value,href:this.pageOptions.find(e=>e.label===t)?.value??t,icon:i.querySelector("#menu-icon").value.trim(),newTab:i.querySelector("#menu-new-tab").checked}),s?"Link saved":"Item added")&&i.close()}}),l=document.createElement("k-combobox");l.id="menu-href",l.className="full",l.setAttribute("placeholder","/about"),l.setOptions(this.pageOptions),i.querySelector("#menu-href-wrap").appendChild(l),a?.href&&(l.value=a.href)};renderLink(e,a){const s=t`
|
|
34
|
+
<div class="d-f" style="align-items: center; gap: var(--spacer_h);">
|
|
35
|
+
<div class="flex">
|
|
36
|
+
<strong class=${a.disabled||!a.available?"tc-muted":""}>${this.iconSrcOf(a)?t`<k-icon src=${this.iconSrcOf(a)}></k-icon> `:this.iconOf(a)?t`<k-icon name=${this.iconOf(a)}></k-icon> `:n}${a.label}</strong>
|
|
37
|
+
<span class="tc-muted"> ${a.navItem?a.navItem.startsWith("kempo:")?"built in":`from ${a.navItem.split(":")[0]}`:a.href}</span>
|
|
38
|
+
${a.available?n:t`<span class="tc-danger"> not on the site: its extension is disabled or removed</span>`}
|
|
39
|
+
${a.newTab?t`<k-icon name="open_in_new"></k-icon>`:n}
|
|
40
|
+
${a.disabled?t`<span class="tc-muted"> (hidden)</span>`:n}
|
|
41
|
+
</div>
|
|
42
|
+
${this.can.update?t`
|
|
43
|
+
<div>
|
|
44
|
+
${a.editable?t`<button class="btn" title="Edit" @click=${()=>this.openLinkEditor(e,a)}><k-icon name="edit"></k-icon></button>`:n}
|
|
45
|
+
<button class="btn" title=${a.disabled?"Show on the site":"Hide from the site"} @click=${()=>this.handleToggle(a)}><k-icon name=${a.disabled?"show":"hide"}></k-icon></button>
|
|
46
|
+
${this.can.delete?t`<button class="btn danger" title="Delete" @click=${()=>this.handleDeleteItem(a)}><k-icon name="delete"></k-icon></button>`:n}
|
|
47
|
+
</div>
|
|
48
|
+
`:n}
|
|
49
|
+
</div>
|
|
50
|
+
`;return this.can.update?t`<k-sortable-item data-id=${a.id}>${s}</k-sortable-item>`:t`<div class="b p mbq">${s}</div>`}renderPlace(e){const s=this.menuAt(e),i=this.otherPlaces(e);return t`
|
|
51
|
+
<div class="mb">
|
|
52
|
+
<div class="d-f mb" style="align-items: center; gap: var(--spacer_h);">
|
|
53
|
+
<div class="flex">
|
|
54
|
+
<h2 class="mb0">${e.label}</h2>
|
|
55
|
+
${e.declared?n:t`<span class="tc-danger">The theme no longer has this place (<strong>${e.location}</strong>), so nothing shows there.</span>`}
|
|
56
|
+
${i.length?t`<span class="tc-muted">Shows the same links as ${i.map(e=>e.label).join(", ")}.</span>`:n}
|
|
57
|
+
</div>
|
|
58
|
+
${this.can.create&&e.declared?t`<button class="primary" @click=${()=>this.openLinkEditor(e)}><k-icon name="add"></k-icon> Add item</button>`:n}
|
|
59
|
+
</div>
|
|
60
|
+
${s?.items.length?t`
|
|
61
|
+
<k-sortable data-menu=${s.id} @sort=${this.handleSort}>
|
|
62
|
+
${s.items.map(t=>this.renderLink(e,t))}
|
|
63
|
+
</k-sortable>
|
|
64
|
+
`:n}
|
|
65
|
+
${e.extensionItems.map(e=>t`
|
|
66
|
+
<div class="b p mbq"><strong>${e.label}</strong> <span class="tc-muted">${e.href} · added by ${e.owner}</span></div>
|
|
67
|
+
`)}
|
|
68
|
+
${e.candidates.map(a=>t`
|
|
69
|
+
<div class="b p mbq d-f" style="align-items: center; gap: var(--spacer_h);">
|
|
70
|
+
<div class="flex"><strong>${a.label}</strong> <span class="tc-muted">${a.href} · added as global content</span></div>
|
|
71
|
+
${this.can.update?t`<button class="btn" @click=${()=>this.handleAdopt(e,a)}>Manage here</button>`:n}
|
|
72
|
+
</div>
|
|
73
|
+
`)}
|
|
74
|
+
${s?.items.length||e.extensionItems.length||e.candidates.length?n:t`<p class="tc-muted">Nothing here yet.</p>`}
|
|
75
|
+
${this.can.update&&this.menus.length>1?t`
|
|
76
|
+
<label class="d-b mtq tc-muted">Show the links of another place here
|
|
77
|
+
<select @change=${t=>this.handleMenuChange(e,t)}>
|
|
78
|
+
<option value="" .selected=${a(!e.menuId)}>This place's own links</option>
|
|
79
|
+
${this.menus.map(n=>t`<option value=${n.id} .selected=${a(e.menuId===n.id)}>${n.name}</option>`)}
|
|
80
|
+
</select>
|
|
81
|
+
</label>
|
|
82
|
+
`:n}
|
|
83
|
+
</div>
|
|
84
|
+
`}renderUnused(){const e=this.menus.filter(e=>!e.locations.length);return e.length?t`
|
|
85
|
+
<h2>Not shown anywhere</h2>
|
|
86
|
+
<p class="tc-muted">These menus are not shown in any place. Pick one in a place's dropdown above to use it again, or delete it.</p>
|
|
87
|
+
${e.map(e=>t`
|
|
88
|
+
<div class="b p mbq d-f" style="align-items: center; gap: var(--spacer_h);">
|
|
89
|
+
<div class="flex"><strong>${e.name}</strong> <span class="tc-muted">${e.items.length} item${1===e.items.length?"":"s"}</span></div>
|
|
90
|
+
${this.can.delete?t`<button class="btn danger" title="Delete menu" @click=${()=>this.handleDeleteMenu(e)}><k-icon name="delete"></k-icon></button>`:n}
|
|
91
|
+
</div>
|
|
92
|
+
`)}
|
|
93
|
+
`:n}render(){return this.loading?t`<p>Loading...</p>`:this.failed?t`<h1>Menus</h1><p class="tc-danger">The menus could not be loaded.</p>`:t`
|
|
94
|
+
<h1>Menus</h1>
|
|
95
|
+
${this.locations.length?t`
|
|
96
|
+
${this.locations.map(e=>this.renderPlace(e))}
|
|
97
|
+
${this.renderUnused()}
|
|
98
|
+
`:t`
|
|
99
|
+
<p>Your theme does not have a place for a menu yet.</p>
|
|
100
|
+
<p class="tc-muted">Mark a place in your navigation, for example <code><location name="main-nav-links" label="Main menu" menu="true" /></code> in your navigation fragment, and it will be listed here.</p>
|
|
101
|
+
`}
|
|
102
|
+
`}}customElements.define("admin-menus",b);
|
|
103
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\Menus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\Menus.js","names":["ShadowComponent","html","nothing","live","listMenus","deleteMenu","assignMenuLocation","saveMenuItem","deleteMenuItems","setMenuItemEnabled","reorderMenuItems","adoptMenuItem","getAllCurrentUserPermissions","Dialog","Toast","Menus","static","loading","state","failed","menus","locations","navItems","can","constructor","super","this","create","update","delete","pages","connectedCallback","permData","perms","Set","permissions","has","load","async","error","data","Boolean","msg","run","request","success","pageOptions","map","page","value","url","label","title","iconOf","item","navItem","find","n","key","icon","iconSrcOf","iconSrc","menuAt","place","m","id","menuId","otherPlaces","filter","l","handleSort","list","e","currentTarget","ids","querySelectorAll","dataset","menu","handleToggle","enabled","disabled","handleDeleteItem","confirm","confirmed","handleDeleteMenu","name","items","length","handleMenuChange","location","target","handleAdopt","menuName","showKind","$dialog","supplied","querySelector","hidden","$description","textContent","description","extension","openLinkEditor","editing","newTab","confirmText","cancelText","confirmAction","chosen","href","o","trim","checked","close","$href","document","createElement","className","setAttribute","setOptions","appendChild","renderLink","body","available","startsWith","split","editable","renderPlace","shared","declared","join","extensionItems","owner","candidates","renderUnused","unused","render","customElements","define"],"sources":["0"],"sourcesContent":["import ShadowComponent from '/kempo-ui/components/ShadowComponent.js';\nimport { html, nothing, live } from '/kempo-ui/lit-all.min.js';\nimport {\n listMenus, deleteMenu, assignMenuLocation,\n saveMenuItem, deleteMenuItems, setMenuItemEnabled, reorderMenuItems, adoptMenuItem,\n getAllCurrentUserPermissions\n} from '/kempo/sdk.js';\nimport Dialog from '/kempo-ui/components/Dialog.js';\nimport Toast from '/kempo-ui/components/Toast.js';\nimport '/kempo-ui/components/Icon.js';\nimport '/kempo-ui/components/Sortable.js';\nimport '/kempo-ui/components/Combobox.js';\n\n/*\n The places in the theme that show a menu (marked `menu=\"true\"`, see Menus in the docs), each with\n its links. Adding a link to a place is the whole job: the menu behind it is made on the first one.\n A place can show another place's links instead, and a menu that no place shows is listed at the end.\n Both are saved as ordinary global content, so nothing here needs the theme to know about menus.\n*/\nexport default class Menus extends ShadowComponent {\n static properties = {\n loading: { state: true },\n failed: { state: true },\n menus: { state: true },\n locations: { state: true },\n navItems: { state: true },\n can: { state: true }\n };\n\n constructor(){\n super();\n this.loading = true;\n this.failed = false;\n this.menus = [];\n this.locations = [];\n this.navItems = [];\n this.can = { create: false, update: false, delete: false };\n this.pages = [];\n }\n\n /*\n Lifecycle Callbacks\n */\n\n async connectedCallback(){\n super.connectedCallback();\n const [, permData] = await getAllCurrentUserPermissions();\n const perms = new Set(permData?.permissions ?? []);\n this.can = {\n create: perms.has('system:globals:create'),\n update: perms.has('system:globals:update'),\n delete: perms.has('system:globals:delete')\n };\n await this.load();\n }\n\n /*\n Data\n */\n\n load = async () => {\n const [error, data] = await listMenus();\n this.loading = false;\n this.failed = Boolean(error);\n if(error){\n Toast.error(error.msg || 'Failed to load menus');\n return;\n }\n this.menus = data.menus;\n this.locations = data.locations;\n this.navItems = data.navItems;\n this.pages = data.pages ?? [];\n };\n\n run = async (request, success) => {\n const [error] = await request;\n if(error){\n Toast.error(error.msg || 'That did not work');\n await this.load();\n return false;\n }\n if(success) Toast.success(success);\n await this.load();\n return true;\n };\n\n /* Every page a link can go to, the site's and its extensions' (/products/, /categories/), for the Link box. */\n get pageOptions(){\n return this.pages.map(page => ({ value: page.url, label: `${page.title} (${page.url})` }));\n }\n\n iconOf = item => item.navItem ? this.navItems.find(n => n.key === item.navItem)?.icon : item.icon;\n\n iconSrcOf = item => item.navItem ? this.navItems.find(n => n.key === item.navItem)?.iconSrc : '';\n\n menuAt = place => this.menus.find(m => m.id === place.menuId);\n\n otherPlaces = place => this.locations.filter(l => l.menuId && l.menuId === place.menuId && l !== place);\n\n /*\n Event Handlers\n */\n\n handleSort = async e => {\n const list = e.currentTarget;\n const ids = [...list.querySelectorAll('k-sortable-item')].map(item => item.dataset.id);\n await this.run(reorderMenuItems({ menuId: list.dataset.menu, ids }));\n };\n\n handleToggle = item => this.run(setMenuItemEnabled({ id: item.id, enabled: item.disabled }));\n\n handleDeleteItem = item => Dialog.confirm(`Delete \"${item.label}\" from the menu?`, async confirmed => {\n if(confirmed) await this.run(deleteMenuItems([item.id]), 'Item deleted');\n });\n\n handleDeleteMenu = menu => Dialog.confirm(\n `Delete the unused \"${menu.name}\" menu and its ${menu.items.length} item${menu.items.length === 1 ? '' : 's'}? This cannot be undone.`,\n async confirmed => {\n if(confirmed) await this.run(deleteMenu(menu.id), 'Menu deleted');\n }\n );\n\n handleMenuChange = (place, e) => this.run(assignMenuLocation({ location: place.location, menuId: e.target.value }));\n\n handleAdopt = (place, item) => this.run(adoptMenuItem({ id: item.id, menuName: place.label }), `\"${item.label}\" can now be reordered and edited here`);\n\n showKind = ($dialog, key) => {\n const supplied = this.navItems.find(n => n.key === key);\n $dialog.querySelector('#menu-link-fields').hidden = Boolean(supplied);\n const $description = $dialog.querySelector('#menu-kind-description');\n $description.hidden = !supplied;\n $description.textContent = supplied ? supplied.description || `${supplied.label} from ${supplied.extension}.` : '';\n };\n\n openLinkEditor = (place, item) => {\n const editing = Boolean(item);\n const $dialog = Dialog.create(html`\n <div class=\"p\">\n ${!editing && this.navItems.length ? html`\n <div class=\"mb\">\n <label class=\"d-b mbq\" for=\"menu-kind\">What to add<small class=\"d-b tc-muted\">A link to a page or address, or something an installed extension offers, such as a cart.</small></label>\n <select id=\"menu-kind\" class=\"full\" @change=${e => this.showKind($dialog, e.target.value)}>\n <option value=\"\">A link</option>\n ${[...new Set(this.navItems.map(n => n.extension))].map(extension => html`\n <optgroup label=${extension === 'kempo' ? 'Built in' : extension}>\n ${this.navItems.filter(n => n.extension === extension).map(n => html`<option value=${n.key}>${n.label}</option>`)}\n </optgroup>\n `)}\n </select>\n </div>\n <p id=\"menu-kind-description\" class=\"tc-muted mb\" hidden></p>\n ` : nothing}\n <div id=\"menu-link-fields\">\n <div class=\"mb\">\n <label class=\"d-b mbq\" for=\"menu-label\">Label</label>\n <input type=\"text\" id=\"menu-label\" class=\"full\" placeholder=\"About\" .value=${item?.label ?? ''}>\n </div>\n <div class=\"mb\">\n <label class=\"d-b mbq\" for=\"menu-href\">Link<small class=\"d-b tc-muted\">Choose one of your pages, or type any address.</small></label>\n <div id=\"menu-href-wrap\"></div>\n </div>\n <div class=\"mb\">\n <label class=\"d-b mbq\" for=\"menu-icon\">Icon (optional)<small class=\"d-b tc-muted\">The name of an icon, such as <strong>home</strong> or <strong>star</strong>.</small></label>\n <input type=\"text\" id=\"menu-icon\" class=\"full\" .value=${item?.icon ?? ''}>\n </div>\n <label class=\"checkbox\"><input type=\"checkbox\" id=\"menu-new-tab\" ?checked=${item?.newTab}> Open in a new tab</label>\n </div>\n </div>\n `, {\n title: editing ? 'Edit link' : `Add an item to ${place.label}`,\n confirmText: editing ? 'Save' : 'Add item',\n cancelText: 'Cancel',\n confirmAction: async () => {\n // A combobox reports the label of a chosen option, so a chosen page is mapped back to its address.\n const chosen = $dialog.querySelector('#menu-href').value;\n const navItem = $dialog.querySelector('#menu-kind')?.value;\n const saved = await this.run(navItem ? saveMenuItem({ location: place.location, navItem }) : saveMenuItem({\n id: item?.id,\n location: place.location,\n label: $dialog.querySelector('#menu-label').value,\n href: this.pageOptions.find(o => o.label === chosen)?.value ?? chosen,\n icon: $dialog.querySelector('#menu-icon').value.trim(),\n newTab: $dialog.querySelector('#menu-new-tab').checked\n }), editing ? 'Link saved' : 'Item added');\n if(saved) $dialog.close();\n }\n });\n\n const $href = document.createElement('k-combobox');\n $href.id = 'menu-href';\n $href.className = 'full';\n $href.setAttribute('placeholder', '/about');\n $href.setOptions(this.pageOptions);\n $dialog.querySelector('#menu-href-wrap').appendChild($href);\n if(item?.href) $href.value = item.href;\n };\n\n /*\n Rendering\n */\n\n renderLink(place, item){\n const body = html`\n <div class=\"d-f\" style=\"align-items: center; gap: var(--spacer_h);\">\n <div class=\"flex\">\n <strong class=${item.disabled || !item.available ? 'tc-muted' : ''}>${this.iconSrcOf(item) ? html`<k-icon src=${this.iconSrcOf(item)}></k-icon> ` : this.iconOf(item) ? html`<k-icon name=${this.iconOf(item)}></k-icon> ` : nothing}${item.label}</strong>\n <span class=\"tc-muted\"> ${item.navItem ? (item.navItem.startsWith('kempo:') ? 'built in' : `from ${item.navItem.split(':')[0]}`) : item.href}</span>\n ${item.available ? nothing : html`<span class=\"tc-danger\"> not on the site: its extension is disabled or removed</span>`}\n ${item.newTab ? html`<k-icon name=\"open_in_new\"></k-icon>` : nothing}\n ${item.disabled ? html`<span class=\"tc-muted\"> (hidden)</span>` : nothing}\n </div>\n ${this.can.update ? html`\n <div>\n ${item.editable ? html`<button class=\"btn\" title=\"Edit\" @click=${() => this.openLinkEditor(place, item)}><k-icon name=\"edit\"></k-icon></button>` : nothing}\n <button class=\"btn\" title=${item.disabled ? 'Show on the site' : 'Hide from the site'} @click=${() => this.handleToggle(item)}><k-icon name=${item.disabled ? 'show' : 'hide'}></k-icon></button>\n ${this.can.delete ? html`<button class=\"btn danger\" title=\"Delete\" @click=${() => this.handleDeleteItem(item)}><k-icon name=\"delete\"></k-icon></button>` : nothing}\n </div>\n ` : nothing}\n </div>\n `;\n return this.can.update\n ? html`<k-sortable-item data-id=${item.id}>${body}</k-sortable-item>`\n : html`<div class=\"b p mbq\">${body}</div>`;\n }\n\n renderPlace(place){\n const menu = this.menuAt(place);\n const shared = this.otherPlaces(place);\n return html`\n <div class=\"mb\">\n <div class=\"d-f mb\" style=\"align-items: center; gap: var(--spacer_h);\">\n <div class=\"flex\">\n <h2 class=\"mb0\">${place.label}</h2>\n ${place.declared ? nothing : html`<span class=\"tc-danger\">The theme no longer has this place (<strong>${place.location}</strong>), so nothing shows there.</span>`}\n ${shared.length ? html`<span class=\"tc-muted\">Shows the same links as ${shared.map(l => l.label).join(', ')}.</span>` : nothing}\n </div>\n ${this.can.create && place.declared ? html`<button class=\"primary\" @click=${() => this.openLinkEditor(place)}><k-icon name=\"add\"></k-icon> Add item</button>` : nothing}\n </div>\n ${menu?.items.length ? html`\n <k-sortable data-menu=${menu.id} @sort=${this.handleSort}>\n ${menu.items.map(item => this.renderLink(place, item))}\n </k-sortable>\n ` : nothing}\n ${place.extensionItems.map(item => html`\n <div class=\"b p mbq\"><strong>${item.label}</strong> <span class=\"tc-muted\">${item.href} · added by ${item.owner}</span></div>\n `)}\n ${place.candidates.map(item => html`\n <div class=\"b p mbq d-f\" style=\"align-items: center; gap: var(--spacer_h);\">\n <div class=\"flex\"><strong>${item.label}</strong> <span class=\"tc-muted\">${item.href} · added as global content</span></div>\n ${this.can.update ? html`<button class=\"btn\" @click=${() => this.handleAdopt(place, item)}>Manage here</button>` : nothing}\n </div>\n `)}\n ${!menu?.items.length && !place.extensionItems.length && !place.candidates.length ? html`<p class=\"tc-muted\">Nothing here yet.</p>` : nothing}\n ${this.can.update && this.menus.length > 1 ? html`\n <label class=\"d-b mtq tc-muted\">Show the links of another place here\n <select @change=${e => this.handleMenuChange(place, e)}>\n <option value=\"\" .selected=${live(!place.menuId)}>This place's own links</option>\n ${this.menus.map(m => html`<option value=${m.id} .selected=${live(place.menuId === m.id)}>${m.name}</option>`)}\n </select>\n </label>\n ` : nothing}\n </div>\n `;\n }\n\n renderUnused(){\n const unused = this.menus.filter(m => !m.locations.length);\n if(!unused.length) return nothing;\n return html`\n <h2>Not shown anywhere</h2>\n <p class=\"tc-muted\">These menus are not shown in any place. Pick one in a place's dropdown above to use it again, or delete it.</p>\n ${unused.map(menu => html`\n <div class=\"b p mbq d-f\" style=\"align-items: center; gap: var(--spacer_h);\">\n <div class=\"flex\"><strong>${menu.name}</strong> <span class=\"tc-muted\">${menu.items.length} item${menu.items.length === 1 ? '' : 's'}</span></div>\n ${this.can.delete ? html`<button class=\"btn danger\" title=\"Delete menu\" @click=${() => this.handleDeleteMenu(menu)}><k-icon name=\"delete\"></k-icon></button>` : nothing}\n </div>\n `)}\n `;\n }\n\n render(){\n if(this.loading) return html`<p>Loading...</p>`;\n if(this.failed) return html`<h1>Menus</h1><p class=\"tc-danger\">The menus could not be loaded.</p>`;\n return html`\n <h1>Menus</h1>\n ${this.locations.length ? html`\n ${this.locations.map(place => this.renderPlace(place))}\n ${this.renderUnused()}\n ` : html`\n <p>Your theme does not have a place for a menu yet.</p>\n <p class=\"tc-muted\">Mark a place in your navigation, for example <code><location name=\"main-nav-links\" label=\"Main menu\" menu=\"true\" /></code> in your navigation fragment, and it will be listed here.</p>\n `}\n `;\n }\n}\n\ncustomElements.define('admin-menus', Menus);\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{resolve as
|
|
1
|
+
import{existsSync as r}from"fs";import{resolve as s,join as e}from"path";import t from"../../../../server/utils/permissions/currentUserHasPermission.js";import o from"../../../../server/utils/fragments/getFragmentMetadata.js";import i from"../../../../server/utils/fragments/deleteFragment.js";const n=r(e(process.cwd(),"public"))?e(process.cwd(),"public"):s(import.meta.dirname,"../../../../app-public");export default async(r,s)=>{const e=r.cookies.session_token,[a,f]=await t(e,"system:fragments:delete");if(a)return s.status(a.code).json({error:a.msg});if(!f)return s.status(403).json({error:"Insufficient permissions"});const{files:m}=r.body||{};if(!m?.length)return s.status(400).json({error:"No files specified"});for(const r of m){const[e,t]=await o({rootDir:n,file:r});if(!e&&t.locked)return s.status(403).json({error:`Cannot delete locked fragment: ${r}`})}const[c,u]=await i({rootDir:n,files:m});if(c)return s.status(c.code).json({error:c.msg});s.json(u)};
|
|
2
2
|
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\fragments\DELETE.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src\\kempo\\api\\fragments\\DELETE.js","names":["resolve","join","currentUserHasPermission","getFragmentMetadata","deleteFragment","rootDir","
|
|
1
|
+
{"version":3,"file":"src\\kempo\\api\\fragments\\DELETE.js","names":["existsSync","resolve","join","currentUserHasPermission","getFragmentMetadata","deleteFragment","rootDir","process","cwd","dirname","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","files","body","length","file","metaError","metadata","locked","result"],"sources":["0"],"sourcesContent":["import { existsSync } from 'fs';\r\nimport { resolve, join } from 'path';\r\nimport currentUserHasPermission from '../../../../server/utils/permissions/currentUserHasPermission.js';\r\nimport getFragmentMetadata from '../../../../server/utils/fragments/getFragmentMetadata.js';\r\nimport deleteFragment from '../../../../server/utils/fragments/deleteFragment.js';\r\n\r\nconst rootDir = existsSync(join(process.cwd(), 'public')) ? join(process.cwd(), 'public') : resolve(import.meta.dirname, '../../../../app-public');\r\n\r\nexport default async (request, response) => {\r\n\tconst token = request.cookies.session_token;\r\n\tconst [permError, hasPermission] = await currentUserHasPermission(token, 'system:fragments:delete');\r\n\r\n\tif(permError){\r\n\t\treturn response.status(permError.code).json({ error: permError.msg });\r\n\t}\r\n\r\n\tif(!hasPermission){\r\n\t\treturn response.status(403).json({ error: 'Insufficient permissions' });\r\n\t}\r\n\r\n\tconst { files } = request.body || {};\r\n\r\n\tif(!files?.length){\r\n\t\treturn response.status(400).json({ error: 'No files specified' });\r\n\t}\r\n\r\n\tfor(const file of files){\r\n\t\tconst [metaError, metadata] = await getFragmentMetadata({ rootDir, file });\r\n\t\tif(!metaError && metadata.locked){\r\n\t\t\treturn response.status(403).json({ error: `Cannot delete locked fragment: ${file}` });\r\n\t\t}\r\n\t}\r\n\r\n\tconst [error, result] = await deleteFragment({ rootDir, files });\r\n\r\n\tif(error){\r\n\t\treturn response.status(error.code).json({ error: error.msg });\r\n\t}\r\n\r\n\tresponse.json(result);\r\n};\r\n"],"mappings":"qBAASA,MAAkB,uBAClBC,UAASC,MAAY,cACvBC,MAA8B,0EAC9BC,MAAyB,mEACzBC,MAAoB,uDAE3B,MAAMC,EAAUN,EAAWE,EAAKK,QAAQC,MAAO,WAAaN,EAAKK,QAAQC,MAAO,UAAYP,cAAoBQ,QAAS,yCAE1GC,MAAOC,EAASC,KAC9B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBd,EAAyBU,EAAO,2BAEzE,GAAGG,EACF,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGhE,IAAIL,EACH,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAME,MAAEA,GAAUZ,EAAQa,MAAQ,CAAC,EAEnC,IAAID,GAAOE,OACV,OAAOb,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,uBAG3C,IAAI,MAAMK,KAAQH,EAAM,CACvB,MAAOI,EAAWC,SAAkBxB,EAAoB,CAAEE,UAASoB,SACnE,IAAIC,GAAaC,EAASC,OACzB,OAAOjB,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,kCAAkCK,KAE9E,CAEA,MAAOL,EAAOS,SAAgBzB,EAAe,CAAEC,UAASiB,UAExD,GAAGF,EACF,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGxDV,EAASQ,KAAKU","ignoreList":[]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{resolve as
|
|
1
|
+
import{existsSync as s}from"fs";import{resolve as r,join as t}from"path";import o from"../../../../server/utils/permissions/currentUserHasPermission.js";import e from"../../../../server/utils/fragments/listFragments.js";const i=s(t(process.cwd(),"public"))?t(process.cwd(),"public"):r(import.meta.dirname,"../../../../app-public");export default async(s,r)=>{const t=s.cookies.session_token,[n,m]=await o(t,"system:fragments:read");if(n)return r.status(n.code).json({error:n.msg});if(!m)return r.status(403).json({error:"Insufficient permissions"});const[a,c]=await e({rootDir:i});if(a)return r.status(a.code).json({error:a.msg});r.json(c)};
|
|
2
2
|
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\fragments\GET.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src\\kempo\\api\\fragments\\GET.js","names":["resolve","join","currentUserHasPermission","listFragments","rootDir","
|
|
1
|
+
{"version":3,"file":"src\\kempo\\api\\fragments\\GET.js","names":["existsSync","resolve","join","currentUserHasPermission","listFragments","rootDir","process","cwd","dirname","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","data"],"sources":["0"],"sourcesContent":["import { existsSync } from 'fs';\r\nimport { resolve, join } from 'path';\r\nimport currentUserHasPermission from '../../../../server/utils/permissions/currentUserHasPermission.js';\r\nimport listFragments from '../../../../server/utils/fragments/listFragments.js';\r\n\r\nconst rootDir = existsSync(join(process.cwd(), 'public')) ? join(process.cwd(), 'public') : resolve(import.meta.dirname, '../../../../app-public');\r\n\r\nexport default async (request, response) => {\r\n\tconst token = request.cookies.session_token;\r\n\tconst [permError, hasPermission] = await currentUserHasPermission(token, 'system:fragments:read');\r\n\r\n\tif(permError){\r\n\t\treturn response.status(permError.code).json({ error: permError.msg });\r\n\t}\r\n\r\n\tif(!hasPermission){\r\n\t\treturn response.status(403).json({ error: 'Insufficient permissions' });\r\n\t}\r\n\r\n\tconst [error, data] = await listFragments({ rootDir });\r\n\r\n\tif(error){\r\n\t\treturn response.status(error.code).json({ error: error.msg });\r\n\t}\r\n\r\n\tresponse.json(data);\r\n};\r\n"],"mappings":"qBAASA,MAAkB,uBAClBC,UAASC,MAAY,cACvBC,MAA8B,0EAC9BC,MAAmB,sDAE1B,MAAMC,EAAUL,EAAWE,EAAKI,QAAQC,MAAO,WAAaL,EAAKI,QAAQC,MAAO,UAAYN,cAAoBO,QAAS,yCAE1GC,MAAOC,EAASC,KAC9B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBb,EAAyBS,EAAO,yBAEzE,GAAGG,EACF,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGhE,IAAIL,EACH,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAOA,EAAOE,SAAclB,EAAc,CAAEC,YAE5C,GAAGe,EACF,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGxDV,EAASQ,KAAKG","ignoreList":[]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{resolve as
|
|
1
|
+
import{existsSync as r}from"fs";import{resolve as s,join as e}from"path";import t from"../../../../server/utils/permissions/currentUserHasPermission.js";import o from"../../../../server/utils/auth/getSession.js";import i from"../../../../server/utils/fragments/createFragment.js";const n=r(e(process.cwd(),"public"))?e(process.cwd(),"public"):s(import.meta.dirname,"../../../../app-public");export default async(r,s)=>{const e=r.cookies.session_token,[a,m]=await t(e,"system:fragments:create");if(a)return s.status(a.code).json({error:a.msg});if(!m)return s.status(403).json({error:"Insufficient permissions"});const{directory:u,name:c}=r.body;if(!c)return s.status(400).json({error:"Fragment name is required"});const[,p]=await o({token:e}),f=p?.user?.name||"",[d,j]=await i({rootDir:n,directory:u,name:c,author:f});if(d)return s.status(d.code).json({error:d.msg});s.status(201).json(j)};
|
|
2
2
|
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\fragments\POST.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src\\kempo\\api\\fragments\\POST.js","names":["resolve","join","currentUserHasPermission","getSession","createFragment","rootDir","
|
|
1
|
+
{"version":3,"file":"src\\kempo\\api\\fragments\\POST.js","names":["existsSync","resolve","join","currentUserHasPermission","getSession","createFragment","rootDir","process","cwd","dirname","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","directory","name","body","sessionData","author","user","data"],"sources":["0"],"sourcesContent":["import { existsSync } from 'fs';\r\nimport { resolve, join } from 'path';\r\nimport currentUserHasPermission from '../../../../server/utils/permissions/currentUserHasPermission.js';\r\nimport getSession from '../../../../server/utils/auth/getSession.js';\r\nimport createFragment from '../../../../server/utils/fragments/createFragment.js';\r\n\r\nconst rootDir = existsSync(join(process.cwd(), 'public')) ? join(process.cwd(), 'public') : resolve(import.meta.dirname, '../../../../app-public');\r\n\r\nexport default async (request, response) => {\r\n\tconst token = request.cookies.session_token;\r\n\tconst [permError, hasPermission] = await currentUserHasPermission(token, 'system:fragments:create');\r\n\r\n\tif(permError){\r\n\t\treturn response.status(permError.code).json({ error: permError.msg });\r\n\t}\r\n\r\n\tif(!hasPermission){\r\n\t\treturn response.status(403).json({ error: 'Insufficient permissions' });\r\n\t}\r\n\r\n\tconst { directory, name } = request.body;\r\n\r\n\tif(!name){\r\n\t\treturn response.status(400).json({ error: 'Fragment name is required' });\r\n\t}\r\n\r\n\tconst [, sessionData] = await getSession({ token });\r\n\tconst author = sessionData?.user?.name || '';\r\n\r\n\tconst [error, data] = await createFragment({ rootDir, directory, name, author });\r\n\r\n\tif(error){\r\n\t\treturn response.status(error.code).json({ error: error.msg });\r\n\t}\r\n\r\n\tresponse.status(201).json(data);\r\n};\r\n"],"mappings":"qBAASA,MAAkB,uBAClBC,UAASC,MAAY,cACvBC,MAA8B,0EAC9BC,MAAgB,qDAChBC,MAAoB,uDAE3B,MAAMC,EAAUN,EAAWE,EAAKK,QAAQC,MAAO,WAAaN,EAAKK,QAAQC,MAAO,UAAYP,cAAoBQ,QAAS,yCAE1GC,MAAOC,EAASC,KAC9B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBd,EAAyBU,EAAO,2BAEzE,GAAGG,EACF,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGhE,IAAIL,EACH,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAME,UAAEA,EAASC,KAAEA,GAASb,EAAQc,KAEpC,IAAID,EACH,OAAOZ,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,8BAG3C,MAAO,CAAEK,SAAqBtB,EAAW,CAAES,UACrCc,EAASD,GAAaE,MAAMJ,MAAQ,IAEnCH,EAAOQ,SAAcxB,EAAe,CAAEC,UAASiB,YAAWC,OAAMG,WAEvE,GAAGN,EACF,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGxDV,EAASM,OAAO,KAAKE,KAAKS","ignoreList":[]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{resolve as
|
|
1
|
+
import{existsSync as r}from"fs";import{resolve as s,join as e}from"path";import t from"../../../../../server/utils/permissions/currentUserHasPermission.js";import o from"../../../../../server/utils/fragments/getFragmentMetadata.js";import i from"../../../../../server/utils/fragments/disableFragment.js";const a=r(e(process.cwd(),"public"))?e(process.cwd(),"public"):s(import.meta.dirname,"../../../../../app-public");export default async(r,s)=>{const e=r.cookies.session_token,[n,m]=await t(e,"system:pages:update");if(n)return s.status(n.code).json({error:n.msg});if(!m)return s.status(403).json({error:"Insufficient permissions"});const{file:u}=r.body||{};if(!u)return s.status(400).json({error:"File path is required"});const[c,f]=await o({rootDir:a,file:u});if(!c&&f.locked)return s.status(403).json({error:"Cannot disable a locked fragment"});const[p,l]=await i({rootDir:a,file:u});if(p)return s.status(p.code).json({error:p.msg});s.json(l)};
|
|
2
2
|
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\fragments\disable\PUT.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src\\kempo\\api\\fragments\\disable\\PUT.js","names":["resolve","join","currentUserHasPermission","getFragmentMetadata","disableFragment","rootDir","
|
|
1
|
+
{"version":3,"file":"src\\kempo\\api\\fragments\\disable\\PUT.js","names":["existsSync","resolve","join","currentUserHasPermission","getFragmentMetadata","disableFragment","rootDir","process","cwd","dirname","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","file","body","metaError","metadata","locked","result"],"sources":["0"],"sourcesContent":["import { existsSync } from 'fs';\r\nimport { resolve, join } from 'path';\r\nimport currentUserHasPermission from '../../../../../server/utils/permissions/currentUserHasPermission.js';\r\nimport getFragmentMetadata from '../../../../../server/utils/fragments/getFragmentMetadata.js';\r\nimport disableFragment from '../../../../../server/utils/fragments/disableFragment.js';\r\n\r\nconst rootDir = existsSync(join(process.cwd(), 'public')) ? join(process.cwd(), 'public') : resolve(import.meta.dirname, '../../../../../app-public');\r\n\r\nexport default async (request, response) => {\r\n\tconst token = request.cookies.session_token;\r\n\tconst [permError, hasPermission] = await currentUserHasPermission(token, 'system:pages:update');\r\n\r\n\tif(permError){\r\n\t\treturn response.status(permError.code).json({ error: permError.msg });\r\n\t}\r\n\r\n\tif(!hasPermission){\r\n\t\treturn response.status(403).json({ error: 'Insufficient permissions' });\r\n\t}\r\n\r\n\tconst { file } = request.body || {};\r\n\r\n\tif(!file){\r\n\t\treturn response.status(400).json({ error: 'File path is required' });\r\n\t}\r\n\r\n\tconst [metaError, metadata] = await getFragmentMetadata({ rootDir, file });\r\n\tif(!metaError && metadata.locked){\r\n\t\treturn response.status(403).json({ error: 'Cannot disable a locked fragment' });\r\n\t}\r\n\r\n\tconst [error, result] = await disableFragment({ rootDir, file });\r\n\r\n\tif(error){\r\n\t\treturn response.status(error.code).json({ error: error.msg });\r\n\t}\r\n\r\n\tresponse.json(result);\r\n};\r\n"],"mappings":"qBAASA,MAAkB,uBAClBC,UAASC,MAAY,cACvBC,MAA8B,6EAC9BC,MAAyB,sEACzBC,MAAqB,2DAE5B,MAAMC,EAAUN,EAAWE,EAAKK,QAAQC,MAAO,WAAaN,EAAKK,QAAQC,MAAO,UAAYP,cAAoBQ,QAAS,4CAE1GC,MAAOC,EAASC,KAC9B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBd,EAAyBU,EAAO,uBAEzE,GAAGG,EACF,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGhE,IAAIL,EACH,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAME,KAAEA,GAASZ,EAAQa,MAAQ,CAAC,EAElC,IAAID,EACH,OAAOX,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,0BAG3C,MAAOI,EAAWC,SAAkBtB,EAAoB,CAAEE,UAASiB,SACnE,IAAIE,GAAaC,EAASC,OACzB,OAAOf,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,qCAG3C,MAAOA,EAAOO,SAAgBvB,EAAgB,CAAEC,UAASiB,SAEzD,GAAGF,EACF,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGxDV,EAASQ,KAAKQ","ignoreList":[]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{resolve as
|
|
1
|
+
import{existsSync as s}from"fs";import{resolve as r,join as e}from"path";import t from"../../../../../server/utils/permissions/currentUserHasPermission.js";import o from"../../../../../server/utils/fragments/enableFragment.js";const i=s(e(process.cwd(),"public"))?e(process.cwd(),"public"):r(import.meta.dirname,"../../../../../app-public");export default async(s,r)=>{const e=s.cookies.session_token,[n,a]=await t(e,"system:pages:update");if(n)return r.status(n.code).json({error:n.msg});if(!a)return r.status(403).json({error:"Insufficient permissions"});const{file:m}=s.body||{};if(!m)return r.status(400).json({error:"File path is required"});const[p,u]=await o({rootDir:i,file:m});if(p)return r.status(p.code).json({error:p.msg});r.json(u)};
|
|
2
2
|
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\fragments\enable\PUT.js.map
|