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
package/admin/js/views/search.js
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
import{apiRequest as
|
|
1
|
+
import{apiRequest as N}from"../api.js";import{addBannerAction as ee}from"../lib/plugin-chrome.js";import{helpPopovers as me}from"../lib/help-popover.js";import{openMenuAt as te,sidebarCount as fe}from"../lib/tool-kit.js";import{POSITION_LABEL as be,clampNotes as he,formFromSettings as O,liveChips as ye,maxRadius as ge,payloadFromForm as c,previewState as se,radiusForSize as ke,resultRow as Se,snapshot as U}from"../lib/search-settings-arrange.js";const K=/^\/search(\?.*)?$/,we="#/search",b="search.draft",xe='<span class="ssx-chip" data-bind-class="cls">{{text}}</span>',Me=`
|
|
2
|
+
<div class="ssx-row" data-url="{{url}}" tabindex="0">
|
|
3
|
+
<span class="ssx-rank">{{rank}}</span>
|
|
4
|
+
<span class="ssx-main"><strong>{{title}}</strong><small>{{meta}}</small><span class="ssx-snippet">{{snippet}}</span></span>
|
|
5
|
+
<span class="ssx-chip">{{score}}</span>
|
|
6
|
+
<button type="button" class="ssx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
|
|
7
|
+
</div>`;let u=null,oe=!1;function Ee(){oe||!window.R?.use||(oe=!0,R.use((i,v,o)=>!(K.test(v?.path||"")&&!K.test(i?.path||""))||!u?.dirty()?o():E.confirm("You have unsaved search settings. Leave anyway? They are kept as a draft for your next visit.").then(r=>{r?o():history.replaceState(null,"","#/search")})),M.subscribe("router:afterChange",({to:i}={})=>{if(u&&!K.test(i?.path||"")){const v=u;u=null,v.dispose()}}))}export const searchView={templateUrl:"/admin/js/templates/search.html",async onMount(i){if(Ee(),u){const e=u;u=null,e.dispose()}const v=[],o=e=>(v.push(e),e);let P=!1;const r=i.get(0).querySelector(".ssx")||i.get(0),$=()=>!P&&r.isConnected,H=E.loader(i.get(0),{type:"dots"});let j,A=null;try{[j,A]=await Promise.all([N("/search/settings"),N("/search/_count").catch(()=>null)])}catch(e){H.destroy(),E.toast(`Could not load the search settings: ${e.message||e}`,{type:"error"});return}if(H.destroy(),!r.isConnected)return;const w=M.observable(j),B=O(j),s={};for(const[e,t]of Object.entries(B))s[e]=M.observable(t);const h=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].peek()])),p=()=>Object.fromEntries(Object.keys(s).map(e=>[e,s[e].value])),x=M.observable(U(B)),L=M.computed(()=>U(p())!==x.value),T=M.observable(!1),Y=M.observable(A?{searchable:A.count}:null),V=M.observable([]);o(M.effect(()=>{V.value=ye(w.value,Y.value).map(e=>({text:e.text,cls:e.tone?`is-${e.tone}`:""}))}));let m=Number(s.iconSize.peek())||52;o(M.effect(()=>{const e=Number(s.iconSize.value)||52;if(e===m)return;const t=ke(s.iconRadius.peek(),m,e);m=e,String(t)!==String(s.iconRadius.peek())&&(s.iconRadius.value=String(t))}));const ne=M.computed(()=>ge(s.iconSize.value)),Q=r.querySelector("#ssx-radius");o(M.effect(()=>{Q&&(Q.max=String(ne.value))}));const y=M.observable(""),g=M.observable([]),z=M.observable(""),C=M.observable(""),ae=M.computed(()=>se(y.value,s.minQueryLength.value).note);let _=0,q=null;async function re(){const e=se(y.peek(),s.minQueryLength.peek()),t=++_;if(!e.run){g.value=[],C.value="",z.value="";return}const a=c(h()).maxResults;try{const n=await N(`/search/preview?q=${encodeURIComponent(y.peek().trim())}&max=${a}`);if(t!==_||!$())return;g.value=(n.results||[]).map(Se),z.value="";const l=g.peek().length;C.value=l?`${l} result${l===1?"":"s"} (showing up to ${n.maxResults}) from ${n.coverage?.searchable??"?"} searchable pages.`:`Nothing found in ${n.coverage?.searchable??"?"} searchable pages.`,n.coverage&&(Y.value=n.coverage)}catch(n){if(t!==_)return;g.value=[],C.value="",z.value=`Search failed: ${n.message||n}`}}o(M.effect(()=>{y.value,s.maxResults.value,clearTimeout(q),q=setTimeout(re,c(h()).debounceMs)})),o(()=>clearTimeout(q));async function F(){if(!T.peek()){T.value=!0;try{const e=await N("/search/settings",{method:"PUT",body:JSON.stringify(c(h()))});if(!$())return;const t=e?.settings||c(h());w.value=t;const a=O(t);m=Number(a.iconSize)||52;for(const[n,l]of Object.entries(a))s[n].value=l;x.value=U(a);try{S.remove?S.remove(b):S.set(b,null)}catch{}fe(we),E.toast(t.enabled?"Search settings saved.":"Saved - search is off on the public site.",{type:"success"})}catch(e){E.toast(`Not saved: ${e.message||e}`,{type:"error"})}finally{T.value=!1}}}function ie(){const e=O(w.peek());m=Number(e.iconSize)||52;for(const[t,a]of Object.entries(e))s[t].value=a}r.querySelector(".ssx-chips").innerHTML=xe;const f=r.querySelector(".ssx-list");f.innerHTML=Me;const k=e=>`${e}px`,ce={...s,dirty:L,saving:T,chips:V,query:y,results:g,previewNote:ae,previewError:z,previewMeta:C,clampText:M.computed(()=>he(p()).join(" ")),sizeText:M.computed(()=>k(c(p()).iconSize)),radiusText:M.computed(()=>k(c(p()).iconRadius)),sizePx:M.computed(()=>k(c(p()).iconSize)),radiusPx:M.computed(()=>k(c(p()).iconRadius)),glyphPx:M.computed(()=>k(Math.round(c(p()).iconSize*20/52))),positionText:M.computed(()=>be[s.position.value]||s.position.value)},le=E.tabs(r.querySelector(".ssx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),ue=M.applyBindings(ce,r,{methods:{save:()=>F(),discard:()=>ie(),useNavbar:()=>{s.displayMode.value="navbar"},useFloating:()=>{s.displayMode.value="floating"},setPos:e=>{s.position.value=e}}});o(()=>ue.dispose()),o(me(r,{position:"bottom"}));const pe=e=>[{header:e},{label:"Open on the site",icon:"external-link",action:()=>window.open(e,"_blank","noopener")},{label:"Edit the page",icon:"edit",action:()=>{location.hash=`#/pages/edit${e}`}},{label:"Copy address",icon:"copy",action:()=>{const t=new URL(e,location.origin).href;navigator.clipboard?.writeText(t).then(()=>E.toast("Address copied.",{type:"success"}),()=>E.toast(t,{type:"info",duration:8e3}))}}],de=E.contextMenu(f,{cursor:"glyph",match:".ssx-row",inherit:!1,density:"compact",items:e=>pe(e.dataset.url)});o(()=>de?.destroy?.());const J=e=>{const t=e.target.closest(".ssx-row");if(!t)return;const a=e.target.closest(".ssx-kebab");if(a){te(a);return}window.open(t.dataset.url,"_blank","noopener")},W=e=>{const t=e.target.closest(".ssx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),te(t)):e.key==="Enter"&&e.target===t&&window.open(t.dataset.url,"_blank","noopener"))};f.addEventListener("click",J),f.addEventListener("keydown",W),o(()=>{f.removeEventListener("click",J),f.removeEventListener("keydown",W)}),ee(i,{id:"search-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),ee(i,{id:"search-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>F()});const G=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),F())};document.addEventListener("keydown",G,!0),o(()=>document.removeEventListener("keydown",G,!0));const X=()=>{if(L.peek())try{S.set(b,{form:h(),base:x.peek(),at:new Date().toISOString()})}catch{}},Z=e=>{L.peek()&&(X(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Z),o(()=>window.removeEventListener("beforeunload",Z)),u={dirty:()=>L.peek(),dispose(){P=!0,X(),v.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(r);let d=null;try{d=S.get(b)}catch{}if(d?.form){try{S.remove?S.remove(b):S.set(b,null)}catch{}const e=d.base&&d.base!==x.peek(),t=d.at?` (${D(d.at).format("D MMM HH:mm")})`:"",a=e?`You left unsaved search settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved search settings${t}. Restore them?`;if(await E.confirm(a)&&$()){const n={...O(w.peek()),...d.form};m=Number(n.iconSize)||52;for(const[l,ve]of Object.entries(n))s[l]&&(s[l].value=ve)}}}};
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import{api as G,apiRequest as w}from"../api.js";import{placeTools as Re}from"../lib/sidebar-grouping.js";import{addBannerAction as fe}from"../lib/plugin-chrome.js";import{helpPopovers as Ae}from"../lib/help-popover.js";import{openMenuAt as Q,sidebarCount as he}from"../lib/tool-kit.js";import{changedTabs as He,describeCacheKey as X,draftOf as Pe,expiresIn as $e,formFromSettings as De,homeValue as je,payloadFromForm as Ke,settingsProblems as _e,snapshot as ve,TAB_FIELDS as Ie}from"../lib/site-settings-arrange.js";const Z=/^\/settings(\?.*)?$/,be="#/settings",T="settings.draft",Be=`
|
|
2
|
+
<div class="stx-problem">
|
|
3
|
+
<span class="stx-chip" data-bind-class="tone === 'danger' ? 'is-bad' : 'is-warn'">{{label}}</span>
|
|
4
|
+
<span>{{text}}</span>
|
|
5
|
+
<button type="button" class="btn btn-sm btn-ghost" data-go="{{tab}}">Go there</button>
|
|
6
|
+
</div>`,qe=`
|
|
7
|
+
<div class="stx-row" data-id="{{id}}" tabindex="0">
|
|
8
|
+
<span class="stx-main"><strong>{{path}}</strong><small>{{meta}}</small></span>
|
|
9
|
+
<span class="stx-chip">{{expires}}</span>
|
|
10
|
+
<button type="button" class="stx-kebab" aria-label="Actions for {{path}}" title="Actions">\u22EE</button>
|
|
11
|
+
</div>`,Fe=["general","seo","footer","email","backToTop","cookies","breadcrumbs","css","cache"];async function ze(l,p){const[r,O]=await Promise.all([H.get("/api/sidebar/menu").catch(()=>({items:[]})),H.get("/api/sidebar/registered-items").catch(()=>[])]),a=new Map([["/","Dashboard (built in)"]]),u=(C,m)=>{for(const o of C||[]){if(!o||o.hidden||o.restricted)continue;const f=typeof o.url=="string"?o.url:"";f.startsWith("#/")&&f.length>2&&!a.has(f.slice(1))&&a.set(f.slice(1),`${m?`${m} \u203A `:""}${o.text||f}`),u(o.items,o.url?m:o.text||m)}};u(Re(r?.items||[],O||[]),""),a.has(p)||a.set(p,`${p} (not in the sidebar)`),l.replaceChildren();for(const[C,m]of a){const o=document.createElement("option");o.value=C,o.textContent=m,l.appendChild(o)}}let y=null,ye=!1;function Ne(){ye||!window.R?.use||(ye=!0,R.use((l,p,r)=>!(Z.test(p?.path||"")&&!Z.test(l?.path||""))||!y?.dirty()?r():E.confirm("You have unsaved settings. Leave anyway? They are kept as a draft for your next visit (not the SMTP password).").then(a=>{a?r():history.replaceState(null,"","#/settings")})),M.subscribe("router:afterChange",({to:l}={})=>{if(y&&!Z.test(l?.path||"")){const p=y;y=null,p.dispose()}}))}export const settingsView={templateUrl:"/admin/js/templates/settings.html",async onMount(l){if(Ne(),y){const e=y;y=null,e.dispose()}const p=[],r=e=>(p.push(e),e);let O=!1;const a=l.get(0).querySelector(".stx")||l.get(0),u=()=>!O&&a.isConnected,C=E.loader(l.get(0),{type:"dots"});let m,o;try{[m,o]=await Promise.all([G.settings.get(),w("/settings/custom-css").then(e=>e?.css||"").catch(()=>null)])}catch(e){C.destroy(),E.toast(`Could not load the settings: ${e.message||e}`,{type:"error"});return}if(C.destroy(),!a.isConnected)return;const f=o!==null,ee=M.observable(m||{}),te=De(m||{},o||""),n={};for(const[e,t]of Object.entries(te))n[e]=M.observable(t);const B=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].value])),se=()=>Object.fromEntries(Object.keys(n).map(e=>[e,n[e].peek()])),g=M.observable(te),q=M.computed(()=>ve(g.value)),L=M.computed(()=>ve(B())!==q.value),A=M.observable(!1),P=M.observable(""),F=M.observable(!1),ne=M.computed(()=>{const e=new Set(He(B(),g.value));return Object.fromEntries(Object.keys(Ie).map(t=>[t,e.has(t)]))}),z=M.observable([]);r(M.effect(()=>{z.value=_e(B()).map(e=>({...e,label:e.tone==="danger"?"Mail":"Check"}))}));const ae=M.observable(""),$=M.observable(!1),j=M.observable(""),K=M.observable(""),h=M.observable(null),N=M.observable([]),V=M.observable(""),U=M.observable(!0),W=M.observable(""),oe=M.observable([]);r(M.effect(()=>{const e=V.value.trim().toLowerCase(),t=Date.now();oe.value=[...N.value].reverse().filter(s=>!e||String(s.key).toLowerCase().includes(e)||(s.tags||[]).some(i=>String(i).toLowerCase().includes(e))).map(s=>{const i=X(s.key);return{id:s.key,path:i.path,meta:[i.page?`as ${i.role}`:"not a page",(s.tags||[]).join(", ")].filter(Boolean).join(" \xB7 "),expires:$e(s.expiresAt,t)}})}));const ge={...n,saving:A,dirty:L,tabs:ne,problems:z,testTo:ae,testing:$,testResult:j,testTone:K,cacheQuery:V,cacheLoading:U,cacheError:W,cacheRows:oe,errorText:P,problemsText:M.computed(()=>z.value.length?"yes":""),noSmtp:M.computed(()=>!String(n.smtpHost.value||"").trim()),dirtyEmail:M.computed(()=>ne.value.email),seoLength:M.computed(()=>{const e=String(n.seoDescription.value||"").trim().length;return e?`${e} characters${e>160?" - search engines usually cut it off after about 150":""}`:""}),cssSize:M.computed(()=>{if(!f)return"Could not load content/custom.css - reload to try again. It will not be saved until it loads.";const e=new Blob([String(n.css.value||"")]).size/1024;return`${e.toFixed(1)} KB of 100 KB${e>100?" - too big to save":""}`}),cacheOn:M.computed(()=>h.value?.enabled===!0),cacheState:M.computed(()=>{const e=h.value;return e?e.enabled?`On (${e.driver})`:"Off":"Status unknown"}),cacheSize:M.computed(()=>{const e=h.value;return e?.enabled&&e.size!==null&&e.size!==void 0?`${e.size}${e.maxItems?` of ${e.maxItems}`:""} entries`:""}),cacheHits:M.computed(()=>{const e=h.value?.hitRate;return e==null?"":`${Math.round(e*100)}% served from the cache`}),cacheToggleText:M.computed(()=>h.value?.enabled?"Turn off":"Turn on"),cacheEmpty:M.computed(()=>V.value.trim()?"Nothing matches.":h.value?.enabled?"The cache is empty - pages are cached as visitors open them.":"The cache is off.")};async function Y(){if(A.peek())return;const e=se(),t=g.peek(),s=String(e.css)!==String(t.css);if(s&&!f){E.toast("The custom CSS did not load, so it cannot be saved - reload the screen first.",{type:"error"});return}A.value=!0,P.value="";const i=String(e.title).trim()!==String(t.title).trim();try{await G.settings.save(Ke(e,ee.peek(),{adminHomeReady:F.peek()})),s&&await w("/settings/custom-css",{method:"PUT",body:JSON.stringify({css:String(e.css)})});const c=await G.settings.get().catch(()=>null);if(!u())return;c&&(ee.value=c);const b={...e,smtpPass:""};n.smtpPass.value="",g.value=b;try{S.remove?S.remove(T):S.set(T,null)}catch{}document.dispatchEvent(new CustomEvent("dm:admin-home-changed")),i&&document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild")),he(be),E.toast("Settings saved.",{type:"success"})}catch(c){P.value=c.message||"Could not save the settings.",E.toast("Settings not saved - see the message at the top.",{type:"error"})}finally{A.value=!1}}async function ke(){await E.confirm("Throw away your unsaved settings?")&&(re(g.peek()),P.value="")}function re(e){for(const[t,s]of Object.entries(e))n[t]&&(n[t].value=s);d&&d.getValue()!==String(n.css.peek())?d.setValue(String(n.css.peek())):!d&&v&&(v.value=String(n.css.peek()))}async function we(){if(!$.peek()){$.value=!0,K.value="",j.value="Sending\u2026";try{const e=await w("/settings/test-email",{method:"POST",body:JSON.stringify({to:ae.peek().trim()||void 0})});if(!u())return;j.value=e?.message||"Test email sent.",K.value="is-ok"}catch(e){if(!u())return;j.value=e.message||"Could not send the test email.",K.value="is-bad"}finally{$.value=!1}}}async function _(){U.value=!0;try{const[e,t]=await Promise.all([w("/cache/status").catch(()=>null),w("/cache/keys")]);if(!u())return;h.value=e,N.value=Array.isArray(t?.entries)?t.entries:[],W.value=""}catch(e){if(!u())return;W.value=`Could not read the cache: ${e.message||e}`}finally{U.value=!1}}async function Ee(){if(await E.confirm("Clear the whole response cache? The next visitor to each page renders it afresh.")){try{await w("/cache/clear",{method:"POST"}),E.toast("Cache cleared.",{type:"success"})}catch(e){E.toast(e.message||"Could not clear the cache.",{type:"error"})}u()&&await _()}}async function Se(){const e=h.peek()?.enabled===!0;if(!(e&&!await E.confirm("Turn the response cache off? Every visit will render its page afresh until it is turned back on. The choice is saved (config/cache.json)."))){try{h.value=await w("/cache/toggle",{method:"POST",body:JSON.stringify({enabled:!e})}),E.toast(e?"Cache turned off.":"Cache turned on.",{type:"success"}),he(be)}catch(t){E.toast(t.message||"Could not change the cache.",{type:"error"})}u()&&await _()}}const ie=e=>N.peek().find(t=>t.key===e);function Me(e){const t=ie(e);if(!t)return[];const s=X(t.key);return[{header:s.path},{label:"Open the page",icon:"external-link",disabled:!s.page,action:()=>window.open(s.path,"_blank","noopener")},{label:"Copy key",icon:"copy",action:async()=>{try{await navigator.clipboard.writeText(t.key),E.toast("Key copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}},...s.page?[{label:"Copy address",icon:"link",action:async()=>{try{await navigator.clipboard.writeText(new URL(s.path,location.origin).href),E.toast("Address copied.",{type:"success"})}catch{E.toast("Could not copy - your browser blocked the clipboard.",{type:"warning"})}}}]:[]]}a.querySelector(".stx-problems").innerHTML=Be;const x=a.querySelector(".stx-list");x.innerHTML=qe;const Ce=E.tabs(a.querySelector(".stx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),xe=M.applyBindings(ge,a,{methods:{save:Y,discard:ke,sendTest:we,refreshCache:_,clearCache:Ee,toggleCache:Se,corner:e=>{n.bcPosition.value=e}}});r(()=>xe.dispose()),r(Ae(a,{position:"bottom"}));const Te=e=>{const t=e.target.closest("[data-go]");if(!t)return;const s=Fe.indexOf(t.dataset.go);s>=0&&Ce.activate(s)};a.querySelector(".stx-problems").addEventListener("click",Te);const J=a.querySelector("#stx-admin-home");ze(J,je(n.adminHome.peek())).then(()=>{u()&&(J.value=n.adminHome.peek(),F.value=!0)}).catch(()=>{});const v=a.querySelector(".stx-css");v.value=String(n.css.peek());let d=null;if(!f)v.disabled=!0;else if(E.editor)try{d=E.editor(v,{mode:"code",language:"css",lineNumbers:!0,showToolbar:!1,minHeight:420,placeholder:"/* Add your custom CSS here */",characterCount:!0,onChange:e=>{n.css.value=e}})}catch{d=null}if(d)d._editorEl&&d._editorEl.addEventListener("keydown",e=>{if(!(e.ctrlKey||e.metaKey)||e.key!=="c"&&e.key!=="x")return;const t=d._editorEl;if(t.selectionStart!==t.selectionEnd||typeof t.value!="string")return;e.preventDefault();const s=t.value,i=t.selectionStart,c=s.lastIndexOf(`
|
|
12
|
+
`,i-1)+1,b=s.indexOf(`
|
|
13
|
+
`,i),Le=b===-1?s.slice(c):s.slice(c,b+1);navigator.clipboard?.writeText(Le).catch(()=>{}),e.key==="x"?(t.value=s.slice(0,c)+(b===-1?"":s.slice(b+1)),t.selectionStart=t.selectionEnd=c,t.dispatchEvent(new Event("input",{bubbles:!0})),E.toast("Line cut",{type:"info",duration:1500})):E.toast("Line copied",{type:"info",duration:1500})});else{const e=()=>{n.css.value=v.value};v.addEventListener("input",e),r(()=>v.removeEventListener("input",e))}r(()=>{try{d?.destroy?.()}catch{}});const Oe=E.contextMenu(x,{cursor:"glyph",match:".stx-row",inherit:!1,density:"compact",items:e=>Me(e.dataset.id)});r(()=>Oe?.destroy?.());const ce=e=>{const t=ie(e.dataset.id),s=t&&X(t.key);s?.page?window.open(s.path,"_blank","noopener"):Q(e)},le=e=>{const t=e.target.closest(".stx-row");if(!t)return;const s=e.target.closest(".stx-kebab");if(s){Q(s);return}ce(t)},ue=e=>{const t=e.target.closest(".stx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),Q(t)):e.key==="Enter"&&e.target===t&&ce(t))};x.addEventListener("click",le),x.addEventListener("keydown",ue),r(()=>{x.removeEventListener("click",le),x.removeEventListener("keydown",ue)}),fe(l,{id:"settings-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),fe(l,{id:"settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>Y()});const de=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),Y())};document.addEventListener("keydown",de,!0),r(()=>document.removeEventListener("keydown",de,!0));const pe=()=>{if(L.peek())try{S.set(T,{form:Pe(se()),base:q.peek(),at:new Date().toISOString()})}catch{}},me=e=>{L.peek()&&(pe(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",me),r(()=>window.removeEventListener("beforeunload",me)),y={dirty:()=>L.peek(),dispose(){O=!0,pe(),p.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(a),_();let k=null;try{k=S.get(T)}catch{}if(k?.form){try{S.remove?S.remove(T):S.set(T,null)}catch{}const e=k.base&&k.base!==q.peek(),t=k.at?` (${D(k.at).format("D MMM HH:mm")})`:"",s=e?`You left unsaved settings${t}, but the saved settings have changed since. Restore your version anyway?`:`You left unsaved settings${t}. Restore them?`;if(await E.confirm(s)&&u()){const i={...g.peek()};for(const[c,b]of Object.entries(k.form))c in i&&c!=="smtpPass"&&(i[c]=b);f||(i.css=g.peek().css),re(i),F.peek()&&(J.value=n.adminHome.peek())}}}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as a,getUser as N}from"../api.js";function I(c){return{string:"string",email:"email",tel:"string",number:"number",textarea:"textarea",select:"select",radio:"select",checkbox:"boolean","checkbox-group":"select",date:"string",time:"string",url:"string",hidden:"string"}[c]||"string"}export const userEditorView={templateUrl:"/admin/js/templates/user-editor.html",async onMount(c){const b=window.location.hash.match(/#\/users\/edit\/([^/]+)/),u=b?b[1]:null,r=!!u,C=E.loader(c.get(0),{type:"dots"});let o,m,y,v;try{[o,m,y,v]=await Promise.all([r?a.users.get(u).catch(()=>null):Promise.resolve(null),a.collections.listEntries("roles",{limit:100}).catch(()=>null),a.collections.get("user-profiles").catch(()=>null),a.projects.list().catch(()=>[])])}finally{C.destroy()}if(r&&!o){E.toast("User not found.",{type:"error"}),R.navigate("/users");return}c.find("#editor-title").text(r?"Edit User":"New User"),c.find("#cancel-btn").on("click",()=>R.navigate("/users"));const O=N(),h=e=>(m?.entries||[]).find(i=>i.data.name===e)?.data?.level??99,B=h(O?.role),j=(m?.entries||[]).filter(e=>(e.data.level??99)>B||r&&e.data.name===o?.role).map(e=>({value:e.data.name,label:e.data.label})).sort((e,i)=>h(e.value)-h(i.value)),S=[{value:"admin",label:"Admin"},{value:"user",label:"User"}],A=j.length>0?j:S,P=(v||[]).map(e=>({value:e.slug,label:e.name||e.slug})),q=[{value:"",label:"(none)"},...P],L={name:{type:"string",required:!0,minLength:2,label:"Full Name",formConfig:{placeholder:"Jane Smith"}},email:{type:"email",required:!0,label:"Email Address",formConfig:{placeholder:"jane@example.com"}},role:{type:"select",required:!0,label:"Primary role",options:A,formConfig:{tooltip:"Shown as the user's badge. Used for hierarchy comparisons."}},additionalRoles:{type:"multiselect",label:"Additional roles",options:A,formConfig:{placeholder:"",tooltip:"Optional - extra roles whose permissions also apply. Useful for someone who is both, say, a recruiter AND a content editor."}},ownedByProject:{type:"select",label:"Owned by project",options:q,formConfig:{tooltip:"Administrative tag - controls where this user appears in the sidebar Projects section. Mapped to meta.project."}},projects:{type:"multiselect",label:"Project access",options:P,formConfig:{placeholder:"",tooltip:"Access scope - limits which artefacts the user can see and edit. Leave empty for no restriction."}},isActive:{type:"boolean",label:"Active account"},password:{type:"password",required:!r,minLength:r?0:8,label:r?"New Password":"Password",formConfig:{placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022",autocomplete:"new-password",tooltip:r?"Leave blank to keep current password":"Minimum 8 characters"}}},T=o?{name:o.name||"",email:o.email||"",role:o.role||"user",additionalRoles:Array.isArray(o.additionalRoles)?o.additionalRoles:[],ownedByProject:o.meta?.project||"",projects:Array.isArray(o.projects)?o.projects:[],isActive:o.isActive!==!1}:{role:"user",additionalRoles:[],ownedByProject:"",projects:[],isActive:!0};F.render("#user-form-container",L,T,{layout:"stacked",submitText:r?"Update User":"Create User",onSubmit:async e=>{if(r&&e.password&&e.password.length<8)return E.toast("Password must be at least 8 characters.",{type:"warning"}),!1;const t=(Array.isArray(e.additionalRoles)?e.additionalRoles:[]).filter(s=>s&&s!==e.role).filter((s,d,n)=>n.indexOf(s)===d),l=(Array.isArray(e.projects)?e.projects:[]).filter(Boolean).filter((s,d,n)=>n.indexOf(s)===d),p=e.ownedByProject||null,g={name:e.name,email:e.email,role:e.role,additionalRoles:t,projects:l,meta:{...o?.meta||{},project:p},isActive:!!e.isActive};e.password&&(g.password=e.password);try{let s;r?s=await a.users.update(u,g):s=await a.users.create(g);const d=y?.fields||[];if(d.length>0){const n={};for(const w of d)w.name in e&&(n[w.name]=e[w.name]);const x=r?u:s?.id;x&&Object.keys(n).length>0&&await a.users.update(x,{profile:n})}E.toast("User saved successfully.",{type:"success"}),R.navigate("/users")}catch(s){return E.toast(`Save failed: ${s.message||"Unknown error"}`,{type:"error"}),!1}}});const U=y?.fields||[];if(U.length>0){c.find("#profile-card").show();const e={},i={};for(const t of U){const l={type:I(t.type),label:t.label||t.name};t.required&&(l.required=!0),t.placeholder&&(l.formConfig={placeholder:t.placeholder}),t.options&&Array.isArray(t.options)&&(l.options=t.options.map(p=>typeof p=="string"?{value:p,label:p}:p)),e[t.name]=l,i[t.name]=o?.profile?.[t.name]??""}F.render("#profile-form-container",e,i,{layout:"stacked",submitText:r?"Update Profile":"Save Profile",onSubmit:async t=>{const f=u;if(!f)return E.toast("Save the user first, then update the profile.",{type:"info"}),!1;try{await a.users.update(f,{profile:t}),E.toast("Profile saved.",{type:"success"})}catch(l){return E.toast(`Profile save failed: ${l.message||"Unknown error"}`,{type:"error"}),!1}}})}Domma.icons.scan()}};
|
|
1
|
+
import{api as Y,getUser as Ve}from"../api.js";import{addBannerAction as Ae}from"../lib/plugin-chrome.js";import{helpPopovers as Ge}from"../lib/help-popover.js";import{sidebarCount as Te}from"../lib/tool-kit.js";import{roleIndex as Je,initials as We}from"../lib/users-arrange.js";import{fieldKind as ue,fieldOptions as de}from"../lib/my-profile-arrange.js";import{buildUserPayload as Xe,checkUserForm as Re,draftOf as ze,editRights as Qe,emailConflict as Ze,formFromUser as pe,roleChoices as et,snapshot as oe}from"../lib/user-editor-arrange.js";const tt=/^\/users\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,ne=a=>String(a||"").split("?")[0].replace(/\/$/,""),p=a=>String(a??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),re=a=>{if(!a)return"";try{return D(a).format("D MMM YYYY")}catch{return String(a).slice(0,10)}},at=`<button type="button" class="uex-pick-chip" data-role="{{name}}" data-bind-class="on && 'is-on'" data-bind-disabled="disabled" title="{{title}}" data-bind-aria-pressed="on ? 'true' : 'false'">{{label}}</button>`,st=`<button type="button" class="uex-pick-chip" data-project="{{slug}}" data-bind-class="on && 'is-on'" data-bind-aria-pressed="on ? 'true' : 'false'" title="{{title}}">{{name}}</button>`;let k=null,Ne=!1;function ot(){Ne||!window.R?.use||(Ne=!0,R.use((a,r,v)=>{const c=ne(r?.path);return!(tt.test(r?.path||"")&&ne(a?.path)!==c)||!k?.dirty()||k.path!==c?v():E.confirm("You have unsaved changes to this user. Leave anyway? They are kept for your next visit - except a password, which is typed again.").then(L=>{L?v():history.replaceState(null,"",`#${c}`)})}),M.subscribe("router:afterChange",({to:a}={})=>{if(k&&ne(a?.path)!==k.path){const r=k;k=null,r.dispose()}}))}function nt(a,r){const v=ue(a.type),c=`uex-pf-${r}`,s=a.required?' <span class="text-danger">*</span>':"",L=a.help||a.description?` <span data-help="${p(a.help||a.description)}"></span>`:"",P=`<label class="form-label" for="${c}">${p(a.label||a.name)}${s}${L}</label>`,i=`<small class="uex-err" data-bind-hidden="!pe_${r}.value" data-bind-text="pe_${r}.value"></small>`,F=a.placeholder?` placeholder="${p(a.placeholder)}"`:"";switch(v){case"textarea":return`<div class="uex-wide">${P}<textarea id="${c}" class="form-input" rows="3"${F} data-model="pf_${r}.value"></textarea>${i}</div>`;case"select":return`<div>${P}<select id="${c}" class="form-input" data-model="pf_${r}.value"><option value="">-</option>${de(a).map(m=>`<option value="${p(m.value)}">${p(m.label)}</option>`).join("")}</select>${i}</div>`;case"checkbox":return`<div><label class="uex-check"><input id="${c}" type="checkbox" data-model="pf_${r}.value"> ${p(a.label||a.name)}${s}</label>${L}${i}</div>`;case"multi":return`<div class="uex-wide">${P}<div data-multi="${r}">${de(a).map((m,u)=>`<label class="uex-check"><input type="checkbox" data-opt="${p(m.value)}" data-bind-checked="po_${r}_${u}.value"> ${p(m.label)}</label>`).join("")}</div>${i}</div>`;default:return`<div>${P}<input id="${c}" type="${v}" class="form-input"${F} data-model="pf_${r}.value">${i}</div>`}}function ve(a,r){const v=a.querySelector(".dm-plugin-banner-name");if(!v)return;const c=v.querySelector(".dm-plugin-banner-parent")||Object.assign(document.createElement("span"),{className:"dm-plugin-banner-parent",textContent:"Users"}),s=v.querySelector(".dm-plugin-banner-sep")||Object.assign(document.createElement("span"),{className:"dm-plugin-banner-sep",textContent:"\u203A"});s.setAttribute("aria-hidden","true"),v.replaceChildren(c,s,document.createTextNode(r))}export const userEditorView={templateUrl:"/admin/js/templates/user-editor.html",async onMount(a){if(ot(),k){const e=k;k=null,e.dispose()}const r=ne(location.hash.slice(1)),v=r.match(/^\/users\/edit\/([^/?#]+)/),c=v?decodeURIComponent(v[1]):null,s=!c,L=s&&new URLSearchParams(location.hash.split("?")[1]||"").get("project")||"",P=[],i=e=>(P.push(e),e);let F=!1;const m=a.get(0),u=m.querySelector(".uex")||m,A=()=>!F&&u.isConnected,K=M.observable(!0),me=M.observable(""),V=M.observable(!1),_=M.observable(null),O=M.observable(""),y=M.observable(""),W=M.observable(""),_e=M.observable(!1),$=M.observable("user"),h=M.observable([]),X=M.observable(""),x=M.observable([]),z=M.observable(!0),q=M.observable(""),G=M.observable(""),fe=M.observable(!1),he=M.observable(!1),be=M.observable([]),ge=M.observable([]),T=M.observable({self:!1,canEdit:!0,reason:""}),B=M.observable([]);let w={};const ke=M.observable([]);let b=[];const N=new Map,ye=M.observable(0),g=M.observable(pe(null,[])),Oe=()=>(ye.value,Object.fromEntries([...N].map(([e,t])=>[e,t.value]))),qe=()=>Object.fromEntries([...N].map(([e,t])=>[e,t.peek()])),we=()=>({name:O.value,email:y.value,password:W.value,role:$.value,additionalRoles:h.value,ownedByProject:X.value,projects:x.value,isActive:z.value,profile:Oe()}),le=()=>({name:O.peek(),email:y.peek(),password:W.peek(),role:$.peek(),additionalRoles:h.peek(),ownedByProject:X.peek(),projects:x.peek(),isActive:z.peek(),profile:qe()}),Q=M.computed(()=>Re(we(),{isNew:s,fields:b,saved:s?void 0:g.value,choices:B.value})),U=M.computed(()=>!K.value&&T.value.canEdit&&oe(we())!==oe(g.value)),H=M.computed(()=>!T.value.canEdit);i(M.effect(()=>{const e=new Set(h.value);be.value=B.value.filter(t=>t.name!==$.value).map(t=>({name:t.name,label:t.label,on:e.has(t.name),disabled:!t.assignable&&!e.has(t.name),title:t.assignable||e.has(t.name)?`Level ${t.level}`:t.reason}))})),i(M.effect(()=>{const e=new Set(x.value);ge.value=ke.value.map(t=>({slug:t.slug,name:t.name||t.slug,on:e.has(t.slug),title:t.slug}))}));const Be={loading:K,loadError:me,saving:V,name:O,email:y,password:W,showPw:_e,role:$,ownedByProject:X,isActive:z,hasProfile:fe,hasProjects:he,extraChips:be,projectChips:ge,err:Q,dirty:U,locked:H,isSelf:M.computed(()=>T.value.self),lockedReason:M.computed(()=>T.value.reason),emailErr:M.computed(()=>Q.value.email||(q.value&&y.value.trim().toLowerCase()===G.value?q.value:"")),cannotSave:M.computed(()=>V.value||H.value||!U.value||Object.keys(Q.value).length>0||!!(q.value&&y.value.trim().toLowerCase()===G.value)),saveText:s?"Create user":"Save",dirtyText:s?"This user has not been created yet.":"You have unsaved changes.",pwLabel:M.computed(()=>s?"Password":"New password"),pwPlaceholder:M.computed(()=>s?"At least 8 characters":"Leave empty to keep it"),avatar:M.computed(()=>We({name:O.value,email:y.value})),shownName:M.computed(()=>O.value.trim()||(s?"New user":_.value?.name||"")),shownEmail:M.computed(()=>y.value.trim()||""),roleText:M.computed(()=>[$.value,...h.value.filter(t=>t!==$.value)].filter(Boolean).map(t=>w[t]?.label||t).join(" + ")||"-"),accessHint:M.computed(()=>x.value.length?`Sees only ${x.value.length===1?"that project":"those projects"}, plus Core.`:"Nothing ticked: no restriction - sees every project."),factsText:M.computed(()=>{const e=_.value;return s||!e?"Not created yet":[e.createdAt&&`Added ${re(e.createdAt)}`,e.lastLogin?`last signed in ${re(e.lastLogin)}`:"never signed in",e.updatedAt&&`changed ${re(e.updatedAt)}`].filter(Boolean).join(" \xB7 ")})},Z=`user-editor.draft.${c||"new"}`;function ee(e){O.value=e.name,y.value=e.email,W.value="",$.value=e.role,h.value=[...e.additionalRoles||[]],X.value=e.ownedByProject||"",x.value=[...e.projects||[]],z.value=e.isActive!==!1;for(const[t,o]of N)t in(e.profile||{})&&(o.value=e.profile[t])}function Ie(e,t){const o=Ze(e);return o?(q.value=o,G.value=String(t||"").trim().toLowerCase(),u.querySelector("#uex-email")?.focus(),!0):!1}async function ie(){if(V.peek()||K.peek()||H.peek())return;const e=le(),t=Re(e,{isNew:s,fields:b,saved:s?void 0:g.peek(),choices:B.peek()});if(Object.keys(t).length){E.toast("Check the fields marked in red first.",{type:"warning"});return}const o=Xe(g.peek(),e,{isNew:s,fields:b,meta:_.peek()?.meta||{}});if(!o){E.toast("Nothing has changed.",{type:"info"});return}if(!(!s&&T.peek().self&&o.update.role&&(w[o.update.role]?.level??99)>(w[g.peek().role]?.level??99)&&!await E.confirm(`Give yourself the less senior role "${p(w[o.update.role]?.label||o.update.role)}"? You will not be able to raise it again yourself.`))){V.value=!0;try{if(s){const d=await Y.users.create(o.create);if(!A())return;te(),g.value=le(),E.toast(`Created ${d?.name||e.name.trim()}.`,{type:"success"}),Te("#/users"),location.hash=d?.id?`#/users/edit/${encodeURIComponent(d.id)}`:"#/users";return}const n=await Y.users.update(c,o.update);if(!A())return;_.value=n;const l=pe(n,b);if(g.value=l,ee(l),te(),ve(m,n.name||n.email||"Edit"),T.peek().self){try{S.set("auth_user",{...S.get("auth_user")||{},name:n.name,email:n.email})}catch{}const d=document.getElementById("topbar-user-name");d&&n.name&&(d.textContent=n.name)}Te("#/users"),E.toast("Saved.",{type:"success"})}catch(n){Ie(n,e.email)||E.toast(`Not saved: ${n.message||n}`,{type:"error"})}finally{V.value=!1}}}async function Ue(){U.peek()&&(!await E.confirm("Throw away the changes you have not saved?")||!A()||(ee(g.peek()),te()))}function $e(){if(!U.peek())return;const e=ze(le());if(oe({...e,password:""})!==oe(g.peek()))try{S.set(Z,{form:e,at:new Date().toISOString(),basedOn:_.peek()?.updatedAt||null})}catch{}}function te(){try{S.remove?S.remove(Z):S.set(Z,null)}catch{}}u.querySelector(".uex-roles").innerHTML=at,u.querySelector(".uex-projects").innerHTML=st;const Me=e=>{if(H.peek())return;const t=e.target.closest(".uex-pick-chip");if(!(!t||t.disabled)){if(t.dataset.role){const o=t.dataset.role,n=h.peek();h.value=n.includes(o)?n.filter(l=>l!==o):[...n,o]}else if(t.dataset.project){const o=t.dataset.project,n=x.peek();x.value=n.includes(o)?n.filter(l=>l!==o):[...n,o]}}};u.addEventListener("click",Me),i(()=>u.removeEventListener("click",Me)),i(M.effect(()=>{q.peek()&&y.value.trim().toLowerCase()!==G.peek()&&(q.value="",G.value="")})),i(M.effect(()=>{const e=$.value;h.peek().includes(e)&&(h.value=h.peek().filter(t=>t!==e))})),Ae(a,{id:"user-editor-back",icon:"arrow-left",title:"Back to users",onClick:()=>{location.hash="#/users"}}),Ae(a,{id:"user-editor-save",icon:"save",title:s?"Create user (Ctrl+S)":"Save (Ctrl+S)",onClick:()=>ie()}),ve(m,s?"New user":"Edit");const Ee=e=>{!(e.ctrlKey||e.metaKey)||e.key.toLowerCase()!=="s"||!A()||(e.preventDefault(),e.stopImmediatePropagation(),ie())};document.addEventListener("keydown",Ee,!0),i(()=>document.removeEventListener("keydown",Ee,!0));const Se=e=>{U.peek()&&($e(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Se),i(()=>window.removeEventListener("beforeunload",Se)),k={path:r,dirty:()=>U.peek(),dispose(){F=!0,$e(),P.splice(0).reverse().forEach(e=>{try{e()}catch{}})}};let f=null,xe=null,je=null,j=[];try{[f,je,xe,j]=await Promise.all([s?Promise.resolve(null):Y.users.get(c),Y.collections.listEntries("roles",{limit:1e3}).catch(()=>null),Y.collections.get("user-profiles").catch(()=>null),Y.projects.list().catch(()=>[])])}catch(e){if(!A())return;me.value=e?.status===404?"There is no such user - it may have been deleted.":`Could not load this user: ${e.message||e}`,K.value=!1,I.scan(u);return}if(!A())return;j=Array.isArray(j)?j:[],ke.value=j,w=Je(je?.entries||[]),Object.keys(w).length||(w={admin:{label:"Admin",level:1},user:{label:"User",level:2}});const Ce=Ve()||{};T.value=Qe(Ce,f,w),B.value=et(w,Ce,f),_.value=f;const He=u.querySelector(".uex-role-select");He.innerHTML=B.peek().map(e=>`<option value="${p(e.name)}"${e.assignable?"":" disabled"}>${p(e.label)}${e.assignable?"":" - only a more senior role can give it"}</option>`).join("");const De=u.querySelector(".uex-owner-select");De.innerHTML=`<option value="">(none)</option>${j.map(e=>`<option value="${p(e.slug)}">${p(e.name||e.slug)}</option>`).join("")}`,he.value=j.length>0;const Le=B.peek().filter(e=>e.assignable).map(e=>e.name),Ye=Le.includes("user")?"user":Le.at(-1)||"user";b=(xe?.fields||[]).filter(e=>e&&e.name);const J=pe(f,b,{project:j.some(e=>e.slug===L)?L:"",defaultRole:Ye});b.forEach(e=>N.set(e.name,M.observable(J.profile[e.name])));const Fe=M.applyBindings(Be,u,{methods:{save:ie,discard:Ue}});i(()=>Fe.dispose());const ce=b.map((e,t)=>({f:e,i:t})).filter(({f:e})=>ue(e.type)!=="hidden");if(ce.length){const e=u.querySelector(".uex-profile");e.innerHTML=ce.map(({f:l,i:d})=>nt(l,d)).join("");const t={};b.forEach((l,d)=>{t[`pf_${d}`]=N.get(l.name),t[`pe_${d}`]=M.computed(()=>Q.value[`profile.${l.name}`]||""),ue(l.type)==="multi"&&de(l).forEach((ae,se)=>{t[`po_${d}_${se}`]=M.computed(()=>(N.get(l.name).value||[]).includes(ae.value))})});const o=M.applyBindings(t,e);i(()=>o.dispose());const n=l=>{const d=l.target.closest("[data-multi]");if(!d||!l.target.matches("input[data-opt]"))return;const ae=N.get(b[Number(d.dataset.multi)].name),se=l.target.dataset.opt,Pe=ae.peek()||[];ae.value=l.target.checked?[...new Set([...Pe,se])]:Pe.filter(Ke=>Ke!==se)};e.addEventListener("change",n),i(()=>e.removeEventListener("change",n))}fe.value=ce.length>0,i(Ge(u,{position:"bottom"})),g.value=J,ee(J),ye.value++,K.value=!1,ve(m,s?"New user":f.name||f.email||"Edit"),I.scan(u),H.peek()||u.querySelector("#uex-name")?.focus();let C=null;try{C=S.get(Z)}catch{}if(C?.form&&!H.peek()){te();const e=C.at?` (${re(C.at)})`:"",t=!s&&C.basedOn&&f?.updatedAt&&C.basedOn!==f.updatedAt?" This user has been changed since - restoring puts your version over it.":"",o=s?"the new user you started":`"${p(f.name||f.email)}"`;await E.confirm(`You left unsaved changes to ${o}${e}.${t} Restore them?`)&&A()&&ee({...J,...C.form,profile:{...J.profile,...C.form.profile||{}}})}}};
|
package/admin/js/views/users.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import{api as
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import{api as N,apiRequest as U,getUser as X}from"../api.js";import{addBannerAction as Z}from"../lib/plugin-chrome.js";import{helpPopovers as ee}from"../lib/help-popover.js";import{openMenuAt as L,settingsStore as te,sidebarCount as q,viewLifetime as se}from"../lib/tool-kit.js";import{filterByProject as oe}from"../lib/project-context.js";import{FILTERS as ae,SORTS as ne,arrangeUsers as ie,canManage as P,roleIndex as re,userRow as le}from"../lib/users-arrange.js";const Y=/^#\/(projects\/[^/?#]+\/)?users\/?(\?.*)?$/,B="#/users",ce=`
|
|
2
|
+
<div class="usx-row" data-id="{{id}}" tabindex="0" data-bind-class="rowCls">
|
|
3
|
+
<span class="usx-avatar" aria-hidden="true">{{initials}}</span>
|
|
4
|
+
<span class="usx-main"><strong>{{name}}</strong><small>{{meta}}</small></span>
|
|
5
|
+
<span class="usx-chips">
|
|
6
|
+
<span class="usx-chip is-ok" data-bind-hidden="!self">You</span>
|
|
7
|
+
<span class="usx-chip" data-bind-class="roleTone">{{roleLabel}}</span>
|
|
8
|
+
<span class="usx-chip is-outline" data-bind-hidden="!extraRoles" title="Additional roles">+ {{extraRoles}}</span>
|
|
9
|
+
<span class="usx-chip" data-bind-hidden="!scope" title="Project access - sees only these (and Core)">{{scope}}</span>
|
|
10
|
+
<span class="usx-chip is-warn" data-bind-hidden="!inactive" title="Cannot sign in">Inactive</span>
|
|
11
|
+
</span>
|
|
12
|
+
<button type="button" class="usx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
|
|
13
|
+
</div>`,O=o=>String(o??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),F=te("users.list",o=>({filter:ae.includes(o?.filter)?o.filter:"all",sort:ne.includes(o?.sort)?o.sort:"name"})),n=se(Y);function de(){const o=location.hash.match(/^#\/projects\/([^/?#]+)\/users/);if(!o)return null;try{return decodeURIComponent(o[1])}catch{return o[1]}}export const usersView={templateUrl:"/admin/js/templates/users.html",async onMount(o){n.reset();const i=o.get(0).querySelector(".usx")||o.get(0),p=()=>Y.test(location.hash)&&i.isConnected,m=X(),r=de(),w=F.read(),l=M.observable([]),u=M.observable({}),x=M.observable(!0),h=M.observable(""),c=M.observable(w.filter),v=M.observable(w.sort),f=M.observable(""),k=M.observable(""),b=M.observable(0);n.add(M.effect(()=>{F.write({filter:c.value,sort:v.value})}));const R=M.observable([]);n.add(M.effect(()=>{const e=u.value,t=s=>{try{return D(s).fromNow?D(s).fromNow():D(s).format("D MMM YYYY HH:mm")}catch{return s}};R.value=ie(l.value,{filter:c.value,role:f.value,query:k.value,sort:v.value}).map(s=>le(s,{roles:e,me:m,when:t}))}));const H=M.computed(()=>{const e=l.value,t=e.filter(s=>s.isActive===!1).length;return{all:e.length,active:e.length-t,inactive:t}}),K=M.computed(()=>l.value.length?"Nobody matches.":"No users yet - New user in the banner adds one."),_=M.computed(()=>r?`Showing only users in the project "${r}"${b.value?` - ${b.value} more elsewhere`:""}.`:""),$=e=>l.peek().find(t=>t.id===e);async function y(){try{const[e,t]=await Promise.all([N.users.list(),U("/collections/roles/entries?limit=100",{method:"GET"}).catch(()=>({entries:[]}))]);if(!p())return;let s=Array.isArray(e)?e:[];if(r){const a=oe(s,r);b.value=s.length-a.length,s=a}u.value=re(t?.entries||[]),G(),l.value=s,h.value=""}catch(e){h.value=`Could not load the users: ${e.message||e}`}finally{x.value=!1}}function G(){const e=i.querySelector('.usx-select[aria-label="Role"]');if(!e)return;const t=f.peek();e.querySelectorAll('option:not([value=""])').forEach(s=>s.remove()),Object.entries(u.peek()).sort((s,a)=>s[1].level-a[1].level).forEach(([s,a])=>{const g=document.createElement("option");g.value=s,g.textContent=a.label,e.appendChild(g)}),e.value=t}const A=e=>{location.hash=`#/users/edit/${encodeURIComponent(e)}`};async function C(e,t){if(!(!t&&!await E.confirm(`Make ${O(e.name||e.email)} inactive? They will not be able to sign in until the account is made active again. Their work stays.`)))try{await U(`/users/${encodeURIComponent(e.id)}`,{method:"PUT",body:JSON.stringify({isActive:t})}),E.toast(`${e.name||e.email} is ${t?"active":"inactive"}.`,{type:"success"}),q(B),p()&&await y()}catch(s){E.toast(`Not changed: ${s.message||s}`,{type:"error"})}}async function J(e){if(await E.confirm(`Delete ${O(e.name||e.email)}? They lose access at once and this cannot be undone.`))try{await N.delete(`/users/${encodeURIComponent(e.id)}`),E.toast(`Deleted ${e.name||e.email}.`,{type:"success"}),q(B),p()&&await y()}catch(t){E.toast(`Not deleted: ${t.message||t}`,{type:"error"})}}async function V(e){try{await navigator.clipboard.writeText(e.email||""),E.toast("Email copied.",{type:"success"})}catch{E.toast(e.email||"No email",{type:"info",duration:6e3})}}function W(e){const t=$(e);if(!t)return[];const s=P(m,t,u.peek()),a=s.self?"not on your own account":"your role is not senior to theirs";return[{header:t.name||t.email},{label:s.self?"Edit my account":s.edit?"Edit":`Edit - ${a}`,icon:"edit",disabled:!s.edit,action:()=>A(t.id)},{label:"Copy email",icon:"copy",disabled:!t.email,action:()=>V(t)},{type:"divider"},t.isActive===!1?{label:s.other?"Make active":`Make active - ${a}`,icon:"play",disabled:!s.other,action:()=>C(t,!0)}:{label:s.other?"Make inactive\u2026":`Make inactive - ${a}`,icon:"pause",disabled:!s.other,action:()=>C(t,!1)},{label:s.other?"Delete\u2026":`Delete - ${a}`,icon:"trash",danger:s.other,disabled:!s.other,action:()=>J(t)}]}const d=i.querySelector(".usx-list");d.innerHTML=ce;const z=M.applyBindings({users:l,loading:x,loadError:h,filter:c,sort:v,role:f,query:k,rows:R,counts:H,emptyText:K,projectNote:_},i,{methods:{showAll:()=>{c.value="all"},showActive:()=>{c.value="active"},showInactive:()=>{c.value="inactive"}}});n.add(()=>z.dispose()),n.add(ee(i,{position:"bottom"}));const Q=E.contextMenu(d,{cursor:"glyph",match:".usx-row",inherit:!1,density:"compact",items:e=>W(e.dataset.id)});n.add(()=>Q?.destroy?.());const S=e=>{const t=$(e);t&&(P(m,t,u.peek()).edit?A(e):E.toast("Your role is not senior to theirs, so you cannot edit this account.",{type:"info"}))},T=e=>{const t=e.target.closest(".usx-row");if(!t)return;const s=e.target.closest(".usx-kebab");if(s){L(s);return}S(t.dataset.id)},j=e=>{const t=e.target.closest(".usx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),L(t)):e.key==="Enter"&&e.target===t&&S(t.dataset.id))};d.addEventListener("click",T),d.addEventListener("keydown",j),n.add(()=>{d.removeEventListener("click",T),d.removeEventListener("keydown",j)}),Z(o,{id:"users-new",icon:"plus",title:"New user",onClick:()=>{location.hash=r?`#/users/new?project=${encodeURIComponent(r)}`:"#/users/new"}}),I.scan(i),await y()}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as v}from"../api.js";let g=null,f=null,C={};async function T(e){const t=e.find("#view-project").get(0);if(t)try{(await v.projects.list()).forEach(a=>{const l=document.createElement("option");l.value=a.slug,l.textContent=a.name||a.slug,t.appendChild(l)})}catch{}}export const viewEditorView={templateUrl:"/admin/js/templates/view-editor.html",async onMount(e){g=null,f=null,C={};const t=window.location.hash.match(/\/views\/edit\/([^/?#]+)/);t&&(g=t[1]),E.tabs(e.find("#view-editor-tabs").get(0));const n=E.loader(e.get(0),{type:"dots"});try{await L(e),await D(e),await _(e),await O(e),await T(e),e.find("#view-source").get(0)?.addEventListener("change",async()=>{await k(e)}),g&&(e.find("#view-editor-title").text("Edit View"),await V(e,g))}finally{n.destroy()}e.find("#add-stage-btn").off("click").on("click",()=>{const a=e.find("#add-stage-type").val()||"$match";q(e,{type:a,config:{}})}),e.find("#save-view-btn").off("click").on("click",async()=>{await X(e)}),Q(e),H(e),Domma.icons.scan()}};async function L(e){const t=e.find("#view-source").get(0);if(t)try{(await v.collections.list()).forEach(n=>{const a=document.createElement("option");a.value=n.slug,a.textContent=`${n.title} (${n.slug})`,t.appendChild(a)})}catch{}}async function O(e){const t=e.find("#view-block-name").get(0);if(t)try{(await v.blocks.list()).forEach(n=>{const a=document.createElement("option");a.value=n.name,a.textContent=`${n.name}.html`,t.appendChild(a)})}catch{}}async function D(e){const t=e.find("#view-connection").get(0);if(t)try{const n=await v.collections.getConnections();Object.keys(n).forEach(a=>{if(!t.querySelector(`option[value="${a}"]`)){const l=document.createElement("option");l.value=a,l.textContent=a,t.appendChild(l)}})}catch{}}async function _(e){const t=e.find("#view-roles-checkboxes").get(0);t&&["admin","manager","editor","subscriber"].forEach(n=>{const a=document.createElement("label");a.style.cssText="display:flex;align-items:center;gap:.5rem;cursor:pointer;";const l=document.createElement("input");l.type="checkbox",l.value=n,l.dataset.role=n,l.className="view-role-cb",l.checked=n==="admin",a.appendChild(l),a.appendChild(document.createTextNode(n)),t.appendChild(a)})}async function k(e){const t=e.find("#view-source").val();if(!t){f=null;return}try{f=(await v.collections.get(t))?.fields||[]}catch{f=null}e.find(".stage-card[data-guided]").each(function(){const n=this.dataset.stageType,a=this.querySelector(".stage-guided");a&&(n==="$match"&&z(a),n==="$sort"&&J(a))}),A(e,null)}async function V(e,t){try{const n=await v.views.get(t);if(!n){E.toast("View not found.",{type:"error"}),R.navigate("/views");return}const a=n.pipeline?.source;a&&(e.find("#view-source").val(a),await k(e)),F(e,n)}catch(n){E.toast(n.message||"Failed to load view.",{type:"error"}),R.navigate("/views")}}function F(e,t){e.find("#view-title").val(t.title||""),e.find("#view-slug").val(t.slug||""),e.find("#view-description").val(t.description||""),e.find("#view-source").val(t.pipeline?.source||""),e.find("#view-bundled").prop("checked",!!t.bundled),C=t.meta||{},e.find("#view-project").val(t.meta?.project||""),e.find("#view-connection").val(t.connection||"default"),e.find("#view-display-mode").val(t.display?.mode||"table"),e.find("#view-page-size").val(t.display?.pageSize||25),e.find("#view-block-name").val(t.display?.block||"");const n=t.access?.roles||["admin"];e.find(".view-role-cb").each(function(){this.checked=n.includes(this.value)}),e.find("#view-public").prop("checked",t.access?.public||!1);const a=t.access?.rowLevel;a&&(e.find("#view-rowlevel-enabled").prop("checked",!0),e.find("#view-rowlevel-config").css("display","flex"),e.find("#view-rowlevel-mode").val(a.mode||"owner"),e.find("#view-rowlevel-userkey").val(a.userKey||"id"),a.mode==="field"&&(e.find("#view-rowlevel-field-group").css("display",""),e.find("#view-rowlevel-field").val(a.field||"")));const l=e.find("#pipeline-stages-list").get(0);if(l)for(;l.firstChild;)l.removeChild(l.firstChild);(t.pipeline?.stages||[]).forEach(i=>q(e,i)),A(e,t.display?.columns||[]),N(e)}const I=[{value:"eq",label:"= equals"},{value:"ne",label:"\u2260 not equals"},{value:"gt",label:"> greater than"},{value:"lt",label:"< less than"},{value:"gte",label:"\u2265 greater or equal"},{value:"lte",label:"\u2264 less or equal"},{value:"contains",label:"~ contains (regex)"},{value:"in",label:"\u2208 in (comma list)"}];function y(e=!1){const t=[];return e&&t.push({value:"",label:"- select field -"}),(f||[]).forEach(n=>{t.push({value:`data.${n.name}`,label:`${n.label||n.name} (${n.name})`})}),t.length===0||t.length===1&&e?(t.push({value:"__meta.createdAt",label:"Created At"}),t.push({value:"__meta.updatedAt",label:"Updated At"})):(t.push({value:"__meta.createdAt",label:"Created At (meta)"}),t.push({value:"__meta.updatedAt",label:"Updated At (meta)"})),t}function h(e,t){const n=document.createElement("select");return n.className="form-input form-input--sm",e.forEach(a=>{const l=document.createElement("option");l.value=a.value,l.textContent=a.label,String(a.value)===String(t)&&(l.selected=!0),n.appendChild(l)}),n}function S(e,t,n="text"){const a=document.createElement("input");return a.type=n,a.className="form-input form-input--sm",a.placeholder=e,a.value=t??"",a}function M(e){if(!f?.length)return null;const t=e?.startsWith("data.")?e.slice(5):null;return t&&f.find(n=>n.name===t)||null}function b(e,t,n=""){const a=M(e);if(a?.type==="select"&&(t==="eq"||t==="ne")&&a.options?.length){const d=document.createElement("select");d.className="form-input form-input--sm match-val",d.style.flex="1";const o=document.createElement("option");return o.value="",o.textContent="- select value -",d.appendChild(o),(a.options||[]).forEach(c=>{const s=typeof c=="string"?c:c.value??"",r=typeof c=="string"?c:c.label||s;if(!s||s==="undefined")return;const u=document.createElement("option");u.value=s,u.textContent=r,s===n&&(u.selected=!0),d.appendChild(u)}),d}const l=t==="in"?"val1, val2, val3":t==="contains"?"search pattern (regex)":t==="gt"||t==="lt"||t==="gte"||t==="lte"?"0":"value",i=document.createElement("input");return i.type="text",i.className="form-input form-input--sm match-val",i.style.flex="1",i.placeholder=l,i.value=n,i}function $(e){const t=[],n={$eq:"eq",$ne:"ne",$gt:"gt",$lt:"lt",$gte:"gte",$lte:"lte",$in:"in"};return Object.entries(e||{}).forEach(([a,l])=>{if(typeof l=="object"&&l!==null&&!Array.isArray(l)){if("$regex"in l){t.push({field:a,op:"contains",val:String(l.$regex??"")});return}Object.entries(l).forEach(([i,d])=>{const o=n[i];o&&t.push({field:a,op:o,val:Array.isArray(d)?d.join(", "):String(d)})})}else t.push({field:a,op:"eq",val:String(l??"")})}),t}function x(e,t,n="",a="eq",l=""){const i=document.createElement("div");i.className="match-condition-row",i.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;flex-wrap:wrap;";const d=h([{value:"",label:"- field -"},...y()],n);d.className+=" match-field",d.style.minWidth="140px";const o=h(I,a);o.className+=" match-op",o.style.minWidth="160px";const c=document.createElement("div");c.className="match-val-wrap",c.style.cssText="flex:1;min-width:120px;display:flex;",c.appendChild(b(n,a,l));const s=()=>{const m=c.querySelector(".match-val")?.value??"";for(;c.firstChild;)c.removeChild(c.firstChild);c.appendChild(b(d.value,o.value,m))};d.addEventListener("change",s),o.addEventListener("change",s);const r=document.createElement("button");r.type="button",r.className="btn btn-sm btn-ghost",r.title="Remove";const u=document.createElement("span");u.setAttribute("data-icon","x"),r.appendChild(u),r.addEventListener("click",()=>i.remove()),i.appendChild(d),i.appendChild(o),i.appendChild(c),i.appendChild(r),e.insertBefore(i,t),Domma.icons.scan(i)}function U(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const n=t.$match||t||{},a="$or"in n;let l=[];a?(n.$or||[]).forEach(r=>$(r).forEach(u=>l.push(u))):l=$(n);const i=document.createElement("div");i.style.cssText="display:flex;align-items:center;gap:.5rem;margin-bottom:.6rem;";const d=document.createElement("label");d.className="form-label",d.style.marginBottom="0",d.textContent="Match:";const o=document.createElement("select");o.className="form-input form-input--sm match-logic",o.style.width="auto",[{value:"and",label:"ALL conditions (AND)"},{value:"or",label:"ANY condition (OR)"}].forEach(r=>{const u=document.createElement("option");u.value=r.value,u.textContent=r.label,(a?"or":"and")===r.value&&(u.selected=!0),o.appendChild(u)}),i.appendChild(d),i.appendChild(o),e.appendChild(i);const c=document.createElement("button");c.type="button",c.className="btn btn-ghost btn-sm match-add-btn";const s=document.createElement("span");s.setAttribute("data-icon","plus"),c.appendChild(s),c.appendChild(document.createTextNode(" Add Condition")),c.addEventListener("click",()=>{x(e,c),Domma.icons.scan(e)}),e.appendChild(c),l.forEach(r=>x(e,c,r.field,r.op,r.val)),l.length===0&&x(e,c),Domma.icons.scan(e)}function z(e){e.querySelectorAll(".match-condition-row").forEach(t=>{const n=t.querySelector(".match-field");if(n){const d=n.value,o=h([{value:"",label:"- field -"},...y()],d);o.className=n.className,o.style.cssText=n.style.cssText;const c=t.querySelector(".match-val-wrap"),s=t.querySelector(".match-op");c&&s&&o.addEventListener("change",()=>{const r=c.querySelector(".match-val")?.value??"";for(;c.firstChild;)c.removeChild(c.firstChild);c.appendChild(b(o.value,s.value,r))}),n.parentNode.replaceChild(o,n)}const a=t.querySelector(".match-val-wrap"),l=t.querySelector(".match-field")?.value,i=t.querySelector(".match-op")?.value;if(a&&l&&i){const d=a.querySelector(".match-val")?.value??"";for(;a.firstChild;)a.removeChild(a.firstChild);a.appendChild(b(l,i,d))}})}function B(e){const t=e.querySelector(".match-logic")?.value||"and",n=l=>{const i=l.querySelector(".match-field")?.value?.trim(),d=l.querySelector(".match-op")?.value,o=l.querySelector(".match-val")?.value?.trim()??"";if(!i)return null;const c=i.startsWith("__")?i.slice(2):i,s={};switch(d){case"eq":s[c]=o;break;case"ne":s[c]={$ne:o};break;case"gt":s[c]={$gt:isNaN(o)?o:Number(o)};break;case"lt":s[c]={$lt:isNaN(o)?o:Number(o)};break;case"gte":s[c]={$gte:isNaN(o)?o:Number(o)};break;case"lte":s[c]={$lte:isNaN(o)?o:Number(o)};break;case"contains":s[c]={$regex:o,$options:"i"};break;case"in":s[c]={$in:o.split(",").map(r=>r.trim()).filter(Boolean)};break}return Object.keys(s).length?s:null},a=[];return e.querySelectorAll(".match-condition-row").forEach(l=>{const i=n(l);i&&a.push(i)}),a.length===0?{}:t==="or"?{$or:a}:Object.assign({},...a)}function W(e,t={}){for(;e.firstChild;)e.removeChild(e.firstChild);const n=t.$sort||t||{},a=Object.entries(n),l=document.createElement("button");l.type="button",l.className="btn btn-ghost btn-sm";const i=document.createElement("span");i.setAttribute("data-icon","plus"),l.appendChild(i),l.appendChild(document.createTextNode(" Add Sort")),e.appendChild(l);const d=(o="",c=1)=>{const s=document.createElement("div");s.className="sort-row",s.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const r=h([{value:"",label:"- field -"},...y()],o);r.className+=" sort-field";const u=h([{value:"1",label:"\u2191 Ascending"},{value:"-1",label:"\u2193 Descending"}],String(c));u.className+=" sort-dir";const m=document.createElement("button");m.type="button",m.className="btn btn-sm btn-ghost",m.title="Remove";const p=document.createElement("span");p.setAttribute("data-icon","x"),m.appendChild(p),m.addEventListener("click",()=>{s.remove()}),s.appendChild(r),s.appendChild(u),s.appendChild(m),e.insertBefore(s,l),Domma.icons.scan(s)};l.addEventListener("click",()=>{d()}),a.forEach(([o,c])=>d(o,c)),a.length===0&&d(),Domma.icons.scan(e)}function J(e){e.querySelectorAll(".sort-row").forEach(t=>{const n=t.querySelector(".sort-field"),a=n?.value,l=h([{value:"",label:"- field -"},...y()],a);l.className=n.className,n.parentNode.replaceChild(l,n)})}function P(e){const t={};return e.querySelectorAll(".sort-row").forEach(n=>{const a=n.querySelector(".sort-field")?.value?.trim(),l=parseInt(n.querySelector(".sort-dir")?.value,10)||1;if(!a)return;const i=a.startsWith("__")?a.slice(2):a;t[i]=l}),t}function q(e,t){const n=e.find("#pipeline-stages-list").get(0);if(!n)return;const a=n.querySelector(".stage-empty-placeholder");a&&a.remove();const l=["$match","$sort"].includes(t.type),i=document.createElement("div");i.className="card mb-2 stage-card",i.dataset.stageType=t.type,l&&(i.dataset.guided="true");const d=document.createElement("div");d.className="card-header",d.style.cssText="display:flex;align-items:center;gap:.5rem;";const o=document.createElement("code");o.textContent=t.type,o.style.cssText="flex:1;font-size:.85rem;";const c=document.createElement("button");c.type="button",c.className="btn btn-sm btn-ghost";const s=document.createElement("span");s.setAttribute("data-icon","trash"),c.appendChild(s),c.addEventListener("click",()=>{i.remove(),n.querySelector(".stage-card")||n.appendChild(K())}),d.appendChild(o),d.appendChild(c);const r=document.createElement("div");if(r.className="card-body",l){const u=document.createElement("div");u.className="stage-guided",r.appendChild(u),t.type==="$match"&&U(u,t.config),t.type==="$sort"&&W(u,t.config)}else{const u=document.createElement("label");u.className="form-label",u.textContent="Stage Config (JSON)";const m=document.createElement("small");m.className="text-muted",m.style.cssText="display:block;margin-bottom:.4rem;",m.textContent=`Enter the inner config for ${t.type} - e.g. for $lookup: { from, localField, foreignField, as }. Do not wrap in { "${t.type}": ... }.`;const p=document.createElement("textarea");p.className="form-input stage-config",p.rows=5,p.style.cssText="font-family:monospace;font-size:.8rem;resize:vertical;",p.placeholder="{}",p.value=Object.keys(t.config||{}).length?JSON.stringify(t.config,null,2):"",r.appendChild(u),r.appendChild(m),r.appendChild(p)}i.appendChild(d),i.appendChild(r),n.appendChild(i),Domma.icons.scan(i)}function K(){const e=document.createElement("p");return e.className="text-muted stage-empty-placeholder",e.textContent="No stages yet. Add a stage to filter, join, or transform your data.",e.style.cssText="text-align:center;padding:2rem 0;",e}function Y(e){const t=[];return e.find(".stage-card").each(function(){const n=this.dataset.stageType,a=this.querySelector(".stage-guided");let l;if(a)n==="$match"&&(l=B(a)),n==="$sort"&&(l=P(a));else{const i=this.querySelector(".stage-config")?.value?.trim()||"{}";try{l=JSON.parse(i)}catch{throw new Error(`Invalid JSON in ${n} stage config`)}}t.push({type:n,config:l})}),t}function A(e,t){const n=e.find("#view-columns-builder").get(0);if(!n)return;for(;n.firstChild;)n.removeChild(n.firstChild);const a=t??(f||[]).map(o=>({key:`data.${o.name}`,label:o.label||o.name})),l=(o="",c="")=>{const s=document.createElement("div");s.className="col-row",s.style.cssText="display:flex;gap:.4rem;align-items:center;margin-bottom:.4rem;";const r=f?.length?h([{value:"",label:"- field -"},...y()],o):S("data.fieldName",o);r.className+=" col-key";const u=S("Display label",c);u.className+=" col-label",u.style.flex="1",f?.length&&r.tagName==="SELECT"&&r.addEventListener("change",()=>{if(!u.value){const w=(f||[]).find(j=>`data.${j.name}`===r.value);w&&(u.value=w.label||w.name)}});const m=document.createElement("button");m.type="button",m.className="btn btn-sm btn-ghost",m.title="Remove column";const p=document.createElement("span");p.setAttribute("data-icon","x"),m.appendChild(p),m.addEventListener("click",()=>{s.remove(),Domma.icons.scan(n)}),s.appendChild(r),s.appendChild(u),s.appendChild(m),n.insertBefore(s,n.lastChild),Domma.icons.scan(s)};a.forEach(o=>l(o.key,o.label));const i=document.createElement("button");i.type="button",i.className="btn btn-ghost btn-sm";const d=document.createElement("span");d.setAttribute("data-icon","plus"),i.appendChild(d),i.appendChild(document.createTextNode(" Add Column")),i.addEventListener("click",()=>{l(),Domma.icons.scan(n)}),n.appendChild(i),Domma.icons.scan(n)}function G(e){const t=[];return e.find(".col-row").each(function(){const n=this.querySelector(".col-key")?.value?.trim(),a=this.querySelector(".col-label")?.value?.trim();n&&t.push({key:n,label:a||n})}),t}function H(e){const t=e.find("#view-display-mode").get(0);t&&(t.addEventListener("change",()=>N(e)),N(e))}function N(e){const t=e.find("#view-display-mode").val()||"table",n=e.find("#view-columns-section").get(0),a=e.find("#view-block-section").get(0);n&&(n.style.display=t==="table"?"":"none"),a&&(a.style.display=t==="block"?"":"none")}function Q(e){const t=e.find("#view-rowlevel-enabled").get(0),n=e.find("#view-rowlevel-config").get(0),a=e.find("#view-rowlevel-mode").get(0),l=e.find("#view-rowlevel-field-group").get(0);t&&(t.addEventListener("change",()=>{n&&(n.style.display=t.checked?"flex":"none")}),a&&a.addEventListener("change",()=>{l&&(l.style.display=a.value==="field"?"":"none")}))}async function X(e){const t=e.find("#view-title").val().trim();if(!t){E.toast("Title is required.",{type:"warning"});return}const n=e.find("#view-source").val();if(!n){E.toast("Source collection is required (Source tab).",{type:"warning"});return}let a;try{a=Y(e)}catch(u){E.toast(u.message,{type:"error"});return}const l=G(e),i=[];e.find(".view-role-cb:checked").each(function(){i.push(this.value)});const d=e.find("#view-rowlevel-enabled").is(":checked");let o=null;if(d){const u=e.find("#view-rowlevel-mode").val()||"owner",m=e.find("#view-rowlevel-userkey").val()||"id";if(o={mode:u,userKey:m},u==="field"){const p=e.find("#view-rowlevel-field").val().trim();if(!p){E.toast("Field name is required for Field Match mode.",{type:"warning"});return}o.field=p}}const c=e.find("#view-project").val()||"",s={title:t,slug:e.find("#view-slug").val().trim()||void 0,description:e.find("#view-description").val().trim(),...e.find("#view-bundled").is(":checked")?{bundled:!0}:{},connection:e.find("#view-connection").val()||"default",pipeline:{source:n,stages:a},display:{mode:e.find("#view-display-mode").val()||"table",columns:l,pageSize:parseInt(e.find("#view-page-size").val(),10)||25,block:e.find("#view-block-name").val()||""},access:{roles:i,public:e.find("#view-public").is(":checked"),rowLevel:o},meta:{...C||{},project:c||null}},r=e.find("#save-view-btn").get(0);r&&(r.disabled=!0);try{if(g)await v.views.update(g,s),E.toast("View updated.",{type:"success"});else{const u=await v.views.create(s);E.toast("View created.",{type:"success"}),R.navigate(`/views/edit/${u.slug}`)}}catch(u){E.toast(u.message||"Failed to save view.",{type:"error"})}finally{r&&(r.disabled=!1)}}
|
|
1
|
+
import{api as C,apiRequest as Fe}from"../api.js";import{addBannerAction as Y}from"../lib/plugin-chrome.js";import{helpPopovers as ht}from"../lib/help-popover.js";import{openMenuAt as ae,sidebarCount as he}from"../lib/tool-kit.js";import{OPERATORS as gt,STAGE_TYPES as _e,cellText as wt,checkView as bt,draftKey as w,errorCount as Be,isGuided as St,matchToRows as kt,move as He,parseStageJson as xt,resultColumns as Et,rowsToMatch as qt,rowsToSort as $t,slugify as Je,snapshot as P,sortToRows as Ct,stageSummary as Mt,stageText as Lt}from"../lib/view-editor-arrange.js";const Rt=/^\/views\/(new|edit\/[^/?#]+)\/?(\?.*)?$/,ne=v=>String(v||"").split("?")[0].replace(/\/$/,""),Tt=["super-admin","admin","user"],At=v=>String(v??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function l(v,m={},...i){const h=document.createElement(v);for(const[q,d]of Object.entries(m))d==null||d===!1||(q==="class"?h.className=d:q==="text"?h.textContent=d:h.setAttribute(q,d===!0?"":d));for(const q of i)q&&h.append(q);return h}let A=null,Ye=!1;function Nt(){Ye||!window.R?.use||(Ye=!0,R.use((v,m,i)=>{const h=ne(m?.path);return!(Rt.test(m?.path||"")&&ne(v?.path)!==h)||!A?.dirty()?i():E.confirm("This view has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(d=>{d?i():history.replaceState(null,"",`#${m.path}`)})}),M.subscribe("router:afterChange",({to:v}={})=>{if(A&&ne(v?.path)!==A.path){const m=A;A=null,m.dispose()}}))}function Ot(v,m){const i=v.querySelector(".dm-plugin-banner-name");if(!i)return;const h=i.querySelector(".dm-plugin-banner-sep");if(h){for(;h.nextSibling;)h.nextSibling.remove();h.after(document.createTextNode(m))}}export const viewEditorView={templateUrl:"/admin/js/templates/view-editor.html",async onMount(v){if(Nt(),A){const e=A;A=null,e.dispose()}const m=v.get(0),i=m.querySelector(".vex")||m,h=$(i),q=[],d=e=>(q.push(e),e);let ge=!1;const j=()=>!ge&&i.isConnected,we=location.hash.match(/^#\/views\/edit\/([^/?#]+)/),f=we?decodeURIComponent(we[1]):null,b=!f,Ge=ne(location.hash.slice(1));let L=[],N=null,be=[];const Se=E.loader(m,{type:"dots"});try{const[e,t,o,a,s,c]=await Promise.all([C.collections.list().catch(()=>[]),C.collections.getConnections().catch(()=>({})),C.blocks.list().catch(()=>[]),C.projects.list().catch(()=>[]),Fe("/collections/roles/entries?limit=1000").catch(()=>null),C.views.list().catch(()=>[])]);be=(c||[]).map(n=>n.slug),F(i.querySelector("#view-source"),(e||[]).map(n=>[n.slug,`${n.title||n.slug} (${n.slug})`])),F(i.querySelector("#view-connection"),Object.keys(t||{}).filter(n=>n!=="default").map(n=>[n,n])),F(i.querySelector("#view-block-name"),(o||[]).map(n=>[n.name,`${n.name}.html`])),F(i.querySelector("#view-project"),(a||[]).map(n=>[n.slug,n.name||n.slug]));const r=(s?.entries||s?.data||[]).map(n=>n.data||n).filter(n=>n?.name).sort((n,p)=>(n.level??9)-(p.level??9)).map(n=>({name:n.name,label:n.label||n.name}));if(We(r.length?r:Tt.map(n=>({name:n,label:n}))),F(i.querySelector("#add-stage-type"),_e.map(n=>[n.type,`${n.type} - ${n.label}`])),!b&&(N=await C.views.get(f),!N))throw new Error("View not found")}catch(e){Se.destroy(),E.toast(e.message==="View not found"?"That view does not exist.":`Could not load the view: ${e.message||e}`,{type:"error"}),R.navigate("/views");return}if(Se.destroy(),!j())return;function F(e,t){if(e)for(const[o,a]of t)e.querySelector(`option[value="${CSS.escape(o)}"]`)||e.append(l("option",{value:o,text:a}))}function We(e){const t=i.querySelector("#view-roles-checkboxes");t.replaceChildren();for(const o of e)t.append(l("label",{},l("input",{type:"checkbox",class:"view-role-cb",value:o.name}),document.createTextNode(o.label)))}function Qe(){const e=(L||[]).map(t=>({value:`data.${t.name}`,label:`${t.label||t.name} (${t.name})`}));return e.push({value:"__meta.createdAt",label:"Created (meta)"},{value:"__meta.updatedAt",label:"Updated (meta)"}),e}function _(e,t,o,a){const s=l("select",{class:`form-input form-input--sm ${o||""}`,"aria-label":a});for(const c of e)s.append(l("option",{value:c.value,text:c.label}));return t&&!e.some(c=>String(c.value)===String(t))&&s.append(l("option",{value:t,text:t})),s.value=t??"",s}function G(e,t){return _([{value:"",label:"- field -"},...Qe()],e,t,"Field")}function ke(e,t,o){const a=(L||[]).find(r=>`data.${r.name}`===e);if(a?.type==="select"&&(t==="eq"||t==="ne")&&a.options?.length){const r=a.options.map(n=>typeof n=="string"?{value:n,label:n}:{value:n.value??"",label:n.label||n.value}).filter(n=>n.value&&n.value!=="undefined");return _([{value:"",label:"- value -"},...r],o,"match-val","Value")}const s=t==="in"?"a, b, c":t==="contains"?"text (a pattern)":["gt","lt","gte","lte"].includes(t)?"0":"value",c=l("input",{type:"text",class:"form-input form-input--sm match-val",placeholder:s,"aria-label":"Value"});return c.value=o??"",c}function Xe(e,t){const{logic:o,rows:a}=kt(t),s=_([{value:"and",label:"ALL conditions (and)"},{value:"or",label:"ANY condition (or)"}],o,"match-logic","Match"),c=l("div",{class:"vex-conds"}),r=l("button",{type:"button",class:"btn btn-sm btn-ghost"},l("span",{"data-icon":"plus","data-icon-size":"13"}),document.createTextNode(" Condition")),n=(p={field:"",op:"eq",val:""})=>{const oe=G(p.field,"match-field"),mt=_(gt,p.op,"match-op","Operator"),yt=l("span",{class:"vex-val"},ke(p.field,p.op,p.val)),ze=l("button",{type:"button",class:"vex-x",title:"Remove this condition","aria-label":"Remove this condition",text:"\xD7"});ze.addEventListener("click",()=>{Ue.remove(),g()});const Ue=l("div",{class:"vex-cond match-condition-row"},oe,mt,yt,ze);c.append(Ue)};r.addEventListener("click",()=>{n(),g()}),e.append(l("div",{class:"vex-logic"},l("span",{class:"vex-muted",text:"Keep rows matching"}),s),c,r),(a.length?a:[void 0]).forEach(p=>n(p))}function Ze(e,t){const o=l("div",{class:"vex-conds"}),a=l("button",{type:"button",class:"btn btn-sm btn-ghost"},l("span",{"data-icon":"plus","data-icon-size":"13"}),document.createTextNode(" Sort field")),s=(r={field:"",dir:1})=>{const n=l("button",{type:"button",class:"vex-x",title:"Remove","aria-label":"Remove this sort",text:"\xD7"}),p=l("div",{class:"vex-sort-row sort-row"},G(r.field,"sort-field"),_([{value:"1",label:"\u2191 Ascending"},{value:"-1",label:"\u2193 Descending"}],String(r.dir),"sort-dir","Direction"),n);n.addEventListener("click",()=>{p.remove(),g()}),o.append(p)};a.addEventListener("click",()=>{s(),g()}),e.append(o,a);const c=Ct(t);(c.length?c:[void 0]).forEach(r=>s(r))}const k=i.querySelector("#pipeline-stages-list");let et=0;function xe(e){const t=l("div",{class:"vex-stage stage-card","data-stage-type":e.type,"data-sid":String(++et)}),o=l("div",{class:"vex-stage-head",tabindex:"0"},l("span",{class:"vex-stage-no"}),l("code",{class:"vex-stage-type",text:e.type}),l("span",{class:"vex-stage-sum"}),l("button",{type:"button",class:"vex-kebab","aria-label":`Actions for this ${e.type} stage`,title:"Actions",text:"\u22EE"})),a=l("div",{class:"vex-stage-body"});if(St(e.type)){t.dataset.guided="true";const s=l("div",{class:"stage-guided"});a.append(s),e.type==="$match"?Xe(s,e.config):Ze(s,e.config)}else{const s=_e.find(r=>r.type===e.type),c=l("textarea",{class:"form-input stage-config",rows:"5",spellcheck:"false","aria-label":`${e.type} settings`,placeholder:s?.example||"{}"});c.value=Lt(e.type,e.config),a.append(l("small",{class:"vex-hint",text:`The inside of ${e.type} only - e.g. ${(s?.example||"{}").replace(/\s+/g," ")}`}),c,l("small",{class:"vex-stage-err",hidden:!0}))}return t.append(o,a),t}function Ee(e,t=null){const o=xe(e);return t?t.after(o):k.append(o),I.scan(o),g(),o}function qe(e){const t=e.dataset.stageType;if(e.dataset.guided){const s=e.querySelector(".stage-guided");if(t==="$match"){const r=[...s.querySelectorAll(".match-condition-row")].map(n=>({field:n.querySelector(".match-field")?.value,op:n.querySelector(".match-op")?.value,val:n.querySelector(".match-val")?.value}));return{type:t,config:qt(s.querySelector(".match-logic")?.value,r),error:""}}const c=[...s.querySelectorAll(".sort-row")].map(r=>({field:r.querySelector(".sort-field")?.value,dir:r.querySelector(".sort-dir")?.value}));return{type:t,config:$t(c),error:""}}const{config:o,error:a}=xt(t,e.querySelector(".stage-config")?.value);return{type:t,config:o,error:a}}const V=()=>[...k.querySelectorAll(":scope > .stage-card")],x=i.querySelector("#view-columns-builder");let B=null;function $e(e={key:"",label:""}){const t=L.length?G(e.key,"col-key"):l("input",{type:"text",class:"form-input form-input--sm col-key font-mono",placeholder:"data.field","aria-label":"Field"});L.length||(t.value=e.key||"");const o=l("input",{type:"text",class:"form-input form-input--sm col-label",placeholder:"Heading","aria-label":"Heading"});o.value=e.label||"";const a=l("button",{type:"button",class:"vex-kebab","aria-label":"Actions for this column",title:"Actions",text:"\u22EE"});return l("div",{class:"vex-col col-row",tabindex:"0"},t,o,a)}function Ce(e){x.replaceChildren();const t=e??L.map(o=>({key:`data.${o.name}`,label:o.label||o.name}));for(const o of t)x.append($e(o));B=l("button",{type:"button",class:"btn btn-sm btn-ghost vex-col-add"},l("span",{"data-icon":"plus","data-icon-size":"13"}),document.createTextNode(" Column")),B.addEventListener("click",()=>{B.before($e()),I.scan(x),g()}),x.append(B),I.scan(x)}const le=()=>[...x.querySelectorAll(":scope > .col-row")];async function Me(e){if(!e){L=[];return}try{L=(await C.collections.get(e))?.fields||[]}catch{L=[]}}function tt(){for(const e of i.querySelectorAll(".match-field, .sort-field, select.col-key")){const t=e.value;e.replaceWith(G(t,[...e.classList].find(o=>["match-field","sort-field","col-key"].includes(o))))}}const u=e=>i.querySelector(`#${e}`);let W={};function Q(){const e=[],t=[];V().forEach((r,n)=>{const p=qe(r);p.error&&t.push({index:n,type:p.type,error:p.error}),e.push({type:p.type,config:p.error?{}:p.config})});const o=le().map(r=>({key:r.querySelector(".col-key")?.value?.trim(),label:r.querySelector(".col-label")?.value?.trim()})).filter(r=>r.key).map(r=>({key:r.key,label:r.label||r.key})),a=u("view-rowlevel-enabled").checked,s=u("view-rowlevel-mode").value||"owner";return{payload:{title:u("view-title").value.trim(),slug:u("view-slug").value.trim()||void 0,description:u("view-description").value.trim(),...u("view-bundled").checked?{bundled:!0}:{},connection:u("view-connection").value||"default",pipeline:{source:u("view-source").value,stages:e},display:{mode:u("view-display-mode").value||"table",columns:o,pageSize:Number.parseInt(u("view-page-size").value,10),block:u("view-block-name").value||""},access:{roles:[...i.querySelectorAll(".view-role-cb:checked")].map(r=>r.value),public:u("view-public").checked,rowLevel:a?{mode:s,userKey:u("view-rowlevel-userkey").value||"id",...s==="field"?{field:u("view-rowlevel-field").value.trim()}:{}}:null},meta:{...W,project:u("view-project").value||null}},stageErrors:t}}async function se(e){W=e?.meta||{},u("view-title").value=e?.title||"",u("view-slug").value=e?.slug||"",u("view-description").value=e?.description||"",u("view-source").value=e?.pipeline?.source||"",u("view-bundled").checked=!!e?.bundled,u("view-project").value=e?.meta?.project||"",u("view-connection").value=e?.connection||"default",u("view-display-mode").value=e?.display?.mode||"table",u("view-page-size").value=String(e?.display?.pageSize??25),u("view-block-name").value=e?.display?.block||"";const t=e?.access?.roles||["admin"];i.querySelectorAll(".view-role-cb").forEach(a=>{a.checked=t.includes(a.value)});for(const a of t)i.querySelector(`.view-role-cb[value="${CSS.escape(a)}"]`)||i.querySelector("#view-roles-checkboxes").append(l("label",{title:"This role no longer exists"},l("input",{type:"checkbox",class:"view-role-cb",value:a,checked:!0}),document.createTextNode(`${a} (missing)`)));u("view-public").checked=!!e?.access?.public;const o=e?.access?.rowLevel;u("view-rowlevel-enabled").checked=!!o,u("view-rowlevel-mode").value=o?.mode||"owner",u("view-rowlevel-userkey").value=o?.userKey||"id",u("view-rowlevel-field").value=o?.field||"",await Me(e?.pipeline?.source||""),k.replaceChildren();for(const a of e?.pipeline?.stages||[])k.append(xe(a));I.scan(k),Ce(e?e.display?.columns||[]:null),Le(),g()}function Le(){const e=u("view-display-mode").value||"table";i.querySelector("#view-columns-section").hidden=e!=="table",i.querySelector("#view-block-section").hidden=e!=="block";const t=u("view-rowlevel-enabled").checked;i.querySelector("#view-rowlevel-config").hidden=!t,i.querySelector("#view-rowlevel-field-group").hidden=!t||u("view-rowlevel-mode").value!=="field",u("view-slug").readOnly=!b}const ie=M.observable(0);function g(){ie.value=ie.peek()+1}const y=M.computed(()=>(ie.value,Q())),O=M.observable(""),X=M.observable(!1),H=M.computed(()=>bt(y.value.payload,{stageErrors:y.value.stageErrors,isNew:b,taken:be})),Z=M.computed(()=>{if(P(y.value.payload)===O.value)return!1;if(b){const t=y.value.payload;return!!(t.title||t.description||t.pipeline.source||t.pipeline.stages.length)}return!0}),ot=M.computed(()=>y.value.payload.pipeline.stages.length),Re=M.computed(()=>{const e=Be(H.value);if(!e)return"";const t=Object.values(H.value).flat()[0];return e===1?t:`${t} (+${e-1} more)`}),at=M.computed(()=>{if(y.value,!b)return"Cannot change once the view exists.";const e=y.value.payload,t=e.slug?Je(e.slug):Je(e.title);return t?`Saved as "${t}".`:""});d(M.effect(()=>{const{payload:e,stageErrors:t}=y.value,o=new Set(t.map(a=>a.index));V().forEach((a,s)=>{a.querySelector(".vex-stage-no").textContent=String(s+1);const c=t.find(n=>n.index===s);a.querySelector(".vex-stage-sum").textContent=c?"":Mt(e.pipeline.stages[s]),a.classList.toggle("has-error",o.has(s));const r=a.querySelector(".vex-stage-err");r&&(r.hidden=!c,r.textContent=c?.error||"")})})),d(M.effect(()=>{const e=y.value.payload.title;Ot(m,e||(b?"New view":f))}));const J=i.querySelector(".vex-results"),T=M.observable(0),ee=M.observable(""),z=M.observable("Open this tab to run the pipeline."),re=M.observable("");let U=[],ce=!1,ue=0,de=null;async function pe(){const{payload:e,stageErrors:t}=y.peek(),o=++ue;if(re.value="",!e.pipeline.source){z.value="Choose the source collection (Source tab) to see results.",J.replaceChildren(),ee.value="";return}const a=T.peek()||e.pipeline.stages.length,s=t.find(c=>c.index<a);if(s){z.value=`Fix stage ${s.index+1} first: ${s.error}`;return}z.value="Running\u2026";try{const c=await Fe("/views/_preview",{method:"POST",body:JSON.stringify({source:e.pipeline.source,connection:e.connection,stages:e.pipeline.stages.slice(0,a),limit:Math.min(e.display.pageSize||10,25)})});if(o!==ue||!j())return;U=c.results||[],z.value=U.length?"":"No rows come out of the pipeline.",ee.value=`${c.total} row${c.total===1?"":"s"}${c.total>U.length?` - the first ${U.length}`:""}${T.peek()?` \xB7 up to stage ${T.peek()}`:""}`,nt(U,e)}catch(c){if(o!==ue)return;z.value="",ee.value="",J.replaceChildren(),re.value=`The pipeline did not run: ${c.message||c}`}}function nt(e,t){if(J.replaceChildren(),!e.length)return;const o=Et(e,!T.peek()&&t.display.mode==="table"?t.display.columns:[]),a=l("table");a.append(l("thead",{},l("tr",{},...o.map(c=>l("th",{text:c.label})))));const s=l("tbody");e.forEach((c,r)=>{const n=l("tr",{tabindex:"0","data-i":String(r)});for(const p of o){const oe=wt(c,p.key);n.append(l("td",{text:oe,title:oe}))}s.append(n)}),a.append(s),J.append(a)}d(M.effect(()=>{const{payload:e}=y.value;T.value;const t=P({s:e.pipeline,c:e.connection,d:e.display.columns,m:e.display.mode,p:e.display.pageSize});clearTimeout(de),ce&&(de=setTimeout(()=>{j()&&t&&pe()},600))})),d(()=>clearTimeout(de));const lt=E.contextMenu(J,{cursor:"glyph",match:"tbody tr",inherit:!1,density:"compact",items:e=>[{header:`Row ${Number(e.dataset.i)+1}`},{label:"Copy as JSON",icon:"copy",action:()=>st(JSON.stringify(U[Number(e.dataset.i)],null,2),"Row copied.")}]});d(()=>lt?.destroy?.());function st(e,t){navigator.clipboard?.writeText(e).then(()=>E.toast(t,{type:"success"}),()=>E.toast("Copy failed.",{type:"error"}))}const Te=E.tabs(i.querySelector(".vex-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),it=[...i.querySelectorAll(".vex-tabs > .tab-list > .tab-item")],ve=4,Ae=()=>{Te.activate?.(ve),fe(ve)};function fe(e){ce=e===ve,ce&&pe()}it.forEach((e,t)=>e.addEventListener("click",()=>fe(t)));function rt(e){const t=V().indexOf(e),o=V().length;return[{header:`Stage ${t+1} \xB7 ${e.dataset.stageType}`},{label:"Move up",icon:"arrow-up",disabled:t===0,action:()=>me(t,-1)},{label:"Move down",icon:"arrow-down",disabled:t===o-1,action:()=>me(t,1)},{label:"Duplicate",icon:"copy",action:()=>{const a=qe(e);if(a.error){E.toast(`Fix this stage first: ${a.error}`,{type:"warning"});return}Ee({type:a.type,config:a.config},e).querySelector(".vex-stage-head")?.focus()}},{label:"Results up to here",icon:"play",action:()=>{T.value=t+1,Ae()}},{type:"divider"},{label:"Delete stage",icon:"trash",danger:!0,action:()=>{e.remove(),g()}}]}function me(e,t){const o=He(V(),e,t);o.forEach(a=>k.append(a)),g(),o[e+t]?.querySelector(".vex-stage-head")?.focus()}const ct=E.contextMenu(k,{cursor:"glyph",match:".stage-card",inherit:!1,density:"compact",items:e=>rt(e)});d(()=>ct?.destroy?.());const Ne=e=>{const t=e.target.closest(".vex-kebab");t&&ae(t)},Oe=e=>{const t=e.target.closest(".vex-stage-head");if(t){if(e.key==="F10"&&e.shiftKey)e.preventDefault(),ae(t);else if(e.altKey&&(e.key==="ArrowUp"||e.key==="ArrowDown")){e.preventDefault();const o=V().indexOf(t.closest(".stage-card"));me(o,e.key==="ArrowUp"?-1:1)}}};k.addEventListener("click",Ne),k.addEventListener("keydown",Oe),d(()=>{k.removeEventListener("click",Ne),k.removeEventListener("keydown",Oe)});const ut=E.contextMenu(x,{cursor:"glyph",match:".col-row",inherit:!1,density:"compact",items:e=>{const t=le(),o=t.indexOf(e),a=s=>{He(t,o,s).forEach(r=>B.before(r)),g()};return[{header:e.querySelector(".col-label")?.value||e.querySelector(".col-key")?.value||"Column"},{label:"Move up",icon:"arrow-up",disabled:o===0,action:()=>a(-1)},{label:"Move down",icon:"arrow-down",disabled:o===t.length-1,action:()=>a(1)},{type:"divider"},{label:"Remove column",icon:"trash",danger:!0,action:()=>{e.remove(),g()}}]}});d(()=>ut?.destroy?.());const De=e=>{const t=e.target.closest(".vex-kebab");t&&ae(t)},Ie=e=>{const t=e.target.closest(".col-row");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),ae(t))};x.addEventListener("click",De),x.addEventListener("keydown",Ie),d(()=>{x.removeEventListener("click",De),x.removeEventListener("keydown",Ie)});function dt(e){const t=e.querySelector(".vex-val"),o=t.querySelector(".match-val")?.value??"";t.replaceChildren(ke(e.querySelector(".match-field")?.value,e.querySelector(".match-op")?.value,o))}const te=e=>{if(!e.target.closest?.(".vex-results")){if(e.type==="change"&&e.target.matches?.(".match-field, .match-op")&&dt(e.target.closest(".match-condition-row")),e.type==="change"&&e.target.matches?.("select.col-key")){const t=e.target.closest(".col-row")?.querySelector(".col-label"),o=L.find(a=>`data.${a.name}`===e.target.value);t&&!t.value&&o&&(t.value=o.label||o.name)}e.target.id==="view-source"&&e.type==="change"&&Me(e.target.value).then(()=>{j()&&(tt(),le().length||Ce(null),g())}),["view-display-mode","view-rowlevel-enabled","view-rowlevel-mode"].includes(e.target.id)&&Le(),g()}};i.addEventListener("input",te),i.addEventListener("change",te);const Ke=e=>{e.target.closest?.("input, textarea, select")&&e.stopPropagation()};i.addEventListener("contextmenu",Ke),d(()=>{i.removeEventListener("input",te),i.removeEventListener("change",te),i.removeEventListener("contextmenu",Ke)}),i.querySelector("#add-stage-btn").addEventListener("click",()=>{Ee({type:i.querySelector("#add-stage-type").value||"$match",config:{}}).querySelector("select, textarea, input")?.focus()});async function ye(){if(X.peek())return;const{payload:e}=y.peek();if(Be(H.peek())){const o=["source","pipeline","display","access"].findIndex(a=>H.peek()[a].length);o>=0&&(Te.activate?.(o),fe(o)),E.toast(`Not saved - ${Re.peek()}`,{type:"warning",duration:6e3});return}X.value=!0;try{if(b){const a=await C.views.create(e);O.value=P(e);try{S.remove?S.remove(w(null)):S.set(w(null),null)}catch{}he("#/views"),E.toast(`Created "${a.title}".`,{type:"success"}),R.navigate(`/views/edit/${encodeURIComponent(a.slug)}`);return}const o=await C.views.update(f,e);N=o||N,W=o?.meta||W,O.value=P(Q().payload);try{S.remove?S.remove(w(f)):S.set(w(f),null)}catch{}he("#/views"),E.toast("Saved.",{type:"success"})}catch(o){E.toast(`Not saved: ${o.message||o}`,{type:"error"})}finally{X.value=!1}}async function pt(){if(await E.confirm("Throw away the changes you have not saved?")){await se(N),O.value=P(Q().payload);try{S.remove?S.remove(w(f)):S.set(w(f),null)}catch{}}}async function vt(){if(!(b||!N)&&await E.confirm(`Delete the view "${At(N.title||f)}"? Pages that show it with [view] will show nothing. This cannot be undone.`))try{await C.views.delete(f),O.value=P(y.peek().payload);try{S.remove?S.remove(w(f)):S.set(w(f),null)}catch{}he("#/views"),E.toast("View deleted.",{type:"success"}),R.navigate("/views")}catch(e){E.toast(`Not deleted: ${e.message||e}`,{type:"error"})}}const ft=M.applyBindings({dirty:Z,saving:X,errs:H,stageCount:ot,errorSummary:Re,slugHint:at,upTo:T,previewMeta:ee,previewNote:z,previewError:re},i,{methods:{save:()=>ye(),discard:()=>pt(),runPreview:()=>pe(),allStages:()=>{T.value=0}}});d(()=>ft.dispose()),d(ht(i,{position:"bottom"})),Y(v,{id:"view-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>ye()}),Y(v,{id:"view-editor-results",icon:"play",title:"Results",onClick:()=>{T.value=0,Ae()}}),b||(Y(v,{id:"view-editor-preview",icon:"eye",title:"Open the preview page",onClick:()=>{location.hash=`#/views/${encodeURIComponent(f)}/preview`}}),Y(v,{id:"view-editor-delete",icon:"trash",title:"Delete this view",onClick:()=>vt()})),Y(v,{id:"view-editor-all",icon:"list",title:"All views",onClick:()=>{location.hash="#/views"}});const Pe=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),ye())};if(document.addEventListener("keydown",Pe,!0),d(()=>document.removeEventListener("keydown",Pe,!0)),await se(N),!j())return;O.value=P(Q().payload);const je=()=>{if(Z.peek())try{S.set(w(f),{payload:y.peek().payload,base:O.peek(),at:new Date().toISOString()})}catch{}},Ve=e=>{Z.peek()&&(je(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Ve),d(()=>window.removeEventListener("beforeunload",Ve)),A={path:Ge,dirty:()=>Z.peek(),dispose(){ge=!0,je(),q.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(i);let K=null;try{K=S.get(w(f))}catch{}if(K?.payload){try{S.remove?S.remove(w(f)):S.set(w(f),null)}catch{}const e=K.base&&K.base!==O.peek(),t=K.at?` (${D(K.at).format("D MMM HH:mm")})`:"",o=b?"the new view you started":"this view",a=e?`You left unsaved changes to ${o}${t}, but the saved view has changed since. Restore your version anyway?`:`You left unsaved changes to ${o}${t}. Restore them?`;if(await E.confirm(a)&&j()){const s=b?K.payload.slug:f;await se({...K.payload,slug:s})}}}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as
|
|
1
|
+
import{api as O}from"../api.js";import{addBannerAction as T}from"../lib/plugin-chrome.js";import{helpPopovers as D}from"../lib/help-popover.js";import{openMenuAt as U,viewLifetime as F}from"../lib/tool-kit.js";import{cellText as j,flattenDoc as H,resultColumns as J}from"../lib/view-editor-arrange.js";const y=F(/^#\/views\/[^/?#]+\/preview\/?(\?.*)?$/),K='<span class="vpx-chip" data-bind-class="cls" title="{{title}}">{{text}}</span>';function a(c,p={},...n){const s=document.createElement(c);for(const[l,o]of Object.entries(p))o!=null&&(l==="class"?s.className=o:l==="text"?s.textContent=o:s.setAttribute(l,o));for(const l of n)l&&s.append(l);return s}function V(c,p){const n=c.querySelector(".dm-plugin-banner-name .dm-plugin-banner-sep");if(n){for(;n.nextSibling;)n.nextSibling.remove();n.after(document.createTextNode(p))}}export const viewPreviewView={templateUrl:"/admin/js/templates/view-preview.html",async onMount(c){y.reset();const p=c.get(0),n=p.querySelector(".vpx")||p,s=()=>n.isConnected&&/\/preview/.test(location.hash),l=location.hash.match(/^#\/views\/([^/?#]+)\/preview/),o=l?decodeURIComponent(l[1]):null;if(!o){R.navigate("/views");return}const r=M.observable(null),i=M.observable(1),d=M.observable(0),u=M.observable(!0),b=M.observable("");let v=[];const N=()=>r.peek()?.display?.pageSize||25,C=M.computed(()=>Math.max(1,Math.ceil(d.value/(r.value?.display?.pageSize||25)))),q=M.computed(()=>{const e=r.value;if(!e)return[];const t=e.access||{},w=e.pipeline?.stages||[];return[{text:`From ${e.pipeline?.source||"-"}`,title:"The collection it reads",cls:""},{text:`${w.length} stage${w.length===1?"":"s"}`,title:w.map(m=>m.type).join(" \u2192 ")||"No stages",cls:""},{text:`Shown as ${e.display?.mode||"table"}`,title:"Display mode",cls:""},{text:`${e.display?.pageSize||25} a page`,title:"Page size",cls:""},t.public?{text:"Public",title:"Anyone, signed in or not",cls:"is-warn"}:{text:`Roles: ${(t.roles||[]).join(", ")||"none"}`,title:"Who can see it",cls:""},...t.rowLevel?[{text:`Own rows only (${t.rowLevel.mode})`,title:"Row-level access - not applied for admins",cls:"is-ok"}]:[],...e.connection&&e.connection!=="default"?[{text:`Connection ${e.connection}`,title:"MongoDB connection",cls:""}]:[]]}),f=n.querySelector(".vpx-results");function P(){f.replaceChildren();const e=r.peek();if(!v.length||!e)return;if((e.display?.mode||"table")==="list"){v.forEach((m,$)=>{const h=a("dl",{class:"vpx-list-item",tabindex:"0","data-i":String($)});for(const[g,S]of Object.entries(H(m)).slice(0,6)){const A=j({[g]:S},g,200);h.append(a("dt",{text:g}),a("dd",{text:A,title:A}))}f.append(h)});return}const t=J(v,e.display?.columns),w=a("tbody");v.forEach((m,$)=>{const h=a("tr",{tabindex:"0","data-i":String($)});for(const g of t){const S=j(m,g.key);h.append(a("td",{text:S,title:S}))}w.append(h)}),f.append(a("table",{},a("thead",{},a("tr",{},...t.map(m=>a("th",{text:m.label})))),w))}let x=0;async function k(){const e=++x;u.value=!0,b.value="";try{const t=await O.views.execute(o,{page:i.peek(),limit:N()});if(e!==x||!s())return;v=t.results||[],d.value=t.total||0,P()}catch(t){if(e!==x)return;v=[],d.value=0,P(),b.value=`The view did not run: ${t.message||t}`}finally{e===x&&(u.value=!1)}}n.querySelector(".vpx-chips").innerHTML=K;const z=M.applyBindings({chips:q,total:d,loading:u,error:b,description:M.computed(()=>r.value?.description||""),countText:M.computed(()=>u.value&&!d.value?"":`${d.value} row${d.value===1?"":"s"}`),pageText:M.computed(()=>`Page ${i.value} of ${C.value}`),canPrev:M.computed(()=>i.value>1&&!u.value),canNext:M.computed(()=>i.value<C.value&&!u.value)},n,{methods:{prev:()=>{i.peek()>1&&(i.value=i.peek()-1,k())},next:()=>{i.peek()<C.peek()&&(i.value=i.peek()+1,k())}}});y.add(()=>z.dispose()),y.add(D(n,{position:"bottom"}));const B=E.contextMenu(f,{cursor:"glyph",match:"tr[data-i], dl[data-i]",inherit:!1,density:"compact",items:e=>{const t=v[Number(e.dataset.i)];return[{header:`Row ${(i.peek()-1)*N()+Number(e.dataset.i)+1}`},{label:"Copy as JSON",icon:"copy",action:()=>navigator.clipboard?.writeText(JSON.stringify(t,null,2)).then(()=>E.toast("Row copied.",{type:"success"}),()=>E.toast("Copy failed.",{type:"error"}))},...t?.id&&r.peek()?.pipeline?.source?[{label:"Open the collection",icon:"database",action:()=>{location.hash=`#/collections/${encodeURIComponent(r.peek().pipeline.source)}/entries`}}]:[]]}});y.add(()=>B?.destroy?.());const L=e=>{const t=e.target.closest("tr[data-i], dl[data-i]");t&&e.key==="F10"&&e.shiftKey&&(e.preventDefault(),U(t))};f.addEventListener("keydown",L),y.add(()=>f.removeEventListener("keydown",L)),T(c,{id:"view-preview-run",icon:"refresh-cw",title:"Run again",onClick:()=>k()}),T(c,{id:"view-preview-edit",icon:"edit",title:"Edit this view",onClick:()=>{location.hash=`#/views/edit/${encodeURIComponent(o)}`}}),T(c,{id:"view-preview-all",icon:"list",title:"All views",onClick:()=>{location.hash="#/views"}});try{const e=await O.views.get(o);if(!s())return;if(!e){E.toast("That view does not exist.",{type:"error"}),R.navigate("/views");return}r.value=e,V(p,e.title||o)}catch(e){if(!s())return;u.value=!1,b.value=`Could not load the view: ${e.message||e}`;return}I.scan(n),await k()}};
|