domma-cms 0.92.1 → 0.94.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 +5 -3
- package/admin/css/admin.css +1 -1
- package/admin/js/app.js +2 -2
- package/admin/js/lib/action-editor-arrange.js +1 -1
- package/admin/js/lib/api-tokens-arrange.js +2 -2
- package/admin/js/lib/block-editor-arrange.js +1 -1
- package/admin/js/lib/blocks-arrange.js +1 -1
- package/admin/js/lib/collection-entries-arrange.js +1 -1
- package/admin/js/lib/components-arrange.js +1 -1
- package/admin/js/lib/dashboard-arrange.js +1 -1
- package/admin/js/lib/dates.js +1 -0
- package/admin/js/lib/forms-arrange.js +1 -1
- package/admin/js/lib/media-arrange.js +1 -1
- package/admin/js/lib/notifications-arrange.js +1 -1
- package/admin/js/lib/pages-arrange.js +1 -1
- package/admin/js/lib/related.js +1 -1
- package/admin/js/lib/timeline-builder.js +2 -2
- package/admin/js/templates/action-editor.html +6 -5
- package/admin/js/templates/actions-list.html +1 -1
- package/admin/js/templates/contacts.html +1 -1
- package/admin/js/templates/docs/api-actions.html +86 -60
- package/admin/js/templates/docs/api-authentication.html +159 -123
- package/admin/js/templates/docs/api-builder.html +197 -0
- package/admin/js/templates/docs/api-collections.html +199 -259
- package/admin/js/templates/docs/api-external.html +225 -0
- package/admin/js/templates/docs/api-forms.html +268 -0
- package/admin/js/templates/docs/api-layouts.html +70 -45
- package/admin/js/templates/docs/api-media.html +57 -80
- package/admin/js/templates/docs/api-navigation.html +66 -22
- package/admin/js/templates/docs/api-pages.html +109 -129
- package/admin/js/templates/docs/api-plugins.html +123 -61
- package/admin/js/templates/docs/api-scaffold.html +185 -0
- package/admin/js/templates/docs/api-settings.html +72 -64
- package/admin/js/templates/docs/api-users.html +74 -107
- package/admin/js/templates/docs/api-views.html +68 -54
- package/admin/js/templates/docs/components-howto.html +20 -17
- package/admin/js/templates/docs/components-reference.html +13 -16
- package/admin/js/templates/docs/components-rules.html +7 -6
- package/admin/js/templates/docs/components-walkthrough.html +19 -19
- package/admin/js/templates/docs/tutorial-crud.html +71 -40
- package/admin/js/templates/docs/tutorial-forms.html +51 -35
- package/admin/js/templates/docs/tutorial-plugin.html +132 -56
- package/admin/js/templates/docs/usage-actions.html +61 -15
- package/admin/js/templates/docs/usage-collections.html +108 -0
- package/admin/js/templates/docs/usage-cta-shortcode.html +14 -3
- package/admin/js/templates/docs/usage-dconfig.html +0 -3
- package/admin/js/templates/docs/usage-editions.html +213 -0
- package/admin/js/templates/docs/usage-media.html +22 -6
- package/admin/js/templates/docs/usage-navigation.html +74 -18
- package/admin/js/templates/docs/usage-pages.html +60 -20
- package/admin/js/templates/docs/usage-plugins.html +89 -17
- package/admin/js/templates/docs/usage-shortcodes.html +123 -70
- package/admin/js/templates/docs/usage-site-settings.html +50 -18
- package/admin/js/templates/docs/usage-tools.html +73 -0
- package/admin/js/templates/docs/usage-users-roles.html +99 -20
- package/admin/js/templates/docs/usage-views.html +36 -19
- package/admin/js/templates/documentation.html +153 -32
- package/admin/js/templates/page-editor.html +0 -5
- package/admin/js/templates/plugin-guide.html +15 -0
- package/admin/js/templates/plugin-guides.html +21 -0
- package/admin/js/templates/pro-docs.html +53 -234
- package/admin/js/templates/tutorials.html +5 -4
- package/admin/js/views/actions-list.js +3 -3
- package/admin/js/views/analytics.js +5 -5
- package/admin/js/views/api-endpoint-editor.js +2 -2
- package/admin/js/views/block-editor.js +4 -4
- package/admin/js/views/blocks.js +4 -4
- package/admin/js/views/collection-editor.js +4 -4
- package/admin/js/views/collection-entries.js +7 -7
- package/admin/js/views/component-editor.js +2 -2
- package/admin/js/views/contacts.js +22 -20
- package/admin/js/views/context-menu-editor.js +5 -5
- package/admin/js/views/doc-pages.js +1 -1
- package/admin/js/views/form-editor.js +4 -4
- package/admin/js/views/form-submissions.js +2 -2
- package/admin/js/views/index.js +1 -1
- package/admin/js/views/media.js +3 -3
- package/admin/js/views/menu-editor.js +13 -13
- package/admin/js/views/menu-locations.js +2 -2
- package/admin/js/views/my-profile.js +1 -1
- package/admin/js/views/page-editor.js +8 -8
- package/admin/js/views/plugin-guides.js +5 -0
- package/admin/js/views/project-detail.js +2 -2
- package/admin/js/views/project-settings.js +1 -1
- package/admin/js/views/role-editor.js +4 -4
- package/admin/js/views/search.js +2 -2
- package/admin/js/views/seo.js +17 -17
- package/admin/js/views/settings.js +3 -3
- package/admin/js/views/theme.js +3 -3
- package/admin/js/views/user-editor.js +1 -1
- package/admin/js/views/users.js +2 -2
- package/admin/js/views/view-editor.js +1 -1
- package/bin/cli.js +13 -13
- package/bin/lib/node-version.js +29 -0
- package/package.json +1 -1
- package/plugins/_lib/admin/mail/compose-window.js +3 -2
- package/plugins/_lib/admin/mail/reader-view.js +7 -6
- package/plugins/_lib/admin/mail/scheduling.js +4 -2
- package/plugins/_lib/admin/mail/templates.js +4 -4
- package/plugins/_lib/admin/ui/dates.js +85 -0
- package/plugins/blog/CLAUDE.md +31 -22
- package/plugins/blog/admin/views/blog.js +3 -2
- package/plugins/blog/admin/views/comments.js +2 -1
- package/plugins/blog/admin/views/post-editor.js +4 -4
- package/plugins/blog/blocks/blog-card-row.html +1 -1
- package/plugins/blog/blocks/blog-card.html +2 -2
- package/plugins/blog/blocks/blog-post-classic.html +2 -2
- package/plugins/blog/blocks/blog-post-essay.html +2 -2
- package/plugins/blog/blocks/blog-post-feature.html +2 -2
- package/plugins/blog/blocks/blog-post-minimal.html +2 -2
- package/plugins/blog/blocks/blog-post-sidebar.html +2 -2
- package/plugins/blog/blocks/blog-post-split.html +2 -2
- package/plugins/blog/docs/guide.md +205 -0
- package/plugins/blog/lib/layouts.js +3 -3
- package/plugins/blog/lib/page.js +2 -1
- package/plugins/blog/plugin.js +3 -3
- package/plugins/blog/plugin.json +4 -4
- package/plugins/blog/tests/layouts.test.js +6 -0
- package/plugins/feedback/CLAUDE.md +22 -3
- package/plugins/feedback/admin/lib/kit.js +6 -7
- package/plugins/feedback/admin/views/feedback.js +79 -10
- package/plugins/feedback/admin/views/send.js +28 -6
- package/plugins/feedback/docs/guide.md +95 -0
- package/plugins/feedback/lib/receiver.js +9 -2
- package/plugins/feedback/lib/sender.js +3 -2
- package/plugins/feedback/plugin.js +54 -6
- package/plugins/feedback/plugin.json +4 -4
- package/plugins/feedback/tests/api.test.js +74 -2
- package/plugins/free-tier.lock.json +49 -44
- package/plugins/mail-reader/CLAUDE.md +33 -18
- package/plugins/mail-reader/docs/guide.md +147 -0
- package/plugins/mail-reader/plugin.json +1 -1
- package/plugins/security/CLAUDE.md +4 -1
- package/plugins/security/admin/views/security.js +5 -5
- package/plugins/security/docs/guide.md +170 -0
- package/plugins/security/plugin.js +2 -1
- package/plugins/security/plugin.json +2 -1
- package/plugins/shopping-cart/CLAUDE.md +7 -1
- package/plugins/shopping-cart/admin/lib/kit.js +5 -2
- package/plugins/shopping-cart/admin/views/orders.js +4 -4
- package/plugins/shopping-cart/admin/views/overview.js +2 -2
- package/plugins/shopping-cart/docs/guide.md +191 -0
- package/plugins/shopping-cart/lib/render.js +2 -1
- package/plugins/shopping-cart/plugin.json +3 -3
- package/public/js/collection-browser.js +2 -2
- package/public/js/site.js +1 -1
- package/scripts/gen-instance-secret.js +3 -1
- package/scripts/setup.js +3 -1
- package/server/middleware/auth.js +2 -1
- package/server/routes/api/actions.js +47 -27
- package/server/routes/api/blocks.js +2 -1
- package/server/routes/api/collections.js +16 -52
- package/server/routes/api/contacts.js +66 -3
- package/server/routes/api/documentation.js +42 -0
- package/server/routes/api/notifications.js +3 -2
- package/server/routes/api/users.js +10 -6
- package/server/server.js +16 -1
- package/server/services/actions.js +110 -34
- package/server/services/adapterRegistry.js +169 -16
- package/server/services/adapters/FileAdapter.js +25 -0
- package/server/services/adapters/MongoAdapter.js +23 -0
- package/server/services/collections.js +104 -1
- package/server/services/connectionManager.js +12 -0
- package/server/services/dates.js +81 -0
- package/server/services/docs.js +13 -2
- package/server/services/markdown.js +75 -26
- package/server/services/notification-sources.js +6 -5
- package/server/services/passwordReset.js +2 -1
- package/server/services/permissionRegistry.js +3 -2
- package/server/services/pluginGuides.js +255 -0
- package/server/services/pluginInstaller.js +54 -13
- package/server/services/plugins.js +29 -1
- package/server/services/presetCollections.js +31 -5
- package/server/services/renderer.js +2 -2
- package/server/services/sidebarBadges.js +3 -1
- package/server/services/tools.js +4 -2
- package/server/templates/page.html +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as g}from"../api.js";import{CardBuilder as rt}from"../lib/card-builder.js";import{TimelineBuilder as lt}from"../lib/timeline-builder.js";import{attachEditorKeybindings as ct,createToolbar as dt,insertAtCursor as pt}from"../lib/markdown-toolbar.js";import{EffectsBuilder as ut}from"../lib/effects-builder.js";import{attachContextMenu as vt}from"../lib/shortcode-context-menu.js";import{registerCustomInserters as ft}from"../lib/shortcode-insert.js";import{populateThemeSelect as mt}from"../lib/themes.js";import{createFoldingManager as bt}from"../lib/editor-folding.js";import{openScribeComposer as gt}from"../lib/scribe-composer.js";import{openCrudTutorial as ht}from"../lib/crud-tutorial.js";import{addBannerAction as
|
|
1
|
+
import{api as g}from"../api.js";import{CardBuilder as rt}from"../lib/card-builder.js";import{TimelineBuilder as lt}from"../lib/timeline-builder.js";import{attachEditorKeybindings as ct,createToolbar as dt,insertAtCursor as pt}from"../lib/markdown-toolbar.js";import{EffectsBuilder as ut}from"../lib/effects-builder.js";import{attachContextMenu as vt}from"../lib/shortcode-context-menu.js";import{registerCustomInserters as ft}from"../lib/shortcode-insert.js";import{populateThemeSelect as mt}from"../lib/themes.js";import{createFoldingManager as bt}from"../lib/editor-folding.js";import{openScribeComposer as gt}from"../lib/scribe-composer.js";import{openCrudTutorial as ht}from"../lib/crud-tutorial.js";import{addBannerAction as B}from"../lib/plugin-chrome.js";import{helpPopovers as Be}from"../lib/help-popover.js";import{onceClosed as G,openMenuAt as $e}from"../lib/tool-kit.js";import{openEditorReference as je}from"../lib/page-editor-reference.js";import{cleanType as Y,DEFAULT_TYPE as X,EDITOR_FONTS as yt,fontHref as wt,isDefaultType as kt,LINE_HEIGHTS as Et,SIZES as Mt,typeVars as qe}from"../lib/editor-type.js";import{getProjectFromHash as St}from"../lib/project-context.js";import{SCREEN as Ct,bannerSection as Tt,draftIsStale as xt,draftKey as Pt,initialProject as Lt,makeDraft as At,problems as Ve,readDraft as Dt,snapshot as $}from"../lib/page-editor-arrange.js";import{fmtDateTime as Z}from"../lib/dates.js";function j(s){const i=document.createElement("template");return i.innerHTML=s.trim(),i.content.firstElementChild}const Ot=Z,He=s=>Z(s.createdAt)+(s.label?` - ${s.label}`:""),Rt=`
|
|
2
2
|
<div class="pe-share">
|
|
3
3
|
<p class="form-hint" style="margin-top:0">Anyone with the link can view this page even while it is unpublished, without
|
|
4
4
|
signing in. The link works for this page only, expires on its own, and can be revoked at any time.</p>
|
|
@@ -30,7 +30,7 @@ import{api as g}from"../api.js";import{CardBuilder as rt}from"../lib/card-builde
|
|
|
30
30
|
.pe-share [hidden] { display: none !important; }
|
|
31
31
|
.pe-share-row { display: flex; align-items: center; gap: .6rem; padding: .5rem 0; border-bottom: 1px solid var(--dm-border-light); }
|
|
32
32
|
.pe-share-text { flex: 1 1 auto; min-width: 0; font-size: .82rem; }
|
|
33
|
-
</style>`;function
|
|
33
|
+
</style>`;function Ke(s){const i=wt(s);if(!i||document.querySelector(`link[data-pe-font="${CSS.escape(s)}"]`))return;const r=Object.assign(document.createElement("link"),{rel:"stylesheet",href:i});r.dataset.peFont=s,document.head.appendChild(r)}const Nt=`
|
|
34
34
|
<div class="pe-settings">
|
|
35
35
|
<p class="pe-settings-note">How the editor looks to you - kept in this browser, for every page.</p>
|
|
36
36
|
<label class="form-label" for="pe-set-font">Font <span data-help="The lettering in the editor (not on the site). Monospace keeps every character the same width, so shortcodes and Markdown line up."></span></label>
|
|
@@ -61,17 +61,17 @@ The quick brown fox - 0O 1lI {} [] () => ==
|
|
|
61
61
|
.pe-settings-sample { white-space: pre-wrap; padding: .75rem 1rem; border-radius: var(--dm-radius-md); border: 1px solid var(--dm-border);
|
|
62
62
|
background: var(--dm-background-alt); font-family: var(--pe-font); font-size: var(--pe-size); line-height: var(--pe-line); }
|
|
63
63
|
.pe-settings-actions { display: flex; gap: .5rem; border-top: 1px solid var(--dm-border); padding-top: .75rem; }
|
|
64
|
-
</style>`;function Ft(
|
|
64
|
+
</style>`;function Ft(s){const i=E.slideover({title:"Editor settings",size:"sm",position:"right"}),r=j(`<div>${Nt}</div>`),a=Y(s.peek()),l={font:M.observable(a.font),size:M.observable(String(a.size)),line:M.observable(String(a.line)),wrap:M.observable(a.wrap)},p=M.computed(()=>Y({font:l.font.value,size:l.size.value,line:l.line.value,wrap:l.wrap.value}));l.isDefault=M.computed(()=>kt(p.value));const v=r.querySelector("#pe-set-font");for(const[f,k]of[["mono","Monospace"],["sans","Other"],["serif",null]]){const F=yt.filter(m=>m.kind===f),V=k?Object.assign(document.createElement("optgroup"),{label:k}):v.querySelector("optgroup:last-of-type");V.append(...F.map(m=>new Option(m.label,m.id))),k&&v.appendChild(V)}r.querySelector("#pe-set-size").append(...Mt.map(f=>new Option(`${f}px`,String(f)))),r.querySelector("#pe-set-line").append(...Et.map(f=>new Option({1.3:"Tight",1.45:"Snug",1.6:"Normal",1.8:"Relaxed",2:"Double"}[f]||String(f),String(f))));const c=r.querySelector(".pe-settings-sample"),u=M.effect(()=>{const f=p.value;Ke(f.font);for(const[k,F]of Object.entries(qe(f)))c.style.setProperty(k,F);c.style.whiteSpace=f.wrap?"pre-wrap":"pre",c.style.overflowX=f.wrap?"":"auto",JSON.stringify(f)!==JSON.stringify(Y(s.peek()))&&(s.value=f,Q("type",f))});l.reset=()=>{l.font.value=X.font,l.size.value=String(X.size),l.line.value=String(X.line),l.wrap.value=X.wrap},l.done=()=>i.close(),i.element.appendChild(r);const T=M.applyBindings(l,r),d=Be(r);return G(i,()=>{T.dispose(),d(),u?.()}),Domma.icons?.scan?.(r),i.open(),i}async function It(s){const i=E.slideover({title:"Share preview link",size:"md",position:"right"}),r=j(`<div>${Rt}</div>`),a={expiry:M.observable("7d"),note:M.observable(""),creating:M.observable(!1),url:M.observable(""),links:M.observable([]),loaded:M.observable(!1),listError:M.observable("")},l=async()=>{try{const v=await g.pages.previewLinks.list(s);a.links.value=v.filter(c=>!c.revokedAt&&!c.expired).map(c=>({id:c.id,title:c.label||"Untitled link",meta:`Expires ${Ot(c.expiresAt)} - ${c.useCount===1?"1 view":`${c.useCount||0} views`}`})),a.listError.value=""}catch(v){a.listError.value="Could not load existing links: "+(v.message||"unknown error")}finally{a.loaded.value=!0}};a.create=async()=>{a.creating.value=!0;try{const v=await g.pages.previewLinks.create(s,a.expiry.peek(),a.note.peek().trim());a.url.value=v.url,a.note.value="",E.toast("Share link created.",{type:"success"}),await l(),r.querySelector(".pe-share-url")?.select()}catch(v){E.toast("Could not create link: "+(v.message||"unknown error"),{type:"error"})}finally{a.creating.value=!1}},a.copy=async()=>{try{await navigator.clipboard.writeText(a.url.peek()),E.toast("Link copied to clipboard.",{type:"success"})}catch{r.querySelector(".pe-share-url")?.select(),E.toast("Press Ctrl+C to copy the selected link.",{type:"info"})}},a.revoke=async v=>{if(await E.confirm("Revoke this link? Anyone holding it loses access immediately."))try{await g.pages.previewLinks.revoke(v),E.toast("Link revoked.",{type:"success"}),await l()}catch(c){E.toast("Could not revoke: "+(c.message||"unknown error"),{type:"error"})}};const p=M.applyBindings(a,r);G(i,()=>p.dispose()),i.element.appendChild(r),i.open(),await l()}function zt(s){return new Promise(i=>{const r=E.slideover({title:"Save Named Version",size:"sm",position:"right"}),a=j(`
|
|
65
65
|
<div style="padding:1rem">
|
|
66
66
|
<label class="form-label" for="pe-version-label">Version Label</label>
|
|
67
67
|
<input id="pe-version-label" type="text" class="form-input" style="margin-bottom:.75rem"
|
|
68
68
|
placeholder="e.g. Initial design, Before restructure" data-model="label.value" data-on-keydown="key">
|
|
69
69
|
<button type="button" class="btn btn-primary" data-on-click="save" data-bind-disabled="saving.value">Save Version</button>
|
|
70
|
-
</div>`);let l=!1;const p={label:M.observable(""),saving:M.observable(!1)};p.save=async()=>{p.saving.value=!0;try{await g.versions.create(
|
|
70
|
+
</div>`);let l=!1;const p={label:M.observable(""),saving:M.observable(!1)};p.save=async()=>{p.saving.value=!0;try{await g.versions.create(s,p.label.peek().trim()||null),E.toast("Named version saved.",{type:"success"}),l=!0,r.close()}catch(c){E.toast("Failed to save version: "+c.message,{type:"error"})}finally{p.saving.value=!1}},p.key=c=>{c.key==="Enter"&&(c.preventDefault(),p.save())};const v=M.applyBindings(p,a);G(r,()=>{v.dispose(),i(l)}),r.element.appendChild(a),r.open(),setTimeout(()=>a.querySelector("input")?.focus(),100)})}let C=null,_e=!1;function Bt(){_e||!window.R?.use||(_e=!0,R.use((s,i,r)=>{const a=i?.path||"",l=s?.path||"";return!(Ct.test(a)&&l!==a)||!C||C.path!==a||!C.dirty()?r():E.confirm("You have unsaved changes to this page. Leave anyway? They are kept as a draft for your next visit.").then(v=>{v?(C?.keepDraft(),r()):history.replaceState(null,"","#"+a)})}),M.subscribe("router:afterChange",({to:s}={})=>{if(C&&(s?.path||"")!==C.path){const i=C;C=null,i.dispose()}}))}function $t(s){return s.split(`
|
|
71
71
|
`).map(i=>i.trimEnd()).join(`
|
|
72
72
|
`).replace(/\n{3,}/g,`
|
|
73
73
|
|
|
74
|
-
`)}const
|
|
74
|
+
`)}const Ue="editor_prefs";function q(){return S.get(Ue)||{viewMode:"split",fullscreen:!1}}function Q(s,i){const r=q();r[s]=i,S.set(Ue,r)}const jt=["edit","details","seo","dconfig","live","history"],We=["split","write","preview"],qt=s=>s.replace(/<\/head>/i,"<style>body.dm-cloaked{opacity:1 !important;transition:none !important}</style></head>");function Je(s,i,r=()=>{}){let a=0;try{a=s.contentWindow.scrollY||0}catch{}s.addEventListener("load",()=>{try{s.contentWindow.scrollTo(0,a)}catch{}r()},{once:!0}),s.srcdoc=i}export const pageEditorView={templateUrl:"/admin/js/templates/page-editor.html",async onMount(s){if(Bt(),C){const e=C;C=null,e.dispose()}const i=window.location.hash.replace(/^#/,""),r=[],a=e=>(r.push(e),e);let l=()=>!1,p=()=>{};const v={path:i,dirty:()=>l(),keepDraft:()=>p(),dispose:()=>{r.splice(0).forEach(e=>{try{e()}catch{}})}};C=v;const c=s.get(0),u=()=>C===v&&c.isConnected,T=window.location.hash.match(/#\/pages\/edit(\/[^?]*)/),d=T?T[1]:null,f=!!d,k=E.loader(c,{type:"dots"}),[F,V,m,ge,he,Ge]=await Promise.all([g.layouts.get().catch(()=>({})),g.settings.get().catch(()=>({})),f?g.pages.get(d).catch(()=>null):null,f?g.pages.list().catch(()=>[]):[],g.projects.list().catch(()=>[]),g.pages.tags().catch(()=>[])]);if(!u()){k.destroy();return}if(k.destroy(),f&&!m){E.toast("Page not found.",{type:"error"}),R.navigate("/pages");return}const y=e=>c.querySelector(e);if(mt(y("#field-theme"),{includeDefault:!0}),await Ut(y("#field-visibility"),m?.visibility),!u())return;const Ye=_t(y("#field-layout"),F,m?.layout||"default");for(const e of he)y("#field-project").append(new Option(e.name||e.slug,e.slug));const ye=[...Array.isArray(ge)?ge:[]].sort((e,t)=>(e.title||e.urlPath).localeCompare(t.title||t.urlPath));for(const e of ye)y("#quick-switch").append(new Option(`${e.title||"Untitled"} (${e.urlPath})`,e.urlPath,!1,e.urlPath===d));const H=q(),o={urlPath:M.observable(d||""),title:M.observable(m?.title||""),description:M.observable(m?.description||""),layout:M.observable(Ye),status:M.observable(m?.status||"draft"),sortOrder:M.observable(String(m?.sortOrder??99)),showInNav:M.observable(!!m?.showInNav),sidebar:M.observable(!!m?.sidebar),breadcrumbs:M.observable(m?m.breadcrumbs!==!1:!0),bundled:M.observable(!!m?.bundled),tags:M.observable(Array.isArray(m?.tags)?m.tags:[]),category:M.observable(m?.category||""),visibility:M.observable(typeof m?.visibility=="string"&&m.visibility?m.visibility:"public"),project:M.observable(Lt({isEdit:f,page:m,projects:he,fromHash:St()||"",urlPath:d||""})),theme:M.observable(m?.theme||""),seoTitle:M.observable(m?.seo?.title||""),seoDesc:M.observable(m?.seo?.description||""),dconfig:M.observable(m?.dconfig?JSON.stringify(m.dconfig,null,2):""),css:M.observable(""),cssOpened:M.observable(!1)},n={bodyRev:M.observable(0),base:M.observable(""),saving:M.observable(!1),tab:M.observable("edit"),viewMode:M.observable(We.includes(H.viewMode)?H.viewMode:"split"),fullscreen:M.observable(!!H.fullscreen),cssMode:M.observable(!1),folded:M.observable(!1),liveStatus:M.observable("Showing unsaved changes"),liveWidth:M.observable("full"),versions:M.observable([]),histLoading:M.observable(!1),histError:M.observable(""),compare:M.observable(null)},w=y("#markdown-editor"),I=y("#css-editor");w.value=m?.content||"";const L=document.createElement("div");L.className="editor-line-numbers editor-line-numbers--foldable",w.parentElement.insertBefore(L,w);const x=bt(w,L,{onFoldChange:()=>{}});let D=!1;x.setGuard(e=>{D=e});const z=()=>x.getUnfoldedContent(),we=M.observable(Y(H.type));a(M.effect(()=>{const e=we.value,t=y("#editor-body");if(t){Ke(e.font);for(const[b,h]of Object.entries(qe(e)))t.style.setProperty(b,h);t.classList.toggle("pe-nowrap",!e.wrap),w.wrap=e.wrap?"soft":"off",I.wrap=e.wrap?"soft":"off"}}));let ke=null;a(()=>{try{ke?.close()}catch{}});const O=()=>(n.bodyRev.value,{urlPath:o.urlPath.value.trim(),title:o.title.value,description:o.description.value,layout:o.layout.value,status:o.status.value,sortOrder:o.sortOrder.value,showInNav:o.showInNav.value,sidebar:o.sidebar.value,breadcrumbs:o.breadcrumbs.value,bundled:o.bundled.value,tags:o.tags.value,category:o.category.value,visibility:o.visibility.value,project:o.project.value,theme:o.theme.value,seoTitle:o.seoTitle.value,seoDesc:o.seoDesc.value,dconfig:o.dconfig.value,body:z(),css:o.cssOpened.value?o.css.value:null}),Xe=M.computed(()=>$(O())!==n.base.value),Ze=M.computed(()=>Ve({urlPath:o.urlPath.value,dconfig:o.dconfig.value})[0]?.text||""),Qe=M.computed(()=>n.versions.value.some(e=>e.checked.value)),et=M.computed(()=>n.versions.value.length>0&&n.versions.value.every(e=>e.checked.value)),tt=M.computed(()=>{const e=n.versions.value.filter(t=>t.checked.value).length;return e>0&&e<n.versions.value.length});l=()=>$(O())!==n.base.peek();const ee=e=>({title:o.title.peek().trim()||"Untitled",description:o.description.peek().trim(),layout:o.layout.peek(),status:o.status.peek(),sortOrder:parseInt(String(o.sortOrder.peek()),10)||99,showInNav:o.showInNav.peek(),sidebar:o.sidebar.peek(),...o.breadcrumbs.peek()?{}:{breadcrumbs:!1},...o.bundled.peek()?{bundled:!0}:{},tags:o.tags.peek(),category:o.category.peek().trim()||null,visibility:o.visibility.peek()||"public",project:o.project.peek()||"",seo:{title:o.seoTitle.peek().trim(),description:o.seoDesc.peek().trim()},dconfig:e,...o.theme.peek()?{theme:o.theme.peek()}:{}}),Ee=()=>{const e=o.dconfig.peek().trim();if(!e)return null;try{return JSON.parse(e)}catch{return null}},Me=()=>o.urlPath.peek().trim()||d||"/",K=y("#markdown-preview");let Se="";g.settings.getCustomCss().then(({css:e})=>{Se=e||""}).catch(()=>{});const Ce=()=>{const e=K?.contentDocument;if(!e?.head)return;const t=o.cssOpened.peek()?o.css.peek():"";if(!t)return;let b=e.getElementById("preview-custom-css");b||(b=e.createElement("style"),b.id="preview-custom-css",e.head.appendChild(b)),b.textContent=t};let te=null;const _=()=>{clearTimeout(te),te=setTimeout(async()=>{if(!(!K||n.viewMode.peek()==="write"))try{const{html:e}=await g.pages.previewFull(Me(),ee(Ee()),z());u()&&Je(K,qt(e),Ce)}catch(e){K.srcdoc='<!doctype html><meta charset="utf-8"><body style="font:14px system-ui;padding:1rem;color:#b91c1c;">Preview failed: '+String(e?.message||"unknown error").replace(/[<&]/g,t=>t==="<"?"<":"&")+"</body>"}},500)},Te=y("#live-preview-frame");let ae=!0,se=null,oe=!1;const ne=async()=>{if(!(oe||!Te)){oe=!0,n.liveStatus.value="Rendering\u2026";try{const{html:e}=await g.pages.previewFull(Me(),ee(Ee()),z());if(!u())return;Je(Te,e),ae=!1,n.liveStatus.value="Showing unsaved changes"}catch(e){n.liveStatus.value="Preview failed - "+(e?.message||"unknown error")}finally{oe=!1}}},xe=()=>{ae=!0,n.tab.peek()==="live"&&(clearTimeout(se),se=setTimeout(ne,600))};a(()=>{clearTimeout(te),clearTimeout(se)});const ie=dt(s.find("#markdown-editor"),s.find("#editor-toolbar"),{spacerDefault:V?.layoutOptions?.spacerSize??40});vt(w);const re=c.querySelector(".editor-toolbar-right");re.querySelectorAll(".editor-view-btn[data-mode]").forEach(e=>{const t=e.getAttribute("data-mode");e.classList.remove("active"),e.setAttribute("data-bind-class",`viewMode.value === '${t}' && 'active'`),e.setAttribute("data-on-click",`setMode('${t}')`)}),y("#fullscreen-btn")?.setAttribute("data-on-click","toggleFullscreen");const at=j(`
|
|
75
75
|
<span class="pe-toolbar-extras" style="display:contents">
|
|
76
76
|
<button type="button" class="editor-view-btn" data-tip="Edit Custom CSS" data-on-click="toggleCss"
|
|
77
77
|
data-bind-class="cssMode.value && 'active'"><span data-icon="css-code"></span></button>
|
|
@@ -83,8 +83,8 @@ The quick brown fox - 0O 1lI {} [] () => ==
|
|
|
83
83
|
<span class="pe-fold-icon" hidden data-bind-hidden="!folded.value"><span data-icon="chevrons-right" data-icon-size="10"></span></span>
|
|
84
84
|
</button>
|
|
85
85
|
<span class="editor-toolbar-sep"></span>
|
|
86
|
-
</span>`);re.prepend(at);const Pe=y("#field-tags"),le=Pe?E.pillbox(Pe,{data:
|
|
87
|
-
`).map((
|
|
86
|
+
</span>`);re.prepend(at);const Pe=y("#field-tags"),le=Pe?E.pillbox(Pe,{data:Ge,value:o.tags.peek(),creatable:!0,searchable:!0,placeholder:"Add tag\u2026",onChange:()=>{o.tags.value=le?[...le.getValue()]:[]}}):null;let ce=!1,de=null;const U=async()=>{ce=!0,n.histLoading.value=!0,n.histError.value="";try{const e=(await g.versions.list(d)).versions||[];if(!u())return;n.versions.value=e.map(t=>({...t,when:Z(t.createdAt),author:t.author||"",label:t.label||"",checked:M.observable(!1)}))}catch(e){n.histError.value="Failed to load versions: "+e.message}finally{n.histLoading.value=!1}},pe=e=>n.versions.peek().find(t=>t.filename===e),ue=async e=>{try{const t=await g.versions.get(d,e.filename);de=e.filename,n.compare.value={label:He(e),old:t.content,current:z()}}catch(t){E.toast("Failed to load version: "+t.message,{type:"error"})}},Le=async e=>{if(!(!e||!await E.confirm("Restore this version? The current page content will be overwritten (a pre-restore snapshot will be saved automatically).")))try{await g.versions.restore(d,e),E.toast("Page restored. Reloading editor\u2026",{type:"success"}),R.navigate("/pages"),setTimeout(()=>R.navigate(`/pages/edit${d}`),300)}catch(t){E.toast("Restore failed: "+t.message,{type:"error"})}},st=async e=>{if(await E.confirm("Delete this version? This cannot be undone."))try{await g.versions.delete(d,e.filename),E.toast("Version deleted.",{type:"success"}),await U()}catch(t){E.toast("Failed to delete: "+t.message,{type:"error"})}},Ae=()=>{n.bodyRev.value=n.bodyRev.peek()+1},De=()=>{o.cssOpened.peek()||(o.css.value=Se,o.cssOpened.value=!0,ct(I))},Oe=e=>{for(const t of["urlPath","title","description","layout","status","sortOrder","category","visibility","project","theme","seoTitle","seoDesc","dconfig"])o[t].value=e[t]==null?"":String(e[t]);for(const t of["showInNav","sidebar","breadcrumbs","bundled"])o[t].value=!!e[t];o.tags.value=Array.isArray(e.tags)?e.tags:[],le?.setValue(o.tags.peek()),x.unfoldAll(),D=!0,w.value=e.body==null?"":String(e.body),D=!1,x.refresh(),e.css!=null&&(De(),o.css.value=String(e.css)),Ae()};n.base.value=$(O());const W=Pt(d||"");p=()=>{if(l())try{S.set(W,At(O(),n.base.peek()))}catch{}};const ve=()=>{try{S.remove?S.remove(W):S.set(W,null)}catch{}},Re=e=>{l()&&(p(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",Re),a(()=>window.removeEventListener("beforeunload",Re));const fe=async()=>{if(n.saving.peek())return;const e=O(),t=Ve(e);if(t.length){E.toast(t[0].text,{type:"warning"}),c.querySelectorAll("#editor-meta-tabs .tab-item")[jt.indexOf(t[0].field==="dconfig"?"dconfig":"details")]?.click();return}const b=(e.dconfig||"").trim(),h=ee(b?JSON.parse(b):null),P=o.cssOpened.peek();n.saving.value=!0;try{if(f){const A={frontmatter:h,body:e.body};e.urlPath!==d&&(A.newUrlPath=e.urlPath),await g.pages.update(d,A)}else await g.pages.create({urlPath:e.urlPath,frontmatter:h,body:e.body});P&&await g.settings.saveCustomCss(o.css.peek()),E.toast(P?"Page and CSS saved.":"Page saved.",{type:"success"}),ve(),n.base.value=$(O()),f?e.urlPath!==d&&R.navigate(`/pages/edit${e.urlPath}`):R.navigate("/pages")}catch(A){E.toast(`Save failed: ${A.message||"Unknown error"}`,{type:"error"})}finally{n.saving.value=!1}},Ne=c.querySelector(".dm-plugin-body")||c,ot={...o,...n,dirty:Xe,problem:Ze,anySelected:Qe,allSelected:et,someSelected:tt,isEdit:f,savedPath:d||"",hasPages:ye.length>0},nt={save:()=>fe(),discard:async()=>{!l()||!await E.confirm("Discard the changes you have not saved?")||(Oe(JSON.parse(n.base.peek())),ve())},switchPage:e=>{const t=e?.target?.value;t&&R.navigate("/pages/edit"+t)},showTab:e=>{n.tab.value=e,e==="live"&&ae&&ne(),e==="history"&&f&&!ce&&U()},setMode:e=>{n.cssMode.peek()&&(n.cssMode.value=!1),n.viewMode.value=e,Q("viewMode",e),e!=="write"&&_()},toggleFullscreen:()=>{n.fullscreen.value=!n.fullscreen.peek(),Q("fullscreen",n.fullscreen.peek())},toggleCss:()=>{n.cssMode.peek()?(n.cssMode.value=!1,n.viewMode.value=We.includes(q().viewMode)?q().viewMode:"split",x.refresh()):(De(),n.cssMode.value=!0,n.viewMode.value="split")},formatMarkdown:()=>{const e=w.selectionStart,t=$t(z());x.unfoldAll(),D=!0,w.value=t,D=!1,w.selectionStart=w.selectionEnd=Math.min(e,t.length),w.dispatchEvent(new Event("input")),x.refresh(),E.toast("Markdown formatted.",{type:"success"})},toggleFold:()=>{n.folded.peek()?x.unfoldAll():x.foldAll(),n.folded.value=!n.folded.peek()},setWidth:e=>{n.liveWidth.value=e;const t=y("#live-preview-stage");t&&(t.scrollTop=0)},refreshLive:()=>ne(),share:async()=>{if(!f){E.toast("Save the page first - a share link needs a page to point at.",{type:"warning"});return}await It(d)},toggleAll:e=>{const t=!!e?.target?.checked;n.versions.peek().forEach(b=>{b.checked.value=t})},deleteSelected:async()=>{const e=n.versions.peek().filter(b=>b.checked.peek()).map(b=>b.filename),t=e.length;if(!(!t||!await E.confirm(`Delete ${t} version${t>1?"s":""}? This cannot be undone.`)))try{await g.versions.bulkDelete(d,e),E.toast(`${t} version${t>1?"s":""} deleted.`,{type:"success"}),await U()}catch(b){E.toast("Delete failed: "+b.message,{type:"error"})}},saveNamed:async()=>{await zt(d)&&u()&&(n.tab.peek()==="history"?await U():ce=!1)},closeCompare:()=>{n.compare.value=null,de=null},restoreCompared:()=>Le(de)};re.querySelectorAll("[data-tip]").forEach(e=>E.tooltip(e,{content:e.getAttribute("data-tip"),position:"top"}));const it=M.applyBindings(ot,Ne,{methods:nt});a(()=>it.dispose()),E.tabs(y("#editor-meta-tabs"));let me=null;a(()=>clearTimeout(me)),w.addEventListener("input",()=>{x.refresh(),L.scrollTop=w.scrollTop,!D&&(clearTimeout(me),me=setTimeout(Ae,120),_(),xe())}),w.addEventListener("scroll",()=>{L.scrollTop=w.scrollTop}),I.addEventListener("scroll",()=>{L.scrollTop=I.scrollTop});let Fe=!0;a(M.effect(()=>{for(const e of Object.keys(o))e!=="css"&&e!=="cssOpened"&&o[e].value;if(n.bodyRev.value,Fe){Fe=!1,_();return}_(),xe()}));let be=null;if(a(()=>clearTimeout(be)),a(M.effect(()=>{const e=o.css.value;n.cssMode.value&&(Vt(L,e,I),clearTimeout(be),be=setTimeout(Ce,400))})),f){const e=y(".pe-version-list"),t=E.contextMenu(e,{cursor:"glyph",match:"tr[data-file]",inherit:!1,density:"compact",items:b=>{const h=pe(b.dataset.file);return h?[{header:He(h)},{label:"Compare with the editor",icon:"columns",action:()=>ue(h)},{label:"Restore this version\u2026",icon:"rotate-ccw",action:()=>Le(h.filename)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>st(h)}]:[]}});a(()=>t?.destroy?.()),e.addEventListener("click",b=>{const h=b.target.closest("tr[data-file]");if(!h||b.target.matches("input"))return;const P=b.target.closest(".pe-kebab");if(P){$e(P);return}const A=pe(h.dataset.file);A&&ue(A)}),e.addEventListener("keydown",b=>{const h=b.target.closest("tr[data-file]");if(h){if(b.key==="F10"&&b.shiftKey)b.preventDefault(),$e(h);else if(b.key==="Enter"&&b.target===h){const P=pe(h.dataset.file);P&&ue(P)}}})}Ht(y("#editor-body"),y("#editor-divider"),a),ft({image:e=>Kt(e),scribe:e=>gt(e),card:e=>rt.open(e,{onMediaBrowse:()=>g.media.list().catch(()=>[])}),timeline:e=>lt.open(e,{onPreview:async t=>{try{return await g.pages.preview(t)}catch{return null}},onListCollections:()=>g.collections.list().catch(()=>[]),onGetCollection:t=>g.collections.get(t).catch(()=>null)})}),ie.onEffects(e=>ut.open(e)),ie.onHelp(()=>je()),ie.onTutorial(e=>ht(e));const Ie=e=>{(e.ctrlKey||e.metaKey)&&!e.shiftKey&&!e.altKey&&e.key.toLowerCase()==="s"&&u()&&(e.preventDefault(),fe())};document.addEventListener("keydown",Ie),a(()=>document.removeEventListener("keydown",Ie));const J=c.querySelector(".dm-plugin-banner-name");if(J){const e=J.lastChild,t=Tt(f,m?.title,d);e&&e.nodeType===3&&J.querySelector(".dm-plugin-banner-sep")?e.textContent=t:J.textContent=`Pages \u203A ${t}`}B(s,{id:"page-editor-back",icon:"arrow-left",title:"Back to Pages",onClick:()=>R.navigate("/pages")}),B(s,{id:"page-editor-help",icon:"help-circle",title:"Editor reference",onClick:()=>je()}),f&&B(s,{id:"page-editor-view",icon:"external-link",title:"View on the site",onClick:()=>window.open(o.urlPath.peek()||d,"_blank","noopener")}),B(s,{id:"page-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>fe()}),B(s,{id:"page-editor-settings",icon:"settings",title:"Editor settings - font, size, spacing",onClick:()=>{ke=Ft(we)}}),a(Be(Ne,{position:"bottom"})),Domma.icons.scan();let ze=null;try{ze=S.get(W)}catch{}const N=Dt(ze);if(N&&(ve(),$(N.state)!==n.base.peek())){const e=N.at?` (${Z(N.at)})`:"",t=xt(N,n.base.peek())?" The saved page has changed since - restoring puts your version back over it (the saved one stays in History).":"";await E.confirm(`You left unsaved changes to this page${e}. Restore them?${t}`)&&u()&&Oe(N.state)}}};function Vt(s,i,r){s.replaceChildren(...String(i).split(`
|
|
87
|
+
`).map((a,l)=>{const p=document.createElement("span");p.className="editor-line-number-row";const v=document.createElement("span");v.className="fold-toggle fold-toggle--empty";const c=document.createElement("span");return c.className="editor-line-num",c.textContent=String(l+1),p.append(v,c),p})),s.scrollTop=r.scrollTop}function Ht(s,i,r){if(!s||!i)return;const a=15,l=85,p=d=>Math.min(l,Math.max(a,d));let v=p(parseFloat(q().splitRatio)||50);const c=d=>{s.style.setProperty("--dm-editor-split",v+"%"),i.setAttribute("aria-valuenow",String(Math.round(v))),d&&Q("splitRatio",v)};c(!1);const u=d=>{const f=s.getBoundingClientRect();f.width&&(v=p((d.clientX-f.left)/f.width*100)),c(!1)},T=()=>{document.removeEventListener("pointermove",u),document.removeEventListener("pointerup",T),s.classList.remove("is-resizing"),i.classList.remove("is-dragging"),c(!0)};r(T),i.addEventListener("pointerdown",d=>{window.matchMedia("(max-width: 768px)").matches||(d.preventDefault(),s.classList.add("is-resizing"),i.classList.add("is-dragging"),document.addEventListener("pointermove",u),document.addEventListener("pointerup",T))}),i.addEventListener("dblclick",()=>{v=50,c(!0)}),i.addEventListener("keydown",d=>{const f=d.shiftKey?10:2;let k=v;if(d.key==="ArrowLeft")k=v-f;else if(d.key==="ArrowRight")k=v+f;else if(d.key==="Home")k=a;else if(d.key==="End")k=l;else if(d.key==="Enter"||d.key===" ")k=50;else return;d.preventDefault(),v=p(k),c(!0)})}async function Kt(s){const i=(await g.media.list().catch(()=>[])).filter(p=>/\.(png|jpe?g|gif|webp|svg)$/i.test(p.name)).map(p=>({url:p.url,name:p.name})),r=j(`
|
|
88
88
|
<div>
|
|
89
89
|
<p class="text-muted p-3" hidden data-bind-hidden="images.length">No images uploaded yet.</p>
|
|
90
90
|
<div class="media-picker-grid" data-each="images key=url">
|
|
@@ -92,4 +92,4 @@ The quick brown fox - 0O 1lI {} [] () => ==
|
|
|
92
92
|
<img data-bind-src="url" data-bind-alt="name"><span data-bind-text="name"></span>
|
|
93
93
|
</div>
|
|
94
94
|
</div>
|
|
95
|
-
</div>`),
|
|
95
|
+
</div>`),a=E.slideover({title:"Insert Image",size:"lg",position:"right"}),l=M.applyBindings({images:i,pick:p=>{pt(s,``),a.close(),s.dispatchEvent(new Event("input",{bubbles:!0}))}},r);G(a,()=>l.dispose()),a.element.appendChild(r),a.open()}function _t(s,i,r){const a=Object.entries(i||{});return s?(s.replaceChildren(...a.map(([l,p])=>new Option(p?.label||l,l))),a.some(([l])=>l===r)?r:a[0]?.[0]||r):r}async function Ut(s,i){if(!s)return;let r=[];try{const u=await g.get("/collections/roles/entries?limit=100");r=Array.isArray(u)?u:u?.entries||[]}catch{}const a=r.map(u=>({name:u.data?.name,label:u.data?.label||u.data?.name,level:u.data?.level??99})).filter(u=>u.name&&u.level!==0).sort((u,T)=>T.level-u.level),l=(u,T)=>Object.assign(document.createElement("option"),{value:u,textContent:T}),p=()=>{typeof i=="string"&&i&&![...s.options].some(u=>u.value===i)&&s.append(l(i,`${i} (custom)`))};if(!a.length)return p();const v=document.createElement("optgroup");v.label="Role and above",a.forEach(u=>v.append(l(u.name,`${u.label} and above`)));const c=document.createElement("optgroup");c.label="Role only",a.forEach(u=>c.append(l(`=${u.name}`,`${u.label} only`))),s.replaceChildren(l("public","Public - everyone"),v,c,l("private","Private - super-admin only")),p()}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
const a=t=>String(t??"").replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n]),l=t=>/^[a-z0-9-]+$/i.test(String(t||""))?t:"package";export function parseGuideHash(t){const n=String(t||"").match(/^#\/docs\/plugins\/([^/?#]+)(?:\/([^/?#]+))?/);return n?{name:decodeURIComponent(n[1]),page:n[2]?decodeURIComponent(n[2]):null}:null}function d(){Domma.icons.scan(),Domma.syntax?.scan?.()}export const pluginGuidesView={templateUrl:"/admin/js/templates/plugin-guides.html",async onMount(t){const n=t.find("#pg-grid").get(0);let o=[];try{o=(await H.get("/api/documentation/plugins"))?.guides??[]}catch{o=[]}if(n?.isConnected){if(!o.length){t.find("#pg-empty").get(0)?.removeAttribute("hidden"),d();return}n.innerHTML=o.map(e=>{const s=e.pages.length>1?`<p class="pg-card-pages">${e.pages.map(p=>a(p.title)).join(" \xB7 ")}</p>`:"";return`<a href="#/docs/plugins/${encodeURIComponent(e.name)}" class="card pg-card">
|
|
2
|
+
<div class="card-body">
|
|
3
|
+
<h3><span data-icon="${a(l(e.icon))}"></span> ${a(e.displayName)}</h3>
|
|
4
|
+
<p class="text-muted">${a(e.description)}</p>${s}
|
|
5
|
+
</div></a>`}).join(""),d()}}},pluginGuideView={templateUrl:"/admin/js/templates/plugin-guide.html",async onMount(t){const n=parseGuideHash(location.hash),o=t.find("#pg-body").get(0);if(!n||!o)return;let e=null,s="";try{const i=`/api/documentation/plugins/${encodeURIComponent(n.name)}`+(n.page?`/${encodeURIComponent(n.page)}`:"");e=await H.get(i)}catch(i){s=i?.status===403||/403|forbidden/i.test(String(i?.message))?"Your role cannot use this plugin, so its guide is not shown.":"There is no guide here. The plugin may be disabled, or the page may have moved."}if(!o.isConnected)return;if(!e?.page){o.innerHTML=`<p class="text-muted">${a(s||"There is no guide here.")}</p><p><a href="#/docs/plugins">See all plugin guides</a></p>`,d();return}t.find("#pg-title").get(0).textContent=`${e.displayName} - ${e.page.title}`;const p=t.find("#pg-icon").get(0);p&&(p.innerHTML=`<span data-icon="${a(l(e.icon))}"></span>`),document.title=`${e.displayName} guide - Domma CMS`;const c=[e.version&&`Version ${e.version}`,e.tier==="free"?"Free plugin":e.tier==="paid"?"Paid plugin":""].filter(Boolean).join(" \xB7 ");if(t.find("#pg-meta").get(0).textContent=[c,e.description].filter(Boolean).join(" - "),e.toolUrl){const i=t.find("#pg-tool").get(0);i.setAttribute("href",e.toolUrl),t.find("#pg-tool-text").get(0).textContent=`Open ${e.displayName}`,i.removeAttribute("hidden")}if(e.pages.length>1){const i=t.find("#pg-pages").get(0);i.innerHTML=e.pages.map(g=>{const r=g.slug===e.page.slug;return`<a href="#/docs/plugins/${encodeURIComponent(e.name)}/${encodeURIComponent(g.slug)}" class="btn btn-sm ${r?"btn-secondary":"btn-ghost"}"${r?' aria-current="page"':""}>${a(g.title)}</a>`}).join(""),i.removeAttribute("hidden")}o.innerHTML=e.page.html,d()}};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as b}from"../api.js";import{openQuickCreate as z,INLINE_TYPES as K}from"../lib/project-quick-create.js";import{addBannerAction as j}from"../lib/plugin-chrome.js";import{helpPopovers as V}from"../lib/help-popover.js";import{openMenuAt as U,viewLifetime as Q}from"../lib/tool-kit.js";import{countsOf as N,projectFromPath as
|
|
1
|
+
import{api as b}from"../api.js";import{openQuickCreate as z,INLINE_TYPES as K}from"../lib/project-quick-create.js";import{addBannerAction as j}from"../lib/plugin-chrome.js";import{helpPopovers as V}from"../lib/help-popover.js";import{openMenuAt as U,viewLifetime as Q}from"../lib/tool-kit.js";import{countsOf as N,projectFromPath as P,relTime as Y,tilesFor as _}from"../lib/project-page-arrange.js";import{fmtDate as G,fmtDateTime as J}from"../lib/dates.js";const q=/^#\/projects\/(?!new(?:[/?#]|$))[^/?#]+\/?(\?.*)?$/,c=Q(q),W=`
|
|
2
2
|
<div class="pdx-tile" data-key="{{key}}" tabindex="0" data-bind-class="cls" style="--c: {{tone}}">
|
|
3
3
|
<span class="pdx-tile-chip" aria-hidden="true"><span data-icon="{{icon}}" data-icon-size="20"></span></span>
|
|
4
4
|
<span class="pdx-tile-body"><strong data-bind-text="countText"></strong><small>{{label}}</small></span>
|
|
@@ -6,4 +6,4 @@ import{api as b}from"../api.js";import{openQuickCreate as z,INLINE_TYPES as K}fr
|
|
|
6
6
|
<button type="button" class="pdx-add" title="New {{singular}}" aria-label="New {{singular}}">+</button>
|
|
7
7
|
<button type="button" class="pdx-kebab" title="Actions" aria-label="Actions for {{label}}">\u22EE</button>
|
|
8
8
|
</span>
|
|
9
|
-
</div>`;export function setBannerSection(s,o){const a=s.querySelector(".dm-plugin-banner-name")?.querySelector(".dm-plugin-banner-sep");if(!a)return;const l=a.nextSibling;l&&l.nodeType===3?l.nodeValue=o:a.after(document.createTextNode(o))}export const projectDetailView={templateUrl:"/admin/js/templates/project-detail.html",async onMount(s){c.reset();const o=
|
|
9
|
+
</div>`;export function setBannerSection(s,o){const a=s.querySelector(".dm-plugin-banner-name")?.querySelector(".dm-plugin-banner-sep");if(!a)return;const l=a.nextSibling;l&&l.nodeType===3?l.nodeValue=o:a.after(document.createTextNode(o))}export const projectDetailView={templateUrl:"/admin/js/templates/project-detail.html",async onMount(s){c.reset();const o=P(location.hash);if(!o){location.hash="#/projects";return}const r=s.get(0).querySelector(".pdx")||s.get(0),a=()=>q.test(location.hash)&&r.isConnected&&P(location.hash)===o,l=encodeURIComponent(o),h=M.observable(null),y=M.observable({}),v=M.observable(!0),g=M.observable(""),d=M.observable(!1),p=M.observable([]),w=M.observable("");c.add(M.effect(()=>{const e=_(y.value,{slug:o,inline:K});p.value=e.tiles,w.value=v.value?"":e.totalText}));const n=()=>h.value||{},$={loading:v,loadError:g,busy:d,tiles:p,totalText:w,name:M.computed(()=>n().name||o),description:M.computed(()=>n().description||""),off:M.computed(()=>h.value&&n().enabled===!1),builtIn:M.computed(()=>n().protected===!0),rootUrl:M.computed(()=>n().rootUrl||""),rootText:M.computed(()=>n().rootUrl?`Pages under ${n().rootUrl}`:"No root URL - pages are filed by hand"),created:M.computed(()=>n().createdAt?G(n().createdAt):""),creator:M.computed(()=>n().createdBy||""),updated:M.computed(()=>Y(n().updatedAt)),updatedTitle:M.computed(()=>n().updatedAt?J(n().updatedAt):"")},C=()=>{try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}};async function u(){try{const[e,t]=await Promise.all([b.projects.get(o),b.projects.artefacts(o).catch(()=>({}))]);if(!a())return;h.value=e,y.value=N(t),g.value="",setBannerSection(s.get(0),e?.name||o);const m=r.querySelector(".pdx-hero-icon");if(m){m.replaceChildren();const k=document.createElement("span");k.setAttribute("data-icon",e?.icon||"folder"),k.setAttribute("data-icon-size","32"),m.appendChild(k),I.scan(m)}}catch(e){if(!a())return;if(/not found|404/i.test(String(e?.message||e))){E.toast("Project not found.",{type:"error"}),location.hash="#/projects";return}g.value=`Could not load the project: ${e.message||e}`}finally{v.value=!1}}async function B(){const e=await b.projects.artefacts(o).catch(()=>null);!a()||!e||(y.value=N(e),C())}async function D(){if(!d.peek()){d.value=!0;try{await b.projects.update(o,{enabled:!0}),E.toast(`"${n().name||o}" enabled - its public pages and API are back.`,{type:"success"}),C(),a()&&await u()}catch(e){E.toast(`Not enabled: ${e.message||e}`,{type:"error"})}finally{d.value=!1}}}const x=e=>p.peek().find(t=>t.key===e),F=()=>({slug:o,name:n().name||o,rootUrl:n().rootUrl||""});function f(e,{inEditor:t=!1}={}){if(e){if(e.inline&&!t){z({type:e.key,project:F(),onCreated:B});return}location.hash=e.newHash}}const A=e=>{e?.has&&(location.hash=e.listHash)},T=e=>{const t=x(e);t&&(t.has?A(t):f(t))};function O(e){const t=x(e);return t?[{header:`${t.label} \xB7 ${t.count}`},{label:"Open the list",icon:"list",disabled:!t.has,action:()=>A(t)},{label:`New ${t.singular}\u2026`,icon:"plus",action:()=>f(t)},...t.inline?[{label:`New ${t.singular} in its editor`,icon:"edit",action:()=>f(t,{inEditor:!0})}]:[]]:[]}const i=r.querySelector(".pdx-tiles");i.innerHTML=W;const R=M.applyBindings($,r,{methods:{enable:D,retry:()=>u()}});c.add(()=>R.dispose()),c.add(V(r,{position:"bottom"})),c.add(M.effect(()=>{p.value,requestAnimationFrame(()=>{i.isConnected&&I.scan(i)})}));const H=E.contextMenu(i,{cursor:"glyph",match:".pdx-tile",inherit:!1,density:"compact",items:e=>O(e.dataset.key)});c.add(()=>H?.destroy?.());const L=e=>{const t=e.target.closest(".pdx-tile");if(t){if(e.target.closest(".pdx-kebab")){U(e.target.closest(".pdx-kebab"));return}if(e.target.closest(".pdx-add")){f(x(t.dataset.key));return}T(t.dataset.key)}},S=e=>{const t=e.target.closest(".pdx-tile");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),U(t)):e.key==="Enter"&&e.target===t&&T(t.dataset.key))};i.addEventListener("click",L),i.addEventListener("keydown",S),c.add(()=>{i.removeEventListener("click",L),i.removeEventListener("keydown",S)}),j(s,{id:"project-overview-settings",icon:"settings",title:"Project settings",onClick:()=>{location.hash=`#/projects/${l}/settings`}}),j(s,{id:"project-overview-refresh",icon:"refresh-cw",title:"Refresh",onClick:()=>u()}),j(s,{id:"project-overview-all",icon:"folder",title:"All projects",onClick:()=>{location.hash="#/projects"}}),I.scan(r),await u(),a()&&I.scan(r)}};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{api as m}from"../api.js";import{makeIconInput as
|
|
1
|
+
import{api as m}from"../api.js";import{makeIconInput as fe}from"../lib/shortcode-modal.js";import{addBannerAction as A}from"../lib/plugin-chrome.js";import{helpPopovers as me}from"../lib/help-popover.js";import{sidebarCount as he}from"../lib/tool-kit.js";import{countsOf as oe,draftKey as U,projectFromPath as ve,settingsForm as B,settingsPayload as ge,settingsRules as ye,settingsSnapshot as re,untagSummary as be}from"../lib/project-page-arrange.js";import{setBannerSection as ne}from"./project-detail.js";import{fmtDateTime as ke}from"../lib/dates.js";const K=r=>String(r??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),C=r=>String(r||"").replace(/^#/,"").split("?")[0].replace(/\/+$/,"");let c=null,ae=!1;function je(){ae||!window.R?.use||(ae=!0,R.use((r,t,h)=>!c||C(t?.path)!==c.path||C(r?.path)===c.path||!c.dirty()?h():E.confirm("You have unsaved changes to this project. Leave anyway? They are kept as a draft for your next visit.").then($=>{$?h():history.replaceState(null,"",`#${t.path}`)})),M.subscribe("router:afterChange",({to:r}={})=>{if(c&&C(r?.path)!==c.path){const t=c;c=null,t.dispose()}}))}export const projectSettingsView={templateUrl:"/admin/js/templates/project-settings.html",async onMount(r){if(je(),c){const e=c;c=null,e.dispose()}const t=ve(location.hash,{settings:!0});if(!t){location.hash="#/projects";return}const h=encodeURIComponent(t),$=`/projects/${h}/settings`,N=[],v=e=>(N.push(e),e);let Y=!1;const p=r.get(0).querySelector(".psx")||r.get(0),O=()=>!Y&&p.isConnected,V=E.loader(r.get(0),{type:"dots"});let g,H={};try{[g,H]=await Promise.all([m.projects.get(t),m.projects.artefacts(t).then(oe).catch(()=>({}))])}catch(e){V.destroy(),E.toast(`Could not load the project: ${e.message||e}`,{type:"error"}),location.hash="#/projects";return}if(V.destroy(),!p.isConnected)return;const n=M.observable(g),d=ye(g),W=B(g),a=Object.fromEntries(Object.entries(W).map(([e,o])=>[e,M.observable(o)])),z=()=>Object.fromEntries(Object.keys(a).map(e=>[e,a[e].value])),G=()=>Object.fromEntries(Object.keys(a).map(e=>[e,a[e].peek()])),y=M.observable(W),x=M.computed(()=>re(y.value)),j=M.computed(()=>re(z())!==x.value),P=M.computed(()=>ge(z(),n.value)),J=e=>M.computed(()=>P.value.errors[e]||""),b=M.observable(!1),l=M.observable(!1),w=M.observable(""),D=M.observable(H),k=M.computed(()=>be(D.value)),se={...a,saving:b,busy:l,dirty:j,errorText:w,slug:t,rootLocked:d.rootLocked,canToggle:d.canToggle,canDelete:d.canDelete,canUntag:d.canUntag,dangerShown:d.canDelete||d.canUntag,nameErr:J("name"),rootErr:J("rootUrl"),cannot:M.computed(()=>b.value||Object.keys(P.value.errors).length>0),filedText:M.computed(()=>{const e=k.value;if(!e.total)return"Nothing is filed under this project.";const o=n.value.rootUrl&&(D.value.pages||0)>0?` Pages under ${n.value.rootUrl} belong here by their address - untagging does not move them; clear the root URL (and save) for that.`:"";return`${e.total} filed here: ${e.text}.${o}`}),noneFiled:M.computed(()=>k.value.total===0),deleteBlocked:M.computed(()=>k.value.total>0||l.value),deleteHint:M.computed(()=>k.value.total?"Untag what is filed here first - a project can only be deleted once nothing is filed under it.":"Deleting removes the project record. It cannot be undone.")},L=()=>{try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}he("#/projects")},T=()=>{try{S.remove?S.remove(U(t)):S.set(U(t),null)}catch{}};async function F(){if(b.peek())return;const{errors:e,payload:o}=P.peek();if(Object.keys(e).length){E.toast("Fix the marked fields first.",{type:"warning"});return}b.value=!0,w.value="";try{const s=await m.projects.update(t,o);if(!O())return;n.value=s||{...n.peek(),...o},y.value=B(n.peek());for(const[f,q]of Object.entries(y.peek()))a[f].value=q;T(),ne(r.get(0),n.peek().name||t),L(),E.toast("Project saved.",{type:"success"})}catch(s){w.value=s.message||"Could not save the project.",E.toast("Not saved - see the message at the top.",{type:"error"})}finally{b.value=!1}}async function ce(){await E.confirm("Throw away your unsaved changes to this project?")&&(Q(y.peek()),w.value="")}function Q(e){for(const[o,s]of Object.entries(e))a[o]&&(a[o].value=s);i&&i.input.value!==String(a.icon.peek())&&(i.input.value=String(a.icon.peek()))}async function X(){const e=await m.projects.artefacts(t).then(oe).catch(()=>null);O()&&e&&(D.value=e)}async function ie(){if(l.peek())return;const e=k.peek();if(!e.total)return;const o=K(n.peek().name||t);if(await E.confirm(`Untag everything filed under "${o}" (${K(e.text)})? It all stays where it is and belongs to Core from then on. The project itself is kept.`)){l.value=!0;try{const s=await m.projects.untagAll(t),f=Object.values(s?.untagged||{}).reduce((q,de)=>q+(Number(de)||0),0);E.toast(`Untagged ${f} artefact${f===1?"":"s"}.${n.peek().rootUrl?` Pages under ${n.peek().rootUrl} stay - they belong here by their address.`:""}`,{type:"success",duration:6e3}),L(),await X()}catch(s){E.toast(`Untag failed: ${s.message||s}`,{type:"error"})}finally{l.value=!1}}}async function le(){if(l.peek())return;const e=K(n.peek().name||t);if(await E.confirm(`Delete the project "${e}"? This cannot be undone.`)){l.value=!0;try{await m.projects.remove(t),T(),y.value=G(),E.toast(`Deleted "${n.peek().name||t}".`,{type:"success"}),L(),location.hash="#/projects"}catch(o){E.toast(`Not deleted: ${o.message||o}`,{type:"error",duration:7e3}),await X()}finally{l.value=!1}}}const ue=p.querySelector(".psx-icon-mount"),i=fe("folder, users, box\u2026",a.icon.peek());i.input.id="psx-icon",i.input.classList.add("form-input");const Z=()=>{a.icon.value=i.input.value};i.input.addEventListener("input",Z),v(()=>i.input.removeEventListener("input",Z)),ue.appendChild(i.el),p.querySelector("#psx-slug").value=t;const pe=M.applyBindings(se,p,{methods:{save:F,discard:ce,untagAll:ie,remove:le}});v(()=>pe.dispose()),v(me(p,{position:"bottom"})),ne(r.get(0),g.name||t),A(r,{id:"project-settings-overview",icon:"eye",title:"Project overview",onClick:()=>{location.hash=`#/projects/${h}`}}),A(r,{id:"project-settings-all",icon:"folder",title:"All projects",onClick:()=>{location.hash="#/projects"}}),A(r,{id:"project-settings-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>F()});const _=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),F())};document.addEventListener("keydown",_,!0),v(()=>document.removeEventListener("keydown",_,!0));const ee=()=>{if(j.peek())try{S.set(U(t),{form:G(),base:x.peek(),at:new Date().toISOString()})}catch{}},te=e=>{j.peek()&&(ee(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",te),v(()=>window.removeEventListener("beforeunload",te)),c={path:C($),dirty:()=>j.peek(),dispose(){Y=!0,ee(),N.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(p);let u=null;try{u=S.get(U(t))}catch{}if(u?.form){T();const e=u.base&&u.base!==x.peek(),o=u.at?` (${ke(u.at)})`:"",s=e?`You left unsaved changes to this project${o}, but it has been changed since. Put your changes back over the current settings?`:`You left unsaved changes to this project${o}. Pick up where you left off?`;if(await E.confirm(s)&&O()){const f=B({...n.peek(),...u.form,enabled:u.form.enabled!==!1});Q(f)}}}};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as O,apiRequest as
|
|
1
|
+
import{api as O,apiRequest as De,getUser as Ue}from"../api.js";import{addBannerAction as be}from"../lib/plugin-chrome.js";import{helpPopovers as Be}from"../lib/help-popover.js";import{openMenuAt as ke,sidebarCount as Pe}from"../lib/tool-kit.js";import{BADGE_OPTIONS as Se}from"../lib/roles-arrange.js";import{checkRole as Ke,expandPerms as He,formFromRole as W,matrixRows as Ge,payloadFromForm as _e,roleFacts as Ve,selfRisks as Ye,setActions as xe,snapshot as F,summaryLines as Je,toggleAction as We}from"../lib/role-editor-arrange.js";import{fmtDateTime as ze}from"../lib/dates.js";const Qe=/^\/roles\/edit\/[^/?#]+(\?.*)?$/,P=o=>String(o||"").split("?")[0],T=o=>`role-editor.draft.${o}`,w=["name","label","level","badgeClass","project","adminAllow","adminHome"],Xe=`
|
|
2
2
|
<div class="rex-row" data-bind-class="kind === 'group' ? 'is-group' : ''" data-id="{{id}}" data-kind="{{kind}}" data-key="{{key}}" data-group="{{group}}" tabindex="0">
|
|
3
3
|
<span class="rex-icon" aria-hidden="true"><span data-icon="{{icon}}" data-icon-size="15"></span></span>
|
|
4
4
|
<span class="rex-main"><strong>{{label}}</strong><small>{{description}}</small></span>
|
|
@@ -6,6 +6,6 @@ import{api as O,apiRequest as Ue,getUser as Be}from"../api.js";import{addBannerA
|
|
|
6
6
|
<button type="button" class="rex-full" data-state="{{state}}" data-bind-hidden="kind === 'group'" data-bind-disabled="locked" title="Every action in this area">Full</button>
|
|
7
7
|
<span class="rex-state">{{meta}}</span>
|
|
8
8
|
<button type="button" class="rex-kebab" aria-label="Actions for {{label}}" title="Actions">\u22EE</button>
|
|
9
|
-
</div>`,Ze="<li>{{text}}</li>",et='<div class="rex-sum"><strong>{{title}}</strong><span>{{text}}</span></div>',
|
|
10
|
-
`).map(t=>t.trim()).filter(Boolean);
|
|
11
|
-
`).map(t=>t.trim()).filter(Boolean);return e.length?e.length===1?`Confined to ${e[0]}`:`Confined to ${e.length} screens`:""}),grantedCount:M.computed(()=>r.isRoot?"all":String(n.value.length)),grantedText:M.computed(()=>r.isRoot?"Every permission (root)":`${n.value.length} of ${
|
|
9
|
+
</div>`,Ze="<li>{{text}}</li>",et='<div class="rex-sum"><strong>{{title}}</strong><span>{{text}}</span></div>',Ee=o=>String(o??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),z=(o,c,p=`${c}s`)=>`${o} ${o===1?c:p}`;function we(o,c){const p=o.querySelector(".dm-plugin-banner-name");if(!p)return;const d=p.querySelector(".dm-plugin-banner-parent"),C=p.querySelector(".dm-plugin-banner-sep");d&&C&&p.replaceChildren(d,C,document.createTextNode(c))}let v=null,Me=!1;function tt(){Me||!window.R?.use||(Me=!0,R.use((o,c,p)=>!(Qe.test(c?.path||"")&&P(o?.path)!==P(c?.path))||!v?.dirty()?p():E.confirm("This role has unsaved changes. Leave anyway? They are kept as a draft for your next visit.").then(C=>{C?p():v&&history.replaceState(null,"",v.hash)})),M.subscribe("router:afterChange",({to:o}={})=>{if(v&&P(o?.path)!==v.path){const c=v;v=null,c.dispose()}}))}export const roleEditorView={templateUrl:"/admin/js/templates/role-editor.html",async onMount(o){if(tt(),v){const e=v;v=null,e.dispose()}const c=Ue();if(!c){R.navigate("/");return}const p=location.hash.match(/^#\/roles\/edit\/([^/?#]+)/),d=p?decodeURIComponent(p[1]):"";if(!d){R.navigate("/roles");return}const C=[],f=e=>(C.push(e),e);let Q=!1;const m=o.get(0).querySelector(".rex")||o.get(0),N=()=>!Q&&m.isConnected,X=E.loader(o.get(0),{type:"dots"});let Z,K,ee,j,H;try{[Z,K,ee,j,H]=await Promise.all([O.get(`/collections/roles/entries/${encodeURIComponent(d)}`),O.get("/auth/permissions-registry"),O.projects.list().catch(()=>[]),O.get("/collections/roles/entries?limit=1000").catch(()=>({entries:[]})),De("/roles/_holders",{method:"GET"}).catch(()=>null)])}catch(e){X.destroy(),E.toast(`Could not load the role: ${e.message||e}`,{type:"error"}),R.navigate("/roles");return}if(X.destroy(),!m.isConnected)return;const h=K.resources||[],te=K.groups||[];let a=Z.data||{};const r=Ve(a),oe=j?.entries||(Array.isArray(j)?j:[]),Re=oe.map(e=>e.data?.name).filter(Boolean),G=e=>Number(H?.counts?.[e]||0),ae=[c.role,...Array.isArray(c.additionalRoles)?c.additionalRoles:[]].filter(Boolean),ne=oe.map(e=>e.data||{}).filter(e=>ae.includes(e.name)&&e.name!==a.name),re={holds:ae.includes(a.name),otherRoot:ne.some(e=>e.level===0),otherGranted:[...new Set(ne.flatMap(e=>He(e.permissions,h)))]},$=W(a,h),l={};for(const e of w)l[e]=M.observable($[e]);const n=M.observable($.granted),D=()=>({...Object.fromEntries(w.map(e=>[e,l[e].value])),granted:n.value}),se=()=>({...Object.fromEntries(w.map(e=>[e,l[e].peek()])),granted:n.peek()}),L=M.observable(F($)),b=M.observable(a.name||""),le=M.observable(a.level),U=M.computed(()=>F(D())!==L.value),B=(e,t)=>JSON.stringify(t.map(s=>JSON.parse(e)[s])),Ce=M.computed(()=>B(F(D()),w)!==B(L.value,w)),$e=M.computed(()=>B(F(D()),["granted"])!==B(L.value,["granted"])),A=M.observable(!1),_=M.computed(()=>Ke(D(),{facts:r,taken:Re,originalName:b.value})),Le=M.computed(()=>A.value||Object.keys(_.value).length>0),ie=M.observable(""),q=M.observable("all"),V=M.observable([]);f(M.effect(()=>{V.value=Ge(h,te,n.value,{query:ie.value,show:q.value,locked:r.isRoot})}));const k=m.querySelector(".rex-matrix");f(M.effect(()=>{V.value,requestAnimationFrame(()=>{k.isConnected&&I.scan(k)})}));const Te=h.reduce((e,t)=>e+(t.actions||[]).length,0),Y=M.computed(()=>Ye({...re,oldLevel:le.value,newLevel:Number.parseInt(l.level.value,10),granted:n.value,isRoot:r.isRoot})),ce=M.observable([]);f(M.effect(()=>{ce.value=Y.value.map((e,t)=>({id:String(t),text:e}))}));const de=M.observable([]);f(M.effect(()=>{const e=String(l.adminAllow.value||"").split(`
|
|
10
|
+
`).map(t=>t.trim()).filter(Boolean);de.value=Je(n.value,h,te,{isRoot:r.isRoot,admin:{allow:e,home:String(l.adminHome.value||"").trim()}})}));const Ae={...l,granted:n,dirty:U,generalDirty:Ce,permsDirty:$e,errors:_,cannot:Le,saving:A,query:ie,show:q,rows:V,risks:Y,riskRows:ce,summary:de,isRoot:r.isRoot,nameLockedText:r.nameLocked,pluginText:r.plugin?`From ${r.plugin}`:"",previewLabel:M.computed(()=>String(l.label.value||"").trim()||l.name.value||"This role"),holdersText:M.computed(()=>H?`Held by ${z(G(b.value),"user")}`:""),renameNote:M.computed(()=>{const e=G(b.value),t=String(l.name.value||"").trim();return t&&t!==b.value&&e?`${z(e,"user")} ${e===1?"holds":"hold"} "${b.value}": renamed, they are on a role that no longer exists until you move them under Users.`:""}),scopeText:M.computed(()=>{const e=String(l.adminAllow.value||"").split(`
|
|
11
|
+
`).map(t=>t.trim()).filter(Boolean);return e.length?e.length===1?`Confined to ${e[0]}`:`Confined to ${e.length} screens`:""}),grantedCount:M.computed(()=>r.isRoot?"all":String(n.value.length)),grantedText:M.computed(()=>r.isRoot?"Every permission (root)":`${n.value.length} of ${Te} actions granted`)},ue=m.querySelector("#rex-badge");for(const e of Se){const t=document.createElement("option");t.value=e.value,t.textContent=e.label,ue.appendChild(t)}if($.badgeClass&&!Se.some(e=>e.value===$.badgeClass)){const e=document.createElement("option");e.value=$.badgeClass,e.textContent=$.badgeClass,ue.appendChild(e)}const qe=m.querySelector("#rex-project");for(const e of ee||[]){const t=document.createElement("option");t.value=e.slug,t.textContent=e.name||e.slug,qe.appendChild(t)}async function J(){if(A.peek())return;const e=Object.values(_.peek());if(e.length){E.toast(e[0],{type:"error"});return}const t=[...Y.peek()],s=String(l.name.peek()||"").trim(),u=G(b.peek());if(!r.nameLocked&&s!==b.peek()&&u&&t.push(`${z(u,"user")} ${u===1?"holds":"hold"} "${b.peek()}" - after renaming they are on a role that no longer exists until you move them.`),t.length&&!await E.confirm(`${t.map(y=>Ee(y)).join("<br><br>")}<br><br>Save anyway?`)||!N())return;A.value=!0;const i=_e(se(),a,{facts:r,resources:h});try{if(await O.put(`/collections/roles/entries/${encodeURIComponent(d)}`,{data:i}),!N())return;a=i;const y=W(a,h);for(const ye of w)l[ye].value=y[ye];n.value=y.granted,L.value=F(y),b.value=a.name,le.value=a.level;try{S.remove?S.remove(T(d)):S.set(T(d),null)}catch{}if(we(o.get(0),a.label||a.name),Pe("#/roles"),re.holds){try{document.dispatchEvent(new CustomEvent("dm:sidebar-rebuild"))}catch{}try{document.dispatchEvent(new CustomEvent("dm:admin-home-changed"))}catch{}}E.toast(`Saved "${a.label||a.name}".`,{type:"success"})}catch(y){E.toast(`Not saved: ${y.message||y}`,{type:"error"})}finally{A.value=!1}}async function Oe(){if(!await E.confirm("Throw away the changes you have not saved?")||!N())return;const e=W(a,h);for(const t of w)l[t].value=e[t];n.value=e.granted}const pe=e=>e?.closest?.(".rex-row");function x(e,t){if(r.isRoot||!e)return;const{kind:s,key:u,group:i}=e.dataset;s==="group"?n.value=xe(n.peek(),h,{group:i,...t}):n.value=xe(n.peek(),h,{resource:u,...t})}const Fe=e=>{const{kind:t,key:s,group:u}=e.dataset,i=r.isRoot,y=e.querySelector(".rex-main strong")?.textContent||s||u;return t==="group"?[{header:`${u} - every area`},{label:"Grant everything",icon:"check-circle",disabled:i,action:()=>x(e,{to:!0})},{label:"View only",icon:"eye",disabled:i,action:()=>x(e,{only:"read"})},{label:"Grant nothing",icon:"x-circle",disabled:i,action:()=>x(e,{to:!1})}]:[{header:y},{label:"Full access",icon:"check-circle",disabled:i,action:()=>x(e,{to:!0})},{label:"View only",icon:"eye",disabled:i,action:()=>x(e,{only:"read"})},{label:"No access",icon:"x-circle",disabled:i,action:()=>x(e,{to:!1})},{type:"divider"},{label:"Copy permission key",icon:"copy",action:()=>{navigator.clipboard?.writeText(s).then(()=>E.toast(`Copied "${s}".`,{type:"success"}),()=>E.toast(s,{type:"info",duration:6e3}))}}]};k.innerHTML=Xe,m.querySelector(".rex-risks").innerHTML=Ze,m.querySelector(".rex-summary").innerHTML=et;const Ie=E.tabs(m.querySelector(".rex-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),Ne=M.applyBindings(Ae,m,{methods:{save:()=>J(),discard:()=>Oe(),showAll:()=>{q.value="all"},showOn:()=>{q.value="on"},showOff:()=>{q.value="off"}}});f(()=>Ne.dispose()),f(Be(m,{position:"bottom"}));const je=E.contextMenu(k,{cursor:"glyph",match:".rex-row",inherit:!1,density:"compact",items:Fe});f(()=>je?.destroy?.());const me=e=>{const t=pe(e.target);if(!t)return;const s=e.target.closest(".rex-kebab");if(s){ke(s);return}if(r.isRoot)return;const u=e.target.closest(".rex-act");if(u){n.value=We(n.peek(),`${t.dataset.key}.${u.dataset.act}`);return}e.target.closest(".rex-full")&&x(t,{to:e.target.closest(".rex-full").dataset.state!=="all"})},ve=e=>{const t=pe(e.target);if(t){if(e.key==="F10"&&e.shiftKey){e.preventDefault(),ke(t);return}e.key==="Enter"&&e.target===t&&t.dataset.kind==="res"&&!r.isRoot&&(e.preventDefault(),x(t,{to:t.querySelector(".rex-full")?.dataset.state!=="all"}))}};k.addEventListener("click",me),k.addEventListener("keydown",ve),f(()=>{k.removeEventListener("click",me),k.removeEventListener("keydown",ve)}),we(o.get(0),a.label||a.name||"Edit"),be(o,{id:"role-editor-back",icon:"arrow-left",title:"Back to roles",onClick:()=>{location.hash="#/roles"}}),be(o,{id:"role-editor-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>J()});const fe=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),J())};document.addEventListener("keydown",fe,!0),f(()=>document.removeEventListener("keydown",fe,!0));const he=()=>{if(U.peek())try{S.set(T(d),{form:se(),base:L.peek(),at:new Date().toISOString()})}catch{}},ge=e=>{U.peek()&&(he(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",ge),f(()=>window.removeEventListener("beforeunload",ge)),v={path:P(location.hash.replace(/^#/,"")),hash:location.hash,dirty:()=>U.peek(),dispose(){Q=!0,he(),C.splice(0).reverse().forEach(e=>{try{e()}catch{}})}},I.scan(m);let g=null;try{g=S.get(T(d))}catch{}if(g?.form){try{S.remove?S.remove(T(d)):S.set(T(d),null)}catch{}const e=g.base&&g.base!==L.peek(),t=g.at?` (${ze(g.at)})`:"",s=Ee(a.label||a.name),u=e?`You left unsaved changes to "${s}"${t}, but the role has been saved since. Restore your version anyway?`:`You left unsaved changes to "${s}"${t}. Restore them?`;if(await E.confirm(u)&&N()){for(const i of w)i in g.form&&(l[i].value=String(g.form[i]??""));Array.isArray(g.form.granted)&&(n.value=[...g.form.granted].sort())}}}};
|
package/admin/js/views/search.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import{apiRequest as N}from"../api.js";import{addBannerAction as
|
|
1
|
+
import{apiRequest as N}from"../api.js";import{addBannerAction as Z}from"../lib/plugin-chrome.js";import{helpPopovers as ve}from"../lib/help-popover.js";import{openMenuAt as ee,sidebarCount as me}from"../lib/tool-kit.js";import{POSITION_LABEL as fe,clampNotes as be,formFromSettings as O,liveChips as he,maxRadius as ye,payloadFromForm as c,previewState as te,radiusForSize as ge,resultRow as ke,snapshot as F}from"../lib/search-settings-arrange.js";import{fmtDateTime as Se}from"../lib/dates.js";const U=/^\/search(\?.*)?$/,we="#/search",b="search.draft",xe='<span class="ssx-chip" data-bind-class="cls">{{text}}</span>',Me=`
|
|
2
2
|
<div class="ssx-row" data-url="{{url}}" tabindex="0">
|
|
3
3
|
<span class="ssx-rank">{{rank}}</span>
|
|
4
4
|
<span class="ssx-main"><strong>{{title}}</strong><small>{{meta}}</small><span class="ssx-snippet">{{snippet}}</span></span>
|
|
5
5
|
<span class="ssx-chip">{{score}}</span>
|
|
6
6
|
<button type="button" class="ssx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
|
|
7
|
-
</div>`;let u=null,
|
|
7
|
+
</div>`;let u=null,se=!1;function Ee(){se||!window.R?.use||(se=!0,R.use((i,v,o)=>!(U.test(v?.path||"")&&!U.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&&!U.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,K=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){K.destroy(),E.toast(`Could not load the search settings: ${e.message||e}`,{type:"error"});return}if(K.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(F(B)),L=M.computed(()=>F(p())!==x.value),T=M.observable(!1),Y=M.observable(A?{searchable:A.count}:null),H=M.observable([]);o(M.effect(()=>{H.value=he(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=ge(s.iconRadius.peek(),m,e);m=e,String(t)!==String(s.iconRadius.peek())&&(s.iconRadius.value=String(t))}));const oe=M.computed(()=>ye(s.iconSize.value)),V=r.querySelector("#ssx-radius");o(M.effect(()=>{V&&(V.max=String(oe.value))}));const y=M.observable(""),g=M.observable([]),z=M.observable(""),C=M.observable(""),ne=M.computed(()=>te(y.value,s.minQueryLength.value).note);let _=0,q=null;async function ae(){const e=te(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(ke),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(ae,c(h()).debounceMs)})),o(()=>clearTimeout(q));async function D(){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=F(a);try{S.remove?S.remove(b):S.set(b,null)}catch{}me(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 re(){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`,ie={...s,dirty:L,saving:T,chips:H,query:y,results:g,previewNote:ne,previewError:z,previewMeta:C,clampText:M.computed(()=>be(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(()=>fe[s.position.value]||s.position.value)},ce=E.tabs(r.querySelector(".ssx-tabs"),{tabSelector:".tab-item",panelSelector:".tab-panel"}),le=M.applyBindings(ie,r,{methods:{save:()=>D(),discard:()=>re(),useNavbar:()=>{s.displayMode.value="navbar"},useFloating:()=>{s.displayMode.value="floating"},setPos:e=>{s.position.value=e}}});o(()=>le.dispose()),o(ve(r,{position:"bottom"}));const ue=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}))}}],pe=E.contextMenu(f,{cursor:"glyph",match:".ssx-row",inherit:!1,density:"compact",items:e=>ue(e.dataset.url)});o(()=>pe?.destroy?.());const Q=e=>{const t=e.target.closest(".ssx-row");if(!t)return;const a=e.target.closest(".ssx-kebab");if(a){ee(a);return}window.open(t.dataset.url,"_blank","noopener")},J=e=>{const t=e.target.closest(".ssx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),ee(t)):e.key==="Enter"&&e.target===t&&window.open(t.dataset.url,"_blank","noopener"))};f.addEventListener("click",Q),f.addEventListener("keydown",J),o(()=>{f.removeEventListener("click",Q),f.removeEventListener("keydown",J)}),Z(i,{id:"search-view-site",icon:"external-link",title:"View site",onClick:()=>window.open("/","_blank","noopener")}),Z(i,{id:"search-save",icon:"save",title:"Save (Ctrl+S)",onClick:()=>D()});const W=e=>{!(e.ctrlKey||e.metaKey)||e.key!=="s"||(e.preventDefault(),e.stopImmediatePropagation(),D())};document.addEventListener("keydown",W,!0),o(()=>document.removeEventListener("keydown",W,!0));const G=()=>{if(L.peek())try{S.set(b,{form:h(),base:x.peek(),at:new Date().toISOString()})}catch{}},X=e=>{L.peek()&&(G(),e.preventDefault(),e.returnValue="")};window.addEventListener("beforeunload",X),o(()=>window.removeEventListener("beforeunload",X)),u={dirty:()=>L.peek(),dispose(){P=!0,G(),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?` (${Se(d.at)})`:"",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,de]of Object.entries(n))s[l]&&(s[l].value=de)}}}};
|