domma-cms 0.75.0 → 0.77.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/CLAUDE.md +1 -1
- package/admin/js/api.js +1 -1
- package/admin/js/app.js +4 -4
- package/admin/js/lib/action-editor-arrange.js +1 -0
- package/admin/js/lib/api-endpoint-editor-arrange.js +2 -0
- package/admin/js/lib/api-tokens-arrange.js +3 -0
- package/admin/js/lib/block-editor-arrange.js +2 -0
- package/admin/js/lib/collection-editor-arrange.js +3 -0
- package/admin/js/lib/collection-entries-arrange.js +1 -0
- package/admin/js/lib/component-editor-arrange.js +27 -0
- package/admin/js/lib/context-menu-editor-arrange.js +1 -0
- package/admin/js/lib/dashboard-arrange.js +1 -0
- package/admin/js/lib/effects-arrange.js +3 -0
- package/admin/js/lib/form-editor-arrange.js +1 -0
- package/admin/js/lib/form-submissions-arrange.js +2 -0
- package/admin/js/lib/layouts-arrange.js +1 -0
- package/admin/js/lib/menu-editor-arrange.js +2 -0
- package/admin/js/lib/my-profile-arrange.js +1 -0
- package/admin/js/lib/notifications-arrange.js +1 -0
- package/admin/js/lib/page-editor-arrange.js +1 -0
- package/admin/js/lib/plugin-code-arrange.js +1 -0
- package/admin/js/lib/plugins-arrange.js +2 -0
- package/admin/js/lib/project-page-arrange.js +1 -0
- package/admin/js/lib/role-editor-arrange.js +3 -0
- package/admin/js/lib/roles-arrange.js +1 -0
- package/admin/js/lib/search-settings-arrange.js +1 -0
- package/admin/js/lib/sidebar-renderer.js +2 -2
- package/admin/js/lib/site-settings-arrange.js +1 -0
- package/admin/js/lib/user-editor-arrange.js +1 -0
- package/admin/js/lib/users-arrange.js +1 -0
- package/admin/js/lib/view-editor-arrange.js +14 -0
- package/admin/js/templates/action-editor.html +223 -147
- package/admin/js/templates/api-endpoint-editor.html +172 -117
- package/admin/js/templates/api-tokens.html +83 -10
- package/admin/js/templates/block-editor.html +194 -203
- package/admin/js/templates/collection-editor.html +191 -113
- package/admin/js/templates/collection-entries.html +99 -36
- package/admin/js/templates/component-editor.html +128 -126
- package/admin/js/templates/context-menu-editor.html +208 -148
- package/admin/js/templates/dashboard.html +184 -22
- package/admin/js/templates/effects.html +155 -72
- package/admin/js/templates/form-editor.html +41 -26
- package/admin/js/templates/form-submissions.html +78 -26
- package/admin/js/templates/layouts.html +126 -43
- package/admin/js/templates/menu-editor.html +57 -17
- package/admin/js/templates/menu-locations.html +54 -11
- package/admin/js/templates/my-profile.html +100 -12
- package/admin/js/templates/notifications.html +70 -10
- package/admin/js/templates/page-editor.html +47 -26
- package/admin/js/templates/plugin-code.html +91 -73
- package/admin/js/templates/plugins.html +100 -28
- package/admin/js/templates/project-detail.html +83 -145
- package/admin/js/templates/project-settings.html +88 -53
- package/admin/js/templates/role-editor.html +158 -83
- package/admin/js/templates/roles.html +78 -7
- package/admin/js/templates/search.html +193 -216
- package/admin/js/templates/settings.html +404 -472
- package/admin/js/templates/user-editor.html +150 -14
- package/admin/js/templates/users.html +69 -9
- package/admin/js/templates/view-editor.html +203 -180
- package/admin/js/templates/view-preview.html +45 -45
- package/admin/js/views/action-editor.js +7 -1
- package/admin/js/views/api-endpoint-editor.js +2 -3
- package/admin/js/views/api-tokens.js +62 -8
- package/admin/js/views/block-editor.js +14 -8
- package/admin/js/views/collection-editor.js +64 -5
- package/admin/js/views/collection-entries.js +50 -1
- package/admin/js/views/component-editor.js +2 -28
- package/admin/js/views/context-menu-editor.js +54 -55
- package/admin/js/views/dashboard.js +7 -1
- package/admin/js/views/effects.js +31 -1
- package/admin/js/views/form-editor.js +7 -7
- package/admin/js/views/form-submissions.js +11 -1
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/layouts.js +81 -1
- package/admin/js/views/login.js +2 -2
- package/admin/js/views/menu-editor.js +21 -43
- package/admin/js/views/menu-locations.js +16 -1
- package/admin/js/views/my-profile.js +1 -1
- package/admin/js/views/notifications.js +18 -1
- package/admin/js/views/page-editor.js +26 -26
- package/admin/js/views/plugin-code.js +23 -4
- package/admin/js/views/plugins.js +97 -39
- package/admin/js/views/project-detail.js +9 -1
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/role-editor.js +11 -3
- package/admin/js/views/roles.js +49 -4
- package/admin/js/views/search.js +7 -1
- package/admin/js/views/settings.js +13 -3
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +13 -4
- package/admin/js/views/view-editor.js +1 -1
- package/admin/js/views/view-preview.js +1 -1
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +97 -40
- package/plugins/_lib/admin/mail/contacts.js +4 -1
- package/plugins/_lib/admin/mail/diagnostics-section.js +4 -1
- package/plugins/_lib/admin/mail/folder-tree.js +1 -1
- package/plugins/_lib/admin/mail/image-senders-section.js +69 -90
- package/plugins/_lib/admin/mail/mail.css +1 -1
- package/plugins/_lib/admin/mail/mail.html +1 -1
- package/plugins/_lib/admin/mail/reader-view.js +303 -44
- package/plugins/_lib/admin/mail/when-closed.js +26 -0
- package/plugins/_lib/mail/readRoutes.js +43 -1
- package/plugins/_lib/mail/unreadBadge.js +42 -0
- package/plugins/free-tier.lock.json +5 -5
- package/plugins/mail-reader/CLAUDE.md +1 -0
- package/plugins/mail-reader/plugin.js +3 -1
- package/plugins/mail-reader/plugin.json +5 -4
- package/server/routes/api/api-tokens.js +56 -0
- package/server/routes/api/auth.js +7 -1
- package/server/routes/api/collections.js +13 -1
- package/server/routes/api/dashboard.js +32 -4
- package/server/routes/api/effects.js +68 -13
- package/server/routes/api/forms.js +104 -13
- package/server/routes/api/layouts.js +93 -2
- package/server/routes/api/notifications.js +140 -14
- package/server/routes/api/plugins.js +135 -4
- package/server/routes/api/roles.js +111 -0
- package/server/routes/api/search.js +38 -4
- package/server/routes/api/settings.js +52 -0
- package/server/routes/api/users.js +65 -3
- package/server/routes/api/views.js +55 -1
- package/server/server.js +12 -0
- package/server/services/components.js +9 -0
- package/server/services/plugins.js +7 -0
- package/server/services/roles.js +3 -0
- package/server/services/search.js +58 -0
- package/server/services/users.js +16 -2
- package/server/services/views.js +14 -0
- package/admin/js/templates/dashboard/activity-feed.html +0 -3
- package/admin/js/templates/dashboard/cache.html +0 -32
- package/admin/js/templates/dashboard/health-detail.html +0 -2
- package/admin/js/templates/dashboard/journeys.html +0 -17
- package/admin/js/templates/dashboard/kpi-strip.html +0 -34
- package/admin/js/templates/dashboard/spike-feed.html +0 -3
- package/admin/js/templates/dashboard/top-pages.html +0 -3
- package/admin/js/templates/dashboard/traffic-chart.html +0 -3
- package/admin/js/templates/plugin-marketplace.html +0 -29
- package/admin/js/views/block-editor-enhance.js +0 -1
- package/admin/js/views/dashboard/lib/escape.js +0 -1
- package/admin/js/views/dashboard/widgets/activity-feed.js +0 -1
- package/admin/js/views/dashboard/widgets/cache.js +0 -1
- package/admin/js/views/dashboard/widgets/health-detail.js +0 -1
- package/admin/js/views/dashboard/widgets/journeys.js +0 -1
- package/admin/js/views/dashboard/widgets/kpi-strip.js +0 -1
- package/admin/js/views/dashboard/widgets/spike-feed.js +0 -6
- package/admin/js/views/dashboard/widgets/top-pages.js +0 -1
- package/admin/js/views/dashboard/widgets/traffic-chart.js +0 -1
- package/admin/js/views/plugin-marketplace.js +0 -1
|
@@ -1 +1,81 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as L,apiRequest as Qe}from"../api.js";import{addBannerAction as Xe}from"../lib/plugin-chrome.js";import{helpPopovers as Me}from"../lib/help-popover.js";import{onceClosed as $e,openMenuAt as Oe,sidebarCount as Ze}from"../lib/tool-kit.js";import{FILTERS as et,WIDTHS as tt,WIDTH_HINTS as at,WIDTH_LABELS as st,WIDTH_SHARE as De,arrangeLayouts as lt,checkLayout as nt,checkOptions as ot,formFromPreset as it}from"../lib/layouts-arrange.js";const ae=/^\/layouts\/?(\?.*)?$/,Te="#/layouts",se="layouts.draft",N="layouts.options.draft",le="layouts.filter",Le=(s="")=>`
|
|
2
|
+
<span class="lyx-shape" aria-hidden="true" data-bind-style-background-color="bg${s}">
|
|
3
|
+
<span class="lyx-shape-nav" data-bind-hidden="!hasNav${s}"></span>
|
|
4
|
+
<span class="lyx-shape-body"><span class="lyx-shape-col" data-bind-style-width="share${s}"></span><span class="lyx-shape-side" data-bind-hidden="!hasSide${s}"></span></span>
|
|
5
|
+
<span class="lyx-shape-foot" data-bind-hidden="!hasFoot${s}"></span>
|
|
6
|
+
</span>`,rt=`
|
|
7
|
+
<div class="lyx-row" data-key="{{key}}" tabindex="0">
|
|
8
|
+
${Le()}
|
|
9
|
+
<span class="lyx-main"><strong>{{label}}</strong><small>{{meta}}</small></span>
|
|
10
|
+
<span class="lyx-chips">
|
|
11
|
+
<span class="lyx-chip" data-bind-hidden="!builtin" title="Built in - can be edited, not deleted">Built in</span>
|
|
12
|
+
<span class="lyx-chip">{{widthLabel}}</span>
|
|
13
|
+
<span class="lyx-chip" data-bind-class="usedTone" title="Pages that use this layout">{{usedText}}</span>
|
|
14
|
+
</span>
|
|
15
|
+
<button type="button" class="lyx-kebab" aria-label="Actions for {{label}}" title="Actions">\u22EE</button>
|
|
16
|
+
</div>`,W=s=>String(s??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function Be(s){const r=document.createElement("template");return r.innerHTML=s.trim(),r.content.firstElementChild}const je=(s,r,i=`${r}s`)=>`${s} ${s===1?r:i}`,ct=s=>`#/pages/edit${s}`;let f=null,ze=!1;function ut(){ze||!window.R?.use||(ze=!0,R.use((s,r,i)=>!(ae.test(r?.path||"")&&!ae.test(s?.path||""))||!f?.dirty()?i():E.confirm("You have unsaved layout changes. Leave anyway? They are kept as a draft for your next visit.").then(d=>{d?i():history.replaceState(null,"",Te)})),M.subscribe("router:afterChange",({to:s}={})=>{if(f&&!ae.test(s?.path||"")){const r=f;f=null,r.dispose()}}))}export const layoutsView={templateUrl:"/admin/js/templates/layouts.html",async onMount(s){if(ut(),f){const e=f;f=null,e.dispose()}const r=[],i=e=>(r.push(e),e);let Y=!1;const d=s.get(0).querySelector(".lyx")||s.get(0),p=()=>!Y&&d.isConnected,y=M.observable({}),A=M.observable({byKey:{},missing:[]}),ne=M.observable(!0),U=M.observable("");let oe="all";try{oe=et.includes(S.get(le))?S.get(le):"all"}catch{}const k=M.observable(oe),ie=M.observable("");i(M.effect(()=>{const e=k.value;try{S.set(le,e)}catch{}}));const re=M.observable([]);i(M.effect(()=>{re.value=lt(y.value,A.value,{filter:k.value,query:ie.value})}));const Pe=M.computed(()=>{const e=Object.values(y.value),t=e.filter(n=>n.builtin).length;return{all:e.length,builtin:t,custom:e.length-t}}),V=M.computed(()=>A.value.missing||[]),Ne=M.computed(()=>{const e=V.value.length;return`${je(e,"page")} name${e===1?"s":""} a layout that no longer exists and show${e===1?"s":""} as Default.`}),H=M.observable({spacerSize:"8",spacerClass:""}),m=M.observable("8"),g=M.observable(""),G=M.observable(!1),ce=()=>({spacerSize:String(m.value??""),spacerClass:String(g.value??"")}),K=M.computed(()=>ot(ce())),x=M.computed(()=>{const e=H.value;return String(m.value??"")!==e.spacerSize||String(g.value??"")!==e.spacerClass}),ue=M.computed(()=>G.value||!x.value||Object.keys(K.value.errors).length>0),Ae=M.computed(()=>x.value?"Unsaved changes - Ctrl+S saves.":"Saved."),B=e=>({...y.peek()[e]||{},key:e}),J=e=>(A.peek().byKey||{})[e]||[];async function Q(){try{const[e,t]=await Promise.all([L.layouts.get(),Qe("/layouts/_usage",{method:"GET"}).catch(()=>({byKey:{},missing:[]}))]);if(!p())return;y.value=e&&typeof e=="object"?e:{},A.value=t&&typeof t=="object"?t:{byKey:{},missing:[]},U.value=""}catch(e){U.value=`Could not load the layouts: ${e.message||e}`}finally{ne.value=!1}}async function He(){const e=await L.layouts.getOptions().catch(()=>({spacerSize:8,spacerClass:""}));if(!p())return;const t={spacerSize:String(e?.spacerSize??8),spacerClass:String(e?.spacerClass??"")};H.value=t,m.value=t.spacerSize,g.value=t.spacerClass}async function de(){if(ue.peek())return;G.value=!0;const{payload:e}=K.peek();try{await L.layouts.saveOptions(e),H.value={spacerSize:String(e.spacerSize),spacerClass:e.spacerClass},m.value=String(e.spacerSize),g.value=e.spacerClass;try{S.remove?S.remove(N):S.set(N,null)}catch{}E.toast("Layout options saved.",{type:"success"})}catch(t){E.toast(`Not saved: ${t.message||t}`,{type:"error"})}finally{G.value=!1}}function Ie(){const e=H.peek();m.value=e.spacerSize,g.value=e.spacerClass}const pe=()=>Ze(Te);async function Ke(e){const t=B(e);if(t.builtin)return;const n=J(e).length,c=n?` ${je(n,"page")} use${n===1?"s":""} it and will show as Default.`:" No page uses it.";if(await E.confirm(`Delete the layout "${W(t.label||e)}"?${c}`))try{await L.layouts.remove(e),E.toast(`Deleted "${t.label||e}".`,{type:"success"}),pe(),p()&&await Q()}catch(o){E.toast(`Not deleted: ${o.message||o}`,{type:"error"})}}async function Re(e){const t=`layout: ${e}`;try{await navigator.clipboard.writeText(t),E.toast(`Copied "${t}" - paste it into a page's frontmatter.`,{type:"success"})}catch{E.toast(`Copy this into a page's frontmatter: ${t}`,{type:"info",duration:7e3})}}const C=new Set;let b=null,ye=!1;function be(e){const t=e===null,n=t?V.peek():J(e),c=t?null:B(e),o=E.slideover({title:t?"Pages on a missing layout":`Pages using ${W(c.label||e)}`,size:"sm",position:"right"}),j=Be(`
|
|
17
|
+
<div class="lyx-form">
|
|
18
|
+
<p class="lyx-note"></p>
|
|
19
|
+
<div class="lyx-pages"></div>
|
|
20
|
+
</div>`);j.querySelector(".lyx-note").textContent=t?"These pages name a layout that no longer exists, so they show as Default. Open one and choose a layout for it.":n.length?"Click a page to open it in the editor.":"No page uses this layout.";const F=j.querySelector(".lyx-pages");for(const l of n){const w=document.createElement("a");w.href=ct(l.urlPath);const z=document.createElement("span");z.textContent=l.title||l.urlPath;const q=document.createElement("small");q.textContent=[l.urlPath,t?`"${l.layout}"`:"",l.status==="draft"?"draft":""].filter(Boolean).join(" \xB7 "),w.append(z,q),w.addEventListener("click",()=>o.close()),F.appendChild(w)}o.element.appendChild(j),C.add(o),$e(o,()=>C.delete(o)),o.open()}function $(e=null,{from:t=null,draft:n=null}={}){const c=e?B(e):null;if(e&&!y.peek()[e]){E.toast(`There is no layout "${e}".`,{type:"warning"});return}const o=!c,j=c||(t?{...B(t),label:`${B(t).label||t} copy`,builtin:!1}:{}),F={...it(j),...n||{}},l=Object.fromEntries(Object.entries(F).map(([a,h])=>[a,M.observable(h)])),w=()=>Object.fromEntries(Object.keys(l).map(a=>[a,l[a].peek()])),z=a=>JSON.stringify(Object.fromEntries(Object.entries(a).map(([h,te])=>[h,typeof te=="boolean"?te:String(te??"")]))),q=z(F),Se=()=>z(w())!==q||!!n||!!(t&&o),_=M.observable(!1),P=M.computed(()=>nt(Object.fromEntries(Object.keys(l).map(a=>[a,l[a].value])),{isNew:o,taken:Object.keys(y.value)})),X=a=>M.computed(()=>P.value.errors[a]||""),We={hasNav:M.computed(()=>l.navbar.value!==!1),hasFoot:M.computed(()=>l.footer.value!==!1),hasSide:M.computed(()=>l.sidebar.value===!0),share:M.computed(()=>De[l.width.value]||De.normal),bg:M.computed(()=>l.useBg.value?l.bgColor.value:"")},Ee={...l,...We,saving:_,labelErr:X("label"),bgErr:X("bgColor"),classErr:X("cssClass"),keyHint:M.computed(()=>o?P.value.key?`Pages will name it "layout: ${P.value.key}".`:"":`Pages name it "layout: ${c.key}" - renaming does not change that.`),widthHint:M.computed(()=>at[l.width.value]||""),builtinNote:!o&&c.builtin===!0,noBg:M.computed(()=>!l.useBg.value),cannot:M.computed(()=>_.value||Object.keys(P.value.errors).length>0),saveText:o?"Create layout":"Save"},Ye=o?t?"Duplicate layout":"New layout":`Edit ${W(c.label||c.key)}`,u=E.slideover({title:Ye,size:"md",position:"right"}),T=Be(`
|
|
21
|
+
<div class="lyx-form">
|
|
22
|
+
<div class="lyx-preview">${Le(".value")}
|
|
23
|
+
<small class="lyx-hint" data-bind-text="keyHint.value"></small></div>
|
|
24
|
+
<p class="lyx-note" data-bind-hidden="!builtinNote">A built-in layout: you can change it, but not delete it.</p>
|
|
25
|
+
<div class="lyx-grid">
|
|
26
|
+
<div>
|
|
27
|
+
<label class="form-label" for="lyx-f-label">Name <span class="text-danger">*</span></label>
|
|
28
|
+
<input id="lyx-f-label" type="text" class="form-input" maxlength="60" placeholder="Dark landing" data-model="label.value">
|
|
29
|
+
<small class="lyx-err" data-bind-hidden="!labelErr.value" data-bind-text="labelErr.value"></small>
|
|
30
|
+
</div>
|
|
31
|
+
<div>
|
|
32
|
+
<label class="form-label" for="lyx-f-width">Width
|
|
33
|
+
<span data-help="How wide the page's content column is. The navbar and footer are not affected."></span></label>
|
|
34
|
+
<select id="lyx-f-width" class="form-input" data-model="width.value">
|
|
35
|
+
${tt.map(a=>`<option value="${a}">${st[a]}</option>`).join("")}
|
|
36
|
+
</select>
|
|
37
|
+
<small class="lyx-hint" data-bind-text="widthHint.value"></small>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="lyx-wide">
|
|
40
|
+
<label class="form-label" for="lyx-f-desc">Description</label>
|
|
41
|
+
<input id="lyx-f-desc" type="text" class="form-input" maxlength="200" placeholder="What it is for - shown in the page editor's picker" data-model="description.value">
|
|
42
|
+
</div>
|
|
43
|
+
<div class="lyx-wide">
|
|
44
|
+
<span class="form-label">Around the content
|
|
45
|
+
<span data-help="Whether pages on this layout show the site's navbar and footer, and the sidebar (for sites whose theme has one)."></span></span>
|
|
46
|
+
<div class="lyx-switches">
|
|
47
|
+
<label><input type="checkbox" class="form-switch" data-model="navbar.value"> Navbar</label>
|
|
48
|
+
<label><input type="checkbox" class="form-switch" data-model="footer.value"> Footer</label>
|
|
49
|
+
<label><input type="checkbox" class="form-switch" data-model="sidebar.value"> Sidebar</label>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
<div>
|
|
53
|
+
<span class="form-label">Background colour
|
|
54
|
+
<span data-help="A colour behind the page's content. Off leaves the theme's own background."></span></span>
|
|
55
|
+
<div class="lyx-colour">
|
|
56
|
+
<input type="checkbox" class="form-switch" aria-label="Use a background colour" data-model="useBg.value">
|
|
57
|
+
<input type="color" aria-label="Background colour" data-model="bgColor.value" data-bind-disabled="noBg.value">
|
|
58
|
+
<small class="lyx-hint" data-bind-text="bg.value || 'Theme background'"></small>
|
|
59
|
+
</div>
|
|
60
|
+
<small class="lyx-err" data-bind-hidden="!bgErr.value" data-bind-text="bgErr.value"></small>
|
|
61
|
+
</div>
|
|
62
|
+
<div>
|
|
63
|
+
<label class="form-label" for="lyx-f-class">CSS class
|
|
64
|
+
<span data-help="Added to the page's body, for your own CSS (content/custom.css) to hook onto - e.g. no-hero."></span></label>
|
|
65
|
+
<input id="lyx-f-class" type="text" class="form-input font-mono" placeholder="e.g. no-hero" data-model="cssClass.value">
|
|
66
|
+
<small class="lyx-err" data-bind-hidden="!classErr.value" data-bind-text="classErr.value"></small>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="lyx-wide">
|
|
69
|
+
<label class="form-label" for="lyx-f-img">Background image
|
|
70
|
+
<span data-help="An image address - /media/hero.jpg for one in the Media library, or a full https:// address."></span></label>
|
|
71
|
+
<input id="lyx-f-img" type="text" class="form-input" placeholder="/media/background.jpg" data-model="bgImage.value">
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="lyx-actions">
|
|
75
|
+
<small class="lyx-hint">Ctrl+Enter saves.</small>
|
|
76
|
+
<span class="lyx-spacer"></span>
|
|
77
|
+
<button type="button" class="btn btn-sm btn-ghost" data-on-click="cancel">Cancel</button>
|
|
78
|
+
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
|
|
79
|
+
<span data-icon="check" data-icon-size="13"></span> <span data-bind-text="saveText"></span></button>
|
|
80
|
+
</div>
|
|
81
|
+
</div>`);async function ke(){if(Ee.cannot.peek())return;_.value=!0;const{payload:a}=P.peek();try{o?await L.layouts.create(a):await L.layouts.update(c.key,a),E.toast(o?`Created "${a.label}".`:"Saved.",{type:"success"}),Z=!0,u.close(),pe(),p()&&await Q()}catch(h){E.toast(`Not saved: ${h.message||h}`,{type:"error"})}finally{_.value=!1}}u.element.appendChild(T);const Ue=Me(T),Ve=M.applyBindings(Ee,T,{methods:{save:ke,cancel:()=>u.close()}}),Ge=a=>{(a.ctrlKey||a.metaKey)&&a.key==="Enter"&&(a.preventDefault(),ke())};T.addEventListener("keydown",Ge);const Ce={key:c?.key??null,dirty:Se,values:w};b=Ce,C.add(u),$e(u,()=>{Ve.dispose(),Ue(),C.delete(u),b===Ce&&(b=null)});const Je=u.close.bind(u);let Z=!1,ee=!1;u.close=async a=>{if(!Z&&!ye&&a!==!0&&Se()){if(ee)return;ee=!0;const h=await E.confirm("Discard the changes you have not saved?");if(ee=!1,!h)return}return Z=!0,Je()},I.scan(T),u.open(),T.querySelector("#lyx-f-label")?.focus()}function Fe(e){const t=y.peek()[e];if(!t)return[];const n=J(e).length;return[{header:t.label||e},{label:"Edit\u2026",icon:"edit",action:()=>$(e)},{label:n?`Pages using it (${n})\u2026`:"Pages using it",icon:"file-text",disabled:!n,action:()=>be(e)},{label:"Duplicate\u2026",icon:"copy",action:()=>$(null,{from:e})},{label:"Copy frontmatter line",icon:"clipboard",action:()=>Re(e)},...t.builtin?[]:[{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>Ke(e)}]]}const O=d.querySelector(".lyx-list");O.innerHTML=rt;const ve=E.tabs(d.querySelector(".lyx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),qe=M.applyBindings({loading:ne,loadError:U,filter:k,query:ie,rows:re,counts:Pe,missing:V,missingText:Ne,spacerSize:m,spacerClass:g,optDirty:x,optCannot:ue,optStatus:Ae,optErrSize:M.computed(()=>K.value.errors.spacerSize||""),optErrClass:M.computed(()=>K.value.errors.spacerClass||"")},d,{methods:{showAll:()=>{k.value="all"},showBuiltin:()=>{k.value="builtin"},showCustom:()=>{k.value="custom"},showMissing:()=>be(null),saveOptions:()=>de(),resetOptions:()=>Ie()}});i(()=>qe.dispose()),i(Me(d,{position:"bottom"}));const _e=E.contextMenu(O,{cursor:"glyph",match:".lyx-row",inherit:!1,density:"compact",items:e=>Fe(e.dataset.key)});i(()=>_e?.destroy?.());const he=e=>{const t=e.target.closest(".lyx-row");if(!t)return;const n=e.target.closest(".lyx-kebab");if(n){Oe(n);return}$(t.dataset.key)},fe=e=>{const t=e.target.closest(".lyx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Oe(t)):e.key==="Enter"&&e.target===t&&$(t.dataset.key))};O.addEventListener("click",he),O.addEventListener("keydown",fe),i(()=>{O.removeEventListener("click",he),O.removeEventListener("keydown",fe)});const me=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||!p()||C.size||(e.preventDefault(),x.peek()&&de())};document.addEventListener("keydown",me,!0),i(()=>document.removeEventListener("keydown",me,!0)),Xe(s,{id:"layouts-new",icon:"plus",title:"New layout",onClick:()=>{ve.activate?.(0),$()}});const ge=e=>{(x.peek()||b?.dirty())&&(xe(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",ge),i(()=>window.removeEventListener("beforeunload",ge));function xe(){try{b?.dirty()&&S.set(se,{key:b.key,values:b.values(),at:new Date().toISOString()}),x.peek()&&S.set(N,{...ce(),at:new Date().toISOString()})}catch{}}if(f={dirty:()=>x.peek()||!!b?.dirty(),dispose(){Y=!0,xe(),ye=!0,[...C].forEach(e=>{try{e.close(!0)}catch{}}),r.splice(0).forEach(e=>{try{e()}catch{}})}},I.scan(d),await Promise.all([Q(),He()]),!p())return;I.scan(d);let D=null,v=null;try{D=S.get(N),v=S.get(se)}catch{}const we=e=>{try{S.remove?S.remove(e):S.set(e,null)}catch{}};if(D&&typeof D=="object"&&"spacerSize"in D&&(we(N),await E.confirm("You left unsaved spacer options. Put them back?")&&p()&&(m.value=String(D.spacerSize??""),g.value=String(D.spacerClass??""),ve.activate?.(1))),v?.values&&p()){we(se);const e=v.key&&y.peek()[v.key],t=e?`your changes to "${W(y.peek()[v.key].label||v.key)}"`:"the new layout you started";await E.confirm(`You left ${t} unsaved. Pick up where you left off?`)&&p()&&$(e?v.key:null,{draft:v.values})}}};
|
package/admin/js/views/login.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as o,isAuthenticated as u,setAuthData as l}from"../api.js";import{themesInDisplayOrder as
|
|
1
|
+
import{api as o,isAuthenticated as u,setAuthData as l,takePendingLogout as p}from"../api.js";import{themesInDisplayOrder as f}from"../lib/themes.js";const h={name:{type:"string",required:!0,minLength:2,label:"Full Name",formConfig:{placeholder:"Your name",autocomplete:"name"}},email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"admin@example.com",autocomplete:"email"}},password:{type:"password",required:!0,minLength:8,label:"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password",tooltip:"Minimum 8 characters"}}},g={email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"you@example.com",autocomplete:"email"}},password:{type:"password",required:!0,label:"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"current-password"}}};export const loginView={templateUrl:"/admin/js/templates/login.html",async onMount(e){if(p(),u()){R.navigate("/");return}const a=window.location.hash,t=a.match(/[?&]token=([a-f0-9]{64})/);if(a.startsWith("#/reset-password")&&t){r(e,"reset"),S(e,t[1]),Domma.icons.scan();return}if(a.startsWith("#/reset-password")){R.navigate("/login");return}let s=!1;const i=E.loader(e.get(0),{type:"dots"});try{s=(await o.auth.setupStatus()).needsSetup}catch{E.toast("Could not reach the server.",{type:"error"})}finally{i.destroy()}s?(r(e,"setup"),k(e)):(r(e,"login"),C(e)),Domma.icons.scan()}};const b={email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"you@example.com",autocomplete:"email"}}},w={password:{type:"password",required:!0,minLength:8,label:"New Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password",tooltip:"Minimum 8 characters"}},confirmPassword:{type:"password",required:!0,label:"Confirm Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password"}}},y=["setup","onboarding-site","onboarding-theme","onboarding-done","login","forgot","forgot-success","reset"];function r(e,a){y.forEach(t=>e.find(`#${t}-panel`).hide()),e.find(`#${a}-panel`).show(),Domma.icons.scan()}function d(e,a,t){e.find(`#${a}`).text(t).show()}function k(e){F.render("#setup-form-container",h,{},{layout:"stacked",submitText:"Create admin account",onSubmit:async a=>{try{const t=await o.auth.setup(a);l(t),r(e,"onboarding-site"),v(e)}catch(t){return E.toast(t.message||"Setup failed. Please try again.",{type:"error"}),!1}}})}function v(e){e.find("#ob-site-skip").on("click",a=>{a.preventDefault(),r(e,"onboarding-theme"),c(e),m(e)}),e.find("#ob-site-btn").on("click",async()=>{e.find("#ob-site-error").hide();const a=e.find("#ob-title").val().trim(),t=e.find("#ob-tagline").val().trim(),s=e.find("#ob-site-btn").prop("disabled",!0).text("Saving\u2026");try{const i=await o.settings.get();await o.settings.save({...i,title:a||i.title,tagline:t||i.tagline,seo:{...i.seo||{},defaultTitle:a||i.seo&&i.seo.defaultTitle}});const n=await o.navigation.get();await o.navigation.save({...n,brand:{...n.brand||{},text:a||n.brand.text}}),r(e,"onboarding-theme"),c(e),m(e)}catch(i){d(e,"ob-site-error",i.message||"Could not save site details.")}finally{s.prop("disabled",!1).text("Continue")}})}function c(e){const a=e.find("#theme-grid").empty();f().forEach(t=>{const s=$(`
|
|
2
2
|
<div class="theme-swatch" data-theme="${t.id}" title="${t.id}">
|
|
3
3
|
<div class="theme-swatch-preview" style="background:${t.bg}">
|
|
4
4
|
<div class="theme-swatch-accent" style="background:${t.primary}"></div>
|
|
@@ -8,4 +8,4 @@ import{api as o,isAuthenticated as u,setAuthData as l}from"../api.js";import{the
|
|
|
8
8
|
<span class="theme-swatch-mode">${t.dark?"Dark":"Light"}</span>
|
|
9
9
|
</div>
|
|
10
10
|
</div>
|
|
11
|
-
`);
|
|
11
|
+
`);s.on("click",()=>{e.find(".theme-swatch").removeClass("selected"),s.addClass("selected")}),a.append(s)}),e.find('[data-theme="charcoal-dark"]').addClass("selected")}function m(e){e.find("#ob-theme-skip").on("click",a=>{a.preventDefault(),r(e,"onboarding-done")}),e.find("#ob-theme-btn").on("click",async()=>{e.find("#ob-theme-error").hide();const a=e.find(".theme-swatch.selected").attr("data-theme")||"charcoal-dark",t=e.find("#ob-theme-btn").prop("disabled",!0).text("Applying\u2026");try{const s=await o.settings.get();await o.settings.save({...s,theme:a}),r(e,"onboarding-done")}catch(s){d(e,"ob-theme-error",s.message||"Could not save theme.")}finally{t.prop("disabled",!1).text("Apply theme")}})}function C(e){F.render("#login-form-container",g,{},{layout:"stacked",submitText:"Sign in",onSubmit:async a=>{try{const t=await o.auth.login(a);l(t),R.navigate("/")}catch(t){return E.toast(t.message||"Invalid credentials. Please try again.",{type:"error"}),!1}}}),e.find("#forgot-link").on("click",a=>{a.preventDefault(),r(e,"forgot"),x(e)})}function x(e){F.render("#forgot-form-container",b,{},{layout:"stacked",submitText:"Send reset link",onSubmit:async t=>{try{await o.auth.forgotPassword(t.email)}catch{}r(e,"forgot-success"),Domma.icons.scan()}});const a=t=>{t.preventDefault(),r(e,"login")};e.find("#forgot-back-link").on("click",a),e.find("#forgot-success-back-link").on("click",a)}function S(e,a){F.render("#reset-form-container",w,{},{layout:"stacked",submitText:"Set new password",onSubmit:async t=>{if(t.password!==t.confirmPassword)return E.toast("Passwords do not match.",{type:"error"}),!1;try{await o.auth.resetPassword(a,t.password),E.toast("Password updated. Please sign in.",{type:"success"}),window.location.hash="#/login"}catch(s){return E.toast(s.message||"Link expired or invalid.",{type:"error"}),!1}}})}
|
|
@@ -1,47 +1,25 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
<div class="menu-tree-row menu-tree-row--separator" data-id="${
|
|
3
|
-
<span class="menu-tree-grip" data-icon="move"></span>
|
|
1
|
+
import{api as T}from"../api.js";import{colourToCss as St,HIGHLIGHT_HOVER as ot,HIGHLIGHT_ACTIVE as at,HIGHLIGHT_HOVER_LABELS as it,HIGHLIGHT_ACTIVE_LABELS as st}from"/public/js/menu-decor.mjs?v=20260918-item-highlight";import{openIconPicker as $t}from"../lib/card-builder.js";import{applyDrop as At}from"../lib/menu-tree.js";import{placeTools as xt,ensureLockedTools as Et,isLockedUrl as Lt,TOOLS_FOLDER_KEY as _t}from"../lib/sidebar-grouping.js";import{flatten as Ht,nest as Ct,uid as Ee}from"../lib/menu-rows.js";import{addBannerAction as Me}from"../lib/plugin-chrome.js";import{helpPopovers as It}from"../lib/help-popover.js";import{onceClosed as Tt,openMenuAt as ze,sidebarCount as Dt}from"../lib/tool-kit.js";import{addChild as Rt,bindingFrom as jt,duplicateRow as Nt,indentRow as Ot,insertBelow as Pt,isStructural as ae,menuFromForm as Bt,menuProblems as lt,moveRow as rt,outdentRow as Ft,siblingsOf as Mt,snapshot as Le}from"../lib/menu-editor-arrange.js";const zt=760,qt="admin-sidebar",Wt=/^\/menus\/(new|edit\/[^?#]+)(\?.*)?$/;function w(t){return String(t||"").replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<")}const _e=t=>String(t??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function Kt(t,r){let l=0,i=r.find(o=>o.id===t);for(;i&&i.parentId;)l++,i=r.find(o=>o.id===i.parentId);return l}function ct(t,r){const{top:l,height:i}=t.getBoundingClientRect(),o=i?(r-l)/i:.5;return o<.25?"above":o>.75?"below":"into"}const dt=["me-drop-above","me-drop-below","me-drop-into"];function qe(t){t.querySelectorAll(".menu-tree-row").forEach(r=>r.classList.remove(...dt))}function Ut(t,r,l){let i=null;t.querySelectorAll(".menu-tree-row").forEach(o=>{const f=o.querySelector(".menu-tree-grip");o.draggable=!1,f&&(f.addEventListener("mousedown",()=>{o.draggable=!0}),f.addEventListener("mouseup",()=>{o.draggable=!1})),o.addEventListener("dragstart",h=>{i=o.dataset.id,o.style.opacity="0.45",h.dataTransfer.effectAllowed="move";try{h.dataTransfer.setData("text/plain",i)}catch{}}),o.addEventListener("dragend",()=>{o.style.opacity="",o.draggable=!1,i=null,qe(t)}),o.addEventListener("dragover",h=>{if(!i||i===o.dataset.id)return;h.preventDefault(),h.dataTransfer.dropEffect="move";const d=ct(o,h.clientY);qe(t),o.classList.add(`me-drop-${d}`)}),o.addEventListener("dragleave",()=>o.classList.remove(...dt)),o.addEventListener("drop",h=>{h.preventDefault();const d=o.dataset.id,C=ct(o,h.clientY);if(qe(t),!i)return;const $=At(r(),i,d,C);if($){const N=r().find(A=>A.id===i);N&&(N.touched=!0)}i=null,$&&l()})})}function ut(t){const r=new Set,l=new Set,i=o=>{for(const f of t.filter(h=>h.parentId===o))!f.type&&Lt(f.url)&&!l.has(f.url)&&(l.add(f.url),r.add(f.id)),i(f.id)};return i(null),r}const We='<button type="button" class="btn btn-sm btn-ghost mex-kebab" aria-label="Actions" title="Actions (right-click, or Shift+F10)">\u22EE</button>';function Gt(t,r,l=!1){const i=l?ut(r):new Set,o=[];function f(h){for(const d of r.filter(C=>C.parentId===h)){const $=`style="margin-left:${Kt(d.id,r)*28}px"`;if(d.type==="separator"){o.push(`
|
|
2
|
+
<div class="menu-tree-row menu-tree-row--separator" data-id="${d.id}" tabindex="0" ${$}>
|
|
3
|
+
<span class="menu-tree-grip" data-icon="move" title="Drag to move"></span>
|
|
4
4
|
<span class="me-sep-line"><span class="me-sep-label">Separator</span></span>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
<
|
|
9
|
-
<button class="btn btn-sm btn-ghost me-up" data-tooltip="Move up"><span data-icon="arrow-up"></span></button>
|
|
10
|
-
<button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
|
|
11
|
-
<button class="btn btn-sm btn-ghost me-remove" data-tooltip="Remove"><span data-icon="trash"></span></button>
|
|
12
|
-
</div>
|
|
13
|
-
`);continue}if(p.type==="spacer"){const M=["xs","sm","md","lg","xl","flex"].map(s=>`<option value="${s}"${(p.size||"md")===s?" selected":""}>${s==="flex"?"flexible":s}</option>`).join("");t.append(`
|
|
14
|
-
<div class="menu-tree-row menu-tree-row--spacer" data-id="${p.id}" style="margin-left:${x*28}px">
|
|
15
|
-
<span class="menu-tree-grip" data-icon="move"></span>
|
|
5
|
+
${We}
|
|
6
|
+
</div>`);continue}if(d.type==="spacer"){const te=["xs","sm","md","lg","xl","flex"].map(B=>`<option value="${B}"${(d.size||"md")===B?" selected":""}>${B==="flex"?"flexible":B}</option>`).join("");o.push(`
|
|
7
|
+
<div class="menu-tree-row menu-tree-row--spacer" data-id="${d.id}" tabindex="0" ${$}>
|
|
8
|
+
<span class="menu-tree-grip" data-icon="move" title="Drag to move"></span>
|
|
16
9
|
<span class="me-spacer-line">
|
|
17
10
|
<span class="me-spacer-label">Spacer</span>
|
|
18
|
-
<select class="form-input me-spacer-size"
|
|
11
|
+
<select class="form-input me-spacer-size" title="Spacer size - flexible fills the leftover room" aria-label="Spacer size">${te}</select>
|
|
19
12
|
</span>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<div class="menu-tree-row menu-tree-row--slim${p.hidden?" menu-tree-row--hidden":""}${v?" menu-tree-row--tool":""}${v&&!L?" menu-tree-row--default":""}" data-id="${p.id}" style="margin-left:${x*28}px">
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<button class="btn btn-sm btn-ghost me-ins-item" data-tooltip="Add item below"><span data-icon="plus"></span></button>
|
|
34
|
-
<button class="btn btn-sm btn-ghost me-ins-sep" data-tooltip="Add separator below"><span data-icon="minus"></span></button>
|
|
35
|
-
<button class="btn btn-sm btn-ghost me-outdent" data-tooltip="Outdent"><span data-icon="chevron-left"></span></button>
|
|
36
|
-
<button class="btn btn-sm btn-ghost me-indent" data-tooltip="Indent"><span data-icon="chevron-right"></span></button>
|
|
37
|
-
<button class="btn btn-sm btn-ghost me-up" data-tooltip="Move up"><span data-icon="arrow-up"></span></button>
|
|
38
|
-
<button class="btn btn-sm btn-ghost me-down" data-tooltip="Move down"><span data-icon="arrow-down"></span></button>
|
|
39
|
-
${q?'<button class="btn btn-sm btn-ghost" disabled data-tooltip="Always shown"><span data-icon="eye-off"></span></button>':`<button class="btn btn-sm btn-ghost me-hidden ${p.hidden?"active":""}" data-tooltip="${p.hidden?"Show":"Hide"}"><span data-icon="eye-off"></span></button>`}
|
|
40
|
-
<button class="btn btn-sm btn-secondary me-edit" data-tooltip="Edit"><span data-icon="edit"></span></button>
|
|
41
|
-
${Z}
|
|
42
|
-
</div>
|
|
43
|
-
`),o(p.id)}}o(null),Domma.icons.scan(t.get(0)),t.get(0).querySelectorAll("[data-tooltip]").forEach(m=>{E.tooltip(m,{content:m.getAttribute("data-tooltip"),position:"top"})})}function j(t,c){t.find(".menu-tree-row").each(function(){const r=$(this).data("id"),i=c.find(k=>k.id===r);if(!i||i.type==="separator")return;if(i.type==="spacer"){const k=$(this).find(".me-spacer-size");k.length&&(i.size=k.val());return}const a=$(this).find(".me-text");a.length&&(i.text=a.val());const o=$(this).find(".me-url");o.length&&(i.url=o.val());const m=$(this).find(".me-icon");m.length&&(i.icon=m.val());const p=$(this).find(".me-vis");p.length&&(i.visibility=p.val());const x=$(this).find(".me-perm");x.length&&(i.permission=x.val())})}function Ut(t,c){const r={};for(const i of c){const a=t.querySelector(`[name="${i}"]`);a&&(r[i]=a.type==="checkbox"?a.checked:a.value)}return r}function w(t,c={},...r){const i=document.createElement(t);Object.assign(i,c);for(const a of r)a!=null&&i.append(a);return i}function O(t,c,r){const i=w("label",{className:"form-field"},w("span",{className:"form-label",textContent:t}),c);return r&&i.append(w("small",{className:"form-hint",textContent:r,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),i}function Pt(t,c){const r=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(c||[]).map(a=>[a.name,`${a.label||a.name} and above`]),...(c||[]).filter(a=>a.level!==0).map(a=>[`=${a.name}`,`${a.label||a.name} only`]),["private","Private - super-admin only"]];t&&!r.some(([a])=>a===t)&&r.push([t,`${t} (custom)`]);const i=D(r,t||"");return i.name="visibility",i}function D(t,c){const r=w("select",{className:"form-select"});return t.forEach(([i,a])=>r.add(new Option(a,i))),r.value=c??"",r}function dt(t,c){const r=typeof c=="string"&&c.startsWith("#"),i=D([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],r?"__custom__":c||""),a=w("input",{type:"color",className:"form-input me-colour-hex",value:r?c:"#000000"});a.style.display=r?"":"none",i.addEventListener("change",()=>{a.style.display=i.value==="__custom__"?"":"none"});const o=O(t,i);return o.append(a),o._read=()=>i.value==="__custom__"?a.value:i.value,o}function Bt(t,c){const r=w("input",{className:"form-input me-icon",name:"icon",value:c||""}),i=w("span",{className:"me-icon-preview"});c&&i.setAttribute("data-icon",c);const a=w("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),o=m=>{m?i.setAttribute("data-icon",m):i.removeAttribute("data-icon"),Domma.icons.scan(i.parentNode||i)};return a.addEventListener("click",()=>_t(m=>{r.value=m,o(m)})),r.addEventListener("input",()=>o(r.value.trim())),O(t,w("div",{className:"me-icon-field"},r,a,i))}function ct(...t){return w("div",{className:"me-field-grid"},...t)}async function Dt(t,c,r,i,a){const o=b=>w("h4",{className:"me-form-section",textContent:b}),m=O("Label",w("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),p=w("input",{className:"form-input",name:"url",value:t.url||""}),x=O("URL",p,t.tool||t.locked?"A Tool's address is its identity, so it cannot be changed here.":"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items.");(t.tool||t.locked)&&(p.readOnly=!0);const k=Bt("Icon",t.icon||""),v=Pt(t.visibility||"",i),L=O("Visibility",v,t.locked?"Always in the sidebar for everyone who may use it, so it cannot be restricted here.":'Who can see this item. "Role and above" shows it to that role and anyone more senior; "Role only" shows it to holders of that role alone (and the super-admin). Blank = everyone.');t.locked&&(v.disabled=!0);const V=D([["","- no permission gate -"],...(c||[]).map(b=>[b.key,b.label||b.key])],t.permission||"");V.name="permission";const q=O("Permission",V,t.tool||t.locked?"A Tool is guarded by its own permission, which always applies.":"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass.");(t.tool||t.locked)&&(V.disabled=!0);const Z=O("Badge text",w("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),W=D([["","- none -"],...(r||[]).map(b=>[b.slug,b.title||b.slug])],t.badge?.countFrom||"");W.name="badgeCount";const C=O("Badge count from",W,"Show a live count of this collection\u2019s entries as the badge."),M=dt("Badge colour",t.badge?.variant||""),s=D([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");s.name="renderAs";const A=O("Render as",s,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),y=O("Pill style",D([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));y.querySelector("select").name="pillStyle";const J=dt("Pill colour",t.pill?.variant||""),Y=w("div",{className:"me-pill-wrap"},ct(y,J));Y.style.display=t.pill?"":"none",s.addEventListener("change",()=>{Y.style.display=s.value==="pill"?"":"none"});const Q=dt("Text colour",t.colour||""),U=D([["","- use the menu default -"],...mt.map(b=>[b,ht[b]||b])],t.highlight?.hover||"");U.name="hlHover";const at=O("Hover effect",U,"Overrides the menu-wide hover effect for this item only."),N=D([["","- use the menu default -"],...ut.map(b=>[b,bt[b]||b])],t.highlight?.active||"");N.name="hlActive";const tt=O("When selected",N,"How this item is marked when it points at the page being viewed."),X=dt("Highlight colour",t.highlight?.accent||""),st=w("button",{className:"btn btn-primary",textContent:"Apply"}),F=w("div",{className:"me-item-form"},o("Link"),m,x,k,o("Access"),ct(L,q),o("Badge"),ct(Z,M),C,o("Appearance"),A,Y,Q,o("Highlighting"),ct(at,tt),X,st),R=E.slideover({title:"Edit menu item",size:"lg",position:"right"});R.element.appendChild(F),R.open();const K=R.element.closest(".dm-slideover")||R.element.querySelector(".dm-slideover")||R.element;K.style.setProperty("width",Et+"px","important"),K.style.setProperty("max-width","95vw","important"),Domma.icons.scan(F);const _=b=>F.querySelector(`[name="${b}"]`)?.value||"";st.addEventListener("click",()=>{t.text=_("text"),t.icon=_("icon"),t.locked||(t.visibility=_("visibility")),!t.tool&&!t.locked?(t.url=_("url"),t.permission=_("permission")):t.touched=!0;const b=_("badgeText"),G=_("badgeCount"),et=M._read();if(t.badge=b||G?{...b&&{text:b},...G&&{countFrom:G},...et&&{variant:et}}:null,_("renderAs")==="pill"){const B=J._read();t.pill={style:_("pillStyle")||"filled",...B&&{variant:B}}}else t.pill=null;t.colour=Q._read();const nt=_("hlHover"),z=_("hlActive"),P=X._read();t.highlight=nt||z||P?{...nt&&{hover:nt},...z&&{active:z},...P&&{accent:P}}:null,R.close(),a()})}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){const c=location.hash.match(/^#\/menus\/edit\/(.+)$/),r=c?decodeURIComponent(c[1]):null,i=!r,a=E.loader(t.get(0),{type:"dots"});let o,m,p,x,k,v,L;try{if(i)o={slug:"",name:"",description:"",items:[]};else try{o=await S.menus.get(r)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}m=await S.projects.list().catch(()=>[]),p=await H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),x=await S.collections.list().catch(()=>[]),k=await S.get("/collections/roles/entries?limit=100").catch(()=>null),v=await S.menuLocations.registry().catch(()=>[]),L=await S.pages.list().catch(()=>[])}finally{a.destroy()}const q=(Array.isArray(k)?k:k?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,n)=>n.level-e.level),Z='<option value="">- no permission gate -</option>',W=e=>Z+(p.resources||[]).map(n=>{const l=(e||"")===n.key?" selected":"";return`<option value="${n.key}"${l}>${n.label||n.key}</option>`}).join("");t.find("#me-title").text(i?"New menu":`Edit "${o.slug}"`);const C=!i&&r===Rt;let M=[];C&&(M=await H.get("/api/sidebar/registered-items").catch(()=>(E.toast("Could not load the Tools - they are not shown, and saving keeps their places as they are.",{type:"warning",duration:8e3}),[])));let s=jt(C?Ot(Ht(o.items||[],M,{forEditor:!0}),{leaveInHiddenFolders:!0}):o.items);const A=t.find("#me-items-list");function y(){zt(A,s,W,C),Ft(A,()=>s,y)}y(),t.find("#me-add-item").on("click",()=>{j(A,s),s.push({id:it(),parentId:null,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}),y()}),t.find("#me-add-separator").on("click",()=>{j(A,s),s.push({id:it(),parentId:null,type:"separator"}),y()}),t.find("#me-add-spacer").on("click",()=>{j(A,s),s.push({id:it(),parentId:null,type:"spacer",size:"md"}),y()});function J(e){return function(){j(A,s);const n=$(this).closest(".menu-tree-row").data("id"),l=s.findIndex(f=>f.id===n);if(l===-1)return;const u=s[l],d={separator:{type:"separator"},spacer:{type:"spacer",size:"md"}}[e],h=d?{id:it(),parentId:u.parentId,...d}:{id:it(),parentId:u.parentId,text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1};s.splice(l+1,0,h),y()}}t.off("click",".me-ins-item").on("click",".me-ins-item",J("item")),t.off("click",".me-ins-sep").on("click",".me-ins-sep",J("separator"));function Y(e){const n=u=>s.find(d=>!d.type&&!d.url&&!d.tool&&(d.key||"").toLowerCase()===u)||s.find(d=>!d.type&&!d.url&&!d.tool&&!d.key&&(d.text||"").toLowerCase()===u),l=n(String(e.tool.folder||"").toLowerCase())||n(Tt);e.parentId=l?l.id:null,s.splice(s.indexOf(e),1),s.push(e),Object.assign(e,{text:e.tool.defaults.text,icon:e.tool.defaults.icon,visibility:"",hidden:!1,badge:null,pill:null,colour:"",highlight:null,touched:!1}),e.tool={...e.tool,persisted:!1}}t.off("click",".me-remove").on("click",".me-remove",function(){const e=$(this).closest(".menu-tree-row").data("id"),n=s.find(f=>f.id===e),l=f=>{let I=f;for(;I;){if(I.id===e)return!0;I=s.find(T=>T.id===I.parentId)}return!1},u=C?lt(s):new Set;if(u.has(e))return;const d=s.filter(f=>f.id!==e&&f.tool&&l(f)),h=s.filter(f=>f.id!==e&&!f.tool&&u.has(f.id)&&l(f));s=s.filter(f=>!l(f)||d.includes(f)||h.includes(f));for(const f of h)f.parentId=n?.parentId??null;for(const f of d)f.tool.orphan?f.parentId=n?.parentId??null:Y(f);y()}),t.off("click",".me-indent").on("click",".me-indent",function(){j(A,s);const e=$(this).closest(".menu-tree-row").data("id"),n=s.find(d=>d.id===e);if(!n)return;const l=s.filter(d=>d.parentId===n.parentId),u=l.findIndex(d=>d.id===e);u<=0||["separator","spacer"].includes(l[u-1].type)||(n.parentId=l[u-1].id,n.touched=!0,y())}),t.off("click",".me-outdent").on("click",".me-outdent",function(){j(A,s);const e=$(this).closest(".menu-tree-row").data("id"),n=s.find(u=>u.id===e);if(!n||n.parentId==null)return;const l=s.find(u=>u.id===n.parentId);n.parentId=l?l.parentId:null,n.touched=!0,y()}),t.off("click",".me-up").on("click",".me-up",function(){j(A,s);const e=$(this).closest(".menu-tree-row").data("id"),n=s.filter(h=>h.parentId===s.find(f=>f.id===e)?.parentId),l=n.findIndex(h=>h.id===e);if(l<=0)return;n[l].touched=!0;const u=s.indexOf(n[l]),d=s.indexOf(n[l-1]);[s[u],s[d]]=[s[d],s[u]],y()}),t.off("click",".me-down").on("click",".me-down",function(){j(A,s);const e=$(this).closest(".menu-tree-row").data("id"),n=s.filter(h=>h.parentId===s.find(f=>f.id===e)?.parentId),l=n.findIndex(h=>h.id===e);if(l===-1||l>=n.length-1)return;n[l].touched=!0;const u=s.indexOf(n[l]),d=s.indexOf(n[l+1]);[s[u],s[d]]=[s[d],s[u]],y()}),t.off("click",".me-hidden").on("click",".me-hidden",function(){j(A,s);const e=$(this).closest(".menu-tree-row").data("id"),n=s.find(l=>l.id===e);n&&!(C&<(s).has(n.id))&&(n.hidden=!n.hidden,n.touched=!0),y()}),t.off("click",".me-reset").on("click",".me-reset",function(){j(A,s);const e=$(this).closest(".menu-tree-row").data("id"),n=s.find(l=>l.id===e);!n?.tool||n.tool.orphan||(Y(n),y())}),t.off("click",".me-edit").on("click",".me-edit",function(){const e=$(this).closest(".menu-tree-row").data("id"),n=s.find(l=>l.id===e);!n||["separator","spacer"].includes(n.type)||(n.locked=C&<(s).has(n.id),Dt(n,p.resources||[],x,q,()=>y()))}),E.tabs(t.find("#me-tabs").get(0)),t.find("#me-variant").val(o.variant||""),t.find("#me-position").val(o.position||"");const Q=t.find("#me-appearOnHover").get(0);Q&&(Q.checked=o.appearOnHover===!0),t.find("#me-float-anchor").val(o.float?.anchor||"TL"),t.find("#me-float-offsetX").val(o.float?.offsetX??""),t.find("#me-float-offsetY").val(o.float?.offsetY??""),t.find("#me-float-width").val(o.float?.width??""),t.find("#me-float-radius").val(o.float?.radius??""),t.find("#me-float-shadow").val(o.float?.shadow||""),t.find("#me-float-opacity").val(o.float?.opacity??""),t.find("#me-float-title").val(o.float?.title||"");for(const[e,n]of[["#me-float-collapsible",o.float?.collapsible],["#me-float-collapsed",o.float?.collapsed],["#me-float-movable",o.float?.movable],["#me-float-resizable",o.float?.resizable]]){const l=t.find(e).get(0);l&&(l.checked=n===!0)}const U=o.float?.accent||"",at=U.startsWith("#"),N=t.find("#me-float-accent"),tt=t.find("#me-float-accent-hex");N.val(at?"__custom__":U),at&&tt.val(U);const X=()=>{tt.get(0).style.display=N.val()==="__custom__"?"":"none"};X(),N.on("change",X);const st=()=>N.val()==="__custom__"?tt.val():N.val(),F=()=>{const e=t.find("#me-position").val()==="floating";t.find(".dm-float-field").each(function(){this.style.display=e?"":"none"}),e&&X();const n=t.find("#me-orientation").val()==="vertical";t.find(".dm-vertical-field").each(function(){this.style.display=n?"":"none"});const l=!!(t.find("#me-float-collapsible").get(0)||{}).checked;t.find(".dm-float-collapsed-field").each(function(){this.style.display=e&&l?"":"none"})};t.find("#me-orientation").val(o.orientation||""),t.find("#me-align").val(o.align||""),t.find("#me-side").val(o.side||"left"),F(),t.find("#me-position").on("change",F),t.find("#me-orientation").on("change",F),t.find("#me-float-collapsible").on("change",F);const R=(e,n,l,u)=>{const d=t.find(e).get(0);d&&(d.innerHTML="",d.add(new Option(u,"")),n.forEach(h=>d.add(new Option(l[h]||h,h))))};R("#me-highlight-hover",mt,ht,"- theme default -"),R("#me-highlight-active",ut,bt,"- theme default -"),t.find("#me-highlight-hover").val(o.highlight?.hover||""),t.find("#me-highlight-active").val(o.highlight?.active||"");const K=o.highlight?.accent||"",_=K.startsWith("#"),b=t.find("#me-highlight-accent"),G=t.find("#me-highlight-accent-hex");b.val(_?"__custom__":K),_&&G.val(K);const et=()=>{G.get(0).style.display=b.val()==="__custom__"?"":"none"};et(),b.on("change",et);const nt=()=>b.val()==="__custom__"?G.val():b.val();t.find("#me-fontFamily").val(o.style?.fontFamily||""),t.find("#me-fontSize").val(o.style?.fontSize||""),t.find("#me-fontWeight").val(o.style?.fontWeight||""),t.find("#me-letterSpacing").val(o.style?.letterSpacing||""),t.find("#me-iconSize").val(o.style?.iconSize||"");const z=t.find("#me-bind-slot"),P=t.find("#me-bind-projects"),B=t.find("#me-bind-pages");z.html('<option value="">- not bound -</option>'+(v||[]).map(e=>`<option value="${g(e.slot)}">${g(e.label||e.slot)} (${g(e.slot)})</option>`).join("")),P.html(m.map(e=>`<option value="${g(e.slug)}">${g(e.name||e.slug)}</option>`).join("")),z.val(o.binding?.slot||"");const yt=new Set(o.binding?.projects||[]);Array.from(P.get(0).options).forEach(e=>{e.selected=yt.has(e.value)}),B.val((o.binding?.pages||[]).join(`
|
|
44
|
-
`)),t.find("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+L.map(e=>{const n=e.urlPath||e.url||"";return n?`<option value="${g(n)}">${g(e.title||n)} - ${g(n)}</option>`:""}).join("")),t.find("#me-bind-page-add").on("click",()=>{const e=t.find("#me-bind-page-pick").val();if(!e)return;const n=B.val().split(`
|
|
45
|
-
`).map(l=>l.trim()).filter(Boolean);n.includes(e)||n.push(e),B.val(n.join(`
|
|
46
|
-
`)),ot()});function pt(){const e=z.val();return e?{slot:e,projects:Array.from(P.get(0).selectedOptions).map(n=>n.value),pages:B.val().split(`
|
|
47
|
-
`).map(n=>n.trim()).filter(Boolean)}:null}function ot(){const e=pt(),n=t.find("#me-bind-summary");if(!e){n.html('<span class="text-muted">Not bound - this menu renders only where it is mapped or placed by a shortcode.</span>');return}const l=[];e.projects.length&&l.push(`pages in ${e.projects.map(h=>`<code>${g(h)}</code>`).join(", ")}`),e.pages.length&&l.push(`pages matching ${e.pages.map(h=>`<code>${g(h)}</code>`).join(", ")}`);const u=e.pages.filter(h=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(h)),d=u.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${u.map(h=>`<code>${g(h)}</code>`).join(", ")}</div>`:"";n.html(l.length?`Takes over the <code>${g(e.slot)}</code> slot on ${l.join(" and ")}.${d}`:`<span class="text-warning">Slot <code>${g(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}z.on("change",ot),P.on("change",ot),B.on("input",ot),ot();const{getProjectFromHash:wt}=await import("../lib/project-context.js"),kt=i?wt():null,ft=t.find("#me-project");ft.html('<option value="">- none -</option>'+m.map(e=>`<option value="${g(e.slug)}">${g(e.name||e.slug)}</option>`).join("")),t.find("#me-slug").val(o.slug||""),t.find("#me-name").val(o.name||""),t.find("#me-description").val(o.description||""),ft.val(o.meta?.project||kt||""),Domma.icons.scan(t.get(0)),t.find("#me-save").on("click",async()=>{j(A,s);const e={variant:t.find("#me-variant").val(),position:t.find("#me-position").val(),appearOnHover:!!(t.find("#me-appearOnHover").get(0)||{}).checked,floatAnchor:t.find("#me-float-anchor").val(),floatOffsetX:t.find("#me-float-offsetX").val(),floatOffsetY:t.find("#me-float-offsetY").val(),floatWidth:t.find("#me-float-width").val(),floatRadius:t.find("#me-float-radius").val(),floatShadow:t.find("#me-float-shadow").val(),floatAccent:st(),floatOpacity:t.find("#me-float-opacity").val(),floatTitle:t.find("#me-float-title").val().trim(),floatCollapsible:!!(t.find("#me-float-collapsible").get(0)||{}).checked,floatCollapsed:!!(t.find("#me-float-collapsed").get(0)||{}).checked,floatMovable:!!(t.find("#me-float-movable").get(0)||{}).checked,floatResizable:!!(t.find("#me-float-resizable").get(0)||{}).checked,highlightHover:t.find("#me-highlight-hover").val(),highlightActive:t.find("#me-highlight-active").val(),highlightAccent:nt(),orientation:t.find("#me-orientation").val(),align:t.find("#me-align").val(),side:t.find("#me-side").val(),fontFamily:t.find("#me-fontFamily").val(),fontSize:t.find("#me-fontSize").val(),fontWeight:t.find("#me-fontWeight").val(),letterSpacing:t.find("#me-letterSpacing").val(),iconSize:t.find("#me-iconSize").val()},n={slug:t.find("#me-slug").val().trim(),name:t.find("#me-name").val().trim(),description:t.find("#me-description").val().trim(),project:t.find("#me-project").val()},l=["fontFamily","fontSize","fontWeight","letterSpacing","iconSize"],u=Object.fromEntries(Object.entries(e).filter(([I,T])=>l.includes(I)&&T)),d={};if(e.position==="floating"){e.floatAnchor&&(d.anchor=e.floatAnchor);for(const[I,T]of[["offsetX",e.floatOffsetX],["offsetY",e.floatOffsetY],["width",e.floatWidth],["radius",e.floatRadius],["opacity",e.floatOpacity]])T!==""&&T!=null&&Number.isFinite(Number(T))&&(d[I]=Number(T));e.floatShadow&&(d.shadow=e.floatShadow),e.floatAccent&&(d.accent=e.floatAccent),e.floatTitle&&(d.title=e.floatTitle),e.floatCollapsible&&(d.collapsible=!0),e.floatCollapsible&&e.floatCollapsed&&(d.collapsed=!0),e.floatMovable&&(d.movable=!0),e.floatResizable&&(d.resizable=!0)}const h={};e.highlightHover&&(h.hover=e.highlightHover),e.highlightActive&&(h.active=e.highlightActive),e.highlightAccent&&(h.accent=e.highlightAccent);const f={slug:n.slug,name:n.name,description:n.description,...e.variant&&{variant:e.variant},...e.position&&{position:e.position},...e.orientation&&{orientation:e.orientation},...e.orientation==="vertical"&&e.side&&{side:e.side},...e.orientation!=="vertical"&&e.align&&{align:e.align},...Object.keys(d).length&&{float:d},...Object.keys(h).length&&{highlight:h},...e.appearOnHover&&{appearOnHover:!0},...Object.keys(u).length&&{style:u},binding:pt(),items:Ct(s),meta:{...o.meta||{},project:n.project||null}};try{if(i)await S.menus.create(f);else if(f.slug!==o.slug){await S.menus.create(f);const I=await S.menuLocations.get();let T=!1;for(const[xt,At]of Object.entries(I))At===o.slug&&(I[xt]=f.slug,T=!0);T&&await S.menuLocations.save(I),await S.menus.remove(o.slug)}else await S.menus.update(o.slug,f);E.toast("Saved.",{type:"success"}),location.hash="#/menus/edit/"+encodeURIComponent(f.slug)}catch(I){E.toast(`Save failed: ${I.message||I}`,{type:"error"})}})}};
|
|
13
|
+
${We}
|
|
14
|
+
</div>`);continue}const N=d.badge&&(d.badge.text||d.badge.countFrom)?`<span class="dm-menu-badge">${w(String(d.badge.text||"#"))}</span>`:"",A=d.tool,g=A&&(A.persisted||d.touched),K=A?A.orphan?' <span class="badge badge-secondary me-tool-badge" title="A placeholder for a Tool that is not enabled - it returns here when it is">Not enabled</span>':` <span class="badge badge-secondary me-tool-badge" title="${g?"A Tool, placed here by this menu":"A Tool in its default place - move or edit it to place it here"}">${w(A.source||"Tool")}${g?"":" \xB7 default"}</span>`:"",ie=i.has(d.id),ee=St(d.colour),U=ee?` style="color:${w(ee)}"`:"",se=ie?'<button type="button" class="btn btn-sm btn-ghost" disabled title="Always shown - it can be moved, not hidden" aria-label="Always shown"><span data-icon="lock"></span></button>':`<button type="button" class="btn btn-sm btn-ghost me-hidden ${d.hidden?"active":""}" title="${d.hidden?"Hidden - click to show":"Hide"}" aria-label="${d.hidden?"Show":"Hide"}" aria-pressed="${d.hidden?"true":"false"}"><span data-icon="${d.hidden?"eye-off":"eye"}"></span></button>`;o.push(`
|
|
15
|
+
<div class="menu-tree-row menu-tree-row--slim${d.hidden?" menu-tree-row--hidden":""}${A?" menu-tree-row--tool":""}${A&&!g?" menu-tree-row--default":""}" data-id="${d.id}" tabindex="0" ${$}>
|
|
16
|
+
<span class="menu-tree-grip" data-icon="move" title="Drag to move"></span>
|
|
17
|
+
<span class="me-row-label${!d.text&&!A?" mex-untitled":""}"${U}>${w(d.text||A?.label||"(no label - click to add one)")}${N}${K}</span>
|
|
18
|
+
<span class="me-row-url">${w(d.url||"")}</span>
|
|
19
|
+
${se}
|
|
20
|
+
<button type="button" class="btn btn-sm btn-ghost me-edit" title="Edit" aria-label="Edit"><span data-icon="edit"></span></button>
|
|
21
|
+
${We}
|
|
22
|
+
</div>`),f(d.id)}}f(null),t.innerHTML=o.join("")||'<p class="mex-note">No items yet - Add item, or right-click an item for more.</p>',I.scan(t)}function k(t,r={},...l){const i=document.createElement(t);Object.assign(i,r);for(const o of l)o!=null&&i.append(o);return i}function j(t,r,l){const i=k("label",{className:"form-field"},k("span",{className:"form-label",textContent:t}),r);return l&&i.append(k("small",{className:"form-hint",textContent:l,style:"display:block;font-size:.78em;opacity:.7;margin-top:.25rem;line-height:1.35"})),i}function Vt(t,r){const l=[["","- Everyone -"],["public","Public - anyone, incl. logged-out"],...(r||[]).map(o=>[o.name,`${o.label||o.name} and above`]),...(r||[]).filter(o=>o.level!==0).map(o=>[`=${o.name}`,`${o.label||o.name} only`]),["private","Private - super-admin only"]];t&&!l.some(([o])=>o===t)&&l.push([t,`${t} (custom)`]);const i=Q(l,t||"");return i.name="visibility",i}function Q(t,r){const l=k("select",{className:"form-select"});return t.forEach(([i,o])=>l.add(new Option(o,i))),l.value=r??"",l}function He(t,r){const l=typeof r=="string"&&r.startsWith("#"),i=Q([["","- none -"],["primary","primary"],["success","success"],["danger","danger"],["warning","warning"],["info","info"],["neutral","neutral"],["__custom__","Custom\u2026"]],l?"__custom__":r||""),o=k("input",{type:"color",className:"form-input me-colour-hex",value:l?r:"#000000"});o.style.display=l?"":"none",i.addEventListener("change",()=>{o.style.display=i.value==="__custom__"?"":"none"});const f=j(t,i);return f.append(o),f._read=()=>i.value==="__custom__"?o.value:i.value,f}function Yt(t,r){const l=k("input",{className:"form-input me-icon",name:"icon",value:r||""}),i=k("span",{className:"me-icon-preview"});r&&i.setAttribute("data-icon",r);const o=k("button",{type:"button",className:"btn btn-sm btn-ghost",textContent:"Browse\u2026"}),f=h=>{h?i.setAttribute("data-icon",h):i.removeAttribute("data-icon"),Domma.icons.scan(i.parentNode||i)};return o.addEventListener("click",()=>$t(h=>{l.value=h,f(h)})),l.addEventListener("input",()=>f(l.value.trim())),j(t,k("div",{className:"me-icon-field"},l,o,i))}function Ce(...t){return k("div",{className:"me-field-grid"},...t)}async function Xt(t,r,l,i,o){const f=t.text||t.tool?.label||"",h=c=>k("h4",{className:"me-form-section",textContent:c}),d=j("Label",k("input",{className:"form-input",name:"text",value:t.text||""}),"The text shown for this item."),C=k("input",{className:"form-input",name:"url",value:t.url||""}),$=j("URL",C,t.tool||t.locked?"A Tool's address is its identity, so it cannot be changed here.":"Where it links. Must start with /, #, https://, or mailto: . Leave blank for a parent that only groups sub-items.");(t.tool||t.locked)&&(C.readOnly=!0);const N=Yt("Icon",t.icon||""),A=Vt(t.visibility||"",i),g=j("Visibility",A,t.locked?"Always in the sidebar for everyone who may use it, so it cannot be restricted here.":'Who can see this item. "Role and above" shows it to that role and anyone more senior; "Role only" shows it to holders of that role alone (and the super-admin). Blank = everyone.');t.locked&&(A.disabled=!0);const K=Q([["","- no permission gate -"],...(r||[]).map(c=>[c.key,c.label||c.key])],t.permission||"");K.name="permission";const ie=j("Permission",K,t.tool||t.locked?"A Tool is guarded by its own permission, which always applies.":"Hide unless the viewer holds this specific permission. Independent of Visibility - both must pass.");(t.tool||t.locked)&&(K.disabled=!0);const ee=j("Badge text",k("input",{className:"form-input",name:"badgeText",value:t.badge?.text||""})),U=Q([["","- none -"],...(l||[]).map(c=>[c.slug,c.title||c.slug])],t.badge?.countFrom||"");U.name="badgeCount";const se=j("Badge count from",U,"Show a live count of this collection\u2019s entries as the badge."),te=He("Badge colour",t.badge?.variant||""),B=Q([["link","Link"],["pill","Pill"]],t.pill?"pill":"link");B.name="renderAs";const Ie=j("Render as",B,"Pill draws the item as a rounded chip. Pill styling applies to top-level navbar items."),G=j("Pill style",Q([["filled","Filled"],["outline","Outline"]],t.pill?.style||"filled"));G.querySelector("select").name="pillStyle";const pe=He("Pill colour",t.pill?.variant||""),le=k("div",{className:"me-pill-wrap"},Ce(G,pe));le.style.display=t.pill?"":"none",B.addEventListener("change",()=>{le.style.display=B.value==="pill"?"":"none"});const p=He("Text colour",t.colour||""),O=Q([["","- use the menu default -"],...ot.map(c=>[c,it[c]||c])],t.highlight?.hover||"");O.name="hlHover";const re=j("Hover effect",O,"Overrides the menu-wide hover effect for this item only."),fe=Q([["","- use the menu default -"],...at.map(c=>[c,st[c]||c])],t.highlight?.active||"");fe.name="hlActive";const ye=j("When selected",fe,"How this item is marked when it points at the page being viewed."),F=He("Highlight colour",t.highlight?.accent||""),x=k("button",{className:"btn btn-primary",textContent:"Apply",title:"Apply to the item (Ctrl+Enter) - Save the menu to keep it"}),z=k("div",{className:"me-item-form"},h("Link"),d,$,N,h("Access"),Ce(g,ie),h("Badge"),Ce(ee,te),se,h("Appearance"),Ie,le,p,h("Highlighting"),Ce(re,ye),F,x),y=E.slideover({title:f?`Edit ${_e(f)}`:"Edit menu item",size:"lg",position:"right"});y.element.appendChild(z);const X=()=>JSON.stringify([...z.querySelectorAll("input, select, textarea")].map(c=>c.type==="checkbox"?c.checked:c.value)),ne=X();let V=!1,ce=!1;Tt(y);const we=y.close.bind(y);y.close=async c=>{if(!V&&c!==!0&&X()!==ne){if(ce)return;ce=!0;const Y=await E.confirm("Discard the changes to this item?");if(ce=!1,!Y)return}return V=!0,we()},z.addEventListener("keydown",c=>{(c.ctrlKey||c.metaKey)&&c.key==="Enter"&&(c.preventDefault(),x.click())}),y.open();const he=y.element.closest(".dm-slideover")||y.element.querySelector(".dm-slideover")||y.element;he.style.setProperty("width",zt+"px","important"),he.style.setProperty("max-width","95vw","important"),Domma.icons.scan(z);const L=c=>z.querySelector(`[name="${c}"]`)?.value||"";x.addEventListener("click",()=>{t.text=L("text"),t.icon=L("icon"),t.locked||(t.visibility=L("visibility")),!t.tool&&!t.locked?(t.url=L("url"),t.permission=L("permission")):t.touched=!0;const c=L("badgeText"),Y=L("badgeCount"),oe=te._read();if(t.badge=c||Y?{...c&&{text:c},...Y&&{countFrom:Y},...oe&&{variant:oe}}:null,L("renderAs")==="pill"){const ve=pe._read();t.pill={style:L("pillStyle")||"filled",...ve&&{variant:ve}}}else t.pill=null;t.colour=p._read();const de=L("hlHover"),ue=L("hlActive"),me=F._read();t.highlight=de||ue||me?{...de&&{hover:de},...ue&&{active:ue},...me&&{accent:me}}:null,V=!0,y.close(!0),o()})}let W=null,mt=!1;function Jt(){mt||!window.R?.use||(mt=!0,R.use((t,r,l)=>{const i=r?.path||"";return!(Wt.test(i)&&(t?.path||"")!==i)||!W?.dirty()?l():E.confirm(`You have unsaved changes to the menu "${_e(W.name())}". Leave anyway? They are kept as a draft for your next visit.`).then(f=>{f?l():history.replaceState(null,"",`#${i}`)})}),M.subscribe("router:afterChange",({to:t}={})=>{if(W&&(t?.path||"")!==W.path){const r=W;W=null,r.dispose()}}))}function pt(t,r){const l=t.querySelector(".dm-plugin-banner-name"),i=l?.querySelector(".dm-plugin-banner-sep");if(!(!l||!i)){for(;i.nextSibling;)i.nextSibling.remove();l.appendChild(document.createTextNode(r))}}export const menuEditorView={templateUrl:"/admin/js/templates/menu-editor.html",async onMount(t){if(Jt(),W){const e=W;W=null,e.dispose()}const r=[],l=e=>(r.push(e),e);let i=!1;const o=t.get(0).querySelector(".mex")||t.get(0),f=()=>!i&&o.isConnected,h=location.hash.replace(/^#/,""),d=location.hash.match(/^#\/menus\/edit\/([^?#]+)/),C=d?decodeURIComponent(d[1]):null,$=!C,N=`menu-editor.draft.${C||"new"}`,A=E.loader(t.get(0),{type:"dots"});let g,K,ie,ee,U,se,te;try{if($)g={slug:"",name:"",description:"",items:[]};else try{g=await T.menus.get(C)}catch(e){E.toast(`Failed to load menu: ${e.message||e}`,{type:"error"}),location.hash="#/menus";return}[K,ie,ee,U,se,te]=await Promise.all([T.projects.list().catch(()=>[]),H.get("/api/auth/permissions-registry").catch(()=>({resources:[]})),T.collections.list().catch(()=>[]),T.get("/collections/roles/entries?limit=100").catch(()=>null),T.menuLocations.registry().catch(()=>[]),T.pages.list().catch(()=>[])])}finally{A.destroy()}if(!o.isConnected)return;const Ie=(Array.isArray(U)?U:U?.entries||[]).map(e=>({name:e.data?.name,label:e.data?.label,level:e.data?.level??99})).filter(e=>e.name).sort((e,n)=>n.level-e.level),G=!$&&C===qt;let pe=[];if(G&&(pe=await H.get("/api/sidebar/registered-items").catch(()=>(E.toast("Could not load the Tools - they are not shown, and saving keeps their places as they are.",{type:"warning",duration:8e3}),[])),!o.isConnected))return;const le=e=>Ht(G?Et(xt(e||[],pe,{forEditor:!0}),{leaveInHiddenFolders:!0}):e);let p=le(g.items);const O=M.observable(!1),re=M.observable(!1),fe=M.observable(""),ye=M.observable(0);let F="";const x=o.querySelector("#me-items-list");function z(){Gt(x,p,G),Ut(x,()=>p,()=>{z(),q()}),ye.value=p.filter(e=>!ae(e)).length}const y=()=>{z(),q()},X=()=>G?ut(p):new Set,ne=e=>p.find(n=>n.id===e);function V(e){requestAnimationFrame(()=>x.querySelector(`.menu-tree-row[data-id="${CSS.escape(e)}"]`)?.focus())}function ce(e){const n=u=>p.find(m=>!m.type&&!m.url&&!m.tool&&(m.key||"").toLowerCase()===u)||p.find(m=>!m.type&&!m.url&&!m.tool&&!m.key&&(m.text||"").toLowerCase()===u),s=n(String(e.tool.folder||"").toLowerCase())||n(_t);e.parentId=s?s.id:null,p.splice(p.indexOf(e),1),p.push(e),Object.assign(e,{text:e.tool.defaults.text,icon:e.tool.defaults.icon,visibility:"",hidden:!1,badge:null,pill:null,colour:"",highlight:null,touched:!1}),e.tool={...e.tool,persisted:!1}}const we=e=>!X().has(e.id)&&!(e.tool&&!e.tool.orphan);async function he(e){const n=ne(e);if(!n||!we(n))return;const s=b=>{let Z=b;for(;Z;){if(Z.id===e)return!0;Z=p.find(P=>P.id===Z.parentId)}return!1},u=p.filter(b=>b.id!==e&&s(b));if(u.length&&!await E.confirm(`Remove "${_e(n.text||n.type||"this item")}" and the ${u.length} item${u.length===1?"":"s"} beneath it?`))return;const m=X(),v=u.filter(b=>b.tool),_=u.filter(b=>!b.tool&&m.has(b.id));p=p.filter(b=>!s(b)||v.includes(b)||_.includes(b));for(const b of _)b.parentId=n.parentId??null;for(const b of v)b.tool.orphan?b.parentId=n.parentId??null:ce(b);y()}function L(e){const n=ne(e);!n||ae(n)||X().has(e)||(n.hidden=!n.hidden,n.touched=!0,y())}function c(e){const n=ne(e);!n||ae(n)||(n.locked=G&&X().has(n.id),Xt(n,ie.resources||[],ee,Ie,()=>{y(),V(e)}))}function Y(e){const n={id:Ee(),parentId:null,...e==="separator"?{type:e}:e==="spacer"?{type:e,size:"md"}:{text:"",url:"/",icon:"",visibility:"",permission:"",hidden:!1,bundled:!1}};p.push(n),y(),ae(n)?V(n.id):c(n.id)}const oe=e=>n=>{e(n)&&(y(),V(n))},de=oe(e=>rt(p,e,-1)),ue=oe(e=>rt(p,e,1)),me=oe(e=>Ot(p,e)),ve=oe(e=>Ft(p,e));function ft(e){const n=ne(e);if(!n)return[];const s=ae(n),u=X().has(e),m=Mt(p,e),v=m.findIndex(P=>P.id===e),_=n.tool,b=_&&(_.persisted||n.touched),Z=P=>()=>{const be=Pt(p,e,P,Ee());y(),be&&!ae(be)?c(be.id):be&&V(be.id)};return[{header:s?n.type==="separator"?"Separator":"Spacer":n.text||_?.label||"(untitled)"},...s?[]:[{label:"Edit\u2026",icon:"edit",action:()=>c(e)}],{type:"divider"},{label:"Add item below",icon:"plus",action:Z("item")},{label:"Add sub-item",icon:"corner-down-right",disabled:s,action:()=>{const P=Rt(p,e,Ee());y(),P&&c(P.id)}},{label:"Add separator below",icon:"minus",action:Z("separator")},{label:"Add spacer below",icon:"move",action:Z("spacer")},{type:"divider"},{label:"Move up",icon:"arrow-up",shortcut:"Alt+\u2191",disabled:v<=0,action:()=>de(e)},{label:"Move down",icon:"arrow-down",shortcut:"Alt+\u2193",disabled:v<0||v>=m.length-1,action:()=>ue(e)},{label:"Indent",icon:"chevron-right",shortcut:"Alt+\u2192",disabled:v<=0||ae(m[v-1]),action:()=>me(e)},{label:"Outdent",icon:"chevron-left",shortcut:"Alt+\u2190",disabled:n.parentId==null,action:()=>ve(e)},{type:"divider"},...s?[]:[{label:n.hidden?"Show":"Hide",icon:n.hidden?"eye":"eye-off",disabled:u,action:()=>L(e)}],{label:"Duplicate",icon:"copy",disabled:!!_,action:()=>{const P=Nt(p,e,Ee);y(),P&&V(P.id)}},..._&&!_.orphan&&b?[{label:"Back to its default place",icon:"rotate-ccw",action:()=>{ce(n),y()}}]:[],{label:u?"Remove - always in the sidebar":_&&!_.orphan?"Remove - a Tool is hidden, not removed":"Remove\u2026",icon:"trash",danger:!0,disabled:!we(n),action:()=>he(e)}]}const ht=E.contextMenu(x,{cursor:"glyph",match:".menu-tree-row",inherit:!1,density:"compact",items:e=>ft(e.dataset.id)});l(()=>ht?.destroy?.());const Ke=e=>{const n=e.target.closest(".menu-tree-row");if(!n)return;const s=n.dataset.id;if(e.target.closest(".mex-kebab")){ze(e.target.closest(".mex-kebab"));return}if(e.target.closest(".me-hidden")){L(s);return}if(e.target.closest(".me-edit")){c(s);return}e.target.closest("select, .menu-tree-grip, button")||c(s)},Ue=e=>{const n=e.target.closest(".menu-tree-row");if(!n||e.target!==n){n&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),ze(n));return}const s=n.dataset.id;if(e.key==="F10"&&e.shiftKey)e.preventDefault(),ze(n);else if(e.key==="Enter")e.preventDefault(),c(s);else if(e.key==="Delete")e.preventDefault(),he(s);else if(e.altKey&&e.key==="ArrowUp")e.preventDefault(),de(s);else if(e.altKey&&e.key==="ArrowDown")e.preventDefault(),ue(s);else if(e.altKey&&e.key==="ArrowRight")e.preventDefault(),me(s);else if(e.altKey&&e.key==="ArrowLeft")e.preventDefault(),ve(s);else if(e.key==="ArrowDown"||e.key==="ArrowUp"){const u=[...x.querySelectorAll(".menu-tree-row")],m=u.indexOf(n)+(e.key==="ArrowDown"?1:-1);u[m]&&(e.preventDefault(),u[m].focus())}},Ge=e=>{if(!e.target.matches(".me-spacer-size"))return;const n=ne(e.target.closest(".menu-tree-row")?.dataset.id);n&&(n.size=e.target.value,q())};x.addEventListener("click",Ke),x.addEventListener("keydown",Ue),x.addEventListener("change",Ge),l(()=>{x.removeEventListener("click",Ke),x.removeEventListener("keydown",Ue),x.removeEventListener("change",Ge)});const a=e=>t.find(e),ke=a("#me-float-accent"),Te=a("#me-float-accent-hex"),De=()=>{Te.get(0).style.display=ke.val()==="__custom__"?"":"none"},vt=()=>ke.val()==="__custom__"?Te.val():ke.val(),Se=a("#me-highlight-accent"),Re=a("#me-highlight-accent-hex"),Ve=()=>{Re.get(0).style.display=Se.val()==="__custom__"?"":"none"},gt=()=>Se.val()==="__custom__"?Re.val():Se.val(),Ye=()=>{const e=a("#me-position").val()==="floating";a(".dm-float-field").each(function(){this.style.display=e?"":"none"}),e&&De();const n=a("#me-orientation").val()==="vertical";a(".dm-vertical-field").each(function(){this.style.display=n?"":"none"});const s=!!(a("#me-float-collapsible").get(0)||{}).checked;a(".dm-float-collapsed-field").each(function(){this.style.display=e&&s?"":"none"})},Xe=(e,n,s,u)=>{const m=a(e).get(0);m&&(m.innerHTML="",m.add(new Option(u,"")),n.forEach(v=>m.add(new Option(s[v]||v,v))))};Xe("#me-highlight-hover",ot,it,"- theme default -"),Xe("#me-highlight-active",at,st,"- theme default -");const je=a("#me-bind-slot"),Ne=a("#me-bind-projects"),$e=a("#me-bind-pages");je.html('<option value="">- not bound -</option>'+(se||[]).map(e=>`<option value="${w(e.slot)}">${w(e.label||e.slot)} (${w(e.slot)})</option>`).join("")),Ne.html(K.map(e=>`<option value="${w(e.slug)}">${w(e.name||e.slug)}</option>`).join("")),a("#me-bind-page-pick").html('<option value="">- pick a page -</option>'+te.map(e=>{const n=e.urlPath||e.url||"";return n?`<option value="${w(n)}">${w(e.title||n)} - ${w(n)}</option>`:""}).join(""));const Je=()=>jt({slot:je.val(),projects:Array.from(Ne.get(0).selectedOptions).map(e=>e.value),pagesText:$e.val()});function Oe(){const e=Je(),n=a("#me-bind-summary");if(!e){n.html('<span class="text-muted">Not bound - this menu renders only where it is mapped or placed by a shortcode.</span>');return}const s=[];e.projects.length&&s.push(`pages in ${e.projects.map(v=>`<code>${w(v)}</code>`).join(", ")}`),e.pages.length&&s.push(`pages matching ${e.pages.map(v=>`<code>${w(v)}</code>`).join(", ")}`);const u=e.pages.filter(v=>!/^\/[^\s?#*]*(\/\*)?$|^\/\*$/.test(v)),m=u.length?`<div class="text-danger" style="margin-top:.4rem">Not valid page patterns: ${u.map(v=>`<code>${w(v)}</code>`).join(", ")}</div>`:"";n.html(s.length?`Takes over the <code>${w(e.slot)}</code> slot on ${s.join(" and ")}.${m}`:`<span class="text-warning">Slot <code>${w(e.slot)}</code> selected but no project or page chosen - the binding will never match.</span>`)}const{getProjectFromHash:bt}=await import("../lib/project-context.js");if(!f())return;const yt=$?bt():null;a("#me-project").html('<option value="">- none -</option>'+K.map(e=>`<option value="${w(e.slug)}">${w(e.name||e.slug)}</option>`).join(""));function Pe(e){p=le(e.items),a("#me-variant").val(e.variant||""),a("#me-position").val(e.position||"");const n=a("#me-appearOnHover").get(0);n&&(n.checked=e.appearOnHover===!0),a("#me-float-anchor").val(e.float?.anchor||"TL"),a("#me-float-offsetX").val(e.float?.offsetX??""),a("#me-float-offsetY").val(e.float?.offsetY??""),a("#me-float-width").val(e.float?.width??""),a("#me-float-radius").val(e.float?.radius??""),a("#me-float-shadow").val(e.float?.shadow||""),a("#me-float-opacity").val(e.float?.opacity??""),a("#me-float-title").val(e.float?.title||"");for(const[v,_]of[["#me-float-collapsible",e.float?.collapsible],["#me-float-collapsed",e.float?.collapsed],["#me-float-movable",e.float?.movable],["#me-float-resizable",e.float?.resizable]]){const b=a(v).get(0);b&&(b.checked=_===!0)}const s=e.float?.accent||"";ke.val(s.startsWith("#")?"__custom__":s),s.startsWith("#")&&Te.val(s),De(),a("#me-orientation").val(e.orientation||""),a("#me-align").val(e.align||""),a("#me-side").val(e.side||"left"),a("#me-highlight-hover").val(e.highlight?.hover||""),a("#me-highlight-active").val(e.highlight?.active||"");const u=e.highlight?.accent||"";Se.val(u.startsWith("#")?"__custom__":u),u.startsWith("#")&&Re.val(u),Ve(),a("#me-fontFamily").val(e.style?.fontFamily||""),a("#me-fontSize").val(e.style?.fontSize||""),a("#me-fontWeight").val(e.style?.fontWeight||""),a("#me-letterSpacing").val(e.style?.letterSpacing||""),a("#me-iconSize").val(e.style?.iconSize||""),je.val(e.binding?.slot||"");const m=new Set(e.binding?.projects||[]);Array.from(Ne.get(0).options).forEach(v=>{v.selected=m.has(v.value)}),$e.val((e.binding?.pages||[]).join(`
|
|
23
|
+
`)),a("#me-slug").val(e.slug||""),a("#me-name").val(e.name||""),a("#me-description").val(e.description||""),a("#me-project").val(e.meta?.project||yt||""),Ye(),Oe(),z()}const ge=e=>!!(a(e).get(0)||{}).checked;function Ae(){const e={variant:a("#me-variant").val(),position:a("#me-position").val(),appearOnHover:ge("#me-appearOnHover"),floatAnchor:a("#me-float-anchor").val(),floatOffsetX:a("#me-float-offsetX").val(),floatOffsetY:a("#me-float-offsetY").val(),floatWidth:a("#me-float-width").val(),floatRadius:a("#me-float-radius").val(),floatShadow:a("#me-float-shadow").val(),floatAccent:vt(),floatOpacity:a("#me-float-opacity").val(),floatTitle:a("#me-float-title").val(),floatCollapsible:ge("#me-float-collapsible"),floatCollapsed:ge("#me-float-collapsed"),floatMovable:ge("#me-float-movable"),floatResizable:ge("#me-float-resizable"),highlightHover:a("#me-highlight-hover").val(),highlightActive:a("#me-highlight-active").val(),highlightAccent:gt(),orientation:a("#me-orientation").val(),align:a("#me-align").val(),side:a("#me-side").val(),fontFamily:a("#me-fontFamily").val(),fontSize:a("#me-fontSize").val(),fontWeight:a("#me-fontWeight").val(),letterSpacing:a("#me-letterSpacing").val(),iconSize:a("#me-iconSize").val()},n={slug:a("#me-slug").val(),name:a("#me-name").val(),description:a("#me-description").val(),project:a("#me-project").val()};return Bt(e,n,Je(),Ct(p),g.meta||{})}let Ze=!1,Be=null;function q(){if(!F)return;const e=Ae();Be=e,O.value=Le(e)!==F,fe.value=O.peek()||Ze?lt(e).join(" "):""}const xe=e=>{e.target.closest("#me-items-list")||(e.target.matches("#me-position, #me-orientation, #me-float-collapsible")&&Ye(),e.target.matches("#me-float-accent")&&De(),e.target.matches("#me-highlight-accent")&&Ve(),e.target.closest("#me-binding")&&Oe(),q())};o.addEventListener("input",xe),o.addEventListener("change",xe),l(()=>{o.removeEventListener("input",xe),o.removeEventListener("change",xe)}),a("#me-bind-page-add").on("click",()=>{const e=a("#me-bind-page-pick").val();if(!e)return;const n=$e.val().split(`
|
|
24
|
+
`).map(s=>s.trim()).filter(Boolean);n.includes(e)||n.push(e),$e.val(n.join(`
|
|
25
|
+
`)),Oe(),q()}),Pe(g),F=Le(Ae()),q();const Qe=()=>{try{S.remove?S.remove(N):S.set(N,null)}catch{}};async function Fe(){if(re.peek())return;const e=Ae(),n=lt(e);if(n.length){Ze=!0,q(),E.toast(n.join(" "),{type:"warning",duration:7e3});return}re.value=!0;try{if($)await T.menus.create(e);else if(e.slug!==g.slug){await T.menus.create(e);const s=await T.menuLocations.get();let u=!1;for(const[m,v]of Object.entries(s))v===g.slug&&(s[m]=e.slug,u=!0);u&&await T.menuLocations.save(s),await T.menus.remove(g.slug)}else await T.menus.update(g.slug,e);if(F=Le(e),O.value=!1,Qe(),Dt("#/menus"),E.toast("Saved.",{type:"success"}),$||e.slug!==g.slug)location.hash="#/menus/edit/"+encodeURIComponent(e.slug);else{g={...g,...e};for(const s of p)s.tool&&s.touched&&(s.tool={...s.tool,persisted:!0});z(),F=Le(Ae()),q(),pt(t.get(0),e.name||e.slug)}if(G)try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}}catch(s){E.toast(`Save failed: ${s.message||s}`,{type:"error"})}finally{re.value=!1}}async function wt(){!O.peek()||!await E.confirm("Throw away the changes you have not saved?")||(Pe(g),q())}E.tabs(a("#me-tabs").get(0));const kt=M.applyBindings({dirty:O,saving:re,problems:fe,itemCount:ye},o,{methods:{save:()=>Fe(),discard:()=>wt(),addItem:()=>Y("item"),addSeparator:()=>Y("separator"),addSpacer:()=>Y("spacer")}});l(()=>kt.dispose()),l(It(o,{position:"bottom"})),pt(t.get(0),$?"New menu":g.name||g.slug),Me(t,{id:"menu-editor-back",icon:"arrow-left",title:"All menus",onClick:()=>{location.hash="#/menus"}}),Me(t,{id:"menu-editor-locations",icon:"map-pin",title:"Menu locations",onClick:()=>{location.hash="#/menu-locations"}}),Me(t,{id:"menu-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>Fe()});const et=e=>{if(!(!(e.ctrlKey||e.metaKey)||e.key!=="s")){if(e.preventDefault(),e.stopImmediatePropagation(),document.querySelector(".me-item-form")?.offsetParent){E.toast("Apply the item first (Ctrl+Enter), then Save.",{type:"info"});return}Fe()}};document.addEventListener("keydown",et,!0),l(()=>document.removeEventListener("keydown",et,!0));const tt=()=>{if(!(!O.peek()||!Be))try{S.set(N,{menu:Be,base:F,at:new Date().toISOString()})}catch{}},nt=e=>{O.peek()&&(tt(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",nt),l(()=>window.removeEventListener("beforeunload",nt)),W={path:h,dirty:()=>O.peek(),name:()=>a("#me-name").val()||g.name||g.slug||"new menu",dispose(){i=!0,tt(),r.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(o);let J=null;try{J=S.get(N)}catch{}if(J?.menu){Qe();const e=J.base&&J.base!==F,n=J.at?` (${D(J.at).format("D MMM HH:mm")})`:"",s=$?"the new menu you started":`the menu "${_e(g.name||g.slug)}"`,u=e?`You left unsaved changes to ${s}${n}, but the saved menu has changed since. Restore your version anyway?`:`You left unsaved changes to ${s}${n}. Restore them?`;await E.confirm(u)&&f()&&(Pe({...J.menu,meta:{...g.meta||{},...J.menu.meta||{}}}),q())}}};
|
|
@@ -1 +1,16 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as g}from"../api.js";import{addBannerAction as N}from"../lib/plugin-chrome.js";import{helpPopovers as te}from"../lib/help-popover.js";import{openMenuAt as V,sidebarCount as ne}from"../lib/tool-kit.js";import{depthOfItems as ae,locationRows as se,locationsPayload as oe,locationsSnapshot as w}from"../lib/menu-editor-arrange.js";const O=/^\/menu-locations\/?(\?.*)?$/,v="menu-locations.draft",le=`
|
|
2
|
+
<div class="mlx-row" data-slot="{{slot}}" tabindex="0" data-bind-class="(changed ? 'is-changed ' : '') + (tooDeep || missing ? 'is-bad' : '')">
|
|
3
|
+
<span class="mlx-main">
|
|
4
|
+
<strong>{{label}}</strong>
|
|
5
|
+
<small><code>{{slot}}</code> <span data-bind-hidden="!source">\xB7 {{source}}</span></small>
|
|
6
|
+
<small class="mlx-desc" data-bind-hidden="!description">{{description}}</small>
|
|
7
|
+
</span>
|
|
8
|
+
<span class="mlx-chips">
|
|
9
|
+
<span class="mlx-chip is-warn" data-bind-hidden="!tooDeep" data-bind-text="depthText"></span>
|
|
10
|
+
<span class="mlx-chip is-danger" data-bind-hidden="!missing">Menu not found</span>
|
|
11
|
+
<span class="mlx-chip" data-bind-hidden="!empty" data-bind-text="emptyText"></span>
|
|
12
|
+
<span class="mlx-chip is-changed" data-bind-hidden="!changed">Changed</span>
|
|
13
|
+
</span>
|
|
14
|
+
<select class="form-input mlx-select" aria-label="Menu for {{label}}"></select>
|
|
15
|
+
<button type="button" class="mlx-kebab" aria-label="Actions for {{label}}" title="Actions">\u22EE</button>
|
|
16
|
+
</div>`,ie=o=>String(o??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");let u=null,W=!1;function re(){W||!window.R?.use||(W=!0,R.use((o,p,s)=>!(O.test(p?.path||"")&&!O.test(o?.path||""))||!u?.dirty()?s():E.confirm("You have unsaved menu locations. Leave anyway? They are kept as a draft for your next visit.").then(d=>{d?s():history.replaceState(null,"","#/menu-locations")})),M.subscribe("router:afterChange",({to:o}={})=>{if(u&&!O.test(o?.path||"")){const p=u;u=null,p.dispose()}}))}export const menuLocationsView={templateUrl:"/admin/js/templates/menu-locations.html",async onMount(o){if(re(),u){const e=u;u=null,e.dispose()}const p=[],s=e=>(p.push(e),e);let k=!1;const d=o.get(0).querySelector(".mlx")||o.get(0),f=()=>!k&&d.isConnected,x=M.observable([]),L=M.observable([]),P=M.observable({}),l=M.observable({}),a=M.observable({}),C=M.observable(!0),$=M.observable(""),y=M.observable(!1),h=M.computed(()=>w(a.value)!==w(l.value)),b=M.observable([]);s(M.effect(()=>{b.value=se(x.value,a.value,l.value,L.value,P.value)}));const _=M.computed(()=>{const e=b.value;return`${e.filter(n=>!n.empty).length} of ${e.length} slot${e.length===1?"":"s"} have a menu`});async function z(){try{const[e,t,n]=await Promise.all([g.menuLocations.registry(),g.menuLocations.get().catch(()=>({})),g.menus.list().catch(()=>[])]),r=await Promise.all((n||[]).map(c=>g.menus.get(c.slug).catch(()=>null)));if(!f())return;x.value=Array.isArray(e)?e:[],L.value=n||[],P.value=Object.fromEntries(r.filter(Boolean).map(c=>[c.slug,ae(c.items||[])])),l.value={...t||{}},a.value={...t||{}},$.value=""}catch(e){$.value=`Could not load the locations: ${e.message||e}`}finally{C.value=!1}}const A=(e,t)=>{a.value={...a.peek(),[e]:t||""}};async function T(){if(!(y.peek()||C.peek())){y.value=!0;try{const e=oe(a.peek(),x.peek());if(await g.menuLocations.save(e),!f())return;l.value={...e},a.value={...e};try{S.remove?S.remove(v):S.set(v,null)}catch{}ne("#/menus"),E.toast("Locations saved.",{type:"success"})}catch(e){E.toast(`Save failed: ${e.message||e}`,{type:"error"})}finally{y.value=!1}}}async function G(){!h.peek()||!await E.confirm("Throw away the changes you have not saved?")||(a.value={...l.peek()})}const i=d.querySelector(".mlx-list");i.innerHTML=le;const J=M.applyBindings({rows:b,loading:C,loadError:$,saving:y,dirty:h,summary:_},d,{methods:{save:()=>T(),discard:()=>G()}});s(()=>J.dispose()),s(te(d,{position:"bottom"}));const Q=()=>{const e=[["","- none -"],...L.peek().map(t=>[t.slug,t.name||t.slug])];for(const t of i.querySelectorAll(".mlx-select")){const n=t.closest(".mlx-row")?.dataset.slot;if(!t.options.length)for(const[c,ee]of e)t.add(new Option(ee,c));const r=a.peek()[n]||"";r&&![...t.options].some(c=>c.value===r)&&t.add(new Option(`${r} (missing)`,r)),t.value!==r&&(t.value=r)}};s(M.effect(()=>{b.value,requestAnimationFrame(()=>{f()&&Q()})}));const q=e=>{e.target.matches(".mlx-select")&&A(e.target.closest(".mlx-row").dataset.slot,e.target.value)},K=e=>{const t=a.peek()[e];t&&(location.hash=`#/menus/edit/${encodeURIComponent(t)}`)},U=(e,t)=>navigator.clipboard?.writeText(e).then(()=>E.toast(`${t} copied.`,{type:"success"}),()=>E.toast(e,{type:"info",duration:8e3})),X=e=>{const t=b.peek().find(n=>n.slot===e);return t?[{header:t.label},{label:t.menu?`Edit "${t.menuName}"`:"Edit its menu",icon:"edit",disabled:!t.menu||t.missing,action:()=>K(e)},{label:"Clear this slot",icon:"x-circle",disabled:!t.menu,action:()=>A(e,"")},{label:"Undo the change",icon:"rotate-ccw",disabled:!t.changed,action:()=>A(e,l.peek()[e]||"")},{type:"divider"},{label:"Copy slot name",icon:"copy",action:()=>U(e,"Slot name")},{label:"Copy [menu location] shortcode",icon:"code",action:()=>U(`[menu location="${e}" /]`,"Shortcode")}]:[]},Z=E.contextMenu(i,{cursor:"glyph",match:".mlx-row",inherit:!1,density:"compact",items:e=>X(e.dataset.slot)});s(()=>Z?.destroy?.());const Y=e=>{const t=e.target.closest(".mlx-row");if(!t)return;const n=e.target.closest(".mlx-kebab");if(n){V(n);return}e.target.closest("select")||K(t.dataset.slot)},B=e=>{const t=e.target.closest(".mlx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),V(t)):e.key==="Enter"&&e.target===t&&K(t.dataset.slot))};i.addEventListener("change",q),i.addEventListener("click",Y),i.addEventListener("keydown",B),s(()=>{i.removeEventListener("change",q),i.removeEventListener("click",Y),i.removeEventListener("keydown",B)}),N(o,{id:"menu-locations-back",icon:"arrow-left",title:"All menus",onClick:()=>{location.hash="#/menus"}}),N(o,{id:"menu-locations-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>T()});const F=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),T())};document.addEventListener("keydown",F,!0),s(()=>document.removeEventListener("keydown",F,!0));const H=()=>{if(h.peek())try{S.set(v,{map:a.peek(),base:w(l.peek()),at:new Date().toISOString()})}catch{}},j=e=>{h.peek()&&(H(),e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",j),s(()=>window.removeEventListener("beforeunload",j)),u={dirty:()=>h.peek(),dispose(){k=!0,H(),p.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(d),await z(),!f())return;let m=null;try{m=S.get(v)}catch{}if(m?.map){try{S.remove?S.remove(v):S.set(v,null)}catch{}const e=m.base&&m.base!==w(l.peek()),t=m.at?` (${D(m.at).format("D MMM HH:mm")})`:"",n=e?`You left unsaved menu locations${t}, but the saved locations have changed since. Restore your version anyway?`:`You left unsaved menu locations${t}. Restore them?`;await E.confirm(ie(n))&&f()&&(a.value={...l.peek(),...m.map})}}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as V}from"../api.js";import{addBannerAction as fe}from"../lib/plugin-chrome.js";import{helpPopovers as ne}from"../lib/help-popover.js";import{accountFacts as re,buildPayload as he,checkForm as se,draftOf as be,fieldKind as H,fieldOptions as U,formFromMe as W,initials as ye,snapshot as T}from"../lib/my-profile-arrange.js";const z=/^\/my-profile\/?(\?.*)?$/,y=t=>String(t??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),Y=t=>{if(!t)return"";try{return D(t).format("D MMM YYYY")}catch{return String(t).slice(0,10)}};let $=null,le=!1;function $e(){le||!window.R?.use||(le=!0,R.use((t,o,r)=>!(z.test(o?.path||"")&&!z.test(t?.path||""))||!$?.dirty()?r():E.confirm("You have unsaved changes to your profile. Leave anyway? They are kept for your next visit - except a new password, which you would type again.").then(i=>{i?r():history.replaceState(null,"","#/my-profile")})),M.subscribe("router:afterChange",({to:t}={})=>{if($&&!z.test(t?.path||"")){const o=$;$=null,o.dispose()}}))}function ge(t,o){const r=H(t.type),m=`mpx-pf-${o}`,i=t.required?' <span class="text-danger">*</span>':"",v=t.help||t.description?` <span data-help="${y(t.help||t.description)}"></span>`:"",u=`<label class="form-label" for="${m}">${y(t.label||t.name)}${i}${v}</label>`,b=`<small class="mpx-err" data-bind-hidden="!pe_${o}.value" data-bind-text="pe_${o}.value"></small>`,s=t.placeholder?` placeholder="${y(t.placeholder)}"`:"";switch(r){case"textarea":return`<div class="mpx-wide">${u}<textarea id="${m}" class="form-input" rows="3"${s} data-model="pf_${o}.value"></textarea>${b}</div>`;case"select":return`<div>${u}<select id="${m}" class="form-input" data-model="pf_${o}.value"><option value="">-</option>${U(t).map(c=>`<option value="${y(c.value)}">${y(c.label)}</option>`).join("")}</select>${b}</div>`;case"checkbox":return`<div><label class="mpx-check"><input id="${m}" type="checkbox" data-model="pf_${o}.value"> ${y(t.label||t.name)}${i}</label>${v}${b}</div>`;case"multi":return`<div class="mpx-wide">${u}<div data-multi="${o}">${U(t).map((c,f)=>`<label class="mpx-check"><input type="checkbox" data-opt="${y(c.value)}" data-bind-checked="po_${o}_${f}.value"> ${y(c.label)}</label>`).join("")}</div>${b}</div>`;default:return`<div>${u}<input id="${m}" type="${r}" class="form-input"${s} data-model="pf_${o}.value">${b}</div>`}}export const myProfileView={templateUrl:"/admin/js/templates/my-profile.html",async onMount(t){if($e(),$){const e=$;$=null,e.dispose()}const o=[],r=e=>(o.push(e),e);let m=!1;const i=t.get(0).querySelector(".mpx")||t.get(0),v=()=>!m&&i.isConnected,u=M.observable(!0),b=M.observable(""),s=M.observable(null),c=M.observable(""),f=M.observable(""),w=M.observable(""),j=M.observable(""),ce=M.observable(!1),P=M.observable(!1),G=M.observable(!1);let p=[];const g=new Map,J=M.observable(0),h=M.observable(W({},[])),ie=()=>(J.value,Object.fromEntries([...g].map(([e,a])=>[e,a.value]))),ue=()=>Object.fromEntries([...g].map(([e,a])=>[e,a.peek()])),Q=()=>({name:c.value,email:f.value,password:w.value,confirm:j.value,profile:ie()}),X=()=>({name:c.peek(),email:f.peek(),password:w.peek(),confirm:j.peek(),profile:ue()}),B=M.computed(()=>se(Q(),p,{saved:h.value})),k=M.computed(()=>!u.value&&T(Q())!==T(h.value)),pe={loading:u,loadError:b,name:c,email:f,password:w,confirm:j,showPw:ce,hasProfile:G,err:B,dirty:k,cannotSave:M.computed(()=>P.value||!k.value||Object.keys(B.value).length>0),emailChanged:M.computed(()=>f.value.trim()!==""&&f.value.trim()!==h.value.email),pwMatch:M.computed(()=>w.value.length>=8&&w.value===j.value),avatar:M.computed(()=>ye(c.value||s.value?.name)),shownName:M.computed(()=>c.value.trim()||s.value?.name||""),shownEmail:M.computed(()=>f.value.trim()||s.value?.email||""),rolesText:M.computed(()=>re(s.value).roles.join(" + ")),accessText:M.computed(()=>re(s.value).access),factsText:M.computed(()=>{const e=s.value;return e?[e.createdAt&&`Member since ${Y(e.createdAt)}`,e.lastLogin&&`last signed in ${Y(e.lastLogin)}`,e.updatedAt&&`changed ${Y(e.updatedAt)}`,e.isActive===!1&&"account disabled"].filter(Boolean).join(" \xB7 "):""})},A=()=>`my-profile.draft.${s.peek()?.id||"me"}`;function N(e){c.value=e.name,f.value=e.email,w.value="",j.value="";for(const[a,l]of g)a in(e.profile||{})&&(l.value=e.profile[a])}async function q(){if(P.peek()||u.peek())return;const e=X();if(Object.keys(se(e,p,{saved:h.peek()})).length){E.toast("Check the fields marked in red first.",{type:"warning"});return}const a=he(h.peek(),e,p);if(!a){E.toast("Nothing has changed.",{type:"info"});return}P.value=!0;try{const l=await V.auth.updateMe(a.payload);if(!v())return;s.value=l;const C=W(l,p);h.value=C,N(C),F();try{S.set("auth_user",{...S.get("auth_user")||{},name:l.name,email:l.email})}catch{}const n=document.getElementById("topbar-user-name");n&&l.name&&(n.textContent=l.name);const d={name:"your name",email:"your email",password:"your password",profile:"your profile"};E.toast(`Saved ${a.parts.map(_=>d[_]).join(", ").replace(/, ([^,]*)$/," and $1")}.`,{type:"success"})}catch(l){E.toast(`Not saved: ${l.message||l}`,{type:"error"})}finally{P.value=!1}}async function de(){k.peek()&&(!await E.confirm("Throw away the changes you have not saved?")||!v()||(N(h.peek()),F()))}function Z(){if(!k.peek())return;const e=be(X());if(T({...e,password:"",confirm:""})!==T(h.peek()))try{S.set(A(),{form:e,at:new Date().toISOString()})}catch{}}function F(){try{S.remove?S.remove(A()):S.set(A(),null)}catch{}}const me=M.applyBindings(pe,i,{methods:{save:q,discard:de}});r(()=>me.dispose()),r(ne(i,{position:"bottom"})),fe(t,{id:"my-profile-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>q()});const ee=e=>{!(e.ctrlKey||e.metaKey)||e.key.toLowerCase()!=="s"||!v()||(e.preventDefault(),e.stopImmediatePropagation(),q())};document.addEventListener("keydown",ee,!0),r(()=>document.removeEventListener("keydown",ee,!0));const te=e=>{k.peek()&&(Z(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",te),r(()=>window.removeEventListener("beforeunload",te)),$={dirty:()=>k.peek(),dispose(){m=!0,Z(),o.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(i);let ae=null;try{const[e,a]=await Promise.all([V.auth.me(),V.collections.get("user-profiles").catch(()=>null)]);if(!v())return;s.value=e,ae=a}catch(e){if(!v())return;b.value=`Could not load your account: ${e.message||e}`,u.value=!1;return}p=(ae?.fields||[]).filter(e=>e&&e.name);const L=W(s.peek(),p),K=p.map((e,a)=>({f:e,i:a})).filter(({f:e})=>H(e.type)!=="hidden");if(p.forEach((e,a)=>g.set(e.name,M.observable(L.profile[e.name]))),K.length){const e=i.querySelector(".mpx-profile");e.innerHTML=K.map(({f:n,i:d})=>ge(n,d)).join("");const a={};p.forEach((n,d)=>{a[`pf_${d}`]=g.get(n.name),a[`pe_${d}`]=M.computed(()=>B.value[`profile.${n.name}`]||""),H(n.type)==="multi"&&U(n).forEach((_,O)=>{a[`po_${d}_${O}`]=M.computed(()=>(g.get(n.name).value||[]).includes(_.value))})});const l=M.applyBindings(a,e);r(()=>l.dispose());const C=n=>{const d=n.target.closest("[data-multi]");if(!d||!n.target.matches("input[data-opt]"))return;const _=g.get(p[Number(d.dataset.multi)].name),O=n.target.dataset.opt,oe=_.peek()||[];_.value=n.target.checked?[...new Set([...oe,O])]:oe.filter(ve=>ve!==O)};e.addEventListener("change",C),r(()=>e.removeEventListener("change",C)),r(ne(e,{position:"bottom"})),I.scan(e)}G.value=K.length>0,h.value=L,N(L),J.value++,u.value=!1;let x=null;try{x=S.get(A())}catch{}if(x?.form){F();const e=x.at?` (${Y(x.at)})`:"";await E.confirm(`You left unsaved changes to your profile${e}. Restore them?`)&&v()&&N({...L,...x.form,profile:{...L.profile,...x.form.profile||{}}})}}};
|
|
@@ -1 +1,18 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as d,apiRequest as b}from"../api.js";import{addBannerAction as O}from"../lib/plugin-chrome.js";import{helpPopovers as Y}from"../lib/help-popover.js";import{openMenuAt as q,sidebarCount as z,viewLifetime as G}from"../lib/tool-kit.js";import{FILTERS as J,arrangeNotifications as Q,filterCounts as X,notificationRow as B}from"../lib/notifications-arrange.js";const P=/^#\/system\/notifications\/?(\?.*)?$/,Z="#/system/notifications",g="notifications.filter",tt=`
|
|
2
|
+
<div class="ntx-row" data-id="{{id}}" tabindex="0" data-bind-class="rowCls" data-bind-aria-expanded="open ? 'true' : 'false'">
|
|
3
|
+
<span class="ntx-dot" aria-hidden="true"></span>
|
|
4
|
+
<span class="ntx-main">
|
|
5
|
+
<span class="ntx-head"><strong>{{title}}</strong>
|
|
6
|
+
<span class="ntx-chip" data-bind-class="'is-' + tone">{{severityLabel}}</span>
|
|
7
|
+
<span class="ntx-chip is-new" data-bind-hidden="!isNew">New</span></span>
|
|
8
|
+
<small>{{meta}}</small>
|
|
9
|
+
<span class="ntx-preview" data-bind-hidden="open">{{preview}}</span>
|
|
10
|
+
<span class="ntx-body" data-bind-hidden="!open">{{body}}</span>
|
|
11
|
+
<span class="ntx-actions" data-bind-hidden="!open || !link">
|
|
12
|
+
<a class="btn btn-sm btn-outline" href="{{link}}" target="_blank" rel="noopener">Open link \u2197</a>
|
|
13
|
+
</span>
|
|
14
|
+
</span>
|
|
15
|
+
<button type="button" class="ntx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
|
|
16
|
+
</div>`,et=s=>String(s??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),o=G(P);async function p(){z(Z);try{const{count:s}=await d.system.notifications.unreadCount(),a=$("#topbar-bell-badge");s>0?a.text(s>99?"99+":String(s)).removeAttr("hidden"):a.attr("hidden","").text("")}catch{}}export const notificationsView={templateUrl:"/admin/js/templates/notifications.html",async onMount(s){o.reset();const a=s.get(0).querySelector(".ntx")||s.get(0),m=()=>P.test(location.hash)&&a.isConnected,i=M.observable([]),f=M.observable(!0),y=M.observable("");let v="all";try{v=J.includes(S.get(g))?S.get(g):"all"}catch{}const r=M.observable(v),w=M.observable(""),u=M.observable("");o.add(M.effect(()=>{const t=r.value;try{S.set(g,t)}catch{}}));const k=M.computed(()=>X(i.value));o.add(M.effect(()=>{const t=k.value;!f.value&&r.peek()!=="all"&&!t[r.peek()]&&(r.value="all")}));const x=M.observable([]);o.add(M.effect(()=>{const t=Date.now(),e=u.value;x.value=Q(i.value,{filter:r.value,query:w.value}).map(n=>B(n,{now:t,expanded:e}))}));const _=M.computed(()=>i.value.length?"No notifications match.":"No notifications - you're all caught up."),C=t=>i.peek().find(e=>String(e.id)===String(t)),U=(t,e)=>{i.value=i.peek().map(n=>String(n.id)===String(t)?e(n):n)},R=t=>{i.value=i.peek().filter(e=>String(e.id)!==String(t))};async function T(){try{const t=await d.system.notifications.list();if(!m())return;const e=Array.isArray(t)?t:[];if(i.value=e.map(n=>({...n,isNew:!!n.unread})),y.value="",e.some(n=>n.unread)){if(await b("/system/notifications/read-all",{method:"POST"}).catch(()=>{}),!m())return;i.value=i.peek().map(n=>({...n,unread:!1})),p()}}catch(t){y.value=`Could not load notifications: ${t.message||t}`}finally{f.value=!1}}async function D(t,e){try{e?await d.system.notifications.markRead(t.id):await b(`/system/notifications/${encodeURIComponent(t.id)}/unread`,{method:"POST"}),U(t.id,n=>({...n,unread:!e})),p()}catch(n){E.toast(`Not changed: ${n.message||n}`,{type:"error"})}}async function A(t){try{await d.system.notifications.dismiss(t.id),R(t.id),p()}catch(e){E.toast(`Could not dismiss it: ${e.message||e}`,{type:"error"})}}async function F(t){if(await E.confirm(`Delete "${et(t.data?.title||"this notification")}" for everyone? Dismissing hides it for you only.`))try{await d.system.notifications.remove(t.id),R(t.id),E.toast("Notification deleted.",{type:"success"}),p()}catch(e){E.toast(/403|forbidden|permission/i.test(String(e.message||e))?"Deleting needs the Notifications delete permission - Dismiss hides it for you.":`Could not delete it: ${e.message||e}`,{type:"error"})}}async function K(){const t=i.peek().length;if(!t){E.toast("Nothing to dismiss.",{type:"info"});return}if(await E.confirm(`Dismiss all ${t} notification${t===1?"":"s"}? They are hidden for you only.`))try{await b("/system/notifications/dismiss-all",{method:"POST"}),i.value=[],p()}catch(e){E.toast(`Could not dismiss them: ${e.message||e}`,{type:"error"})}}async function j(t){const e=t.data||{},n=[e.title,e.body,e.link].filter(Boolean).join(`
|
|
17
|
+
|
|
18
|
+
`);try{await navigator.clipboard.writeText(n),E.toast("Copied.",{type:"success"})}catch{E.toast("Copying is blocked here - select the text instead.",{type:"warning"})}}const h=t=>{u.value=u.peek()===String(t)?"":String(t)};function W(t){const e=C(t);if(!e)return[];const n=B(e);return[{header:n.title},{label:n.open||u.peek()===n.id?"Close":"Read it",icon:"eye",action:()=>h(t)},...n.link?[{label:"Open link",icon:"external-link",action:()=>window.open(n.link,"_blank","noopener")}]:[],{label:"Copy text",icon:"copy",action:()=>j(e)},e.unread?{label:"Mark as read",icon:"check",action:()=>D(e,!0)}:{label:"Mark as unread",icon:"mail",action:()=>D(e,!1)},{type:"divider"},{label:"Dismiss",icon:"x",action:()=>A(e)},{label:"Delete for everyone\u2026",icon:"trash",danger:!0,action:()=>F(e)}]}const c=a.querySelector(".ntx-list");c.innerHTML=tt;const l=t=>()=>{r.value=t},H=M.applyBindings({items:i,loading:f,loadError:y,filter:r,query:w,rows:x,counts:k,emptyText:_},a,{methods:{showAll:l("all"),showNew:l("new"),showCritical:l("critical"),showWarning:l("warning"),showInfo:l("info"),showSuccess:l("success")}});o.add(()=>H.dispose()),o.add(Y(a,{position:"bottom"}));const V=E.contextMenu(c,{cursor:"glyph",match:".ntx-row",inherit:!1,density:"compact",items:t=>W(t.dataset.id)});o.add(()=>V?.destroy?.());const L=t=>{const e=t.target.closest(".ntx-row");if(!e)return;const n=t.target.closest(".ntx-kebab");if(n){q(n);return}t.target.closest("a")||h(e.dataset.id)},N=t=>{const e=t.target.closest(".ntx-row");if(e){if(t.key==="F10"&&t.shiftKey)t.preventDefault(),q(e);else if(t.key==="Enter"&&t.target===e)h(e.dataset.id);else if(t.key==="Delete"&&t.target===e){const n=C(e.dataset.id);n&&A(n)}}};c.addEventListener("click",L),c.addEventListener("keydown",N),o.add(()=>{c.removeEventListener("click",L),c.removeEventListener("keydown",N)}),O(s,{id:"notifications-dismiss-all",icon:"check-circle",title:"Dismiss all",onClick:()=>K()}),O(s,{id:"notifications-refresh",icon:"refresh-cw",title:"Refresh",onClick:()=>{f.value=!0,T()}}),I.scan(a),await T(),m()&&I.scan(a)}};
|