domma-cms 1.8.11 → 1.8.13

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.
@@ -1,9 +1,9 @@
1
- import{apiRequest as R,getToken as io}from"../api.js";import{addBannerAction as po}from"../lib/plugin-chrome.js";import{helpPopovers as Mt}from"../lib/help-popover.js";import{onceClosed as co,openMenuAt as B,sidebarCount as q,viewLifetime as uo}from"../lib/tool-kit.js";import{SHAPES as Tt,whileLoading as Et}from"../lib/loading.js";import{fmtDateTime as mo}from"../lib/dates.js";import{applyChoice as ho,chooseAllPeople as $t,choosePerson as vo,choiceItems as St,cleanOptions as xe,counts as Lt,customTypeRows as wo,homeCard as fo,inviteNote as bo,peopleSummary as go,personMenuItems as yo,personRow as xo,reportPersonRow as ko,defaultOptions as _e,draftKey as Mo,draftQuestion as To,historyRow as Eo,internalTypeRows as $o,isDirty as So,kindTabs as Lo,liveDrafts as Pt,optionsSnapshot as Ct,PLAN_FILTERS as Rt,planMatches as qt,planRow as Po,planTiles as At,productSummary as Co,progressPercent as Ro,progressText as qo,REPORT_FILTERS as It,reportMatches as Nt,reportRow as Ao,stagedRow as Io,typeMenuItems as No,widgetAreaRows as Oo,widgetsFileText as Bo,withDraft as Ot,withFooterChoice as Do,withListPage as Ho,withTypeChoice as Uo}from"../lib/wordpress-import-arrange.js";const Bt=/^#\/wordpress-import\/?(\?.*)?$/,ie="#/wordpress-import",L="/import/wordpress",ke=400,_o=`
1
+ import{apiRequest as R,getToken as uo}from"../api.js";import{addBannerAction as mo}from"../lib/plugin-chrome.js";import{helpPopovers as Et}from"../lib/help-popover.js";import{onceClosed as ho,openMenuAt as B,sidebarCount as q,viewLifetime as vo}from"../lib/tool-kit.js";import{SHAPES as $t,whileLoading as St}from"../lib/loading.js";import{fmtDateTime as wo}from"../lib/dates.js";import{applyChoice as fo,chooseAllPeople as Lt,choosePerson as bo,choiceItems as Pt,cleanOptions as xe,counts as Ct,customTypeRows as go,homeCard as yo,inviteNote as xo,peopleSummary as ko,personMenuItems as Mo,personRow as To,reportPersonRow as Eo,defaultOptions as _e,draftKey as $o,draftQuestion as So,historyRow as Lo,internalTypeRows as Po,isDirty as Co,kindTabs as Ro,liveDrafts as Rt,optionsSnapshot as qt,PLAN_FILTERS as At,planMatches as It,planRow as qo,planTiles as Nt,productSummary as Ao,progressPercent as Io,progressText as No,REPORT_FILTERS as Ot,reportMatches as Bt,reportRow as Oo,stagedRow as Bo,typeMenuItems as Do,widgetAreaRows as Ho,widgetsFileText as Uo,withDraft as Dt,withFooterChoice as _o,withListPage as zo,withTypeChoice as jo}from"../lib/wordpress-import-arrange.js";const Ht=/^#\/wordpress-import\/?(\?.*)?$/,ie="#/wordpress-import",P="/import/wordpress",ke=400,Wo=`
2
2
  <div class="wpx-row" data-id="{{id}}" tabindex="0">
3
3
  <span class="wpx-main"><strong>{{title}}</strong><small>{{meta}}</small></span>
4
4
  <span class="wpx-chips"><span class="wpx-chip" data-bind-class="pillClass">{{pill}}</span></span>
5
5
  <button type="button" class="wpx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
6
- </div>`,Dt=`
6
+ </div>`,Ut=`
7
7
  <div class="wpx-row" data-key="{{key}}" tabindex="0">
8
8
  <span class="wpx-main">
9
9
  <strong>{{title}}</strong>
