kempo 4.5.1 → 4.5.3
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/dist/admin/components/Dashboard.js +67 -0
- package/dist/admin/components/Dashboard.js.map +1 -0
- package/dist/admin/components/dashboardLayout.js +2 -0
- package/dist/admin/components/dashboardLayout.js.map +1 -0
- package/dist/admin/components/widgets/Content.js +4 -0
- package/dist/admin/components/widgets/Content.js.map +1 -0
- package/dist/admin/components/widgets/Extensions.js +5 -0
- package/dist/admin/components/widgets/Extensions.js.map +1 -0
- package/dist/admin/components/widgets/Notifications.js +4 -0
- package/dist/admin/components/widgets/Notifications.js.map +1 -0
- package/dist/admin/components/widgets/Pages.js +23 -0
- package/dist/admin/components/widgets/Pages.js.map +1 -0
- package/dist/admin/components/widgets/Users.js +5 -0
- package/dist/admin/components/widgets/Users.js.map +1 -0
- package/dist/admin/index.page.html +2 -1
- package/dist/admin/nav.fragment.html +57 -58
- package/dist/admin/realtime/index.page.html +2 -2
- package/dist/kempo/api/dashboard/widgets/GET.js +2 -0
- package/dist/kempo/api/dashboard/widgets/GET.js.map +1 -0
- package/dist/kempo/api/notifications/GET.js +1 -1
- package/dist/kempo/api/notifications/[id]/DELETE.js +1 -1
- package/dist/kempo/api/notifications/[id]/handled/POST.js +1 -1
- package/dist/kempo/api/notifications/[id]/read/POST.js +1 -1
- package/dist/kempo/api/notifications/count/GET.js +1 -1
- package/dist/kempo/api/notifications/read-all/POST.js +1 -1
- package/dist/kempo/components/AdminNavFooter.js +2 -0
- package/dist/kempo/components/AdminNavFooter.js.map +1 -0
- package/dist/kempo/components/DashboardWidget.js +18 -0
- package/dist/kempo/components/DashboardWidget.js.map +1 -0
- package/dist/kempo/components/Login.js +1 -1
- package/dist/kempo/components/NotificationBell.js +3 -3
- package/dist/kempo/components/NotificationBell.js.map +1 -1
- package/dist/kempo/components/NotificationList.js +1 -1
- package/dist/kempo/components/Register.js +1 -1
- package/dist/kempo/sdk.js +2 -2
- package/dist/kempo/sdk.js.map +1 -1
- package/dist/kempo/utils/safeNext.js +1 -1
- package/package.json +1 -1
- package/server/db/schema.js +34 -1
- package/server/sdk.js +8 -0
- package/server/utils/dashboard/coreWidgets.js +87 -0
- package/server/utils/dashboard/helpers.js +144 -0
- package/server/utils/dashboard/listDashboardWidgets.js +63 -0
- package/server/utils/dashboard/registerDashboardWidget.js +40 -0
- package/server/utils/dashboard/unregisterDashboardWidget.js +24 -0
- package/server/utils/extensions/installExtension.js +11 -0
- package/server/utils/extensions/uninstallExtension.js +2 -1
- package/server/utils/extensions/updateExtension.js +18 -0
package/README.md
CHANGED
|
@@ -178,6 +178,7 @@ Kempo provides these tables out of the box:
|
|
|
178
178
|
| `realtimeChannel` | How far each realtime channel has been pruned, to tell a client it missed messages |
|
|
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
|
+
| `dashboardWidget` | Widgets extensions add to the admin dashboard: element, module script, size limits, permission |
|
|
181
182
|
|
|
182
183
|
## Frontend SDK
|
|
183
184
|
|
|
@@ -356,6 +357,10 @@ Settings can be managed through the admin panel at `/admin/settings` or the SDK.
|
|
|
356
357
|
|
|
357
358
|
Extensions can push live updates to browsers: declare a channel, publish to it from any server code, and every subscribed browser on any kempo process receives it. Channels can persist messages so a client that reconnects replays what it missed, and a browser client handles connecting and reconnecting. Browsers can also send to a channel, where the extension's own handler receives and answers it, and extensions observe connections through hooks and act on them through SDK functions, so interactive features (chat, collaboration, games) can be built as extensions without touching a socket. Fast traffic can use an in-memory channel that skips the database, and connections are limited and backpressured. It uses a Postgres `LISTEN`/`NOTIFY` bus, so it needs no extra service. See [docs/realtime.md](docs/realtime.md).
|
|
358
359
|
|
|
360
|
+
## Admin Dashboard
|
|
361
|
+
|
|
362
|
+
The admin home page is a grid of widgets one to four blocks wide and one to three tall. Kempo provides widgets for pages, content, users, notifications and extensions; extensions add their own with `kempo.widgets` or `registerDashboardWidget`. Each person chooses which are shown, their order and size, kept in their browser's localStorage, and a widget only loads its script and data once it is shown. Existing sites need `npx drizzle-kit push` to add the `dashboardWidget` table (until then only Kempo's own widgets appear). See [docs/dashboard.md](docs/dashboard.md).
|
|
363
|
+
|
|
359
364
|
## Notifications
|
|
360
365
|
|
|
361
366
|
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).
|
|
@@ -0,0 +1,67 @@
|
|
|
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 l,serializeLayout as o,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=l(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=o(t,{stored:this.#i?null:d(),unavailable:this.#e});this.layout=t,JSON.stringify(e)===JSON.stringify(o(l(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=l(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
|
+
<k-sortable-item class="mbh" data-widget-id=${t.id}>
|
|
3
|
+
<div style="display: flex; flex-wrap: nowrap; align-items: center; gap: var(--spacer); width: ${m}; min-height: 3.5rem;">
|
|
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}
|
|
6
|
+
<strong class=${s?"":"tc-muted"}>${t.title}</strong>
|
|
7
|
+
</div>
|
|
8
|
+
<select style="width: 4.5rem;" aria-label="${t.title} width" ?disabled=${!s} @change=${e=>this.changeDraft(t.id,{width:Number(e.target.value)})}>
|
|
9
|
+
${u(t.minWidth,t.maxWidth).map(t=>e`<option value=${t} ?selected=${t===a}>${t}</option>`)}
|
|
10
|
+
</select>
|
|
11
|
+
<select style="width: 4.5rem;" aria-label="${t.title} height" ?disabled=${!s} @change=${e=>this.changeDraft(t.id,{height:Number(e.target.value)})}>
|
|
12
|
+
${u(t.minHeight,t.maxHeight).map(t=>e`<option value=${t} ?selected=${t===n}>${t}</option>`)}
|
|
13
|
+
</select>
|
|
14
|
+
<button
|
|
15
|
+
class="no-btn pq"
|
|
16
|
+
style="width: 2.5rem;"
|
|
17
|
+
title=${s?"Hide":"Show"}
|
|
18
|
+
aria-label="${s?"Hide":"Show"} ${t.title}"
|
|
19
|
+
@click=${()=>this.changeDraft(t.id,{visible:!s})}
|
|
20
|
+
><k-icon name=${s?"hide":"show"}></k-icon></button>
|
|
21
|
+
</div>
|
|
22
|
+
</k-sortable-item>
|
|
23
|
+
`;renderDraftHeader=()=>e`
|
|
24
|
+
<div class="small tc-muted mbh" style="display: flex; flex-wrap: nowrap; gap: var(--spacer); width: calc(${m} + 3.7rem); padding-left: 3.7rem;">
|
|
25
|
+
<span class="flex">Widget</span>
|
|
26
|
+
<span style="width: 4.5rem;">Width</span>
|
|
27
|
+
<span style="width: 4.5rem;">Height</span>
|
|
28
|
+
<span style="width: 2.5rem;"></span>
|
|
29
|
+
</div>
|
|
30
|
+
`;renderCustomizeDialog=()=>e`
|
|
31
|
+
<k-dialog
|
|
32
|
+
confirm-text="Save"
|
|
33
|
+
confirm-classes="primary ml"
|
|
34
|
+
cancel-text="Cancel"
|
|
35
|
+
.confirmAction=${this.save}
|
|
36
|
+
>
|
|
37
|
+
<h5 slot="title" class="pyh px m0">Customize dashboard</h5>
|
|
38
|
+
<div class="p">
|
|
39
|
+
<p class="small tc-muted">Drag to reorder. Hidden widgets are not loaded. Sizes are in blocks; the dashboard is four blocks wide.</p>
|
|
40
|
+
${this.renderDraftHeader()}
|
|
41
|
+
${a(this.sortVersion,e`
|
|
42
|
+
<k-sortable @sort=${this.handleSort}>${this.draft.map(this.renderDraftRow)}</k-sortable>
|
|
43
|
+
`)}
|
|
44
|
+
<button class="mt" @click=${this.resetDraft}><k-icon name="restart_alt"></k-icon> Reset to defaults</button>
|
|
45
|
+
</div>
|
|
46
|
+
</k-dialog>
|
|
47
|
+
`;renderCell=({widget:t,width:i,height:s})=>{const a=this.#s.get(t.id),n=this.failures[t.id];return e`
|
|
48
|
+
<div
|
|
49
|
+
data-widget-id=${t.id}
|
|
50
|
+
style="grid-column: span ${Math.min(i,this.columns)}; grid-row: span ${s}; min-width: 0;"
|
|
51
|
+
>
|
|
52
|
+
<section class="b r" style="height: 100%; min-height: 0; overflow: hidden;">
|
|
53
|
+
${n?e`<p class="p tc-danger" role="alert"><strong>${t.title}</strong> ${n}</p>`:a??e`<p class="p tc-muted"><strong>${t.title}</strong> Loading…</p>`}
|
|
54
|
+
</section>
|
|
55
|
+
</div>
|
|
56
|
+
`};renderLightDom(){if(this.failure)return e`<p class="tc-danger" role="alert">${this.failure}</p>`;if(this.loading)return e`<p class="tc-muted">Loading…</p>`;const t=this.layout.filter(t=>t.visible);return e`
|
|
57
|
+
<div class="d-f mb" style="align-items: center;">
|
|
58
|
+
<h1 class="flex m0">${this.heading}</h1>
|
|
59
|
+
<button @click=${this.openCustomize}><k-icon name="edit"></k-icon> Customize</button>
|
|
60
|
+
</div>
|
|
61
|
+
${t.length?i:e`<p class="tc-muted">No widgets are shown. Choose Customize to add some.</p>`}
|
|
62
|
+
<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);">
|
|
63
|
+
${s(t,t=>t.widget.id,this.renderCell)}
|
|
64
|
+
</div>
|
|
65
|
+
${this.renderCustomizeDialog()}
|
|
66
|
+
`}}customElements.define("k-dashboard",p);
|
|
67
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\Dashboard.js.map
|
|
@@ -0,0 +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":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export const STORAGE_KEY="kempo:admin:dashboard";const e=(e,t,i,r)=>Number.isInteger(e)?Math.min(Math.max(e,t),i):r;export const resolveLayout=(t,i)=>{const r=new Map(t.map(e=>[e.id,e])),d=i&&"object"==typeof i?i:{},s=(Array.isArray(d.order)?d.order:[]).filter((e,t,i)=>r.has(e)&&i.indexOf(e)===t),o=new Set(s),a=new Set(Array.isArray(d.hidden)?d.hidden:[]),n=d.sizes&&"object"==typeof d.sizes?d.sizes:{};return[...s,...t.map(e=>e.id).filter(e=>!o.has(e))].map(t=>{const i=r.get(t),d=n[t]&&"object"==typeof n[t]?n[t]:{};return{widget:i,visible:o.has(t)?!a.has(t):!1!==i.defaultVisible,width:e(d.width,i.minWidth,i.maxWidth,i.defaultWidth),height:e(d.height,i.minHeight,i.maxHeight,i.defaultHeight)}})};export const serializeLayout=(e,{stored:t,unavailable:i=[]}={})=>{const r=e.map(({widget:e})=>e.id),d=e.filter(({visible:e})=>!e).map(({widget:e})=>e.id),s=Object.fromEntries(e.filter(({widget:e,width:t,height:i})=>t!==e.defaultWidth||i!==e.defaultHeight).map(({widget:e,width:t,height:i})=>[e.id,{width:t,height:i}])),o=t&&"object"==typeof t?t:{},a=Array.isArray(o.order)?o.order:[],n=new Set(i);return a.forEach((e,t)=>{if(!n.has(e)||r.includes(e))return;const i=a.slice(0,t).reverse().find(e=>r.includes(e));r.splice(void 0===i?0:r.indexOf(i)+1,0,e),Array.isArray(o.hidden)&&o.hidden.includes(e)&&d.push(e),o.sizes?.[e]&&(s[e]=o.sizes[e])}),{order:r,hidden:d,sizes:s}};export const reorder=(e,t)=>{const i=e=>-1===t.indexOf(e)?t.length:t.indexOf(e);return e.slice().sort((e,t)=>i(e.widget.id)-i(t.widget.id))};export const readStored=()=>{try{return JSON.parse(localStorage.getItem(STORAGE_KEY))}catch{return null}};export const writeStored=e=>{try{localStorage.setItem(STORAGE_KEY,JSON.stringify(e))}catch{}};export const clearStored=()=>{try{localStorage.removeItem(STORAGE_KEY)}catch{}};
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\dashboardLayout.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\dashboardLayout.js","names":["STORAGE_KEY","clamp","value","min","max","fallback","Number","isInteger","Math","resolveLayout","widgets","stored","byId","Map","map","widget","id","remembered","order","Array","isArray","filter","index","all","has","indexOf","known","Set","hidden","sizes","get","size","visible","defaultVisible","width","minWidth","maxWidth","defaultWidth","height","minHeight","maxHeight","defaultHeight","serializeLayout","layout","unavailable","Object","fromEntries","previous","previousOrder","kept","forEach","includes","before","slice","reverse","find","other","splice","undefined","push","reorder","ids","position","length","sort","a","b","readStored","JSON","parse","localStorage","getItem","writeStored","setItem","stringify","clearStored","removeItem"],"sources":["0"],"sourcesContent":["export const STORAGE_KEY = 'kempo:admin:dashboard';\r\n\r\nconst clamp = (value, min, max, fallback) => Number.isInteger(value) ? Math.min(Math.max(value, min), max) : fallback;\r\n\r\n/*\r\n A layout is an ordered array of `{ widget, visible, width, height }`, one per widget the server\r\n offered. What the browser remembers is smaller (see `serializeLayout`) and is reconciled with what the\r\n server offers on every load, so a widget that is new, gone, or resized in the definition never needs a\r\n migration.\r\n*/\r\n\r\n/*\r\n Merges the server's widget list with what the browser remembered. Widgets it has not seen before are\r\n appended in the server's order, shown or not according to `defaultVisible`; remembered ids the server no\r\n longer offers are dropped; a remembered size is clamped to the widget's current bounds.\r\n*/\r\nexport const resolveLayout = (widgets, stored) => {\r\n const byId = new Map(widgets.map(widget => [widget.id, widget]));\r\n const remembered = stored && typeof stored === 'object' ? stored : {};\r\n const order = (Array.isArray(remembered.order) ? remembered.order : [])\r\n .filter((id, index, all) => byId.has(id) && all.indexOf(id) === index);\r\n const known = new Set(order);\r\n const hidden = new Set(Array.isArray(remembered.hidden) ? remembered.hidden : []);\r\n const sizes = remembered.sizes && typeof remembered.sizes === 'object' ? remembered.sizes : {};\r\n\r\n return [...order, ...widgets.map(widget => widget.id).filter(id => !known.has(id))].map(id => {\r\n const widget = byId.get(id);\r\n const size = sizes[id] && typeof sizes[id] === 'object' ? sizes[id] : {};\r\n return {\r\n widget,\r\n visible: known.has(id) ? !hidden.has(id) : widget.defaultVisible !== false,\r\n width: clamp(size.width, widget.minWidth, widget.maxWidth, widget.defaultWidth),\r\n height: clamp(size.height, widget.minHeight, widget.maxHeight, widget.defaultHeight)\r\n };\r\n });\r\n};\r\n\r\n/*\r\n What gets stored: the full order, the hidden ids, and only the sizes that differ from the widget's\r\n default, so a user who never resized a widget follows it when its default changes.\r\n\r\n `stored` is what was remembered before and `unavailable` the ids the server says exist but are not on\r\n offer right now (a disabled extension, a permission the user lost). Their entries are carried over so the\r\n widget comes back where it was: each is placed straight after the id that preceded it before, and its\r\n hidden state and size are kept. Anything else not in the layout is dropped, which is how an uninstalled\r\n extension is cleaned up.\r\n*/\r\nexport const serializeLayout = (layout, { stored, unavailable = [] } = {}) => {\r\n const order = layout.map(({ widget }) => widget.id);\r\n const hidden = layout.filter(({ visible }) => !visible).map(({ widget }) => widget.id);\r\n const sizes = Object.fromEntries(layout\r\n .filter(({ widget, width, height }) => width !== widget.defaultWidth || height !== widget.defaultHeight)\r\n .map(({ widget, width, height }) => [widget.id, { width, height }]));\r\n\r\n const previous = stored && typeof stored === 'object' ? stored : {};\r\n const previousOrder = Array.isArray(previous.order) ? previous.order : [];\r\n const kept = new Set(unavailable);\r\n\r\n previousOrder.forEach((id, index) => {\r\n if(!kept.has(id) || order.includes(id)) return;\r\n const before = previousOrder.slice(0, index).reverse().find(other => order.includes(other));\r\n order.splice(before === undefined ? 0 : order.indexOf(before) + 1, 0, id);\r\n if(Array.isArray(previous.hidden) && previous.hidden.includes(id)) hidden.push(id);\r\n if(previous.sizes?.[id]) sizes[id] = previous.sizes[id];\r\n });\r\n\r\n return { order, hidden, sizes };\r\n};\r\n\r\n/*\r\n Puts the layout in the order of `ids`. Entries whose id is not listed keep their relative order after\r\n the listed ones. Returns a new array.\r\n*/\r\nexport const reorder = (layout, ids) => {\r\n const position = id => ids.indexOf(id) === -1 ? ids.length : ids.indexOf(id);\r\n return layout.slice().sort((a, b) => position(a.widget.id) - position(b.widget.id));\r\n};\r\n\r\nexport const readStored = () => {\r\n try {\r\n return JSON.parse(localStorage.getItem(STORAGE_KEY));\r\n } catch {\r\n return null;\r\n }\r\n};\r\n\r\nexport const writeStored = value => {\r\n try {\r\n localStorage.setItem(STORAGE_KEY, JSON.stringify(value));\r\n } catch {}\r\n};\r\n\r\nexport const clearStored = () => {\r\n try {\r\n localStorage.removeItem(STORAGE_KEY);\r\n } catch {}\r\n};\r\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import t from"/kempo/components/DashboardWidget.js";import{html as e}from"/kempo-ui/lit-all.min.js";import{listTemplates as a,listFragments as o,listGlobalContent as n}from"/kempo/sdk.js";export default class s extends t{static properties={...t.properties,counts:{state:!0}};constructor(){super(),this.counts=[],this.cta={label:"Manage content",icon:"content",href:"/admin/content/templates"}}load=async()=>{const[t,e,s]=await Promise.all([this.unwrap(a()),this.unwrap(o()),this.unwrap(n())]);this.counts=[{label:"Templates",total:t.total,href:"/admin/content/templates"},{label:"Fragments",total:e.total,href:"/admin/content/fragments"},{label:"Global content",total:s.total,href:"/admin/content/globals"}]};renderContent(){return this.counts.some(({total:t})=>t)?e`${this.counts.map(({label:t,total:a,href:o})=>e`
|
|
2
|
+
<a class="d-f pyq bb" href=${o}><span class="flex">${t}</span><strong>${a}</strong></a>
|
|
3
|
+
`)}`:e`<p class="tc-muted">No templates, fragments or global content yet.</p>`}}customElements.define("k-widget-content",s);
|
|
4
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\widgets\Content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\widgets\\Content.js","names":["DashboardWidget","html","listTemplates","listFragments","listGlobalContent","ContentWidget","static","properties","counts","state","constructor","super","this","cta","label","icon","href","load","async","templates","fragments","globals","Promise","all","unwrap","total","renderContent","some","map","customElements","define"],"sources":["0"],"sourcesContent":["import DashboardWidget from '/kempo/components/DashboardWidget.js';\r\nimport { html } from '/kempo-ui/lit-all.min.js';\r\nimport { listTemplates, listFragments, listGlobalContent } from '/kempo/sdk.js';\r\n\r\n/*\r\n How many templates, fragments and global content entries the site has, each linking to its manager.\r\n*/\r\nexport default class ContentWidget extends DashboardWidget {\r\n static properties = {\r\n ...DashboardWidget.properties,\r\n counts: { state: true }\r\n };\r\n\r\n constructor(){\r\n super();\r\n this.counts = [];\r\n this.cta = { label: 'Manage content', icon: 'content', href: '/admin/content/templates' };\r\n }\r\n\r\n /*\r\n Public Methods\r\n */\r\n\r\n load = async () => {\r\n const [templates, fragments, globals] = await Promise.all([\r\n this.unwrap(listTemplates()),\r\n this.unwrap(listFragments()),\r\n this.unwrap(listGlobalContent())\r\n ]);\r\n this.counts = [\r\n { label: 'Templates', total: templates.total, href: '/admin/content/templates' },\r\n { label: 'Fragments', total: fragments.total, href: '/admin/content/fragments' },\r\n { label: 'Global content', total: globals.total, href: '/admin/content/globals' }\r\n ];\r\n };\r\n\r\n /*\r\n Rendering\r\n */\r\n\r\n renderContent(){\r\n if(!this.counts.some(({ total }) => total)) return html`<p class=\"tc-muted\">No templates, fragments or global content yet.</p>`;\r\n return html`${this.counts.map(({ label, total, href }) => html`\r\n <a class=\"d-f pyq bb\" href=${href}><span class=\"flex\">${label}</span><strong>${total}</strong></a>\r\n `)}`;\r\n }\r\n}\r\n\r\ncustomElements.define('k-widget-content', ContentWidget);\r\n"],"mappings":"OAAOA,MAAqB,sDACnBC,MAAY,mDACZC,mBAAeC,uBAAeC,MAAyB,+BAKjD,MAAMC,UAAsBL,EACzCM,kBAAoB,IACfN,EAAgBO,WACnBC,OAAQ,CAAEC,OAAO,IAGnB,WAAAC,GACEC,QACAC,KAAKJ,OAAS,GACdI,KAAKC,IAAM,CAAEC,MAAO,iBAAkBC,KAAM,UAAWC,KAAM,2BAC/D,CAMAC,KAAOC,UACL,MAAOC,EAAWC,EAAWC,SAAiBC,QAAQC,IAAI,CACxDX,KAAKY,OAAOtB,KACZU,KAAKY,OAAOrB,KACZS,KAAKY,OAAOpB,OAEdQ,KAAKJ,OAAS,CACZ,CAAEM,MAAO,YAAaW,MAAON,EAAUM,MAAOT,KAAM,4BACpD,CAAEF,MAAO,YAAaW,MAAOL,EAAUK,MAAOT,KAAM,4BACpD,CAAEF,MAAO,iBAAkBW,MAAOJ,EAAQI,MAAOT,KAAM,4BAQ3D,aAAAU,GACE,OAAId,KAAKJ,OAAOmB,KAAK,EAAGF,WAAYA,GAC7BxB,CAAI,GAAGW,KAAKJ,OAAOoB,IAAI,EAAGd,QAAOW,QAAOT,UAAWf,CAAI;mCAC/Be,wBAA2BF,mBAAuBW;SAF9BxB,CAAI,wEAIzD,EAGF4B,eAAeC,OAAO,mBAAoBzB","ignoreList":[]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import t from"/kempo/components/DashboardWidget.js";import{html as s}from"/kempo-ui/lit-all.min.js";import{listExtensions as e}from"/kempo/sdk.js";export default class i extends t{static properties={...t.properties,total:{state:!0},disabled:{state:!0}};constructor(){super(),this.total=0,this.disabled=0,this.cta={label:"Add extension",icon:"extension_add",permission:"system:extensions:install",href:"/admin/extensions"}}load=async()=>{const{items:t,total:s}=await this.unwrap(e({limit:100}));this.total=s,this.disabled=t.filter(t=>!t.enabled).length};renderContent(){return this.total?s`
|
|
2
|
+
<div class="h1 m0">${this.total}</div>
|
|
3
|
+
<div class="tc-muted">${1===this.total?"Extension":"Extensions"}${this.disabled?s`, ${this.disabled} disabled`:""}</div>
|
|
4
|
+
`:s`<p class="tc-muted">No extensions installed.</p>`}}customElements.define("k-widget-extensions",i);
|
|
5
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\widgets\Extensions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\widgets\\Extensions.js","names":["DashboardWidget","html","listExtensions","ExtensionsWidget","static","properties","total","state","disabled","constructor","super","this","cta","label","icon","permission","href","load","async","items","unwrap","limit","filter","item","enabled","length","renderContent","customElements","define"],"sources":["0"],"sourcesContent":["import DashboardWidget from '/kempo/components/DashboardWidget.js';\r\nimport { html } from '/kempo-ui/lit-all.min.js';\r\nimport { listExtensions } from '/kempo/sdk.js';\r\n\r\n/*\r\n How many extensions are installed and how many of those are switched off.\r\n*/\r\nexport default class ExtensionsWidget extends DashboardWidget {\r\n static properties = {\r\n ...DashboardWidget.properties,\r\n total: { state: true },\r\n disabled: { state: true }\r\n };\r\n\r\n constructor(){\r\n super();\r\n this.total = 0;\r\n this.disabled = 0;\r\n this.cta = { label: 'Add extension', icon: 'extension_add', permission: 'system:extensions:install', href: '/admin/extensions' };\r\n }\r\n\r\n /*\r\n Public Methods\r\n */\r\n\r\n load = async () => {\r\n const { items, total } = await this.unwrap(listExtensions({ limit: 100 }));\r\n this.total = total;\r\n this.disabled = items.filter(item => !item.enabled).length;\r\n };\r\n\r\n /*\r\n Rendering\r\n */\r\n\r\n renderContent(){\r\n if(!this.total) return html`<p class=\"tc-muted\">No extensions installed.</p>`;\r\n return html`\r\n <div class=\"h1 m0\">${this.total}</div>\r\n <div class=\"tc-muted\">${this.total === 1 ? 'Extension' : 'Extensions'}${this.disabled ? html`, ${this.disabled} disabled` : ''}</div>\r\n `;\r\n }\r\n}\r\n\r\ncustomElements.define('k-widget-extensions', ExtensionsWidget);\r\n"],"mappings":"OAAOA,MAAqB,sDACnBC,MAAY,oDACZC,MAAsB,+BAKhB,MAAMC,UAAyBH,EAC5CI,kBAAoB,IACfJ,EAAgBK,WACnBC,MAAO,CAAEC,OAAO,GAChBC,SAAU,CAAED,OAAO,IAGrB,WAAAE,GACEC,QACAC,KAAKL,MAAQ,EACbK,KAAKH,SAAW,EAChBG,KAAKC,IAAM,CAAEC,MAAO,gBAAiBC,KAAM,gBAAiBC,WAAY,4BAA6BC,KAAM,oBAC7G,CAMAC,KAAOC,UACL,MAAMC,MAAEA,EAAKb,MAAEA,SAAgBK,KAAKS,OAAOlB,EAAe,CAAEmB,MAAO,OACnEV,KAAKL,MAAQA,EACbK,KAAKH,SAAWW,EAAMG,OAAOC,IAASA,EAAKC,SAASC,QAOtD,aAAAC,GACE,OAAIf,KAAKL,MACFL,CAAI;2BACYU,KAAKL;8BACa,IAAfK,KAAKL,MAAc,YAAc,eAAeK,KAAKH,SAAWP,CAAI,KAAKU,KAAKH,oBAAsB;MAHvGP,CAAI,kDAK7B,EAGF0B,eAAeC,OAAO,sBAAuBzB","ignoreList":[]}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import t from"/kempo/components/DashboardWidget.js";import{html as i}from"/kempo-ui/lit-all.min.js";import"/kempo/components/NotificationList.js";import{markAllNotificationsRead as o}from"/kempo/sdk.js";export default class e extends t{constructor(){super(),this.cta={label:"Mark all read",icon:"done_all",onClick:()=>this.markAllRead()}}markAllRead=async()=>{await o(),await(this.querySelector("k-notification-list")?.refresh())};renderContent(){return i`
|
|
2
|
+
<k-notification-list compact unread-only page-size=${3*this.height}></k-notification-list>
|
|
3
|
+
`}}customElements.define("k-widget-notifications",e);
|
|
4
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\widgets\Notifications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\widgets\\Notifications.js","names":["DashboardWidget","html","markAllNotificationsRead","NotificationsWidget","constructor","super","this","cta","label","icon","onClick","markAllRead","async","querySelector","refresh","renderContent","height","customElements","define"],"sources":["0"],"sourcesContent":["import DashboardWidget from '/kempo/components/DashboardWidget.js';\r\nimport { html } from '/kempo-ui/lit-all.min.js';\r\nimport '/kempo/components/NotificationList.js';\r\nimport { markAllNotificationsRead } from '/kempo/sdk.js';\r\n\r\n/*\r\n The signed-in user's unread notifications. The list loads itself, so this widget has nothing to fetch.\r\n*/\r\nexport default class NotificationsWidget extends DashboardWidget {\r\n constructor(){\r\n super();\r\n this.cta = { label: 'Mark all read', icon: 'done_all', onClick: () => this.markAllRead() };\r\n }\r\n\r\n /*\r\n Event Handlers\r\n */\r\n\r\n markAllRead = async () => {\r\n await markAllNotificationsRead();\r\n await this.querySelector('k-notification-list')?.refresh();\r\n };\r\n\r\n /*\r\n Rendering\r\n */\r\n\r\n renderContent(){\r\n return html`\r\n <k-notification-list compact unread-only page-size=${this.height * 3}></k-notification-list>\r\n `;\r\n }\r\n}\r\n\r\ncustomElements.define('k-widget-notifications', NotificationsWidget);\r\n"],"mappings":"OAAOA,MAAqB,sDACnBC,MAAY,iCACd,2EACEC,MAAgC,+BAK1B,MAAMC,UAA4BH,EAC/C,WAAAI,GACEC,QACAC,KAAKC,IAAM,CAAEC,MAAO,gBAAiBC,KAAM,WAAYC,QAAS,IAAMJ,KAAKK,cAC7E,CAMAA,YAAcC,gBACNV,UACAI,KAAKO,cAAc,wBAAwBC,YAOnD,aAAAC,GACE,OAAOd,CAAI;2DAC0D,EAAdK,KAAKU;KAE9D,EAGFC,eAAeC,OAAO,yBAA0Bf","ignoreList":[]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import e from"/kempo/components/DashboardWidget.js";import{html as t,nothing as a}from"/kempo-ui/lit-all.min.js";import{listPages as s,listTemplates as i,createPage as l}from"/kempo/sdk.js";export default class d extends e{static properties={...e.properties,total:{state:!0},disabled:{state:!0},recent:{state:!0}};constructor(){super(),this.total=0,this.disabled=0,this.recent=[],this.cta={label:"New page",icon:"add",permission:"system:pages:create",onClick:()=>this.newPage()}}load=async()=>{const{pages:e,total:t}=await this.unwrap(s({showHidden:!0}));this.total=t,this.disabled=e.filter(e=>e.disabled).length,this.recent=e.slice().sort((e,t)=>(Date.parse(t.updatedAt)||0)-(Date.parse(e.updatedAt)||0))};newPage=async()=>{const{templates:e}=await this.unwrap(i());this.openForm({title:"New page",confirmText:"Create",fields:[{name:"name",label:"Page name",placeholder:"My new page"},{name:"template",label:"Template",type:"select",value:"default",options:e.map(({name:e})=>({value:e,label:e}))}],submit:async({name:e,template:t})=>{const[a,s]=await l({directory:"",name:e,template:t});if(a)return a.msg||"Could not create the page.";window.location.href=`/admin/content/pages/edit?path=${encodeURIComponent(s.file.replace(/\.page\.html$/,""))}`}})};renderHeader(){return t`
|
|
2
|
+
<header class="d-f px pyh bb" style="align-items: center;">
|
|
3
|
+
${this.renderTitle()}
|
|
4
|
+
${this.renderActions()}
|
|
5
|
+
<button class="no-btn pq" title="Refresh" aria-label="Refresh pages" @click=${this.refresh}><k-icon name="restart_alt"></k-icon></button>
|
|
6
|
+
</header>
|
|
7
|
+
`}renderContent(){const e=this.recent.slice(0,3*this.height);return t`
|
|
8
|
+
<div class="d-f mb">
|
|
9
|
+
<div class="flex"><div class="h2 m0">${this.total}</div><div class="small tc-muted">Pages</div></div>
|
|
10
|
+
<div class="flex"><div class="h2 m0">${this.total-this.disabled}</div><div class="small tc-muted">Enabled</div></div>
|
|
11
|
+
<div class="flex"><div class="h2 m0">${this.disabled}</div><div class="small tc-muted">Disabled</div></div>
|
|
12
|
+
</div>
|
|
13
|
+
${e.length?t`
|
|
14
|
+
<div class="small tc-muted mbq">Recently updated</div>
|
|
15
|
+
${e.map(e=>t`
|
|
16
|
+
<a class="d-b py bb" href="/admin/content/pages/edit?path=${encodeURIComponent(e.file.replace(/\.page(-disabled)?\.html$/,""))}">
|
|
17
|
+
${e.title||e.name||e.url}
|
|
18
|
+
<span class="small tc-muted">${e.url}</span>
|
|
19
|
+
</a>
|
|
20
|
+
`)}
|
|
21
|
+
`:a}
|
|
22
|
+
`}}customElements.define("k-widget-pages",d);
|
|
23
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\widgets\Pages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\widgets\\Pages.js","names":["DashboardWidget","html","nothing","listPages","listTemplates","createPage","PagesWidget","static","properties","total","state","disabled","recent","constructor","super","this","cta","label","icon","permission","onClick","newPage","load","async","pages","unwrap","showHidden","filter","page","length","slice","sort","a","b","Date","parse","updatedAt","templates","openForm","title","confirmText","fields","name","placeholder","type","value","options","map","submit","template","error","data","directory","msg","window","location","href","encodeURIComponent","file","replace","renderHeader","renderTitle","renderActions","refresh","renderContent","rows","height","url","customElements","define"],"sources":["0"],"sourcesContent":["import DashboardWidget from '/kempo/components/DashboardWidget.js';\nimport { html, nothing } from '/kempo-ui/lit-all.min.js';\nimport { listPages, listTemplates, createPage } from '/kempo/sdk.js';\n\n/*\n Page totals and the pages edited most recently. Shows more rows the taller the tile is.\n*/\nexport default class PagesWidget extends DashboardWidget {\n static properties = {\n ...DashboardWidget.properties,\n total: { state: true },\n disabled: { state: true },\n recent: { state: true }\n };\n\n constructor(){\n super();\n this.total = 0;\n this.disabled = 0;\n this.recent = [];\n this.cta = { label: 'New page', icon: 'add', permission: 'system:pages:create', onClick: () => this.newPage() };\n }\n\n /*\n Public Methods\n */\n\n load = async () => {\n const { pages, total } = await this.unwrap(listPages({ showHidden: true }));\n this.total = total;\n this.disabled = pages.filter(page => page.disabled).length;\n this.recent = pages.slice().sort((a, b) => (Date.parse(b.updatedAt) || 0) - (Date.parse(a.updatedAt) || 0));\n };\n\n /*\n Event Handlers\n */\n\n /*\n Creates a page at the site root from a template and opens it in the editor. The Pages screen has the\n full dialog, including choosing another directory.\n */\n newPage = async () => {\n const { templates } = await this.unwrap(listTemplates());\n this.openForm({\n title: 'New page',\n confirmText: 'Create',\n fields: [\n { name: 'name', label: 'Page name', placeholder: 'My new page' },\n { name: 'template', label: 'Template', type: 'select', value: 'default', options: templates.map(({ name }) => ({ value: name, label: name })) }\n ],\n submit: async ({ name, template }) => {\n const [error, data] = await createPage({ directory: '', name, template });\n if(error) return error.msg || 'Could not create the page.';\n window.location.href = `/admin/content/pages/edit?path=${encodeURIComponent(data.file.replace(/\\.page\\.html$/, ''))}`;\n }\n });\n };\n\n /*\n Rendering\n */\n\n renderHeader(){\n return html`\n <header class=\"d-f px pyh bb\" style=\"align-items: center;\">\n ${this.renderTitle()}\n ${this.renderActions()}\n <button class=\"no-btn pq\" title=\"Refresh\" aria-label=\"Refresh pages\" @click=${this.refresh}><k-icon name=\"restart_alt\"></k-icon></button>\n </header>\n `;\n }\n\n renderContent(){\n const rows = this.recent.slice(0, this.height * 3);\n return html`\n <div class=\"d-f mb\">\n <div class=\"flex\"><div class=\"h2 m0\">${this.total}</div><div class=\"small tc-muted\">Pages</div></div>\n <div class=\"flex\"><div class=\"h2 m0\">${this.total - this.disabled}</div><div class=\"small tc-muted\">Enabled</div></div>\n <div class=\"flex\"><div class=\"h2 m0\">${this.disabled}</div><div class=\"small tc-muted\">Disabled</div></div>\n </div>\n ${rows.length ? html`\n <div class=\"small tc-muted mbq\">Recently updated</div>\n ${rows.map(page => html`\n <a class=\"d-b py bb\" href=\"/admin/content/pages/edit?path=${encodeURIComponent(page.file.replace(/\\.page(-disabled)?\\.html$/, ''))}\">\n ${page.title || page.name || page.url}\n <span class=\"small tc-muted\">${page.url}</span>\n </a>\n `)}\n ` : nothing}\n `;\n }\n}\n\ncustomElements.define('k-widget-pages', PagesWidget);\n"],"mappings":"OAAOA,MAAqB,sDACnBC,aAAMC,MAAe,+CACrBC,mBAAWC,gBAAeC,MAAkB,+BAKtC,MAAMC,UAAoBN,EACvCO,kBAAoB,IACfP,EAAgBQ,WACnBC,MAAO,CAAEC,OAAO,GAChBC,SAAU,CAAED,OAAO,GACnBE,OAAQ,CAAEF,OAAO,IAGnB,WAAAG,GACEC,QACAC,KAAKN,MAAQ,EACbM,KAAKJ,SAAW,EAChBI,KAAKH,OAAS,GACdG,KAAKC,IAAM,CAAEC,MAAO,WAAYC,KAAM,MAAOC,WAAY,sBAAuBC,QAAS,IAAML,KAAKM,UACtG,CAMAC,KAAOC,UACL,MAAMC,MAAEA,EAAKf,MAAEA,SAAgBM,KAAKU,OAAOtB,EAAU,CAAEuB,YAAY,KACnEX,KAAKN,MAAQA,EACbM,KAAKJ,SAAWa,EAAMG,OAAOC,GAAQA,EAAKjB,UAAUkB,OACpDd,KAAKH,OAASY,EAAMM,QAAQC,KAAK,CAACC,EAAGC,KAAOC,KAAKC,MAAMF,EAAEG,YAAc,IAAMF,KAAKC,MAAMH,EAAEI,YAAc,KAW1Gf,QAAUE,UACR,MAAMc,UAAEA,SAAoBtB,KAAKU,OAAOrB,KACxCW,KAAKuB,SAAS,CACZC,MAAO,WACPC,YAAa,SACbC,OAAQ,CACN,CAAEC,KAAM,OAAQzB,MAAO,YAAa0B,YAAa,eACjD,CAAED,KAAM,WAAYzB,MAAO,WAAY2B,KAAM,SAAUC,MAAO,UAAWC,QAAST,EAAUU,IAAI,EAAGL,WAAW,CAAGG,MAAOH,EAAMzB,MAAOyB,OAEvIM,OAAQzB,OAASmB,OAAMO,eACrB,MAAOC,EAAOC,SAAc9C,EAAW,CAAE+C,UAAW,GAAIV,OAAMO,aAC9D,GAAGC,EAAO,OAAOA,EAAMG,KAAO,6BAC9BC,OAAOC,SAASC,KAAO,kCAAkCC,mBAAmBN,EAAKO,KAAKC,QAAQ,gBAAiB,WASrH,YAAAC,GACE,OAAO3D,CAAI;;UAELc,KAAK8C;UACL9C,KAAK+C;sFACuE/C,KAAKgD;;KAGzF,CAEA,aAAAC,GACE,MAAMC,EAAOlD,KAAKH,OAAOkB,MAAM,EAAiB,EAAdf,KAAKmD,QACvC,OAAOjE,CAAI;;+CAEgCc,KAAKN;+CACLM,KAAKN,MAAQM,KAAKJ;+CAClBI,KAAKJ;;QAE5CsD,EAAKpC,OAAS5B,CAAI;;UAEhBgE,EAAKlB,IAAInB,GAAQ3B,CAAI;sEACuCwD,mBAAmB7B,EAAK8B,KAAKC,QAAQ,4BAA6B;cAC1H/B,EAAKW,OAASX,EAAKc,MAAQd,EAAKuC;2CACHvC,EAAKuC;;;QAGtCjE;KAER,EAGFkE,eAAeC,OAAO,iBAAkB/D","ignoreList":[]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import e from"/kempo/components/DashboardWidget.js";import{html as t}from"/kempo-ui/lit-all.min.js";import{getUsers as s,createUser as a}from"/kempo/sdk.js";export default class i extends e{static properties={...e.properties,total:{state:!0}};constructor(){super(),this.total=0,this.cta={label:"Add user",icon:"person_add",permission:"system:user:create",onClick:()=>this.addUser()}}load=async()=>{this.total=(await this.unwrap(s({limit:1}))).total};addUser=()=>this.openForm({title:"Add user",confirmText:"Create",fields:[{name:"name",label:"Name"},{name:"email",label:"Email",type:"email"},{name:"password",label:"Password",type:"password"}],submit:async({name:e,email:t,password:s})=>{const[i]=await a({name:e,email:t,password:s,emailVerified:!0});if(i)return i.msg||"Could not create the user.";await this.refresh()}});renderContent(){return t`
|
|
2
|
+
<div class="h1 m0">${this.total}</div>
|
|
3
|
+
<div class="tc-muted">${1===this.total?"User":"Users"}</div>
|
|
4
|
+
`}}customElements.define("k-widget-users",i);
|
|
5
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\admin\components\widgets\Users.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\admin\\components\\widgets\\Users.js","names":["DashboardWidget","html","getUsers","createUser","UsersWidget","static","properties","total","state","constructor","super","this","cta","label","icon","permission","onClick","addUser","load","async","unwrap","limit","openForm","title","confirmText","fields","name","type","submit","email","password","error","emailVerified","msg","refresh","renderContent","customElements","define"],"sources":["0"],"sourcesContent":["import DashboardWidget from '/kempo/components/DashboardWidget.js';\r\nimport { html } from '/kempo-ui/lit-all.min.js';\r\nimport { getUsers, createUser } from '/kempo/sdk.js';\r\n\r\n/*\r\n The number of accounts on the site.\r\n*/\r\nexport default class UsersWidget extends DashboardWidget {\r\n static properties = {\r\n ...DashboardWidget.properties,\r\n total: { state: true }\r\n };\r\n\r\n constructor(){\r\n super();\r\n this.total = 0;\r\n this.cta = { label: 'Add user', icon: 'person_add', permission: 'system:user:create', onClick: () => this.addUser() };\r\n }\r\n\r\n /*\r\n Public Methods\r\n */\r\n\r\n load = async () => {\r\n this.total = (await this.unwrap(getUsers({ limit: 1 }))).total;\r\n };\r\n\r\n /*\r\n Event Handlers\r\n */\r\n\r\n addUser = () => this.openForm({\r\n title: 'Add user',\r\n confirmText: 'Create',\r\n fields: [\r\n { name: 'name', label: 'Name' },\r\n { name: 'email', label: 'Email', type: 'email' },\r\n { name: 'password', label: 'Password', type: 'password' }\r\n ],\r\n submit: async ({ name, email, password }) => {\r\n const [error] = await createUser({ name, email, password, emailVerified: true });\r\n if(error) return error.msg || 'Could not create the user.';\r\n await this.refresh();\r\n }\r\n });\r\n\r\n /*\r\n Rendering\r\n */\r\n\r\n renderContent(){\r\n return html`\r\n <div class=\"h1 m0\">${this.total}</div>\r\n <div class=\"tc-muted\">${this.total === 1 ? 'User' : 'Users'}</div>\r\n `;\r\n }\r\n}\r\n\r\ncustomElements.define('k-widget-users', UsersWidget);\r\n"],"mappings":"OAAOA,MAAqB,sDACnBC,MAAY,8CACZC,gBAAUC,MAAkB,+BAKtB,MAAMC,UAAoBJ,EACvCK,kBAAoB,IACfL,EAAgBM,WACnBC,MAAO,CAAEC,OAAO,IAGlB,WAAAC,GACEC,QACAC,KAAKJ,MAAQ,EACbI,KAAKC,IAAM,CAAEC,MAAO,WAAYC,KAAM,aAAcC,WAAY,qBAAsBC,QAAS,IAAML,KAAKM,UAC5G,CAMAC,KAAOC,UACLR,KAAKJ,aAAeI,KAAKS,OAAOlB,EAAS,CAAEmB,MAAO,MAAOd,OAO3DU,QAAU,IAAMN,KAAKW,SAAS,CAC5BC,MAAO,WACPC,YAAa,SACbC,OAAQ,CACN,CAAEC,KAAM,OAAQb,MAAO,QACvB,CAAEa,KAAM,QAASb,MAAO,QAASc,KAAM,SACvC,CAAED,KAAM,WAAYb,MAAO,WAAYc,KAAM,aAE/CC,OAAQT,OAASO,OAAMG,QAAOC,eAC5B,MAAOC,SAAe5B,EAAW,CAAEuB,OAAMG,QAAOC,WAAUE,eAAe,IACzE,GAAGD,EAAO,OAAOA,EAAME,KAAO,mCACxBtB,KAAKuB,aAQf,aAAAC,GACE,OAAOlC,CAAI;2BACYU,KAAKJ;8BACa,IAAfI,KAAKJ,MAAc,OAAS;KAExD,EAGF6B,eAAeC,OAAO,iBAAkBjC","ignoreList":[]}
|
|
@@ -7,96 +7,95 @@
|
|
|
7
7
|
state="expanded"
|
|
8
8
|
style="--aside_padding: 0;"
|
|
9
9
|
persistent-id="admin-nav"
|
|
10
|
-
group-icons='{"Commerce":"storefront"}'
|
|
10
|
+
group-icons='{"Accounts":"manage-accounts","Content":"cards_stack","Settings":"settings","Commerce":"storefront"}'
|
|
11
11
|
>
|
|
12
12
|
<k-aside-toggle>
|
|
13
13
|
<h3 class="m0">Kempo Admin</h3>
|
|
14
14
|
</k-aside-toggle>
|
|
15
|
-
<div class="px pbh">
|
|
16
|
-
<k-notification-bell page-href="/admin/notifications" open-direction="down right"></k-notification-bell>
|
|
17
|
-
</div>
|
|
18
15
|
<k-aside-item
|
|
19
16
|
icon="preview"
|
|
20
17
|
href="/"
|
|
18
|
+
group="Site"
|
|
21
19
|
no-expand
|
|
22
20
|
>Site</k-aside-item>
|
|
23
21
|
<k-aside-item
|
|
24
22
|
icon="cards"
|
|
25
23
|
href="/admin"
|
|
24
|
+
group="Dashboard"
|
|
26
25
|
no-expand
|
|
27
26
|
>Dashboard</k-aside-item>
|
|
28
|
-
<k-aside-
|
|
29
|
-
icon="
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
27
|
+
<k-aside-item
|
|
28
|
+
icon="person"
|
|
29
|
+
href="/admin/accounts/users"
|
|
30
|
+
group="Accounts"
|
|
31
|
+
no-expand
|
|
32
|
+
>Users</k-aside-item>
|
|
33
|
+
<k-aside-item
|
|
34
|
+
icon="group"
|
|
35
|
+
href="/admin/accounts/groups"
|
|
36
|
+
group="Accounts"
|
|
37
|
+
no-expand
|
|
38
|
+
>Groups</k-aside-item>
|
|
39
|
+
<k-aside-item
|
|
40
|
+
icon="lock"
|
|
41
|
+
href="/admin/accounts/permissions"
|
|
42
|
+
group="Accounts"
|
|
43
|
+
no-expand
|
|
44
|
+
>Permissions</k-aside-item>
|
|
45
|
+
<k-aside-item
|
|
46
|
+
icon="browse"
|
|
47
|
+
href="/admin/content/templates"
|
|
48
|
+
group="Content"
|
|
49
|
+
no-expand
|
|
50
|
+
>Templates</k-aside-item>
|
|
51
|
+
<k-aside-item
|
|
52
|
+
icon="docs"
|
|
53
|
+
href="/admin/content/pages"
|
|
54
|
+
group="Content"
|
|
55
|
+
no-expand
|
|
56
|
+
>Pages</k-aside-item>
|
|
57
|
+
<k-aside-item
|
|
58
|
+
icon="content"
|
|
59
|
+
href="/admin/content/fragments"
|
|
60
|
+
group="Content"
|
|
61
|
+
no-expand
|
|
62
|
+
>Fragments</k-aside-item>
|
|
63
|
+
<k-aside-item
|
|
64
|
+
icon="globe"
|
|
65
|
+
href="/admin/content/globals"
|
|
66
|
+
group="Content"
|
|
67
|
+
no-expand
|
|
68
|
+
>Global Content</k-aside-item>
|
|
69
69
|
<k-aside-item
|
|
70
70
|
icon="settings"
|
|
71
71
|
href="/admin/settings"
|
|
72
|
+
group="Settings"
|
|
72
73
|
no-expand
|
|
73
|
-
>Settings</k-aside-item>
|
|
74
|
+
>Admin Settings</k-aside-item>
|
|
74
75
|
<k-aside-item
|
|
75
76
|
icon="extension"
|
|
76
77
|
href="/admin/extensions"
|
|
78
|
+
group="Settings"
|
|
77
79
|
no-expand
|
|
78
80
|
>Extensions</k-aside-item>
|
|
79
|
-
<
|
|
80
|
-
icon="notifications"
|
|
81
|
-
href="/admin/notifications"
|
|
82
|
-
no-expand
|
|
83
|
-
>Notifications</k-aside-item>
|
|
81
|
+
<location name="admin-nav-extensions"></location>
|
|
84
82
|
<k-aside-item
|
|
85
83
|
icon="sensors"
|
|
86
84
|
href="/admin/realtime"
|
|
85
|
+
group="Games"
|
|
87
86
|
no-expand
|
|
88
|
-
>Realtime</k-aside-item>
|
|
89
|
-
<hr>
|
|
90
|
-
<location name="admin-nav-extensions"></location>
|
|
87
|
+
>Realtime Connections</k-aside-item>
|
|
91
88
|
<k-aside-spacer></k-aside-spacer>
|
|
92
|
-
<k-
|
|
93
|
-
labels
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
<k-admin-nav-footer class="pxh pyh">
|
|
90
|
+
<k-theme-switcher class="d-b" labels style="--padding:var(--spacer_h)"></k-theme-switcher>
|
|
91
|
+
<span class="flex"></span>
|
|
92
|
+
<k-notification-bell class="d-b" page-href="/admin/notifications" open-direction="up right" button-class="ph r"></k-notification-bell>
|
|
93
|
+
</k-admin-nav-footer>
|
|
96
94
|
</k-aside>
|
|
97
95
|
<script type="module">
|
|
98
96
|
import '/kempo-ui/components/ThemeSwitcher.js';
|
|
99
97
|
import '/kempo/components/NotificationBell.js';
|
|
98
|
+
import '/kempo/components/AdminNavFooter.js';
|
|
100
99
|
const items = document.querySelectorAll('k-aside-item');
|
|
101
100
|
const currentPath = window.location.pathname;
|
|
102
101
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
locked: true
|
|
3
3
|
-->
|
|
4
|
-
<page title="Realtime - Admin">
|
|
4
|
+
<page title="Realtime Connections - Admin">
|
|
5
5
|
<content>
|
|
6
6
|
<script type="module" src="/kempo-ui/components/Table.js"></script>
|
|
7
7
|
<script type="module" src="/kempo-ui/components/Icon.js"></script>
|
|
8
8
|
|
|
9
9
|
<div class="d-f mb">
|
|
10
|
-
<h1 class="flex mb0">Realtime</h1>
|
|
10
|
+
<h1 class="flex mb0">Realtime Connections</h1>
|
|
11
11
|
<button id="refresh"><k-icon name="replay"></k-icon> Refresh</button>
|
|
12
12
|
</div>
|
|
13
13
|
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import s from"../../../../../server/utils/auth/getSession.js";import e from"../../../../../server/utils/dashboard/listDashboardWidgets.js";export default async(r,t)=>{const[o,i]=await s({token:r.cookies.session_token});if(o)return t.status(500===o.code?500:401).json({error:500===o.code?o.msg:"Authentication required"});const[a,n]=await e({userId:i.user.id});if(a)return t.status(a.code).json({error:a.msg});t.json(n)};
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\dashboard\widgets\GET.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\kempo\\api\\dashboard\\widgets\\GET.js","names":["getSession","listDashboardWidgets","async","request","response","sessionError","sessionData","token","cookies","session_token","status","code","json","error","msg","result","userId","user","id"],"sources":["0"],"sourcesContent":["import getSession from '../../../../../server/utils/auth/getSession.js';\nimport listDashboardWidgets from '../../../../../server/utils/dashboard/listDashboardWidgets.js';\n\nexport default async (request, response) => {\n const [sessionError, sessionData] = await getSession({ token: request.cookies.session_token });\n\n if(sessionError){\n return response.status(sessionError.code === 500 ? 500 : 401).json({ error: sessionError.code === 500 ? sessionError.msg : 'Authentication required' });\n }\n\n const [error, result] = await listDashboardWidgets({ userId: sessionData.user.id });\n\n if(error){\n return response.status(error.code).json({ error: error.msg });\n }\n\n response.json(result);\n};\n"],"mappings":"OAAOA,MAAgB,wDAChBC,MAA0B,+EAElBC,MAAOC,EAASC,KAC7B,MAAOC,EAAcC,SAAqBN,EAAW,CAAEO,MAAOJ,EAAQK,QAAQC,gBAE9E,GAAGJ,EACD,OAAOD,EAASM,OAA6B,MAAtBL,EAAaM,KAAe,IAAM,KAAKC,KAAK,CAAEC,MAA6B,MAAtBR,EAAaM,KAAeN,EAAaS,IAAM,4BAG7H,MAAOD,EAAOE,SAAgBd,EAAqB,CAAEe,OAAQV,EAAYW,KAAKC,KAE9E,GAAGL,EACD,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGzDV,EAASQ,KAAKG","ignoreList":[]}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import t from"../../../../server/utils/auth/getSession.js";import e from"../../../../server/utils/notifications/getNotifications.js";export default async(s,o)=>{const[r,i]=await t({token:s.cookies.session_token});if(r)return o.status(500===r.code?500:401).json({error:500===r.code?r.msg:"Authentication required"});const{unreadOnly:n,limit:u,offset:a}=s.query,[f,c]=await e({userId:i.user.id,unreadOnly:"true"===n,limit:u,offset:a});if(f)return o.status(f.code).json({error:f.msg});o.json(c)};
|
|
2
|
-
//# sourceMappingURL=C:\Users\dusti\dev\kempo
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\notifications\GET.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import t from"../../../../../server/utils/auth/getSession.js";import o from"../../../../../server/utils/notifications/deleteNotification.js";export default async(s,e)=>{const[i,r]=await t({token:s.cookies.session_token});if(i)return e.status(500===i.code?500:401).json({error:500===i.code?i.msg:"Authentication required"});const[n,a]=await o({userId:r.user.id,notificationId:s.params.id});if(n)return e.status(n.code).json({error:n.msg});e.json(a)};
|
|
2
|
-
//# sourceMappingURL=C:\Users\dusti\dev\kempo
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\notifications\[id]\DELETE.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import s from"../../../../../../server/utils/auth/getSession.js";import t from"../../../../../../server/utils/notifications/markHandled.js";export default async(o,e)=>{const[r,i]=await s({token:o.cookies.session_token});if(r)return e.status(500===r.code?500:401).json({error:500===r.code?r.msg:"Authentication required"});const[n,a]=await t({userId:i.user.id,notificationId:o.params.id});if(n)return e.status(n.code).json({error:n.msg});e.json(a)};
|
|
2
|
-
//# sourceMappingURL=C:\Users\dusti\dev\kempo
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\notifications\[id]\handled\POST.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import s from"../../../../../../server/utils/auth/getSession.js";import t from"../../../../../../server/utils/notifications/markRead.js";export default async(o,e)=>{const[r,i]=await s({token:o.cookies.session_token});if(r)return e.status(500===r.code?500:401).json({error:500===r.code?r.msg:"Authentication required"});const[n,a]=await t({userId:i.user.id,notificationId:o.params.id});if(n)return e.status(n.code).json({error:n.msg});e.json(a)};
|
|
2
|
-
//# sourceMappingURL=C:\Users\dusti\dev\kempo
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\notifications\[id]\read\POST.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import s from"../../../../../server/utils/auth/getSession.js";import t from"../../../../../server/utils/notifications/getUnreadCount.js";export default async(e,o)=>{const[r,i]=await s({token:e.cookies.session_token});if(r)return o.status(500===r.code?500:401).json({error:500===r.code?r.msg:"Authentication required"});const[n,u]=await t({userId:i.user.id});if(n)return o.status(n.code).json({error:n.msg});o.json(u)};
|
|
2
|
-
//# sourceMappingURL=C:\Users\dusti\dev\kempo
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\notifications\count\GET.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import s from"../../../../../server/utils/auth/getSession.js";import e from"../../../../../server/utils/notifications/markAllRead.js";export default async(t,o)=>{const[r,i]=await s({token:t.cookies.session_token});if(r)return o.status(500===r.code?500:401).json({error:500===r.code?r.msg:"Authentication required"});const[n,a]=await e({userId:i.user.id});if(n)return o.status(n.code).json({error:n.msg});o.json(a)};
|
|
2
|
-
//# sourceMappingURL=C:\Users\dusti\dev\kempo
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\api\notifications\read-all\POST.js.map
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export default class e extends HTMLElement{#e=null;connectedCallback(){this.#e=this.closest("k-aside"),this.#e?.addEventListener("aside_state_change",this.handleStateChange),this.applyState(this.#e?.state)}disconnectedCallback(){this.#e?.removeEventListener("aside_state_change",this.handleStateChange),this.#e=null}handleStateChange=e=>this.applyState(e.detail.state);applyState=e=>{const t="collapsed"===e;this.classList.toggle("d-f",!t),this.classList.toggle("d-b",t),this.classList.toggle("ta-center",t),this.style.alignItems=t?"":"center"}}customElements.define("k-admin-nav-footer",e);
|
|
2
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\components\AdminNavFooter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src\\kempo\\components\\AdminNavFooter.js","names":["AdminNavFooter","HTMLElement","aside","connectedCallback","this","closest","addEventListener","handleStateChange","applyState","state","disconnectedCallback","removeEventListener","event","detail","collapsed","classList","toggle","style","alignItems","customElements","define"],"sources":["0"],"sourcesContent":["/*\r\n The bottom row of the admin side nav. Its children sit side by side while the parent <k-aside> is\r\n expanded and stack while it is collapsed. Layout is done with kempo-css utility classes toggled on\r\n this element, driven by the aside's `aside_state_change` event.\r\n*/\r\nexport default class AdminNavFooter extends HTMLElement {\r\n #aside = null;\r\n\r\n /*\r\n Lifecycle Callbacks\r\n */\r\n\r\n connectedCallback(){\r\n this.#aside = this.closest('k-aside');\r\n this.#aside?.addEventListener('aside_state_change', this.handleStateChange);\r\n this.applyState(this.#aside?.state);\r\n }\r\n\r\n disconnectedCallback(){\r\n this.#aside?.removeEventListener('aside_state_change', this.handleStateChange);\r\n this.#aside = null;\r\n }\r\n\r\n /*\r\n Event Handlers\r\n */\r\n\r\n handleStateChange = event => this.applyState(event.detail.state);\r\n\r\n /*\r\n Utility Functions\r\n */\r\n\r\n applyState = state => {\r\n const collapsed = state === 'collapsed';\r\n this.classList.toggle('d-f', !collapsed);\r\n this.classList.toggle('d-b', collapsed);\r\n this.classList.toggle('ta-center', collapsed);\r\n this.style.alignItems = collapsed ? '' : 'center';\r\n };\r\n}\r\n\r\ncustomElements.define('k-admin-nav-footer', AdminNavFooter);\r\n"],"mappings":"eAKe,MAAMA,UAAuBC,YAC1CC,GAAS,KAMT,iBAAAC,GACEC,MAAKF,EAASE,KAAKC,QAAQ,WAC3BD,MAAKF,GAAQI,iBAAiB,qBAAsBF,KAAKG,mBACzDH,KAAKI,WAAWJ,MAAKF,GAAQO,MAC/B,CAEA,oBAAAC,GACEN,MAAKF,GAAQS,oBAAoB,qBAAsBP,KAAKG,mBAC5DH,MAAKF,EAAS,IAChB,CAMAK,kBAAoBK,GAASR,KAAKI,WAAWI,EAAMC,OAAOJ,OAM1DD,WAAaC,IACX,MAAMK,EAAsB,cAAVL,EAClBL,KAAKW,UAAUC,OAAO,OAAQF,GAC9BV,KAAKW,UAAUC,OAAO,MAAOF,GAC7BV,KAAKW,UAAUC,OAAO,YAAaF,GACnCV,KAAKa,MAAMC,WAAaJ,EAAY,GAAK,UAI7CK,eAAeC,OAAO,qBAAsBpB","ignoreList":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import e from"/kempo-ui/components/LightComponent.js";import{html as t,nothing as i}from"/kempo-ui/lit-all.min.js";import"/kempo-ui/components/Icon.js";import n from"/kempo-ui/components/Dialog.js";import{currentUserHasPermission as s}from"/kempo/sdk.js";export default class a extends e{static properties={width:{type:Number,reflect:!0},height:{type:Number,reflect:!0},definition:{attribute:!1},loading:{state:!0},failure:{state:!0},ctaAllowed:{state:!0}};constructor(){super(),this.width=2,this.height=1,this.definition=null,this.cta=null,this.loading=!0,this.failure="",this.ctaAllowed=!1}connectedCallback(){super.connectedCallback(),this.classList.add("d-b"),this.style.height="100%",this.refresh(),this.checkCta()}load=async()=>{};refresh=async()=>{try{await this.load(),this.failure=""}catch(e){this.failure=e?.message||"Could not load this widget."}this.loading=!1};getCta(){return this.cta}actions(){return[]}unwrap=async e=>{const[t,i]=await e;if(t)throw new Error(t.msg||"Request failed");return i};openForm=({title:e,fields:t,confirmText:i="Save",submit:s})=>{const a=document.createElement("div");a.className="p";const r={};t.forEach(({name:e,label:t,type:i="text",placeholder:n="",value:s="",options:o=[]})=>{const l=document.createElement("div");l.className="mb";const c=Object.assign(document.createElement("label"),{className:"d-b mbq",textContent:t});let d;"select"===i?(d=document.createElement("select"),d.className="full",o.forEach(e=>d.append(Object.assign(document.createElement("option"),{value:e.value,textContent:e.label,selected:e.value===s})))):d=Object.assign(document.createElement("input"),{type:i,className:"full",placeholder:n,value:s}),r[e]=d,l.append(c,d),a.append(l)});const o=Object.assign(document.createElement("p"),{className:"tc-danger m0",role:"alert"});a.append(o);const l=n.create(a,{title:e,confirmText:i,cancelText:"Cancel",confirmAction:async e=>{e.keepDialogOpen=!0,o.textContent="";const i=Object.fromEntries(t.map(({name:e})=>[e,r[e].value.trim()])),n=t.find(({name:e,required:t=!0})=>t&&!i[e]);if(n)return void(o.textContent=`${n.label} is required.`);const a=await s(i);a?o.textContent=a:l.close()}});return Object.values(r)[0]?.focus(),l};checkCta=async()=>{const e=this.getCta()?.permission;if(!e)return void(this.ctaAllowed=!0);const[t,i]=await s({permission:e});this.ctaAllowed=!t&&!!i?.hasPermission};renderTitle=()=>t`
|
|
2
|
+
${this.definition?.icon?t`<k-icon class="mr" name=${this.definition.icon}></k-icon>`:i}
|
|
3
|
+
<strong class="flex">${this.definition?.title??""}</strong>
|
|
4
|
+
`;renderActions=()=>{const e=this.width<2;return[...this.definition?.actions??[],...this.actions()].map(({label:n,icon:s,href:a,onClick:r,primary:o})=>{const l=s?t`<k-icon name=${s}></k-icon>${e?i:t` ${n}`}`:n,c="btn ml "+(o?"primary":"");return a?t`<a class=${c} href=${a} title=${n} aria-label=${n}>${l}</a>`:t`<button class=${c} title=${n} aria-label=${n} @click=${r}>${l}</button>`})};renderHeader(){return t`<header class="d-f px pyh bb" style="align-items: center;">${this.renderTitle()}${this.renderActions()}</header>`}renderContent(){return i}renderBody(){return this.failure?t`
|
|
5
|
+
<p class="tc-danger" role="alert">${this.failure}</p>
|
|
6
|
+
<button @click=${this.refresh}>Try again</button>
|
|
7
|
+
`:this.loading?t`<p class="tc-muted">Loading…</p>`:this.renderContent()}renderFooter=()=>{const e=this.getCta();if(!e||!this.ctaAllowed||this.failure)return i;const n=t`${e.icon?t`<k-icon name=${e.icon}></k-icon> `:i}${e.label}`;return t`
|
|
8
|
+
<footer class="d-f px pyh bt" style="justify-content: flex-end;">
|
|
9
|
+
${e.href?t`<a class="btn primary" href=${e.href}>${n}</a>`:t`<button class="primary" @click=${e.onClick}>${n}</button>`}
|
|
10
|
+
</footer>
|
|
11
|
+
`};renderLightDom(){return t`
|
|
12
|
+
<div style="display: flex; flex-direction: column; height: 100%; min-height: 0;">
|
|
13
|
+
${this.renderHeader()}
|
|
14
|
+
<div class="p" style="flex: 1; min-height: 0; overflow: auto;">${this.renderBody()}</div>
|
|
15
|
+
${this.renderFooter()}
|
|
16
|
+
</div>
|
|
17
|
+
`}}
|
|
18
|
+
//# sourceMappingURL=C:\Users\dusti\dev\kempo\dist\kempo\components\DashboardWidget.js.map
|