@@ -14,17 +14,17 @@ import{apiRequest as R,getToken as io}from"../api.js";import{addBannerAction as
14
14
  </span>
15
15
  <span class="wpx-chips"><span class="wpx-chip" data-bind-class="pillCls">{{pill}}</span></span>
16
16
  <button type="button" class="wpx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
17
- </div>`,Ht=`
17
+ </div>`,_t=`
18
18
  <div class="wpx-row" data-from="{{from}}" tabindex="0">
19
19
  <span class="wpx-main"><span class="wpx-where">{{from}} \u2192 {{to}}</span><span class="wpx-reason" data-bind-hidden="!reason">{{reason}}</span></span>
20
20
  <span class="wpx-chips"><span class="wpx-chip" data-bind-class="pillClass">{{pill}}</span></span>
21
21
  <button type="button" class="wpx-kebab" aria-label="Actions for {{from}}" title="Actions">\u22EE</button>
22
- </div>`,zo=`
22
+ </div>`,Fo=`
23
23
  <div class="wpx-row" data-id="{{id}}" tabindex="0">
24
24
  <span class="wpx-main"><strong>{{title}}</strong><small>{{meta}}</small><span class="wpx-reason">{{summary}}</span></span>
25
25
  <span class="wpx-chips"><span class="wpx-chip" data-bind-class="pillClass">{{pill}}</span></span>
26
26
  <button type="button" class="wpx-kebab" aria-label="Actions for {{title}}" title="Actions">\u22EE</button>
27
- </div>`,jo=`
27
+ </div>`,Ko=`
28
28
  <div class="wpx-row wpx-type-row" data-type="{{type}}" tabindex="0">
29
29
  <span class="wpx-main">
30
30
  <strong>{{label}}</strong>
@@ -42,7 +42,7 @@ import{apiRequest as R,getToken as io}from"../api.js";import{addBannerAction as
42
42
  </span>
43
43
  <span class="wpx-chips"><span class="wpx-chip" data-bind-class="pillClass">{{pill}}</span></span>
44
44
  <button type="button" class="wpx-kebab" aria-label="Actions for {{label}}" title="Actions">\u22EE</button>
45
- </div>`,Wo=`
45
+ </div>`,Zo=`
46
46
  <div class="wpx-row wpx-person" data-key="{{key}}" tabindex="0">
47
47
  <span class="wpx-main">
48
48
  <strong>{{name}}</strong>
@@ -57,12 +57,12 @@ import{apiRequest as R,getToken as io}from"../api.js";import{addBannerAction as
57
57
  </span>
58
58
  <span class="wpx-chips"><span class="wpx-chip" data-bind-class="pillClass">{{pill}}</span></span>
59
59
  <button type="button" class="wpx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
60
- </div>`,Fo=`
60
+ </div>`,Vo=`
61
61
  <div class="wpx-row" data-key="{{key}}" tabindex="0">
62
62
  <span class="wpx-main"><strong>{{name}}</strong><small>{{meta}}</small><span class="wpx-reason" data-bind-hidden="!reason">{{reason}}</span></span>
63
63
  <span class="wpx-chips"><span class="wpx-chip" data-bind-class="pillClass">{{pill}}</span></span>
64
64
  <button type="button" class="wpx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
65
- </div>`,Ko=`
65
+ </div>`,Yo=`
66
66
  <div class="wpx-row wpx-widget-row" data-area="{{area}}" tabindex="0">
67
67
  <span class="wpx-main">
68
68
  <strong>{{label}}</strong>
@@ -76,7 +76,7 @@ import{apiRequest as R,getToken as io}from"../api.js";import{addBannerAction as
76
76
  </span>
77
77
  <span class="wpx-chips"><span class="wpx-chip" data-bind-class="pillClass">{{pill}}</span></span>
78
78
  <button type="button" class="wpx-kebab" aria-label="Actions for {{label}}" title="Actions">\u22EE</button>
79
- </div>`,Ut='<div class="wpx-tile"><strong>{{value}}</strong><small>{{label}} <span data-bind-hidden="!sub">{{sub}}</span></small></div>',ze='<p class="wpx-alert"><span>{{text}}</span></p>',_t="<li>{{text}}</li>",Zo='<div class="wpx-fact"><dt>{{label}}</dt><dd>{{value}}</dd></div>',je='<button type="button" data-on-click="$root.pickSeg($data)" data-bind-class="cls">{{label}} <b>{{count}}</b></button>',b=uo(Bt);function We(u,l,p){return new Promise((g,k)=>{const v=new XMLHttpRequest,A=new FormData;A.append("file",l),v.open("POST",`/api${u}`);const w=io();w&&v.setRequestHeader("Authorization",`Bearer ${w}`),v.upload.onprogress=m=>{m.lengthComputable&&p(m.loaded/m.total)},v.onload=()=>{let m=null;try{m=JSON.parse(v.responseText)}catch{}v.status>=200&&v.status<300?g(m):k(new Error(m?.error||`The upload failed (${v.status}).`))},v.onerror=()=>k(new Error("The upload failed - the connection dropped.")),v.send(A),b.add(()=>{v.readyState!==4&&v.abort()})})}const Me=u=>u>=1048576?`${Math.round(u/1048576)} MB`:`${Math.max(1,Math.round(u/1024))} KB`,J=u=>String(u??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function Vo(u){const l=document.createElement("template");return l.innerHTML=u.trim(),l.content.firstElementChild}async function Te(u,l){const p=E.confirm(u,l);let g=null;try{g=[...document.querySelectorAll(".dm-dialog-overlay")].at(-1)??null}catch{}const k=b.add(()=>{if(!g)return;const v=g;g=null;try{v.querySelector(".dm-dialog-cancel")?.click(),v.remove()}catch{}});try{return await p}finally{g=null,k()}}function Fe(u){navigator.clipboard?.writeText(u).then(()=>E.toast("Copied.",{type:"success"}),()=>E.toast(u,{type:"info",duration:8e3}))}function U(u,l,p,g){const k=E.contextMenu(u,{cursor:"glyph",match:l,inherit:!1,density:"compact",items:p});b.add(()=>k?.destroy?.());const v=w=>{const m=w.target.closest(l);if(!m||!u.contains(m))return;const d=w.target.closest(".wpx-kebab");if(d){B(d);return}g?.(m)},A=w=>{const m=w.target.closest(l);m&&(w.key==="F10"&&w.shiftKey?(w.preventDefault(),B(m)):w.key==="Enter"&&w.target===m&&g&&(w.preventDefault(),w.stopPropagation(),g(m)))};u.addEventListener("click",v),u.addEventListener("keydown",A),b.add(()=>{u.removeEventListener("click",v),u.removeEventListener("keydown",A)})}export const wordpressImportView={templateUrl:"/admin/js/templates/wordpress-import.html",async onMount(u){b.reset();const l=u.get(0).querySelector(".wpx")||u.get(0),p=()=>Bt.test(location.hash)&&l.isConnected;let g="";try{g=S.get("auth_user")?.id||""}catch{}const k=Mo(g),v=()=>{try{return k?S.get(k):null}catch{return null}},A=e=>{try{k&&S.set(k,e)}catch{}},w=l.querySelector(".wpx-loading");let m;try{m=await Et(w,R(`${L}/status`),Tt.rows)}catch(e){if(!l.isConnected)return;w&&(w.hidden=!0);const t=l.querySelector(".wpx-failed");t&&(t.textContent=`Could not load the importer: ${e.message||e}. Reload the page to try again.`,t.hidden=!1);return}if(!p())return;w&&(w.hidden=!0),l.querySelector(".wpx-body").hidden=!1;const d=M.observable(m.staged||[]),_=M.observable(m.history||[]),Y=M.observable(m.blog||{available:!1}),Ke=M.observable(m.shop||{available:!1}),z=m.limits||{},Ze=m.people?.defaultRole||"",y=M.observable(m.running||null),h=M.observable(""),f=M.observable(null),pe=M.observable(!1),Ee=M.observable(!1),$e=M.observable(""),i=M.observable(_e(Y.peek().available)),j=M.observable("all"),D=M.observable(""),X=M.observable(""),ce=M.observable(""),Q=M.observable(""),de=M.observable(""),ue=M.observable(!1);let me=!0;const Go=()=>d.peek().find(e=>e.id===h.peek())||null,ee=M.computed(()=>d.value.find(e=>e.id===h.value)||null),zt=M.computed(()=>h.value?"preview":"start"),H=M.computed(()=>!!Y.value.available),jt=M.computed(()=>!!(f.value?.shop||Ke.value).available),Ve=M.computed(()=>!!h.value&&So(i.value,H.value)),W=e=>M.computed({read:()=>i.value[e]!==!1,write:t=>{i.value={...i.peek(),[e]:!!t}}}),P=e=>{i.value={...i.peek(),...e}},te=e=>{try{return mo(e)}catch{return e}},Wt=M.computed(()=>d.value.map(e=>Io(e,te))),Ft=M.computed(()=>_.value.map(e=>Eo(e,te))),F=M.computed(()=>(f.value?.items||[]).map(e=>({...e,choice:i.value.choices?.[e.key]}))),Kt=M.computed(()=>Lo(F.value).map(e=>({...e,cls:D.value===e.id?"is-on":"",seg:"kind"}))),Zt=M.computed(()=>Lt(F.value,Rt,qt,D.value)),Vt=M.computed(()=>Rt.map(e=>({...e,count:Zt.value[e.id],cls:j.value===e.id?"is-on":"",seg:"filter"}))),Se=M.computed(()=>{const e=X.value.trim().toLowerCase();return F.value.filter(t=>(!D.value||t.kind===D.value)&&qt(t,j.value)&&(!e||`${t.title} ${t.target} ${t.oldUrl} ${t.postType||""}`.toLowerCase().includes(e)))}),Gt=M.computed(()=>Se.value.slice(0,ke).map(e=>{const t=Po(e);return{...t,pillCls:`${t.pillClass}${e.choice?" is-chosen":""}`}})),Jt=M.computed(()=>Se.value.length>ke?`Showing the first ${ke} of ${Se.value.length} - search or filter to find the rest.`:""),Yt=M.computed(()=>F.value.length?"Nothing matches.":"This export has nothing to import."),Xt=M.computed(()=>At(f.value)),Qt=M.computed(()=>(f.value?.notes||[]).map(e=>({text:e}))),ea=M.computed(()=>(f.value?.redirects||[]).slice(0,ke).map(e=>({...e,pill:e.action==="skip"?"Skipped":e.action==="update"?"Update":"New",pillClass:e.action==="skip"?"is-muted":"is-ok"}))),ta=M.computed(()=>String((f.value?.redirects||[]).filter(e=>e.action!=="skip").length)),aa=M.computed(()=>f.value?.site?.title||ee.value?.siteTitle||"WordPress export"),oa=M.computed(()=>{const e=ee.value,t=f.value?.site;return[t?.url||e?.siteUrl,e?.fileName,t?.wxrVersion&&`WXR ${t.wxrVersion}`,e?.at&&`uploaded ${te(e.at)}`].filter(Boolean).join(" \xB7 ")}),ae=M.computed(()=>Co(f.value)),sa=M.computed(()=>ae.value?.note||""),na=M.computed(()=>ae.value?.tone==="warn"?"is-warn":"is-info"),la=M.computed(()=>(ae.value?.rows||[]).map(e=>({...e,id:e.label}))),ra=M.computed(()=>(ae.value?.notes||[]).map(e=>({text:e}))),ia=M.computed(()=>!!ae.value),T=M.computed(()=>fo(f.value)),pa=M.computed(()=>!!T.value),ca=M.computed(()=>T.value?.wp||""),da=M.computed(()=>T.value?.now||""),ua=M.computed(()=>T.value?.after||""),ma=M.computed(()=>T.value?.tone==="warn"?"":"is-info"),ha=M.computed(()=>(T.value?.notes||[]).map(e=>({text:e}))),va=M.computed(()=>T.value?.editNow||""),wa=M.computed(()=>!!T.value?.frontSwitch),fa=M.computed(()=>!!T.value?.latestSwitch),ba=M.computed(()=>!!T.value?.postsSwitch),ga=M.computed(()=>T.value?.postsText||""),ya=M.computed({read:()=>i.value.latestPosts===!0,write:e=>P({latestPosts:!!e})}),xa=M.computed({read:()=>!!T.value?.postsId&&i.value.postsPage===T.value.postsId,write:e=>P({postsPage:e&&T.peek()?.postsId||0})}),Le=M.computed(()=>wo(f.value)),Pe=M.computed(()=>$o(f.value)),ka=M.computed(()=>!!(Le.value.length||Pe.value.length)),Ma=M.computed(()=>{const e=Pe.value.length;return e?`Not imported: ${e} kind${e===1?"":"s"} of WordPress or plugin housekeeping`:""}),$=M.computed(()=>f.value?.people||null),Ta=M.computed(()=>!!$.value?.rows?.length),Ea=M.computed(()=>($.value?.rows||[]).map(xo)),$a=M.computed(()=>!!$.value?.canCreate),Sa=M.computed(()=>($.value?.rows||[]).some(e=>e.choosable)),Ge=M.computed(()=>go($.value)),La=M.computed(()=>Ge.value.text),Pa=M.computed(()=>Ge.value.tone==="warn"?"is-warn":"is-info"),Ca=M.computed(()=>!!($.value?.canCreate&&$.value?.mailReady&&$.value?.counts?.create)),Ra=M.computed(()=>bo($.value)),qa=M.computed({read:()=>i.value.invite===!0,write:e=>{i.value={...i.peek(),invite:!!e}}}),Aa=M.computed(()=>H.value?`Posts go to ${Y.value.basePath||"/blog"}.`:Y.value.reason||"No Blog is switched on here."),he=M.computed(()=>ee.value?.uploads||null),Ia=M.computed(()=>{const e=he.value;return e?`${e.fileName} - ${e.files} file${e.files===1?"":"s"}, ${Me(e.size)}${e.unsafe?`. ${e.unsafe} with unsafe names (like ../) were ignored`:""}.`:`Zip the wp-content/uploads folder and upload it here (up to ${Me(z.uploads||0)}).`}),Na=M.computed(()=>he.value?"Replace the zip":"Upload the uploads zip"),Je=M.computed(()=>ee.value?.widgets||null),Oa=M.computed(()=>Bo(ee.value,z.widgets||0)),Ba=M.computed(()=>Je.value?"Replace the widgets file":"Upload the widgets file"),Ce=M.observable(""),Ye=M.computed(()=>Oo(f.value)),Xe=M.computed(()=>!!f.value&&!y.value&&!pe.value&&!(i.value.media==="zip"&&!he.value)),Da=M.computed(()=>y.value?`Importing${y.value.siteTitle?` ${y.value.siteTitle}`:""}\u2026`:""),Ha=M.computed(()=>qo(y.value)),Qe=M.computed(()=>Ro(y.value)),Ua=M.observable(Me(z.wxr||0)),et=l.querySelector(".wpx-running .wpx-bar-fill");b.add(M.effect(()=>{et&&(et.style.width=`${Qe.value}%`)}));async function Re(){const e=await R(`${L}/status`);return p()?(d.value=e.staged||[],_.value=e.history||[],Y.value=e.blog||{available:!1},Ke.value=e.shop||{available:!1},e.running&&!y.peek()&&(y.value=e.running,se()),e):null}let ve=0,qe=null;async function K({first:e=!1}={}){const t=h.peek();if(!t)return;const a=++ve;$e.value="";const o=R(`${L}/staged/${t}/preview`,{method:"POST",body:JSON.stringify({options:xe(i.peek(),H.peek())})});try{let r;if(e?(pe.value=!0,r=await Et(l.querySelector(".wpx-plan-loading"),o,Tt.table)):(Ee.value=!0,r=await o),a!==ve||!p()||h.peek()!==t)return;f.value=r.plan}catch(r){if(a!==ve||!p())return;if(r.status===404){E.toast("That upload has gone - upload the export again.",{type:"warning"}),oe(),Re().catch(()=>{});return}$e.value=`Could not read the export: ${r.message||r}`}finally{a===ve&&(pe.value=!1,Ee.value=!1)}}let we="";b.add(M.effect(()=>{const e=`${h.value}|${Ct(i.value)}`;if(e===we)return;const t=we.split("|")[0];we=e,!(!h.value||t!==h.value)&&(clearTimeout(qe),qe=setTimeout(()=>K(),250))})),b.add(()=>clearTimeout(qe)),b.add(M.effect(()=>{const e=h.value,t=i.value,a=Ve.value;if(!e||!me||!k)return;const o=v();A(Ot(o,g,e,a?{options:xe(t,!0),at:new Date().toISOString(),was:e}:null))}));const Ae=e=>{k&&A(Ot(v(),g,e,null))};async function fe(e){if(!d.peek().some(c=>c.id===e))return;const t=d.peek().find(c=>c.id===e),a=Pt(v(),d.peek().map(c=>c.id),g).items[e];if(me=!a,f.value=null,j.value="all",D.value="",X.value="",i.value=_e(H.peek()),we=`${e}|${Ct(i.peek())}`,h.value=e,await K({first:!0}),!p()||h.peek()!==e||!a)return;await new Promise(c=>setTimeout(c,0));const o=To({when:a.at?te(a.at):"",file:t?.fileName||""}),r=await Te(J(o.text),{title:o.title,confirmText:o.confirmText,cancelText:o.cancelText});!p()||h.peek()!==e||(me=!0,r?Ae(e):i.value=xe(a.options,H.peek()))}function oe(){h.value="",f.value=null,me=!0}async function tt(e=h.peek()){const t=d.peek().find(a=>a.id===e);if(t&&await Te(`Discard the upload of <strong>${J(t.siteTitle||t.fileName)}</strong>? Nothing has been imported from it.`,{title:"Discard upload",confirmText:"Discard",danger:!0}))try{if(await R(`${L}/staged/${e}`,{method:"DELETE"}),Ae(e),!p())return;h.peek()===e&&oe(),d.value=d.peek().filter(a=>a.id!==e),q(ie)}catch(a){E.toast(`Not discarded: ${a.message||a}`,{type:"error"})}}async function at(e){if(!e)return;if(Q.value="",!/\.(xml|zip)$/i.test(e.name)){Q.value="Upload the .xml file WordPress exports, or a .zip holding it.";return}if(z.wxr&&e.size>z.wxr*1.2&&/\.xml$/i.test(e.name)){Q.value=`That file is over ${Me(z.wxr)}.`;return}const t=l.querySelector(".wpx-start .wpx-upload .wpx-bar-fill");ce.value=`Uploading ${e.name}\u2026`;try{const a=await We(`${L}/upload`,e,o=>{t&&(t.style.width=`${Math.round(o*100)}%`),ce.value=o<1?`Uploading ${e.name} - ${Math.round(o*100)}%`:`Reading ${e.name}\u2026`});if(!p())return;d.value=[a,...d.peek().filter(o=>o.id!==a.id)],q(ie),await fe(a.id)}catch(a){p()&&(Q.value=a.message||String(a))}finally{p()&&(ce.value="",t&&(t.style.width="0"))}}async function _a(e){const t=h.peek();if(!e||!t)return;if(!/\.zip$/i.test(e.name)){E.toast("Zip the wp-content/uploads folder and upload the .zip.",{type:"warning"});return}const a=l.querySelector(".wpx-zip-fill");de.value=`Uploading ${e.name}\u2026`;try{const o=await We(`${L}/staged/${t}/uploads`,e,r=>{a&&(a.style.width=`${Math.round(r*100)}%`),de.value=r<1?`Uploading ${e.name} - ${Math.round(r*100)}%`:`Checking ${e.name}\u2026`});if(!p())return;d.value=d.peek().map(r=>r.id===o.id?o:r),h.peek()===t&&K()}catch(o){p()&&E.toast(o.message||String(o),{type:"error"})}finally{p()&&(de.value="",a&&(a.style.width="0"))}}async function za(e){const t=h.peek();if(!(!e||!t)){if(!/\.(wie|json)$/i.test(e.name)){E.toast('Upload the .wie file the "Widget Importer & Exporter" plugin exports.',{type:"warning"});return}Ce.value=`Reading ${e.name}\u2026`;try{const a=await We(`${L}/staged/${t}/widgets`,e,()=>{});if(!p())return;d.value=d.peek().map(o=>o.id===a.id?a:o),h.peek()===t&&K()}catch(a){p()&&E.toast(a.message||String(a),{type:"error"})}finally{p()&&(Ce.value="")}}}async function ja(){const e=h.peek();try{const t=await R(`${L}/staged/${e}/widgets`,{method:"DELETE"});if(!p())return;d.value=d.peek().map(a=>a.id===t.id?t:a),K()}catch(t){E.toast(`Not removed: ${t.message||t}`,{type:"error"})}}async function Wa(){const e=h.peek();try{const t=await R(`${L}/staged/${e}/uploads`,{method:"DELETE"});if(!p())return;d.value=d.peek().map(a=>a.id===t.id?t:a),K()}catch(t){E.toast(`Not removed: ${t.message||t}`,{type:"error"})}}async function Fa(){const e=h.peek(),t=f.peek();if(!e||!t||!Xe.peek())return;const o=At(t).filter(c=>c.value&&c.id!=="comments").map(c=>`${c.value} ${c.label.toLowerCase()}`);if(!(!await Te(`Import <strong>${J(t.site?.title||"this export")}</strong>: ${J(o.join(", ")||"nothing new")}? Pages edited here since an earlier import are left alone unless you chose Overwrite.`,{title:"Run the import",confirmText:"Run import"})||!p()||h.peek()!==e))try{const c=await R(`${L}/staged/${e}/run`,{method:"POST",body:JSON.stringify({options:xe(i.peek(),H.peek())})});if(Ae(e),!p())return;y.value={...c.job,siteTitle:t.site?.title||""},oe(),q(ie),se()}catch(c){E.toast(`The import did not start: ${c.message||c}`,{type:"error"})}}let Ie=null;function se(){clearTimeout(Ie);const e=y.peek();e&&(Ie=setTimeout(async()=>{try{const t=await R(`${L}/jobs/${e.id}`);if(!p())return;if(y.value={...t.job,siteTitle:e.siteTitle},t.job.status==="running")return se();if(y.value=null,await Re().catch(()=>{}),q(ie),q("#/pages"),q("#/media"),q("#/plugins/shopping-cart"),q("#/collections"),q("#/blocks"),!p())return;t.job.status==="done"?(E.toast("The import has finished - here is its report.",{type:"success"}),be(t.job.reportId)):(E.toast(`The import stopped: ${t.job.error||"an error"}`,{type:"error"}),t.job.reportId&&be(t.job.reportId))}catch(t){if(!p())return;if(t.status===404){y.value=null,Re().catch(()=>{});return}se()}},700))}b.add(()=>clearTimeout(Ie)),y.peek()&&se();let Z=!1;async function be(e){if(Z||!e)return;Z=!0;let t;try{t=await R(`${L}/history/${e}`)}catch(s){Z=!1,E.toast(`Could not open the report: ${s.message||s}`,{type:"error"});return}if(!p()){Z=!1;return}let a;try{a=E.slideover({title:J(`Report - ${t.run?.siteTitle||"WordPress import"}`),size:"lg",position:"right"})}catch(s){throw Z=!1,s}const o=Vo(`
79
+ </div>`,zt='<div class="wpx-tile"><strong>{{value}}</strong><small>{{label}} <span data-bind-hidden="!sub">{{sub}}</span></small></div>',ze='<p class="wpx-alert"><span>{{text}}</span></p>',jt="<li>{{text}}</li>",Go='<div class="wpx-fact"><dt>{{label}}</dt><dd>{{value}}</dd></div>',je='<button type="button" data-on-click="$root.pickSeg($data)" data-bind-class="cls">{{label}} <b>{{count}}</b></button>',b=vo(Ht);function We(u,l,p){return new Promise((g,T)=>{const v=new XMLHttpRequest,A=new FormData;A.append("file",l),v.open("POST",`/api${u}`);const w=uo();w&&v.setRequestHeader("Authorization",`Bearer ${w}`),v.upload.onprogress=m=>{m.lengthComputable&&p(m.loaded/m.total)},v.onload=()=>{let m=null;try{m=JSON.parse(v.responseText)}catch{}v.status>=200&&v.status<300?g(m):T(new Error(m?.error||`The upload failed (${v.status}).`))},v.onerror=()=>T(new Error("The upload failed - the connection dropped.")),v.send(A),b.add(()=>{v.readyState!==4&&v.abort()})})}const Me=u=>u>=1048576?`${Math.round(u/1048576)} MB`:`${Math.max(1,Math.round(u/1024))} KB`,G=u=>String(u??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function Jo(u){const l=document.createElement("template");return l.innerHTML=u.trim(),l.content.firstElementChild}async function Te(u,l){const p=E.confirm(u,l);let g=null;try{g=[...document.querySelectorAll(".dm-dialog-overlay")].at(-1)??null}catch{}const T=b.add(()=>{if(!g)return;const v=g;g=null;try{v.querySelector(".dm-dialog-cancel")?.click(),v.remove()}catch{}});try{return await p}finally{g=null,T()}}function Fe(u){navigator.clipboard?.writeText(u).then(()=>E.toast("Copied.",{type:"success"}),()=>E.toast(u,{type:"info",duration:8e3}))}function U(u,l,p,g){const T=E.contextMenu(u,{cursor:"glyph",match:l,inherit:!1,density:"compact",items:p});b.add(()=>T?.destroy?.());const v=w=>{const m=w.target.closest(l);if(!m||!u.contains(m))return;const d=w.target.closest(".wpx-kebab");if(d){B(d);return}g?.(m)},A=w=>{const m=w.target.closest(l);m&&(w.key==="F10"&&w.shiftKey?(w.preventDefault(),B(m)):w.key==="Enter"&&w.target===m&&g&&(w.preventDefault(),w.stopPropagation(),g(m)))};u.addEventListener("click",v),u.addEventListener("keydown",A),b.add(()=>{u.removeEventListener("click",v),u.removeEventListener("keydown",A)})}export const wordpressImportView={templateUrl:"/admin/js/templates/wordpress-import.html",async onMount(u){b.reset();const l=u.get(0).querySelector(".wpx")||u.get(0),p=()=>Ht.test(location.hash)&&l.isConnected;let g="";try{g=S.get("auth_user")?.id||""}catch{}const T=$o(g),v=()=>{try{return T?S.get(T):null}catch{return null}},A=e=>{try{T&&S.set(T,e)}catch{}},w=l.querySelector(".wpx-loading");let m;try{m=await St(w,R(`${P}/status`),$t.rows)}catch(e){if(!l.isConnected)return;w&&(w.hidden=!0);const t=l.querySelector(".wpx-failed");t&&(t.textContent=`Could not load the importer: ${e.message||e}. Reload the page to try again.`,t.hidden=!1);return}if(!p())return;w&&(w.hidden=!0),l.querySelector(".wpx-body").hidden=!1;const d=M.observable(m.staged||[]),_=M.observable(m.history||[]),J=M.observable(m.blog||{available:!1}),Ke=M.observable(m.shop||{available:!1}),z=m.limits||{},Ze=m.people?.defaultRole||"",y=M.observable(m.running||null),h=M.observable(""),f=M.observable(null),pe=M.observable(!1),Ee=M.observable(!1),$e=M.observable(""),i=M.observable(_e(J.peek().available)),j=M.observable("all"),D=M.observable(""),Q=M.observable(""),ce=M.observable(""),X=M.observable(""),de=M.observable(""),ue=M.observable(!1);let me=!0;const Qo=()=>d.peek().find(e=>e.id===h.peek())||null,ee=M.computed(()=>d.value.find(e=>e.id===h.value)||null),Wt=M.computed(()=>h.value?"preview":"start"),H=M.computed(()=>!!J.value.available),Ft=M.computed(()=>!!(f.value?.shop||Ke.value).available),Ve=M.computed(()=>!!h.value&&Co(i.value,H.value)),W=e=>M.computed({read:()=>i.value[e]!==!1,write:t=>{i.value={...i.peek(),[e]:!!t}}}),$=e=>{i.value={...i.peek(),...e}},te=e=>{try{return wo(e)}catch{return e}},Kt=M.computed(()=>d.value.map(e=>Bo(e,te))),Zt=M.computed(()=>_.value.map(e=>Lo(e,te))),F=M.computed(()=>(f.value?.items||[]).map(e=>({...e,choice:i.value.choices?.[e.key]}))),Vt=M.computed(()=>Ro(F.value).map(e=>({...e,cls:D.value===e.id?"is-on":"",seg:"kind"}))),Yt=M.computed(()=>Ct(F.value,At,It,D.value)),Gt=M.computed(()=>At.map(e=>({...e,count:Yt.value[e.id],cls:j.value===e.id?"is-on":"",seg:"filter"}))),Se=M.computed(()=>{const e=Q.value.trim().toLowerCase();return F.value.filter(t=>(!D.value||t.kind===D.value)&&It(t,j.value)&&(!e||`${t.title} ${t.target} ${t.oldUrl} ${t.postType||""}`.toLowerCase().includes(e)))}),Jt=M.computed(()=>Se.value.slice(0,ke).map(e=>{const t=qo(e);return{...t,pillCls:`${t.pillClass}${e.choice?" is-chosen":""}`}})),Qt=M.computed(()=>Se.value.length>ke?`Showing the first ${ke} of ${Se.value.length} - search or filter to find the rest.`:""),Xt=M.computed(()=>F.value.length?"Nothing matches.":"This export has nothing to import."),ea=M.computed(()=>Nt(f.value)),ta=M.computed(()=>(f.value?.notes||[]).map(e=>({text:e}))),aa=M.computed(()=>(f.value?.redirects||[]).slice(0,ke).map(e=>({...e,pill:e.action==="skip"?"Skipped":e.action==="update"?"Update":"New",pillClass:e.action==="skip"?"is-muted":"is-ok"}))),oa=M.computed(()=>String((f.value?.redirects||[]).filter(e=>e.action!=="skip").length)),sa=M.computed(()=>f.value?.site?.title||ee.value?.siteTitle||"WordPress export"),na=M.computed(()=>{const e=ee.value,t=f.value?.site;return[t?.url||e?.siteUrl,e?.fileName,t?.wxrVersion&&`WXR ${t.wxrVersion}`,e?.at&&`uploaded ${te(e.at)}`].filter(Boolean).join(" \xB7 ")}),ae=M.computed(()=>Ao(f.value)),la=M.computed(()=>ae.value?.note||""),ra=M.computed(()=>ae.value?.tone==="warn"?"is-warn":"is-info"),ia=M.computed(()=>(ae.value?.rows||[]).map(e=>({...e,id:e.label}))),pa=M.computed(()=>(ae.value?.notes||[]).map(e=>({text:e}))),ca=M.computed(()=>!!ae.value),x=M.computed(()=>yo(f.value)),da=M.computed(()=>!!x.value),ua=M.computed(()=>x.value?.wp||""),ma=M.computed(()=>x.value?.now||""),ha=M.computed(()=>x.value?.after||""),va=M.computed(()=>x.value?.tone==="warn"?"":"is-info"),wa=M.computed(()=>(x.value?.notes||[]).map(e=>({text:e}))),fa=M.computed(()=>x.value?.editNow||""),ba=M.computed(()=>!!x.value?.frontSwitch),ga=M.computed(()=>!!x.value?.latestSwitch),ya=M.computed(()=>!!x.value?.postsSwitch),xa=M.computed(()=>x.value?.postsText||""),Ye=M.computed(()=>x.value?.question||""),Ge=M.computed(()=>!!Ye.value&&typeof i.value.homeReplace!="boolean"),ka=M.computed({read:()=>i.value.latestPosts===!0,write:e=>$({latestPosts:!!e})}),Ma=M.computed({read:()=>!!x.value?.postsId&&i.value.postsPage===x.value.postsId,write:e=>$({postsPage:e&&x.peek()?.postsId||0})}),Le=M.computed(()=>go(f.value)),Pe=M.computed(()=>Po(f.value)),Ta=M.computed(()=>!!(Le.value.length||Pe.value.length)),Ea=M.computed(()=>{const e=Pe.value.length;return e?`Not imported: ${e} kind${e===1?"":"s"} of WordPress or plugin housekeeping`:""}),L=M.computed(()=>f.value?.people||null),$a=M.computed(()=>!!L.value?.rows?.length),Sa=M.computed(()=>(L.value?.rows||[]).map(To)),La=M.computed(()=>!!L.value?.canCreate),Pa=M.computed(()=>(L.value?.rows||[]).some(e=>e.choosable)),Je=M.computed(()=>ko(L.value)),Ca=M.computed(()=>Je.value.text),Ra=M.computed(()=>Je.value.tone==="warn"?"is-warn":"is-info"),qa=M.computed(()=>!!(L.value?.canCreate&&L.value?.mailReady&&L.value?.counts?.create)),Aa=M.computed(()=>xo(L.value)),Ia=M.computed({read:()=>i.value.invite===!0,write:e=>{i.value={...i.peek(),invite:!!e}}}),Na=M.computed(()=>H.value?`Posts go to ${J.value.basePath||"/blog"}.`:J.value.reason||"No Blog is switched on here."),he=M.computed(()=>ee.value?.uploads||null),Oa=M.computed(()=>{const e=he.value;return e?`${e.fileName} - ${e.files} file${e.files===1?"":"s"}, ${Me(e.size)}${e.unsafe?`. ${e.unsafe} with unsafe names (like ../) were ignored`:""}.`:`Zip the wp-content/uploads folder and upload it here (up to ${Me(z.uploads||0)}).`}),Ba=M.computed(()=>he.value?"Replace the zip":"Upload the uploads zip"),Qe=M.computed(()=>ee.value?.widgets||null),Da=M.computed(()=>Uo(ee.value,z.widgets||0)),Ha=M.computed(()=>Qe.value?"Replace the widgets file":"Upload the widgets file"),Ce=M.observable(""),Xe=M.computed(()=>Ho(f.value)),Ua=M.computed(()=>Ge.value?"Answer the home page question first.":""),et=M.computed(()=>!!f.value&&!y.value&&!pe.value&&!(i.value.media==="zip"&&!he.value)&&!Ge.value),_a=M.computed(()=>y.value?`Importing${y.value.siteTitle?` ${y.value.siteTitle}`:""}\u2026`:""),za=M.computed(()=>No(y.value)),tt=M.computed(()=>Io(y.value)),ja=M.observable(Me(z.wxr||0)),at=l.querySelector(".wpx-running .wpx-bar-fill");b.add(M.effect(()=>{at&&(at.style.width=`${tt.value}%`)}));async function Re(){const e=await R(`${P}/status`);return p()?(d.value=e.staged||[],_.value=e.history||[],J.value=e.blog||{available:!1},Ke.value=e.shop||{available:!1},e.running&&!y.peek()&&(y.value=e.running,se()),e):null}let ve=0,qe=null;async function K({first:e=!1}={}){const t=h.peek();if(!t)return;const a=++ve;$e.value="";const o=R(`${P}/staged/${t}/preview`,{method:"POST",body:JSON.stringify({options:xe(i.peek(),H.peek())})});try{let r;if(e?(pe.value=!0,r=await St(l.querySelector(".wpx-plan-loading"),o,$t.table)):(Ee.value=!0,r=await o),a!==ve||!p()||h.peek()!==t)return;f.value=r.plan}catch(r){if(a!==ve||!p())return;if(r.status===404){E.toast("That upload has gone - upload the export again.",{type:"warning"}),oe(),Re().catch(()=>{});return}$e.value=`Could not read the export: ${r.message||r}`}finally{a===ve&&(pe.value=!1,Ee.value=!1)}}let we="";b.add(M.effect(()=>{const e=`${h.value}|${qt(i.value)}`;if(e===we)return;const t=we.split("|")[0];we=e,!(!h.value||t!==h.value)&&(clearTimeout(qe),qe=setTimeout(()=>K(),250))})),b.add(()=>clearTimeout(qe)),b.add(M.effect(()=>{const e=h.value,t=i.value,a=Ve.value;if(!e||!me||!T)return;const o=v();A(Dt(o,g,e,a?{options:xe(t,!0),at:new Date().toISOString(),was:e}:null))}));const Ae=e=>{T&&A(Dt(v(),g,e,null))};async function fe(e){if(!d.peek().some(c=>c.id===e))return;const t=d.peek().find(c=>c.id===e),a=Rt(v(),d.peek().map(c=>c.id),g).items[e];if(me=!a,f.value=null,j.value="all",D.value="",Q.value="",i.value=_e(H.peek()),we=`${e}|${qt(i.peek())}`,h.value=e,await K({first:!0}),!p()||h.peek()!==e||!a)return;await new Promise(c=>setTimeout(c,0));const o=So({when:a.at?te(a.at):"",file:t?.fileName||""}),r=await Te(G(o.text),{title:o.title,confirmText:o.confirmText,cancelText:o.cancelText});!p()||h.peek()!==e||(me=!0,r?Ae(e):i.value=xe(a.options,H.peek()))}function oe(){h.value="",f.value=null,me=!0}async function ot(e=h.peek()){const t=d.peek().find(a=>a.id===e);if(t&&await Te(`Discard the upload of <strong>${G(t.siteTitle||t.fileName)}</strong>? Nothing has been imported from it.`,{title:"Discard upload",confirmText:"Discard",danger:!0}))try{if(await R(`${P}/staged/${e}`,{method:"DELETE"}),Ae(e),!p())return;h.peek()===e&&oe(),d.value=d.peek().filter(a=>a.id!==e),q(ie)}catch(a){E.toast(`Not discarded: ${a.message||a}`,{type:"error"})}}async function st(e){if(!e)return;if(X.value="",!/\.(xml|zip)$/i.test(e.name)){X.value="Upload the .xml file WordPress exports, or a .zip holding it.";return}if(z.wxr&&e.size>z.wxr*1.2&&/\.xml$/i.test(e.name)){X.value=`That file is over ${Me(z.wxr)}.`;return}const t=l.querySelector(".wpx-start .wpx-upload .wpx-bar-fill");ce.value=`Uploading ${e.name}\u2026`;try{const a=await We(`${P}/upload`,e,o=>{t&&(t.style.width=`${Math.round(o*100)}%`),ce.value=o<1?`Uploading ${e.name} - ${Math.round(o*100)}%`:`Reading ${e.name}\u2026`});if(!p())return;d.value=[a,...d.peek().filter(o=>o.id!==a.id)],q(ie),await fe(a.id)}catch(a){p()&&(X.value=a.message||String(a))}finally{p()&&(ce.value="",t&&(t.style.width="0"))}}async function Wa(e){const t=h.peek();if(!e||!t)return;if(!/\.zip$/i.test(e.name)){E.toast("Zip the wp-content/uploads folder and upload the .zip.",{type:"warning"});return}const a=l.querySelector(".wpx-zip-fill");de.value=`Uploading ${e.name}\u2026`;try{const o=await We(`${P}/staged/${t}/uploads`,e,r=>{a&&(a.style.width=`${Math.round(r*100)}%`),de.value=r<1?`Uploading ${e.name} - ${Math.round(r*100)}%`:`Checking ${e.name}\u2026`});if(!p())return;d.value=d.peek().map(r=>r.id===o.id?o:r),h.peek()===t&&K()}catch(o){p()&&E.toast(o.message||String(o),{type:"error"})}finally{p()&&(de.value="",a&&(a.style.width="0"))}}async function Fa(e){const t=h.peek();if(!(!e||!t)){if(!/\.(wie|json)$/i.test(e.name)){E.toast('Upload the .wie file the "Widget Importer & Exporter" plugin exports.',{type:"warning"});return}Ce.value=`Reading ${e.name}\u2026`;try{const a=await We(`${P}/staged/${t}/widgets`,e,()=>{});if(!p())return;d.value=d.peek().map(o=>o.id===a.id?a:o),h.peek()===t&&K()}catch(a){p()&&E.toast(a.message||String(a),{type:"error"})}finally{p()&&(Ce.value="")}}}async function Ka(){const e=h.peek();try{const t=await R(`${P}/staged/${e}/widgets`,{method:"DELETE"});if(!p())return;d.value=d.peek().map(a=>a.id===t.id?t:a),K()}catch(t){E.toast(`Not removed: ${t.message||t}`,{type:"error"})}}async function Za(){const e=h.peek();try{const t=await R(`${P}/staged/${e}/uploads`,{method:"DELETE"});if(!p())return;d.value=d.peek().map(a=>a.id===t.id?t:a),K()}catch(t){E.toast(`Not removed: ${t.message||t}`,{type:"error"})}}async function Va(){const e=h.peek(),t=f.peek();if(!e||!t||!et.peek())return;const o=Nt(t).filter(c=>c.value&&c.id!=="comments").map(c=>`${c.value} ${c.label.toLowerCase()}`);if(!(!await Te(`Import <strong>${G(t.site?.title||"this export")}</strong>: ${G(o.join(", ")||"nothing new")}? Pages edited here since an earlier import are left alone unless you chose Overwrite.`,{title:"Run the import",confirmText:"Run import"})||!p()||h.peek()!==e))try{const c=await R(`${P}/staged/${e}/run`,{method:"POST",body:JSON.stringify({options:xe(i.peek(),H.peek())})});if(Ae(e),!p())return;y.value={...c.job,siteTitle:t.site?.title||""},oe(),q(ie),se()}catch(c){E.toast(`The import did not start: ${c.message||c}`,{type:"error"})}}let Ie=null;function se(){clearTimeout(Ie);const e=y.peek();e&&(Ie=setTimeout(async()=>{try{const t=await R(`${P}/jobs/${e.id}`);if(!p())return;if(y.value={...t.job,siteTitle:e.siteTitle},t.job.status==="running")return se();if(y.value=null,await Re().catch(()=>{}),q(ie),q("#/pages"),q("#/media"),q("#/plugins/shopping-cart"),q("#/collections"),q("#/blocks"),!p())return;t.job.status==="done"?(E.toast("The import has finished - here is its report.",{type:"success"}),be(t.job.reportId)):(E.toast(`The import stopped: ${t.job.error||"an error"}`,{type:"error"}),t.job.reportId&&be(t.job.reportId))}catch(t){if(!p())return;if(t.status===404){y.value=null,Re().catch(()=>{});return}se()}},700))}b.add(()=>clearTimeout(Ie)),y.peek()&&se();let Z=!1;async function be(e){if(Z||!e)return;Z=!0;let t;try{t=await R(`${P}/history/${e}`)}catch(s){Z=!1,E.toast(`Could not open the report: ${s.message||s}`,{type:"error"});return}if(!p()){Z=!1;return}let a;try{a=E.slideover({title:G(`Report - ${t.run?.siteTitle||"WordPress import"}`),size:"lg",position:"right"})}catch(s){throw Z=!1,s}const o=Jo(`
80
80
  <div class="wpx wpx-report">
81
81
  <p class="wpx-note" data-bind-text="meta.value"></p>
82
82
  <div class="wpx-tiles" data-each="tiles.value key=id"></div>
@@ -92,4 +92,4 @@ import{apiRequest as R,getToken as io}from"../api.js";import{addBannerAction as
92
92
  <div class="wpx-list wpx-report-people" data-each="people.value key=key"></div>
93
93
  <div class="wpx-card-title" data-bind-hidden="!redirects.value.length">Redirects</div>
94
94
  <div class="wpx-list wpx-report-redirects" data-each="redirects.value key=from"></div>
95
- </div>`);o.querySelector(".wpx-tiles").innerHTML=Ut,o.querySelector(".wpx-note-list").innerHTML=ze,o.querySelector(".wpx-seg").innerHTML=je,o.querySelector(".wpx-report-items").innerHTML=Dt,o.querySelector(".wpx-report-redirects").innerHTML=Ht,o.querySelector(".wpx-report-people").innerHTML=Fo;const r=M.observable(t.run?.summary?.failed?"failed":"all"),c=/^\/[a-z0-9][a-z0-9/-]{0,200}$/.test(t.home?.previous||"")?t.home.previous:"",De=t.items||[],yt=Lt(De,It,Nt),O=t.run?.summary||{},He=s=>Object.values(O.kinds||{}).reduce((n,x)=>n+(x[s]||0),0),Ja={meta:M.observable([te(t.run?.at),t.run?.by,t.run?.file,t.run?.durationMs?`${Math.max(1,Math.round(t.run.durationMs/1e3))} s`:""].filter(Boolean).join(" \xB7 ")),tiles:M.observable([{id:"made",label:"Made",value:He("created"),sub:""},{id:"updated",label:"Updated",value:He("updated"),sub:""},{id:"kept",label:"Left as is",value:He("kept"),sub:""},{id:"redirects",label:"Redirects",value:(O.redirects?.created||0)+(O.redirects?.updated||0),sub:""},...O.people?[{id:"people",label:"Accounts made",value:O.people.created||0,sub:O.people.invited?`${O.people.invited} emailed`:""}]:[],{id:"comments",label:"Comments",value:O.comments||0,sub:""},{id:"failed",label:"Failed",value:O.failed||0,sub:""}]),notes:M.observable((t.notes||[]).map(s=>({text:s}))),homePrevious:M.observable(c),homePreviousText:M.observable(c?`This site's home page from before the import${t.home?.previousTitle?` ("${t.home.previousTitle}")`:""} is now a draft at ${c} - nothing was deleted. Publish it there, or delete it, from Pages.`:""),people:M.observable((t.people||[]).map(ko)),filters:M.computed(()=>It.filter(s=>s.id==="all"||yt[s.id]).map(s=>({...s,count:yt[s.id],cls:r.value===s.id?"is-on":""}))),rows:M.computed(()=>De.filter(s=>Nt(s,r.value)).slice(0,1e3).map(s=>{const n=Ao(s);return{...n,pillCls:n.pillClass}})),redirects:M.observable((t.redirects||[]).filter(s=>s.result!=="skipped").slice(0,1e3).map(s=>({...s,reason:s.error||"",pill:s.result==="failed"?"Failed":s.result==="updated"?"Updated":"Added",pillClass:s.result==="failed"?"is-bad":"is-ok"})))};a.element.appendChild(o);const Ya=M.applyBindings(Ja,o,{methods:{pickSeg:s=>{r.value=s.id}}}),Xa=Mt(o),xt=new Map(De.map(s=>[s.key,s])),Qa=o.querySelector(".wpx-report-items"),eo=s=>{const n=xt.get(s.dataset.key);if(!n)return[];const x=typeof n.target=="string"&&n.target.startsWith("/")?n.target:"",C=[{header:n.title}];if(x&&n.result!=="failed"&&n.result!=="skipped"&&C.push({label:"Open on the site",icon:"external-link",action:()=>window.open(x,"_blank","noopener")}),x&&(n.kind==="page"||n.kind==="custom"&&!n.collection)&&(n.result==="created"||n.result==="updated"||n.result==="kept")&&C.push({label:"Edit the page",icon:"edit",action:()=>{a.close(),location.hash=`#/pages/edit${x}`}}),n.kind==="widgets"&&/^[a-z0-9][a-z0-9-]{0,79}$/.test(n.target||"")&&["created","updated","kept"].includes(n.result)&&(C.push({label:"Edit the block",icon:"edit",action:()=>{a.close(),location.hash=`#/blocks/edit/${n.target}`}}),n.footer&&C.push({label:"Footer settings",icon:"settings",action:()=>{a.close(),location.hash="#/settings"}})),(n.kind==="collection"||n.kind==="custom")&&n.result!=="failed"&&n.result!=="skipped"&&/^[a-z0-9-]{1,80}$/.test(n.kind==="collection"?n.target:n.collection||"")){const G=n.kind==="collection"?n.target:n.collection;C.push({label:"Open the collection",icon:"database",action:()=>{a.close(),location.hash=`#/collections/${G}/entries`}})}return n.kind==="product"&&n.entryId&&/^[\w-]{1,64}$/.test(n.entryId)&&C.push({label:"Edit in the Shop",icon:"shopping-bag",action:()=>{a.close(),location.hash=`#/plugins/shopping-cart/products/${encodeURIComponent(n.entryId)}`}}),/^https?:\/\//i.test(n.oldUrl)&&C.push({label:"Open the old address",icon:"link",action:()=>window.open(n.oldUrl,"_blank","noopener,noreferrer")}),x&&C.push({label:"Copy the new address",icon:"copy",action:()=>Fe(new URL(x,location.origin).href)}),C},to=E.contextMenu(Qa,{cursor:"glyph",match:".wpx-row",inherit:!1,density:"compact",items:eo}),ao=o.querySelector(".wpx-report-people"),oo=new Map((t.people||[]).map(s=>[s.key,s])),so=E.contextMenu(ao,{cursor:"glyph",match:".wpx-row",inherit:!1,density:"compact",items:s=>{const n=oo.get(s.dataset.key);if(!n)return[];const x=/^[0-9a-f-]{36}$/i.test(n.userId||"")?n.userId:"";return[{header:n.name},...x&&n.result!=="skipped"?[{label:"Open their account",icon:"user",action:()=>{a.close(),location.hash=`#/users/edit/${x}`}}]:[],{label:"Open Users",icon:"users",action:()=>{a.close(),location.hash="#/users"}}]}}),no=o.querySelector(".wpx-report-redirects"),lo=E.contextMenu(no,{cursor:"glyph",match:".wpx-row",inherit:!1,density:"compact",items:s=>[{header:s.dataset.from},...s.dataset.from.includes("*")?[]:[{label:"Try it on the site",icon:"external-link",action:()=>window.open(s.dataset.from,"_blank","noopener")}],{label:"Open SEO \u203A Redirects",icon:"corner-up-right",action:()=>{a.close(),location.hash="#/seo/redirects"}}]}),Ue=s=>{if(s.target.closest(".wpx-open-previous")&&c){a.close(),location.hash=`#/pages/edit${c}`;return}const n=s.target.closest(".wpx-kebab");if(n){B(n);return}const x=s.target.closest(".wpx-report-people .wpx-row");if(x){B(x.querySelector(".wpx-kebab")||x);return}const C=s.target.closest(".wpx-report-items .wpx-row");if(!C)return;const G=xt.get(C.dataset.key);G?.target?.startsWith("/")&&G.result!=="failed"&&G.result!=="skipped"&&window.open(G.target,"_blank","noopener")},kt=s=>{const n=s.target.closest(".wpx-row");n&&(s.key==="F10"&&s.shiftKey?(s.preventDefault(),B(n)):s.key==="Enter"&&s.target===n&&(s.preventDefault(),s.stopPropagation(),Ue({target:n})))};o.addEventListener("click",Ue),o.addEventListener("keydown",kt);const ro=b.add(()=>a.close());co(a,()=>{Z=!1,Ya.dispose(),Xa(),to?.destroy?.(),lo?.destroy?.(),so?.destroy?.(),o.removeEventListener("click",Ue),o.removeEventListener("keydown",kt),ro()}),a.open(),I.scan(o)}async function Ka(e){const t=_.peek().find(a=>a.id===e);if(t&&await Te(`Remove the import of <strong>${J(t.siteTitle||t.source)}</strong> from this list? Everything it imported stays on the site.`,{title:"Remove from the history",confirmText:"Remove"}))try{if(await R(`${L}/history/${e}`,{method:"DELETE"}),!p())return;_.value=_.peek().filter(a=>a.id!==e),q(ie)}catch(a){E.toast(`Not removed: ${a.message||a}`,{type:"error"})}}l.querySelector(".wpx-staged").innerHTML=_o,l.querySelector(".wpx-items").innerHTML=Dt,l.querySelector(".wpx-redirects").innerHTML=Ht,l.querySelector(".wpx-history").innerHTML=zo,l.querySelector(".wpx-tiles").innerHTML=Ut,l.querySelector(".wpx-note-list").innerHTML=ze,l.querySelector(".wpx-home-notes").innerHTML=ze,l.querySelector(".wpx-kinds").innerHTML=je,l.querySelector(".wpx-filters").innerHTML=je,l.querySelector(".wpx-facts").innerHTML=Zo,l.querySelector(".wpx-product-notes").innerHTML=_t,l.querySelector(".wpx-types").innerHTML=jo,l.querySelector(".wpx-internal").innerHTML=_t,l.querySelector(".wpx-people-list").innerHTML=Wo,l.querySelector(".wpx-widget-areas").innerHTML=Ko;const Za={mode:zt,job:y,jobTitle:Da,jobText:Ha,jobPct:Qe,staged:d,stagedRows:Wt,historyRows:Ft,plan:f,planLoading:pe,planError:$e,refreshing:Ee,opt:i,blogAvailable:H,blogNote:Aa,dirty:Ve,canRun:Xe,siteTitle:aa,siteMeta:oa,tiles:Xt,notes:Qt,kinds:Kt,filters:Vt,rows:Gt,moreText:Jt,emptyText:Yt,redirectRows:ea,redirectCount:ta,query:X,uploadText:ce,uploadError:Q,dropping:ue,wxrLimit:Ua,zipInfo:he,zipText:Ia,zipButton:Na,zipUploadText:de,hasPeople:Ta,peopleRows:Ea,peopleCanCreate:$a,peopleChoosable:Sa,peopleNote:La,peopleNoteCls:Pa,inviteAvailable:Ca,inviteText:Ra,optInvite:qa,shopAvailable:jt,hasProducts:ia,productNote:sa,productNoteCls:na,productFacts:la,productNotes:ra,typeRows:Le,internalRows:Pe,hasCustom:ka,internalSummary:Ma,widgetsInfo:Je,widgetsText:Oa,widgetsButton:Ba,widgetsUploadText:Ce,widgetRows:Ye,hasHome:pa,homeWp:ca,homeNow:da,homeAfter:ua,homeAfterCls:ma,homeNotes:ha,homeEdit:va,homeFrontSwitch:wa,homeLatestSwitch:fa,homePostsSwitch:ba,homePostsText:ga,optHome:W("home"),optLatest:ya,optPostsPage:xa,optComments:W("comments"),optMenus:W("menus"),optRedirects:W("redirects"),optDrafts:W("drafts"),optProducts:W("products")},Va=M.applyBindings(Za,l,{methods:{back:()=>oe(),discard:()=>tt(),run:()=>Fa(),resetChoices:()=>{i.value=_e(H.peek())},postsBlog:()=>P({posts:"blog"}),postsPages:()=>P({posts:"pages"}),postsSkip:()=>P({posts:"skip"}),mediaDownload:()=>P({media:"download"}),mediaZip:()=>P({media:"zip"}),mediaSkip:()=>P({media:"skip"}),removeZip:()=>Wa(),removeWidgets:()=>ja(),peopleAll:()=>P({users:$t($.peek(),"create")}),peopleNone:()=>P({users:$t($.peek(),"skip")}),pickSeg:e=>{e.seg==="kind"?D.value=e.id:j.value=e.id},showProducts:()=>{D.value="product",j.value="all",X.value="",l.querySelector(".wpx-items")?.closest(".wpx-card")?.scrollIntoView({behavior:"smooth",block:"start"})}}});b.add(()=>Va.dispose()),b.add(Mt(l,{position:"bottom"}));const ne=l.querySelector(".wpx-file"),ot=()=>{const e=ne.files?.[0];ne.value="",at(e)};ne.addEventListener("change",ot);const V=l.querySelector(".wpx-drop"),st=e=>{e.preventDefault(),ue.value=!0},nt=()=>{ue.value=!1},lt=e=>{e.preventDefault(),ue.value=!1,at(e.dataTransfer?.files?.[0])};V.addEventListener("dragover",st),V.addEventListener("dragleave",nt),V.addEventListener("drop",lt);const ge=l.querySelector(".wpx-zip-file"),rt=()=>{const e=ge.files?.[0];ge.value="",_a(e)};ge.addEventListener("change",rt);const ye=l.querySelector(".wpx-wie-file"),it=()=>{const e=ye.files?.[0];ye.value="",za(e)};ye.addEventListener("change",it),b.add(()=>ye.removeEventListener("change",it)),b.add(()=>{ne.removeEventListener("change",ot),V.removeEventListener("dragover",st),V.removeEventListener("dragleave",nt),V.removeEventListener("drop",lt),ge.removeEventListener("change",rt)}),U(l.querySelector(".wpx-staged"),".wpx-row",e=>[{header:d.peek().find(t=>t.id===e.dataset.id)?.siteTitle||"Upload"},{label:"Preview the import",icon:"eye",action:()=>fe(e.dataset.id)},{label:"Discard the upload",icon:"trash-2",action:()=>tt(e.dataset.id)}],e=>fe(e.dataset.id));const pt=(e,t)=>{i.value={...i.peek(),choices:ho(i.peek().choices,e,t)}};U(l.querySelector(".wpx-items"),".wpx-row",e=>{const t=F.peek().find(o=>o.key===e.dataset.key);if(!t)return[];const a=[{header:t.title}];for(const o of St(t,t.choice))a.push({label:o.label,icon:o.icon,action:()=>pt(t.key,o.id)});return t.oldUrl&&/^https?:\/\//i.test(t.oldUrl)&&a.push({label:"Open the old address",icon:"link",action:()=>window.open(t.oldUrl,"_blank","noopener,noreferrer")}),t.target?.startsWith("/")&&(t.action==="update"||t.action==="keep")&&a.push({label:"Open it on this site",icon:"external-link",action:()=>window.open(t.target,"_blank","noopener")}),a.length===1&&a.push({label:"Nothing to change here",icon:"info",disabled:!0,action:()=>{}}),a},e=>B(e.querySelector(".wpx-kebab")||e));const ct=(e,t)=>{i.value=Uo(i.peek(),e,t)},dt=(e,t)=>{i.value=Ho(i.peek(),e,t)},Ga=e=>{D.value="",j.value="all",X.value=e,l.querySelector(".wpx-items")?.closest(".wpx-card")?.scrollIntoView({behavior:"smooth",block:"start"})},le=l.querySelector(".wpx-types"),ut=e=>{if(!e.target.closest(".wpx-type-ctl"))return;e.stopPropagation();const a=e.target.closest("button[data-as]"),o=e.target.closest(".wpx-type-row");a&&o&&ct(o.dataset.type,a.dataset.as)},mt=e=>{const t=e.target.closest("input[data-list-page]"),a=e.target.closest(".wpx-type-row");t&&a&&dt(a.dataset.type,t.checked)};le.addEventListener("click",ut,!0),le.addEventListener("change",mt),b.add(()=>{le.removeEventListener("click",ut,!0),le.removeEventListener("change",mt)}),U(le,".wpx-type-row",e=>{const t=Le.peek().find(a=>a.type===e.dataset.type);return t?[{header:`${t.label} ("${t.type}")`},...No(t).map(a=>({label:a.label,icon:a.icon,action:()=>{a.id==="show"?Ga(t.type):a.id==="list"||a.id==="nolist"?dt(t.type,a.id==="list"):ct(t.type,a.id)}}))]:[]},e=>B(e.querySelector(".wpx-kebab")||e));const Ne=(e,t)=>{i.value=Do(i.peek(),e,t)},re=l.querySelector(".wpx-widget-areas"),ht=e=>{e.target.closest(".wpx-type-ctl")&&e.stopPropagation()},vt=e=>{const t=e.target.closest("input[data-footer]"),a=e.target.closest(".wpx-widget-row");t&&a&&Ne(a.dataset.area,t.checked)};re.addEventListener("click",ht,!0),re.addEventListener("change",vt),b.add(()=>{re.removeEventListener("click",ht,!0),re.removeEventListener("change",vt)}),U(re,".wpx-widget-row",e=>{const t=Ye.peek().find(r=>r.area===e.dataset.area),a=F.peek().find(r=>r.key===`widgets:${e.dataset.area}`);if(!t)return[];const o=[{header:`${t.label} ("${t.area}")`}];if(t.canPlace&&o.push(t.placeOn?{label:"Leave it out of the footer",icon:"eye-off",action:()=>Ne(t.area,!1)}:{label:"Place in the footer",icon:"layout",action:()=>Ne(t.area,!0)}),a)for(const r of St(a,a.choice))o.push({label:r.label,icon:r.icon,action:()=>pt(a.key,r.id)});return o.length===1&&o.push({label:"Nothing to change here",icon:"info",disabled:!0,action:()=>{}}),o},e=>B(e.querySelector(".wpx-kebab")||e));const wt=(e,t)=>{const a=$.peek()?.rows?.find(o=>o.key===e);a&&P({users:vo(i.peek().users,a,t)})},Oe=l.querySelector(".wpx-people-list"),ft=e=>{if(!e.target.closest(".wpx-type-ctl"))return;e.stopPropagation();const a=e.target.closest("button[data-user]"),o=e.target.closest(".wpx-person");a&&o&&wt(o.dataset.key,a.dataset.user)};Oe.addEventListener("click",ft,!0),b.add(()=>Oe.removeEventListener("click",ft,!0)),U(Oe,".wpx-person",e=>{const t=$.peek()?.rows?.find(a=>a.key===e.dataset.key);return t?[{header:t.name},...yo(t).map(a=>({label:a.label,icon:a.icon,disabled:!!a.disabled,action:()=>{a.disabled||(a.id==="copy"?Fe(t.email):wt(t.key,a.id))}}))]:[]},e=>B(e.querySelector(".wpx-kebab")||e));const N=l.querySelector(".wpx-role");b.add(M.effect(()=>{const e=$.value;if(!N)return;const t=e?.roles||[],a=e?.role||"",o=`${t.map(r=>r.name).join(",")}|${a}`;N.dataset.sig!==o&&(N.dataset.sig=o,N.replaceChildren(...t.map(r=>{const c=document.createElement("option");return c.value=r.name,c.textContent=r.name===Ze?`${r.label} (default)`:r.label,c})),N.value=a)}));const bt=()=>P({userRole:N.value===Ze?"":N.value});N?.addEventListener("change",bt),b.add(()=>N?.removeEventListener("change",bt)),U(l.querySelector(".wpx-redirects"),".wpx-row",e=>[{header:e.dataset.from},{label:"Copy the old address",icon:"copy",action:()=>Fe(e.dataset.from)}],null),U(l.querySelector(".wpx-history"),".wpx-row",e=>[{header:_.peek().find(t=>t.id===e.dataset.id)?.siteTitle||"Import"},{label:"Open the report",icon:"file-text",action:()=>be(e.dataset.id)},{label:"Remove from the history",icon:"trash-2",action:()=>Ka(e.dataset.id)}],e=>be(e.dataset.id)),po(u,{id:"wpx-upload",icon:"upload",title:"Upload an export",onClick:()=>{h.peek()&&oe(),setTimeout(()=>ne.click(),0)}}),I.scan(l);const Be=Pt(v(),d.peek().map(e=>e.id),g);Be.pruned&&k&&A({owner:g,items:Be.items});const gt=Object.entries(Be.items).sort((e,t)=>String(t[1].at).localeCompare(String(e[1].at)))[0];gt&&!y.peek()&&await fe(gt[0])}};
95
+ </div>`);o.querySelector(".wpx-tiles").innerHTML=zt,o.querySelector(".wpx-note-list").innerHTML=ze,o.querySelector(".wpx-seg").innerHTML=je,o.querySelector(".wpx-report-items").innerHTML=Ut,o.querySelector(".wpx-report-redirects").innerHTML=_t,o.querySelector(".wpx-report-people").innerHTML=Vo;const r=M.observable(t.run?.summary?.failed?"failed":"all"),c=/^\/[a-z0-9][a-z0-9/-]{0,200}$/.test(t.home?.previous||"")?t.home.previous:"",De=t.items||[],kt=Ct(De,Ot,Bt),O=t.run?.summary||{},He=s=>Object.values(O.kinds||{}).reduce((n,k)=>n+(k[s]||0),0),Xa={meta:M.observable([te(t.run?.at),t.run?.by,t.run?.file,t.run?.durationMs?`${Math.max(1,Math.round(t.run.durationMs/1e3))} s`:""].filter(Boolean).join(" \xB7 ")),tiles:M.observable([{id:"made",label:"Made",value:He("created"),sub:""},{id:"updated",label:"Updated",value:He("updated"),sub:""},{id:"kept",label:"Left as is",value:He("kept"),sub:""},{id:"redirects",label:"Redirects",value:(O.redirects?.created||0)+(O.redirects?.updated||0),sub:""},...O.people?[{id:"people",label:"Accounts made",value:O.people.created||0,sub:O.people.invited?`${O.people.invited} emailed`:""}]:[],{id:"comments",label:"Comments",value:O.comments||0,sub:""},{id:"failed",label:"Failed",value:O.failed||0,sub:""}]),notes:M.observable((t.notes||[]).map(s=>({text:s}))),homePrevious:M.observable(c),homePreviousText:M.observable(c?`This site's home page from before the import${t.home?.previousTitle?` ("${t.home.previousTitle}")`:""} is now a draft at ${c} - nothing was deleted. Publish it there, or delete it, from Pages.`:""),people:M.observable((t.people||[]).map(Eo)),filters:M.computed(()=>Ot.filter(s=>s.id==="all"||kt[s.id]).map(s=>({...s,count:kt[s.id],cls:r.value===s.id?"is-on":""}))),rows:M.computed(()=>De.filter(s=>Bt(s,r.value)).slice(0,1e3).map(s=>{const n=Oo(s);return{...n,pillCls:n.pillClass}})),redirects:M.observable((t.redirects||[]).filter(s=>s.result!=="skipped").slice(0,1e3).map(s=>({...s,reason:s.error||"",pill:s.result==="failed"?"Failed":s.result==="updated"?"Updated":"Added",pillClass:s.result==="failed"?"is-bad":"is-ok"})))};a.element.appendChild(o);const eo=M.applyBindings(Xa,o,{methods:{pickSeg:s=>{r.value=s.id}}}),to=Et(o),Mt=new Map(De.map(s=>[s.key,s])),ao=o.querySelector(".wpx-report-items"),oo=s=>{const n=Mt.get(s.dataset.key);if(!n)return[];const k=typeof n.target=="string"&&n.target.startsWith("/")?n.target:"",C=[{header:n.title}];if(k&&n.result!=="failed"&&n.result!=="skipped"&&C.push({label:"Open on the site",icon:"external-link",action:()=>window.open(k,"_blank","noopener")}),k&&(n.kind==="page"||n.kind==="custom"&&!n.collection)&&(n.result==="created"||n.result==="updated"||n.result==="kept")&&C.push({label:"Edit the page",icon:"edit",action:()=>{a.close(),location.hash=`#/pages/edit${k}`}}),n.kind==="widgets"&&/^[a-z0-9][a-z0-9-]{0,79}$/.test(n.target||"")&&["created","updated","kept"].includes(n.result)&&(C.push({label:"Edit the block",icon:"edit",action:()=>{a.close(),location.hash=`#/blocks/edit/${n.target}`}}),n.footer&&C.push({label:"Footer settings",icon:"settings",action:()=>{a.close(),location.hash="#/settings"}})),(n.kind==="collection"||n.kind==="custom")&&n.result!=="failed"&&n.result!=="skipped"&&/^[a-z0-9-]{1,80}$/.test(n.kind==="collection"?n.target:n.collection||"")){const Y=n.kind==="collection"?n.target:n.collection;C.push({label:"Open the collection",icon:"database",action:()=>{a.close(),location.hash=`#/collections/${Y}/entries`}})}return n.kind==="product"&&n.entryId&&/^[\w-]{1,64}$/.test(n.entryId)&&C.push({label:"Edit in the Shop",icon:"shopping-bag",action:()=>{a.close(),location.hash=`#/plugins/shopping-cart/products/${encodeURIComponent(n.entryId)}`}}),/^https?:\/\//i.test(n.oldUrl)&&C.push({label:"Open the old address",icon:"link",action:()=>window.open(n.oldUrl,"_blank","noopener,noreferrer")}),k&&C.push({label:"Copy the new address",icon:"copy",action:()=>Fe(new URL(k,location.origin).href)}),C},so=E.contextMenu(ao,{cursor:"glyph",match:".wpx-row",inherit:!1,density:"compact",items:oo}),no=o.querySelector(".wpx-report-people"),lo=new Map((t.people||[]).map(s=>[s.key,s])),ro=E.contextMenu(no,{cursor:"glyph",match:".wpx-row",inherit:!1,density:"compact",items:s=>{const n=lo.get(s.dataset.key);if(!n)return[];const k=/^[0-9a-f-]{36}$/i.test(n.userId||"")?n.userId:"";return[{header:n.name},...k&&n.result!=="skipped"?[{label:"Open their account",icon:"user",action:()=>{a.close(),location.hash=`#/users/edit/${k}`}}]:[],{label:"Open Users",icon:"users",action:()=>{a.close(),location.hash="#/users"}}]}}),io=o.querySelector(".wpx-report-redirects"),po=E.contextMenu(io,{cursor:"glyph",match:".wpx-row",inherit:!1,density:"compact",items:s=>[{header:s.dataset.from},...s.dataset.from.includes("*")?[]:[{label:"Try it on the site",icon:"external-link",action:()=>window.open(s.dataset.from,"_blank","noopener")}],{label:"Open SEO \u203A Redirects",icon:"corner-up-right",action:()=>{a.close(),location.hash="#/seo/redirects"}}]}),Ue=s=>{if(s.target.closest(".wpx-open-previous")&&c){a.close(),location.hash=`#/pages/edit${c}`;return}const n=s.target.closest(".wpx-kebab");if(n){B(n);return}const k=s.target.closest(".wpx-report-people .wpx-row");if(k){B(k.querySelector(".wpx-kebab")||k);return}const C=s.target.closest(".wpx-report-items .wpx-row");if(!C)return;const Y=Mt.get(C.dataset.key);Y?.target?.startsWith("/")&&Y.result!=="failed"&&Y.result!=="skipped"&&window.open(Y.target,"_blank","noopener")},Tt=s=>{const n=s.target.closest(".wpx-row");n&&(s.key==="F10"&&s.shiftKey?(s.preventDefault(),B(n)):s.key==="Enter"&&s.target===n&&(s.preventDefault(),s.stopPropagation(),Ue({target:n})))};o.addEventListener("click",Ue),o.addEventListener("keydown",Tt);const co=b.add(()=>a.close());ho(a,()=>{Z=!1,eo.dispose(),to(),so?.destroy?.(),po?.destroy?.(),ro?.destroy?.(),o.removeEventListener("click",Ue),o.removeEventListener("keydown",Tt),co()}),a.open(),I.scan(o)}async function Ya(e){const t=_.peek().find(a=>a.id===e);if(t&&await Te(`Remove the import of <strong>${G(t.siteTitle||t.source)}</strong> from this list? Everything it imported stays on the site.`,{title:"Remove from the history",confirmText:"Remove"}))try{if(await R(`${P}/history/${e}`,{method:"DELETE"}),!p())return;_.value=_.peek().filter(a=>a.id!==e),q(ie)}catch(a){E.toast(`Not removed: ${a.message||a}`,{type:"error"})}}l.querySelector(".wpx-staged").innerHTML=Wo,l.querySelector(".wpx-items").innerHTML=Ut,l.querySelector(".wpx-redirects").innerHTML=_t,l.querySelector(".wpx-history").innerHTML=Fo,l.querySelector(".wpx-tiles").innerHTML=zt,l.querySelector(".wpx-note-list").innerHTML=ze,l.querySelector(".wpx-home-notes").innerHTML=ze,l.querySelector(".wpx-kinds").innerHTML=je,l.querySelector(".wpx-filters").innerHTML=je,l.querySelector(".wpx-facts").innerHTML=Go,l.querySelector(".wpx-product-notes").innerHTML=jt,l.querySelector(".wpx-types").innerHTML=Ko,l.querySelector(".wpx-internal").innerHTML=jt,l.querySelector(".wpx-people-list").innerHTML=Zo,l.querySelector(".wpx-widget-areas").innerHTML=Yo;const Ga={mode:Wt,job:y,jobTitle:_a,jobText:za,jobPct:tt,staged:d,stagedRows:Kt,historyRows:Zt,plan:f,planLoading:pe,planError:$e,refreshing:Ee,opt:i,blogAvailable:H,blogNote:Na,dirty:Ve,canRun:et,siteTitle:sa,siteMeta:na,tiles:ea,notes:ta,kinds:Vt,filters:Gt,rows:Jt,moreText:Qt,emptyText:Xt,redirectRows:aa,redirectCount:oa,query:Q,uploadText:ce,uploadError:X,dropping:ue,wxrLimit:ja,zipInfo:he,zipText:Oa,zipButton:Ba,zipUploadText:de,hasPeople:$a,peopleRows:Sa,peopleCanCreate:La,peopleChoosable:Pa,peopleNote:Ca,peopleNoteCls:Ra,inviteAvailable:qa,inviteText:Aa,optInvite:Ia,shopAvailable:Ft,hasProducts:ca,productNote:la,productNoteCls:ra,productFacts:ia,productNotes:pa,typeRows:Le,internalRows:Pe,hasCustom:Ta,internalSummary:Ea,widgetsInfo:Qe,widgetsText:Da,widgetsButton:Ha,widgetsUploadText:Ce,widgetRows:Xe,hasHome:da,homeWp:ua,homeNow:ma,homeAfter:ha,homeAfterCls:va,homeNotes:wa,homeEdit:fa,homeFrontSwitch:ba,homeLatestSwitch:ga,homePostsSwitch:ya,homePostsText:xa,homeQuestion:Ye,runWhy:Ua,optHome:W("home"),optLatest:ka,optPostsPage:Ma,optComments:W("comments"),optMenus:W("menus"),optRedirects:W("redirects"),optDrafts:W("drafts"),optProducts:W("products")},Ja=M.applyBindings(Ga,l,{methods:{back:()=>oe(),discard:()=>ot(),run:()=>Va(),resetChoices:()=>{i.value=_e(H.peek())},postsBlog:()=>$({posts:"blog"}),postsPages:()=>$({posts:"pages"}),postsSkip:()=>$({posts:"skip"}),mediaDownload:()=>$({media:"download"}),mediaZip:()=>$({media:"zip"}),mediaSkip:()=>$({media:"skip"}),homeYes:()=>$({homeReplace:!0}),homeNo:()=>$({homeReplace:!1}),removeZip:()=>Za(),removeWidgets:()=>Ka(),peopleAll:()=>$({users:Lt(L.peek(),"create")}),peopleNone:()=>$({users:Lt(L.peek(),"skip")}),pickSeg:e=>{e.seg==="kind"?D.value=e.id:j.value=e.id},showProducts:()=>{D.value="product",j.value="all",Q.value="",l.querySelector(".wpx-items")?.closest(".wpx-card")?.scrollIntoView({behavior:"smooth",block:"start"})}}});b.add(()=>Ja.dispose()),b.add(Et(l,{position:"bottom"}));const ne=l.querySelector(".wpx-file"),nt=()=>{const e=ne.files?.[0];ne.value="",st(e)};ne.addEventListener("change",nt);const V=l.querySelector(".wpx-drop"),lt=e=>{e.preventDefault(),ue.value=!0},rt=()=>{ue.value=!1},it=e=>{e.preventDefault(),ue.value=!1,st(e.dataTransfer?.files?.[0])};V.addEventListener("dragover",lt),V.addEventListener("dragleave",rt),V.addEventListener("drop",it);const ge=l.querySelector(".wpx-zip-file"),pt=()=>{const e=ge.files?.[0];ge.value="",Wa(e)};ge.addEventListener("change",pt);const ye=l.querySelector(".wpx-wie-file"),ct=()=>{const e=ye.files?.[0];ye.value="",Fa(e)};ye.addEventListener("change",ct),b.add(()=>ye.removeEventListener("change",ct)),b.add(()=>{ne.removeEventListener("change",nt),V.removeEventListener("dragover",lt),V.removeEventListener("dragleave",rt),V.removeEventListener("drop",it),ge.removeEventListener("change",pt)}),U(l.querySelector(".wpx-staged"),".wpx-row",e=>[{header:d.peek().find(t=>t.id===e.dataset.id)?.siteTitle||"Upload"},{label:"Preview the import",icon:"eye",action:()=>fe(e.dataset.id)},{label:"Discard the upload",icon:"trash-2",action:()=>ot(e.dataset.id)}],e=>fe(e.dataset.id));const dt=(e,t)=>{i.value={...i.peek(),choices:fo(i.peek().choices,e,t)}};U(l.querySelector(".wpx-items"),".wpx-row",e=>{const t=F.peek().find(o=>o.key===e.dataset.key);if(!t)return[];const a=[{header:t.title}];for(const o of Pt(t,t.choice))a.push({label:o.label,icon:o.icon,action:()=>dt(t.key,o.id)});return t.oldUrl&&/^https?:\/\//i.test(t.oldUrl)&&a.push({label:"Open the old address",icon:"link",action:()=>window.open(t.oldUrl,"_blank","noopener,noreferrer")}),t.target?.startsWith("/")&&(t.action==="update"||t.action==="keep")&&a.push({label:"Open it on this site",icon:"external-link",action:()=>window.open(t.target,"_blank","noopener")}),a.length===1&&a.push({label:"Nothing to change here",icon:"info",disabled:!0,action:()=>{}}),a},e=>B(e.querySelector(".wpx-kebab")||e));const ut=(e,t)=>{i.value=jo(i.peek(),e,t)},mt=(e,t)=>{i.value=zo(i.peek(),e,t)},Qa=e=>{D.value="",j.value="all",Q.value=e,l.querySelector(".wpx-items")?.closest(".wpx-card")?.scrollIntoView({behavior:"smooth",block:"start"})},le=l.querySelector(".wpx-types"),ht=e=>{if(!e.target.closest(".wpx-type-ctl"))return;e.stopPropagation();const a=e.target.closest("button[data-as]"),o=e.target.closest(".wpx-type-row");a&&o&&ut(o.dataset.type,a.dataset.as)},vt=e=>{const t=e.target.closest("input[data-list-page]"),a=e.target.closest(".wpx-type-row");t&&a&&mt(a.dataset.type,t.checked)};le.addEventListener("click",ht,!0),le.addEventListener("change",vt),b.add(()=>{le.removeEventListener("click",ht,!0),le.removeEventListener("change",vt)}),U(le,".wpx-type-row",e=>{const t=Le.peek().find(a=>a.type===e.dataset.type);return t?[{header:`${t.label} ("${t.type}")`},...Do(t).map(a=>({label:a.label,icon:a.icon,action:()=>{a.id==="show"?Qa(t.type):a.id==="list"||a.id==="nolist"?mt(t.type,a.id==="list"):ut(t.type,a.id)}}))]:[]},e=>B(e.querySelector(".wpx-kebab")||e));const Ne=(e,t)=>{i.value=_o(i.peek(),e,t)},re=l.querySelector(".wpx-widget-areas"),wt=e=>{e.target.closest(".wpx-type-ctl")&&e.stopPropagation()},ft=e=>{const t=e.target.closest("input[data-footer]"),a=e.target.closest(".wpx-widget-row");t&&a&&Ne(a.dataset.area,t.checked)};re.addEventListener("click",wt,!0),re.addEventListener("change",ft),b.add(()=>{re.removeEventListener("click",wt,!0),re.removeEventListener("change",ft)}),U(re,".wpx-widget-row",e=>{const t=Xe.peek().find(r=>r.area===e.dataset.area),a=F.peek().find(r=>r.key===`widgets:${e.dataset.area}`);if(!t)return[];const o=[{header:`${t.label} ("${t.area}")`}];if(t.canPlace&&o.push(t.placeOn?{label:"Leave it out of the footer",icon:"eye-off",action:()=>Ne(t.area,!1)}:{label:"Place in the footer",icon:"layout",action:()=>Ne(t.area,!0)}),a)for(const r of Pt(a,a.choice))o.push({label:r.label,icon:r.icon,action:()=>dt(a.key,r.id)});return o.length===1&&o.push({label:"Nothing to change here",icon:"info",disabled:!0,action:()=>{}}),o},e=>B(e.querySelector(".wpx-kebab")||e));const bt=(e,t)=>{const a=L.peek()?.rows?.find(o=>o.key===e);a&&$({users:bo(i.peek().users,a,t)})},Oe=l.querySelector(".wpx-people-list"),gt=e=>{if(!e.target.closest(".wpx-type-ctl"))return;e.stopPropagation();const a=e.target.closest("button[data-user]"),o=e.target.closest(".wpx-person");a&&o&&bt(o.dataset.key,a.dataset.user)};Oe.addEventListener("click",gt,!0),b.add(()=>Oe.removeEventListener("click",gt,!0)),U(Oe,".wpx-person",e=>{const t=L.peek()?.rows?.find(a=>a.key===e.dataset.key);return t?[{header:t.name},...Mo(t).map(a=>({label:a.label,icon:a.icon,disabled:!!a.disabled,action:()=>{a.disabled||(a.id==="copy"?Fe(t.email):bt(t.key,a.id))}}))]:[]},e=>B(e.querySelector(".wpx-kebab")||e));const N=l.querySelector(".wpx-role");b.add(M.effect(()=>{const e=L.value;if(!N)return;const t=e?.roles||[],a=e?.role||"",o=`${t.map(r=>r.name).join(",")}|${a}`;N.dataset.sig!==o&&(N.dataset.sig=o,N.replaceChildren(...t.map(r=>{const c=document.createElement("option");return c.value=r.name,c.textContent=r.name===Ze?`${r.label} (default)`:r.label,c})),N.value=a)}));const yt=()=>$({userRole:N.value===Ze?"":N.value});N?.addEventListener("change",yt),b.add(()=>N?.removeEventListener("change",yt)),U(l.querySelector(".wpx-redirects"),".wpx-row",e=>[{header:e.dataset.from},{label:"Copy the old address",icon:"copy",action:()=>Fe(e.dataset.from)}],null),U(l.querySelector(".wpx-history"),".wpx-row",e=>[{header:_.peek().find(t=>t.id===e.dataset.id)?.siteTitle||"Import"},{label:"Open the report",icon:"file-text",action:()=>be(e.dataset.id)},{label:"Remove from the history",icon:"trash-2",action:()=>Ya(e.dataset.id)}],e=>be(e.dataset.id)),mo(u,{id:"wpx-upload",icon:"upload",title:"Upload an export",onClick:()=>{h.peek()&&oe(),setTimeout(()=>ne.click(),0)}}),I.scan(l);const Be=Rt(v(),d.peek().map(e=>e.id),g);Be.pruned&&T&&A({owner:g,items:Be.items});const xt=Object.entries(Be.items).sort((e,t)=>String(t[1].at).localeCompare(String(e[1].at)))[0];xt&&!y.peek()&&await fe(xt[0])}};
@@ -1328,6 +1328,8 @@ The built-in Tools that can be switched off: `contacts`, `notes`, `todo`, `analy
1328
1328
 
1329
1329
  A Tool that is off keeps its data. Its API answers 404, its sidebar entry, notification and calendar sources go, and (for `analytics`) the page-view beacon is not emitted. Switching one off also switches off any enabled plugin that `requires` it - see `server/services/tools.js`.
1330
1330
 
1331
+ `"staff": true` beside `"enabled": false` keeps a Tool open to a staff session opened by the site's Domma CMS Manager (`/_staff/open`, services/staffAccess.js) and to nobody else, the site's own top role included. Domma CMS Manager sets `{"enabled": false, "locked": true, "staff": true}` for `wordpress-import` on the sites it hosts.
1332
+
1331
1333
  ---
1332
1334
 
1333
1335
  ## `config/connections.json` (Pro)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.8.11",
3
+ "version": "1.8.13",
4
4
  "description": "File-based CMS powered by Domma and Fastify. Run npx domma-cms my-site to create a new project.",
5
5
  "type": "module",
6
6
  "main": "server/server.js",
@@ -371,8 +371,8 @@ export async function pluginsRoutes(fastify, opts = {}) {
371
371
  });
372
372
 
373
373
  // Return merged admin config (sidebar, routes, views) for enabled plugins
374
- fastify.get('/plugins/admin-config', { preHandler: [authenticate] }, async () => {
375
- return getAdminPluginConfig();
374
+ fastify.get('/plugins/admin-config', { preHandler: [authenticate] }, async (request) => {
375
+ return getAdminPluginConfig(request.user);
376
376
  });
377
377
 
378
378
  // ── In-admin code editor (plugins.develop - code execution grade) ──────
@@ -17,6 +17,7 @@
17
17
  * Auth middlewares are accepted as DI options so tests can supply no-ops.
18
18
  */
19
19
  import {authenticate as defaultAuthenticate, checkVisibility} from '../../middleware/auth.js';
20
+ import {isDcmStaff} from '../../services/staffAccess.js';
20
21
  import {getRegisteredSidebarItems} from '../../services/hooks.js';
21
22
  import {filterItemsForUser, getMenu} from '../../services/menus.js';
22
23
  import {isDashboardPro} from '../../services/widgets.js';
@@ -60,6 +61,8 @@ export async function sidebarRoutes(fastify, opts = {}) {
60
61
  ? {...entry, item: {...entry.item, text: boardLabel}}
61
62
  : entry))
62
63
  .filter(entry => !entry?.item?.hidden && checkVisibility(request.user, entry?.item?.visibility))
64
+ // A staff-only Tool's entry (services/tools.js `staff`): the Manager's staff sessions only.
65
+ .filter(entry => !entry?.item?.staffOnly || isDcmStaff(request.user))
63
66
  .map(entry => Array.isArray(entry.item?.items)
64
67
  ? {...entry, item: {...entry.item, items: filterItemsForUser(entry.item.items, request.user)}}
65
68
  : entry);
@@ -2,7 +2,8 @@
2
2
  * Built-in Tool switches - Contacts, Notes, Todo, Analytics (services/tools.js).
3
3
  *
4
4
  * GET /api/tools - each built-in Tool, on/off, locked, and the plugins that depend on it (plugins.read)
5
- * GET /api/tools/enabled - {name: boolean}, for any signed-in user (plugin screens hide what needs a Tool that is off)
5
+ * GET /api/tools/enabled - {name: boolean}, for any signed-in user (plugin screens hide what needs a Tool that is off);
6
+ * a staff-only Tool (`staff: true`, services/tools.js) is true for a staff session only
6
7
  * PUT /api/tools/:name - {enabled, cascade?} (plugins.update). Answers 423 when the site's manager has
7
8
  * locked it, and 409 with the plan when the switch moves other Tools too - repeat
8
9
  * with `cascade: true` to apply. Restarts the site like a plugin switch does.
@@ -12,7 +13,10 @@ import {
12
13
  requirePermission as defaultRequirePermission
13
14
  } from '../../middleware/auth.js';
14
15
  import {discoverPlugins, getPluginStates, runLifecycleHook, savePluginState} from '../../services/plugins.js';
15
- import {CORE_TOOLS, getToolStates, isCoreTool, isToolEnabled, planSwitch, saveToolState, switchableUnits} from '../../services/tools.js';
16
+ import {
17
+ CORE_TOOLS, getToolStates, isCoreTool, isToolEnabled, isToolStaffOnly, planSwitch, saveToolState, switchableUnits, toolOpenAt
18
+ } from '../../services/tools.js';
19
+ import {isDcmStaff, isStaffRequest} from '../../services/staffAccess.js';
16
20
  import {autoRestartEnabled, hasSupervisor, scheduleRestart} from './plugins.js';
17
21
 
18
22
  /**
@@ -39,15 +43,22 @@ export async function applyCascade(cascaded, fastify) {
39
43
  * the Tool's prefix; the inner routes must NOT be given it again, or every
40
44
  * route lands at /api/notes/api/notes/... (0.88.0 did exactly that on dcm).
41
45
  *
46
+ * Off with `staff: true` (services/tools.js) lets a staff session through
47
+ * (services/staffAccess.js) and nobody else - not the site's own top role;
48
+ * the routes' own authenticate/permission checks still run.
49
+ *
42
50
  * @param {string} name the Tool (services/tools.js CORE_TOOLS)
43
51
  * @param {Function} routes its Fastify plugin
44
52
  * @param {(name: string) => boolean} [isEnabled]
53
+ * @param {{isStaffOnly?: (name: string) => boolean, staffPass?: (request: object) => Promise<boolean>}} [opts]
45
54
  */
46
- export function toolGate(name, routes, isEnabled = isToolEnabled) {
55
+ export function toolGate(name, routes, isEnabled = isToolEnabled, {isStaffOnly = isToolStaffOnly, staffPass = isStaffRequest} = {}) {
47
56
  const displayName = CORE_TOOLS.find(t => t.name === name)?.displayName || name;
48
57
  return async (scope) => {
49
58
  scope.addHook('onRequest', async (request, reply) => {
50
- if (!isEnabled(name)) return reply.code(404).send({error: `${displayName} is switched off on this site.`});
59
+ if (isEnabled(name)) return;
60
+ if (isStaffOnly(name) && await staffPass(request)) return;
61
+ return reply.code(404).send({error: `${displayName} is switched off on this site.`});
51
62
  });
52
63
  await scope.register(routes);
53
64
  };
@@ -74,6 +85,8 @@ export async function toolsRoutes(fastify, opts = {}) {
74
85
  ...t,
75
86
  enabled: u.enabled,
76
87
  locked: u.locked,
88
+ // Off for the site, kept open to the Manager's staff sessions.
89
+ staff: u.staff === true,
77
90
  // Enabled plugins that stop, or lose something, if this goes off.
78
91
  requiredBy: plugins.filter(p => p.enabled && p.requires.includes(t.name)).map(p => ({name: p.name, displayName: p.displayName})),
79
92
  usedBy: plugins.filter(p => p.enabled && t.name in p.uses).map(p => ({name: p.name, displayName: p.displayName, reason: p.uses[t.name]}))
@@ -81,9 +94,10 @@ export async function toolsRoutes(fastify, opts = {}) {
81
94
  });
82
95
  });
83
96
 
84
- fastify.get('/tools/enabled', {preHandler: [authenticate]}, async () => {
97
+ fastify.get('/tools/enabled', {preHandler: [authenticate]}, async (request) => {
85
98
  const states = getToolStates();
86
- return Object.fromEntries(CORE_TOOLS.map(t => [t.name, states[t.name]?.enabled !== false]));
99
+ const staff = isDcmStaff(request.user);
100
+ return Object.fromEntries(CORE_TOOLS.map(t => [t.name, toolOpenAt(states[t.name], staff)]));
87
101
  });
88
102
 
89
103
  fastify.put('/tools/:name', canUpdate, async (request, reply) => {
package/server/server.js CHANGED
@@ -578,7 +578,7 @@ await app.register(scaffoldRoutes, {prefix: '/api'});
578
578
  // Each can be switched off (config/tools.json, services/tools.js): the gate is
579
579
  // read per request, so an API stops answering the moment the switch is saved,
580
580
  // before the restart that takes its sidebar entry and sources away.
581
- const {isToolEnabled} = await import('./services/tools.js');
581
+ const {isToolEnabled, isToolStaffOnly} = await import('./services/tools.js');
582
582
  const {toolGate, toolsRoutes} = await import('./routes/api/tools.js');
583
583
  await app.register(toolGate('notes', notesRoutes), {prefix: '/api/notes'});
584
584
  await app.register(toolGate('todo', todoRoutes), {prefix: '/api/todo'});
@@ -635,7 +635,13 @@ await app.register(relatedRoutes, {prefix: '/api/related'});
635
635
  const {registerSidebarItem} = await import('./services/hooks.js');
636
636
  // `countUrl` is the badge: the sidebar fetches it and shows {count, label, tone}
637
637
  // (see fetchCounts in admin/js/lib/sidebar-renderer.js).
638
- // A built-in Tool switched off gets no entry (its switch restarts the site).
638
+ // A built-in Tool switched off gets no entry (its switch restarts the site) - unless it is
639
+ // off with `staff: true` (the Manager's lock on hosted sites): then the entry is there for
640
+ // the Manager's staff sessions only (`staffOnly`, checked per viewer by /api/sidebar/registered-items).
641
+ const toolEntry = (name, item) => {
642
+ if (isToolEnabled(name)) registerSidebarItem({item: {source: 'Built in', ...item}});
643
+ else if (isToolStaffOnly(name)) registerSidebarItem({item: {source: 'Built in', ...item, staffOnly: true}});
644
+ };
639
645
  const coreToolItems = [
640
646
  {text: 'Notes', url: '#/notes', icon: 'edit-3', permission: 'notes', countUrl: '/api/notes/count'},
641
647
  {text: 'Todo', url: '#/todo', icon: 'check-square', permission: 'todo', countUrl: '/api/todo/count'},
@@ -644,12 +650,8 @@ const coreToolItems = [
644
650
  {text: 'Contacts', url: '#/contacts', icon: 'users', permission: 'contacts', countUrl: '/api/contacts/count', inventory: true},
645
651
  {text: 'SEO', url: '#/seo', icon: 'search', permission: 'seo', countUrl: '/api/seo/count'}
646
652
  ];
647
- for (const item of coreToolItems) {
648
- if (isToolEnabled(item.permission)) registerSidebarItem({item: {source: 'Built in', ...item}});
649
- }
650
- if (isToolEnabled('wordpress-import')) {
651
- registerSidebarItem({item: {source: 'Built in', text: 'Import from WordPress', url: '#/wordpress-import', icon: 'download', permission: 'import', countUrl: '/api/import/wordpress/count', inventory: true}});
652
- }
653
+ for (const item of coreToolItems) toolEntry(item.permission, item);
654
+ toolEntry('wordpress-import', {text: 'Import from WordPress', url: '#/wordpress-import', icon: 'download', permission: 'import', countUrl: '/api/import/wordpress/count', inventory: true});
653
655
  // Todo's to-dos on the calendar (registered when routes/api/todo.js loads).
654
656
  if (!isToolEnabled('todo')) {
655
657
  const {unregisterCalendarSource} = await import('./services/hooks.js');
@@ -747,6 +749,10 @@ await app.register(docsPublicRoutes);
747
749
  // An instant call's page, /_call/<id> (a reserved prefix, like /_preview) - before the catch-all.
748
750
  const { callPublicRoutes } = await import('./routes/call-public.js');
749
751
  await app.register(callPublicRoutes);
752
+ // A staff session from the site's Manager (/_staff/open?ticket=…, services/staffAccess.js) - the one way
753
+ // into a Tool switched off with `staff: true`. A ticket only works on a site its Manager started.
754
+ const { staffAccessRoutes } = await import('./services/staffAccess.js');
755
+ await app.register(staffAccessRoutes);
750
756
 
751
757
  // The outreach unsubscribe link (/campaign-unsubscribe) - core's, so it outlives the Tool that
752
758
  // sent the email. After the plugins: an older Campaigns Pro that registers it itself wins.
@@ -45,10 +45,11 @@ import * as defaultRolesService from './roles.js';
45
45
  import {getAdminScope, normaliseAdminScope} from './roles.js';
46
46
  import {authenticateFor} from './pluginScope.js';
47
47
  import {getEffectiveRoles} from './userRoles.js';
48
+ import {isDcmStaff} from './staffAccess.js';
48
49
  import * as defaultUsersService from './users.js';
49
50
  import {registerLoginCheck, registerPasswordCheck} from './authChecks.js';
50
51
  import {invalidateTags as cacheInvalidateTags} from './cache/index.js';
51
- import {CORE_TOOLS, getToolStates, unmetRequirements} from './tools.js';
52
+ import {CORE_TOOLS, getToolStates, toolOpenAt, unmetRequirements} from './tools.js';
52
53
 
53
54
  const PLUGINS_DIR = path.resolve('plugins');
54
55
 
@@ -1353,7 +1354,10 @@ export function viewSection(manifest, viewName) {
1353
1354
  return text && text !== (manifest.displayName || manifest.name) ? text : '';
1354
1355
  }
1355
1356
 
1356
- export async function getAdminPluginConfig() {
1357
+ /**
1358
+ * @param {object} [viewer] request.user - a staff-only Tool (services/tools.js) is on for a staff session only
1359
+ */
1360
+ export async function getAdminPluginConfig(viewer = null) {
1357
1361
  const manifests = await discoverPlugins();
1358
1362
  const states = getPluginStates();
1359
1363
 
@@ -1432,10 +1436,12 @@ export async function getAdminPluginConfig() {
1432
1436
  }
1433
1437
  }
1434
1438
 
1435
- // Built-in Tools and whether each is on, so the admin can turn a link to a
1436
- // switched-off one away (services/tools.js).
1439
+ // Built-in Tools and whether each is on FOR THIS VIEWER, so the admin can
1440
+ // turn a link to a switched-off one away (services/tools.js; `staff: true`
1441
+ // keeps one open to the Manager's staff sessions).
1437
1442
  const toolStates = getToolStates();
1438
- const tools = Object.fromEntries(CORE_TOOLS.map(t => [t.name, {displayName: t.displayName, enabled: toolStates[t.name]?.enabled !== false}]));
1443
+ const staff = isDcmStaff(viewer);
1444
+ const tools = Object.fromEntries(CORE_TOOLS.map(t => [t.name, {displayName: t.displayName, enabled: toolOpenAt(toolStates[t.name], staff)}]));
1439
1445
 
1440
1446
  return { sidebar, routes, views, css, meta, takeovers: getToolTakeovers(), tools };
1441
1447
  }
@@ -3,7 +3,11 @@
3
3
  *
4
4
  * - Redirects: a global onRequest guard (GET/HEAD, never /api or /admin);
5
5
  * rules in content/seo/redirects.json (admin/js/lib/seo-redirects.js).
6
- * A renamed page gets one (content:pageRenamed), older rules re-aimed.
6
+ * A renamed page gets one (content:pageRenamed), older rules re-aimed -
7
+ * never one FROM "/" (the home address). A page at "/" always wins over a
8
+ * rule from "/": made, saved or moved there published, the rule is removed
9
+ * (as a page moved anywhere removes the rule from its new address), and at
10
+ * start an automatic one an older version left is removed if one is there.
7
11
  * - What search engines are given: the 'seo:meta', 'sitemap:entries' and
8
12
  * 'robots:lines' transforms (hooks.js section 8) - a URL's overrides,
9
13
  * Organization / BlogPosting JSON-LD, sitemap exclusions, robots rules.
@@ -30,7 +34,7 @@ import {applySeo, robotsLines} from '../../../admin/js/lib/seo-schema.js';
30
34
  import {checkSettings, excluded, mergeDefaults} from '../../../admin/js/lib/seo-settings.js';
31
35
  import {getConfig, saveConfig} from '../../config.js';
32
36
  import {hooks as coreHooks, getSitemapSources, registerTransform} from '../hooks.js';
33
- import {listPages} from '../content.js';
37
+ import {getPage, listPages} from '../content.js';
34
38
  import {invalidateTags} from '../cache/index.js';
35
39
 
36
40
  const SKIP = /^\/(api|admin)(\/|$)/;
@@ -129,7 +133,28 @@ async function seo(fastify, {enabled = true, getBaseUrl = null, rolesService = n
129
133
  const saveHits = setInterval(() => store.saveHits(), 60_000);
130
134
  saveHits.unref?.();
131
135
 
136
+ /**
137
+ * A page is at "/" now: a rule from "/" would hide it, so it goes - the page wins, as it does at any
138
+ * address a page is moved to (onMoved). `autoOnly`: only rules SEO added itself (the start-up tidy).
139
+ */
140
+ const clearHomeRule = (why, {autoOnly = false} = {}) => {
141
+ const drop = store.rules().filter(r => r.from === '/' && (!autoOnly || r.auto));
142
+ if (!drop.length) return;
143
+ const ids = new Set(drop.map(r => r.id));
144
+ store.setRules(store.rulesWithHits().filter(r => !ids.has(r.id)));
145
+ sitemapChanged();
146
+ log.info(`[seo] a page is at / (${why}): the redirect from / to ${drop.map(r => r.to || 'gone').join(', ')} was removed.`);
147
+ };
148
+ // Earlier versions added "/ -> its new address" when the home page was moved away, hiding any page made at "/" since.
149
+ // Only a page the public can see wins: a draft at "/" answers 404 to them, so a redirect from "/" stays until it is published.
150
+ const liveHome = (page) => Boolean(page) && page.status !== 'draft';
151
+ try { if (store.rules().some(r => r.from === '/' && r.auto) && liveHome(await getPage('/').catch(() => null))) clearHomeRule('found at start', {autoOnly: true}); } catch { /* left as it is */ }
152
+
132
153
  const onRenamed = ({from, to, redirect} = {}) => {
154
+ // A page moved to "/" lives there now, whatever the settings - a rule from "/" would hide it.
155
+ if (to && normalisePath(to) === '/') {
156
+ getPage('/').then((p) => { if (liveHome(p)) clearHomeRule(`moved there from ${from}`); }).catch(() => {});
157
+ }
133
158
  // redirect: false - the old address is not to lead to the new one (the home page moved aside).
134
159
  if (!cfg.redirects.onRename || !from || !to || redirect === false) return;
135
160
  try {
@@ -140,6 +165,12 @@ async function seo(fastify, {enabled = true, getBaseUrl = null, rolesService = n
140
165
  }
141
166
  };
142
167
  coreHooks.on('content:pageRenamed', onRenamed);
168
+ // Made at "/", or saved there as published (a draft made first, then published).
169
+ const onSaved = ({page, urlPath} = {}) => {
170
+ if (urlPath && normalisePath(urlPath) === '/' && liveHome(page)) { try { clearHomeRule('published there'); } catch { /* left as it is */ } }
171
+ };
172
+ coreHooks.on('content:pageCreated', onSaved);
173
+ coreHooks.on('content:pageUpdated', onSaved);
143
174
 
144
175
  // ---- 2. What search engines are given ----
145
176
  registerTransform('seo:meta', (meta, ctx) => {
@@ -327,6 +358,8 @@ async function seo(fastify, {enabled = true, getBaseUrl = null, rolesService = n
327
358
  fastify.addHook('onClose', (_i, done) => {
328
359
  clearInterval(timer); clearInterval(saveHits); clearTimeout(first);
329
360
  coreHooks.off?.('content:pageRenamed', onRenamed);
361
+ coreHooks.off?.('content:pageCreated', onSaved);
362
+ coreHooks.off?.('content:pageUpdated', onSaved);
330
363
  store.close();
331
364
  done();
332
365
  });