domma-cms 1.8.9 → 1.8.11

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/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.11] - 2026-10-16
4
+
5
+ - **Import from WordPress sets the home page.** A WordPress export does not say which page was the home page, but it gives each page's address, and the home page's address is the site's own - so that page is found, and only that one (a page merely called "Home" is just a page). The preview's new Home page card says which page WordPress showed at its home address, what this site's home page is now, and what will happen. By default the WordPress home page becomes this site's home page; the page that was there is never deleted or overwritten - it moves to /previous-home as a draft, and the preview and the report say so, with a link to it. Menus that lead to / still do, nothing redirects to the old page, and the WordPress home page's own old address (/home/, say) and its ?page_id= links lead to /. This happens once: run the import again and the home page is updated where it is, left as it is if you edited it here (unless you choose Overwrite), and left alone altogether if you have changed the home page yourself since - the report says so.
6
+ - The posts page cannot be read from the export either, so an empty page called Blog, News, Posts, Articles or Journal (or named like the Blog's address) is offered as a suggestion. Confirm it and no empty page is made: its old address, links and menu items lead to the Blog - or, when posts become pages, it becomes the list of those posts. When WordPress showed its latest posts on the home page, the card says so and, with a Blog switched on, offers to show the latest posts on this site's home page too (off unless you choose it).
7
+
8
+ ## [1.8.10] - 2026-10-16
9
+
10
+ - Domma Docs pages now show signed-in visitors their own menus. They were always drawn as for an anonymous visitor and cached as one copy for everybody; they now recognise who is looking, like every other page, and keep one cached copy per set of roles, so a menu item meant for admins is never shown to the public.
11
+
3
12
  ## [1.8.9] - 2026-10-16
4
13
 
5
14
  - **Import from WordPress brings the widgets across.** A WordPress export has no widgets in it, so the import screen now takes a second, optional file: the `.wie` file the free "Widget Importer & Exporter" WordPress plugin exports (or put it in the same zip as the export). Each widget area becomes a block named wp-… (Content › Blocks), its widgets in order under their titles. Text and custom HTML come across cleaned - no scripts, WordPress shortcodes left as text to rebuild - with links and pictures pointing at their new addresses; an image widget brings its picture with the media; a navigation menu shows the menu imported from it; recent posts and categories show the Blog's own lists, and archives a link to the Blog; a search widget becomes a link that opens the site search. Anything else (a calendar, an RSS feed, a plugin's widget) is listed to rebuild - nothing is guessed. A footer widget area is also added to the footer of every page, after whatever is there, unless you switch that off in the preview's new Widgets card; other areas are blocks you can put on a page with `[block]`. Run it again and a block you edited here is left alone unless you choose Overwrite, nothing is added to the footer twice, and an area you took out of the footer is not put back.
package/CLAUDE.md CHANGED
@@ -416,6 +416,21 @@ A built-in Tool (`wordpress-import` in `services/tools.js`, permission `import.w
416
416
  choice; `removed` = an earlier run placed it and it is gone now - not put back; `full` = the footer already has
417
417
  `FOOTER_BLOCKS_MAX` (8), which Site Settings also refuses to exceed). A widget that comes out empty is listed, never dropped. `map.json` keeps `{block, hash,
418
418
  placed}`; an edited block is "Left as is" unless Overwrite.
419
+ - **The home page** (`front.js`, pure; `plan.home`): a WXR has no options, so the front page is the ONE published page whose
420
+ `<link>` is the home address (`homeKeys()`: channel link / base_blog_url; scheme, www and trailing slash ignored) - never
421
+ by title; two such pages = none. `options.home` (default on) gives it `/`; the first run that does so moves the page at
422
+ `/` to `/previous-home` (`freePath`, so `-2`…) as a draft (`runHome()` in index.js, before the Pages phase) via
423
+ `renamePage(…, {redirect: false, source: 'wordpress-import'})` - `redirect: false` stops SEO's onRename rule (it would
424
+ redirect `/` itself) and menus are NOT relinked (`/` stays `/`). `mapping.items.home` records it; while it exists -
425
+ in ANY source's mapping (`site.homeSetBy`: once per site, not per export) - nothing moves again (`claimHome()` is the
426
+ one place that decides a move). If the new home page is not made, `putHomeBack()` returns the old one to `/`. A
427
+ password-protected front page is not offered (it would arrive as a draft); a posts page an earlier run imported as a
428
+ page is not offered either (a redirect would hide it). A home page changed by hand since is left
429
+ and `plan.home.changed` says so. The front page's slug address and `?page_id=` lead to `/`. The posts page is only a
430
+ suggestion (`suggestPostsPage()`: empty, published, slug in `POSTS_PAGE_SLUGS` or the Blog's base) confirmed by
431
+ `options.postsPage` = its id: with the Blog it is a skip row (`postsPage`) whose address/query/menu items lead to the
432
+ Blog; posts-as-pages, it becomes their index (`postsIndex`, `postsIndexBody()`). No front page: `options.latestPosts`
433
+ (off) makes a `home:latest` page holding `[blog-list]`, through the same claim.
419
434
  - **Footer blocks** (`site.json` footer.blocks, Site Settings > Footer): `buildFooterBlocks()` in `renderer.js` renders
420
435
  each as `[block template="…" /]` through `parseMarkdown` into `<div class="cms-footer-blocks">` INSIDE
421
436
  `#site-footer` - on BOTH render paths (`footerBlocksHtml`). site.js lifts that node out before `$footer.html()` (which
@@ -1 +1 @@
1
- export const KIND_LABEL={page:"Page",post:"Post",product:"Product",custom:"Custom item",collection:"Collection",media:"Media",menu:"Menu",category:"Category",widgets:"Widget area"},KIND_PLURAL={page:"Pages",post:"Posts",product:"Products",custom:"Custom items",collection:"Collections",media:"Media",menu:"Menus",category:"Categories",widgets:"Widgets"},KIND_ICON={page:"file-text",post:"edit-3",product:"shopping-bag",custom:"box",collection:"database",media:"image",menu:"menu",category:"tag",widgets:"layout"},PLACE_TEXT={add:"Goes in the footer, after what is there now",there:"Already in the footer",off:"Not put in the footer (you chose this)",removed:"Not put back in the footer - it was taken out here since the last import",full:"Not put in the footer - it already has 8 blocks; make room in Site Settings \u203A Footer"},TYPE_AS={collection:{text:"Collection",tone:"ok"},pages:{text:"Pages",tone:"info"},skip:{text:"Skipped",tone:"muted"}},PRODUCT_TYPE_LABEL={simple:"Simple",variable:"With options",grouped:"Grouped",external:"External"},ACTION={create:{text:"New",tone:"ok"},rename:{text:"New, renamed",tone:"warn"},update:{text:"Update",tone:"info"},keep:{text:"Left as is",tone:"muted"},skip:{text:"Skipped",tone:"muted"}},RESULT={created:{text:"Made",tone:"ok"},updated:{text:"Updated",tone:"info"},kept:{text:"Left as is",tone:"muted"},skipped:{text:"Skipped",tone:"muted"},failed:{text:"Failed",tone:"bad"}},PLAN_FILTERS=[{id:"all",label:"All"},{id:"new",label:"New"},{id:"update",label:"Updates"},{id:"renamed",label:"Renamed"},{id:"keep",label:"Left as is"},{id:"skip",label:"Skipped"},{id:"rebuild",label:"Needs rebuilding"}],REPORT_FILTERS=[{id:"all",label:"All"},{id:"created",label:"Made"},{id:"updated",label:"Updated"},{id:"kept",label:"Left as is"},{id:"skipped",label:"Skipped"},{id:"failed",label:"Failed"},{id:"rebuild",label:"Needs rebuilding"}];const p=e=>!!(e.builder||e.needs||e.shortcodes?.length),u=e=>[e.builder?`Built with ${e.builder}`:"",e.needs||"",...(e.shortcodes||[]).map(t=>`[${t}]`)].filter(Boolean).join(" ");export function planMatches(e,t){switch(t){case"new":return e.action==="create"||e.action==="rename";case"update":return e.action==="update";case"renamed":return e.action==="rename"||!!e.replaces;case"keep":return e.action==="keep";case"skip":return e.action==="skip";case"rebuild":return p(e);default:return!0}}export function reportMatches(e,t){return t==="rebuild"?p(e):t==="all"||!t||e.result===t}export function counts(e,t,o,i=""){const n=e.filter(s=>!i||s.kind===i);return Object.fromEntries(t.map(s=>[s.id,n.filter(a=>o(a,s.id)).length]))}export function kindTabs(e){const t=["page","post","product","custom","collection","media","menu","category","widgets"],o={};for(const i of e)o[i.kind]=(o[i.kind]||0)+1;return[{id:"",label:"Everything",count:e.length},...t.filter(i=>o[i]).map(i=>({id:i,label:KIND_PLURAL[i],count:o[i]}))]}export function planRow(e){const t=ACTION[e.action]||ACTION.skip;let o=e.target?e.kind==="menu"?`Menu "${e.target}"`:e.kind==="collection"?`Collection "${e.target}"`:e.kind==="widgets"?`Block "${e.target}"`:e.target:"";e.kind==="widgets"&&e.place&&e.action!=="skip"&&(o=`${o}${o?" \xB7 ":""}${PLACE_TEXT[e.place]||""}`),e.kind==="custom"&&e.as==="collection"&&e.collection&&e.action!=="skip"&&(o=`In the collection "${e.collection}"${e.listAt?`, listed at ${e.listAt}`:""}`);const i=[e.kind==="custom"&&e.postType?`${KIND_LABEL.custom} (${e.postType})`:KIND_LABEL[e.kind]||e.kind];return e.listFor&&i.push("list page"),e.status&&(e.kind==="page"||e.kind==="post"||e.kind==="custom")&&i.push(e.status),e.kind==="collection"&&e.fields&&i.push(`${e.fields} fields`),e.kind==="product"&&(e.productType&&i.push(PRODUCT_TYPE_LABEL[e.productType]||e.productType),e.variants&&i.push(`${e.variants} option${e.variants===1?"":"s"}`),e.action!=="skip"&&e.status&&i.push(e.status==="active"?"on sale":"draft")),e.comments&&i.push(`${e.comments} comment${e.comments===1?"":"s"}`),e.count&&i.push(e.kind==="widgets"?`${e.count} widget${e.count===1?"":"s"}`:`${e.count} item${e.count===1?"":"s"}`),e.kind==="widgets"&&e.widgetTypes?.length&&i.push(e.widgetTypes.join(", ")),{key:e.key,kind:e.kind,icon:KIND_ICON[e.kind]||"file",title:e.title||"(untitled)",pill:e.replaces?"Replaces a page":t.text,pillClass:`is-${e.replaces?"warn":t.tone}`,where:o,meta:i.join(" \xB7 "),reason:e.reason||"",rebuild:u(e),chosen:!!e.choice}}export function reportRow(e){const t=RESULT[e.result]||RESULT.skipped;return{key:e.key,kind:e.kind,icon:KIND_ICON[e.kind]||"file",title:e.title||"(untitled)",pill:t.text,pillClass:`is-${t.tone}`,where:e.target||"",meta:[KIND_LABEL[e.kind]||e.kind,e.format==="html"?"kept as HTML":"",e.kind==="widgets"&&e.placed?"added to the footer":""].filter(Boolean).join(" \xB7 "),reason:[e.reason,...e.notes||[]].filter(Boolean).join(" "),rebuild:u(e)}}export function choiceItems(e,t){const o=[];return t&&o.push({id:"reset",label:"Undo my choice",icon:"rotate-ccw"}),t!=="skip"&&e.action!=="skip"&&e.action!=="keep"&&o.push({id:"skip",label:"Skip this",icon:"x-circle"}),t!=="overwrite"&&e.action==="keep"&&o.push({id:"overwrite",label:"Overwrite my changes here",icon:"refresh-cw"}),t!=="overwrite"&&e.action==="rename"&&(e.kind==="page"||e.kind==="post")&&e.wanted&&e.kind==="page"&&o.push({id:"overwrite",label:`Replace the page at ${e.wanted}`,icon:"refresh-cw"}),t!=="import"&&e.action==="skip"&&/Deleted (on this site|from the Blog|from the Shop|from the collection)/.test(e.reason||"")&&o.push({id:"import",label:"Import again",icon:"download"}),o}export function applyChoice(e,t,o){const i={...e||{}};return!o||o==="reset"?delete i[t]:i[t]=o,i}export function defaultOptions(e=!1){return{posts:e?"blog":"pages",media:"download",comments:!0,menus:!0,redirects:!0,drafts:!0,products:!0,types:{},listPages:{},choices:{},users:{},userRole:"",invite:!1,footer:{}}}export function cleanOptions(e={},t=!1){const o=defaultOptions(t),i=(n,s,a)=>s.includes(n)?n:a;return{posts:i(e.posts,t?["blog","pages","skip"]:["pages","skip"],o.posts),media:i(e.media,["download","zip","skip"],o.media),comments:e.comments!==!1,menus:e.menus!==!1,redirects:e.redirects!==!1,drafts:e.drafts!==!1,products:e.products!==!1,types:Object.fromEntries(Object.entries(e.types||{}).filter(([n,s])=>f.test(n)&&(s==="pages"||s==="skip"))),listPages:Object.fromEntries(Object.entries(e.listPages||{}).filter(([n,s])=>f.test(n)&&s===!1)),choices:Object.fromEntries(Object.entries(e.choices||{}).filter(([n,s])=>typeof n=="string"&&["skip","overwrite","import"].includes(s))),users:Object.fromEntries(Object.entries(e.users||{}).filter(([n,s])=>g.test(n)&&(s==="create"||s==="skip"))),userRole:typeof e.userRole=="string"&&b.test(e.userRole)?e.userRole:"",invite:e.invite===!0,footer:Object.fromEntries(Object.entries(e.footer||{}).filter(([n,s])=>h.test(n)&&s===!1))}}const h=/^[A-Za-z0-9_-]{1,100}$/;export function withFooterChoice(e,t,o){const i={...e.footer||{}};return o?delete i[t]:i[t]=!1,{...e,footer:i}}export function optionsSnapshot(e){const t=cleanOptions(e,!0),o=Object.keys(t.choices).sort().map(l=>`${l}=${t.choices[l]}`).join(","),i=Object.keys(t.types).sort().map(l=>`${l}=${t.types[l]}`).join(","),n=Object.keys(t.listPages).sort().join(","),s=Object.keys(t.users).sort().map(l=>`${l}=${t.users[l]}`).join(","),a=Object.keys(t.footer).sort().join(",");return[t.posts,t.media,t.comments,t.menus,t.redirects,t.drafts,t.products,o,i,n,s,t.userRole,t.invite,a].join("|")}const f=/^[a-z0-9_-]{1,40}$/;export function withTypeChoice(e,t,o){const i={...e.types||{}};return o==="pages"||o==="skip"?i[t]=o:delete i[t],{...e,types:i}}export function withListPage(e,t,o){const i={...e.listPages||{}};return o?delete i[t]:i[t]=!1,{...e,listPages:i}}export const isDirty=(e,t)=>optionsSnapshot(e)!==optionsSnapshot(defaultOptions(t)),draftKey=e=>e?`wordpress-import.draft.${e}`:"";export function liveDrafts(e,t,o){if(!e||typeof e!="object"||!e.items||typeof e.items!="object")return{items:{},pruned:!1};if(e.owner&&o&&e.owner!==o)return{items:{},pruned:!1};const i={};let n=!1;for(const[s,a]of Object.entries(e.items))t.includes(s)&&a?.options&&a.was===s?i[s]=a:n=!0;return{items:i,pruned:n}}export function withDraft(e,t,o,i){const n={...e&&e.owner===t&&e.items||{}};return i?n[o]=i:delete n[o],{owner:t,items:n}}export function draftQuestion({when:e="",file:t=""}={}){return{title:"Choices you left last time",text:`You changed the import options${t?` for ${t}`:""}${e?` (${e})`:""} and left before running it. Throw away those choices and start from the defaults?`,confirmText:"Throw away",cancelText:"Keep them"}}export function progressText(e){if(!e)return"";const t=e.total?` \xB7 ${e.done} of ${e.total}`:"";return`${e.phase||"Working"}${t}${e.current?` \xB7 ${String(e.current).slice(0,80)}`:""}`}export const progressPercent=e=>e?.total?Math.min(100,Math.round(e.done/e.total*100)):0;export function planTiles(e){const t=e?.summary?.counts||{},o=i=>t[i]?t[i].total-t[i].skip:0;return[{id:"page",label:"Pages",value:o("page"),of:t.page?.total||0},{id:"post",label:"Posts",value:o("post"),of:t.post?.total||0},...t.product?[{id:"product",label:"Products",value:o("product"),of:t.product.total}]:[],...t.custom?[{id:"custom",label:"Custom items",value:o("custom"),of:t.custom.total}]:[],{id:"media",label:"Media",value:o("media"),of:t.media?.total||0},{id:"menu",label:"Menus",value:o("menu"),of:t.menu?.total||0},...e?.people?.rows?.length?[{id:"people",label:"New accounts",value:e.people.counts?.create||0,of:0}]:[],...t.widgets?[{id:"widgets",label:"Widget areas",value:o("widgets"),of:t.widgets.total}]:[],{id:"comments",label:"Comments",value:e?.summary?.comments||0,of:0},{id:"redirects",label:"Redirects",value:e?.summary?.redirects||0,of:0}].map(i=>({...i,sub:i.of&&i.of!==i.value?`of ${i.of}`:""}))}const g=/^author:(\d{1,12}|login-[\w.-]{1,60})$/,b=/^[a-z0-9][a-z0-9_-]{0,59}$/i;export const PERSON={create:{text:"New account",tone:"ok"},match:{text:"Matched",tone:"info"},linked:{text:"Linked before",tone:"info"},same:{text:"Same email",tone:"muted"},skip:{text:"No account",tone:"muted"}};export function creditText(e={}){const t=(o,i,n)=>e[o]?`${e[o]} ${e[o]===1?i:n}`:"";return[t("post","post","posts"),t("page","page","pages"),t("product","product","products"),t("custom","item","items")].filter(Boolean).join(", ")||"nothing in this import"}export function personRow(e){const t=PERSON[e.action]||PERSON.skip,o=e.problem==="email";return{key:e.key,name:e.name||"Author",meta:[e.email||(o?"no usable email":""),creditText(e.items)].filter(Boolean).join(" \xB7 "),reason:e.reason||"",pill:o?"No email":t.text,pillClass:`is-${o?"warn":t.tone}${e.choice?" is-chosen":""}`,choosable:!!e.choosable,segCreate:e.action==="create"?"is-on":"",segSkip:e.action==="skip"?"is-on":""}}export function chooseAllPeople(e,t){const o={};for(const i of e?.rows||[])i.choosable&&(o[i.key]=t);return o}export function choosePerson(e,t,o){const i={...e||{}},n=t.total?"create":"skip";return!o||o==="reset"||o===n?delete i[t.key]:i[t.key]=o,i}export function personMenuItems(e){const t=[];return e.choosable&&e.action!=="create"&&t.push({id:"create",label:"Make a new account",icon:"user-plus"}),e.choosable&&e.action!=="skip"&&t.push({id:"skip",label:"Do not make an account",icon:"user-x"}),e.choice&&t.push({id:"reset",label:"Undo my choice",icon:"rotate-ccw"}),e.email&&e.problem!=="email"&&t.push({id:"copy",label:"Copy the email address",icon:"copy"}),t.length||t.push({id:"none",label:"Nothing to choose for them",icon:"info",disabled:!0}),t}export function peopleSummary(e){const t=e?.counts||{};if(!e?.rows?.length)return{text:"This export names no authors.",tone:"info"};const o=[];t.create&&o.push(`${t.create} new account${t.create===1?"":"s"}`),t.match+t.linked&&o.push(`${t.match+t.linked} matched to ${t.match+t.linked===1?"a user":"users"} here`),t.skip&&o.push(`${t.skip} without an account`);let i=`${e.rows.length} author${e.rows.length===1?"":"s"}: ${o.join(", ")||"nothing to do"}.`;return t.create&&(i+=" New accounts have no password - nobody can sign in to one until a password is set."),!e.canCreate&&e.why&&(i+=` ${e.why}`),{text:i,tone:t.problems||!e.canCreate?"warn":"info"}}export function inviteNote(e){return e?.canCreate?e.mailReady?e.counts?.create?"Sent once the import has finished: an invitation saying an account has been made for them, with a link to set their password that works for 7 days.":"Nobody new to email.":"No mail server is set up (Settings \u203A Email), so this cannot be sent. After the import, use Users \u203A \u22EE \u203A Copy a reset link instead.":""}export function reportPersonRow(e){const t={created:{text:"Account made",tone:"ok"},matched:{text:"Matched",tone:"info"},linked:{text:"Linked before",tone:"info"},same:{text:"Same email",tone:"muted"},skipped:{text:"No account",tone:"muted"},failed:{text:"Failed",tone:"bad"}},o=t[e.result]||t.skipped,i=[creditText(e.items)];return e.result==="created"&&i.push(e.invited?"invited by email":e.inviteFailed?"the invitation was not sent":"no password set yet"),{key:e.key,name:e.name||"Author",meta:i.join(" \xB7 "),reason:e.reason||"",pill:o.text,pillClass:`is-${o.tone}`,userId:e.userId||""}}export function historyRow(e,t=o=>o){const o=e.summary||{},i=Object.values(o.kinds||{}).reduce((a,l)=>a+(l.created||0),0),n=Object.values(o.kinds||{}).reduce((a,l)=>a+(l.updated||0),0),s=[];return i&&s.push(`${i} made`),o.people?.created&&s.push(`${o.people.created} account${o.people.created===1?"":"s"}`),n&&s.push(`${n} updated`),o.redirects?.created&&s.push(`${o.redirects.created} redirect${o.redirects.created===1?"":"s"}`),o.rebuild&&s.push(`${o.rebuild} to rebuild`),o.failed&&s.push(`${o.failed} failed`),e.status==="failed"&&s.splice(0,s.length,"Stopped before the end - run it again to finish"),{id:e.id,title:e.siteTitle||e.source||"WordPress site",meta:[t(e.at),e.by,e.file].filter(Boolean).join(" \xB7 "),summary:s.join(", ")||"Nothing changed",pill:e.status==="failed"?"Stopped":e.status==="partial"?"Some failed":"Done",pillClass:e.status==="failed"||e.status==="partial"?"is-bad":"is-ok"}}export function stagedRow(e,t=o=>o){const o=e.counts||{},i=[o.page&&`${o.page} pages`,o.post&&`${o.post} posts`,o.product&&`${o.product} products`,o.attachment&&`${o.attachment} media`,e.widgets&&`${e.widgets.widgets} widget${e.widgets.widgets===1?"":"s"}`].filter(Boolean);return{id:e.id,title:e.siteTitle||e.fileName||"WordPress export",meta:[e.fileName,t(e.at),i.join(", ")].filter(Boolean).join(" \xB7 "),pill:e.uploads?"Uploads zip added":e.widgets?"Widgets added":"Waiting",pillClass:e.uploads||e.widgets?"is-ok":"is-muted"}}export function productSummary(e){const t=e?.summary?.products;if(!t?.total)return null;const o=t.shop||e.shop||{},i=e.options?.products!==!1,n=Object.entries(t.types||{}).map(([d,m])=>`${m} ${(PRODUCT_TYPE_LABEL[d]||d).toLowerCase()}`).join(", "),s=e.summary.counts?.product||{},a=[{label:"In the export",value:`${t.total}${n?` (${n})`:""}`}];o.available&&i&&(a.push({label:"On sale in the Shop",value:String(t.onSale||0)}),a.push({label:"As drafts",value:String(t.drafts||0)}),s.keep&&a.push({label:"Left as is",value:String(s.keep)}),a.push({label:"Skipped",value:String(s.skip||0)})),t.rebuild&&a.push({label:"Need rebuilding",value:String(t.rebuild)});let l,r="info";return o.available?i?l=`Products go to the Shop at ${o.basePath||"/shop"}.`:l='Products are not being imported - switch on "WooCommerce products" above to bring them in.':(l=o.reason||"Switch on the Shop to bring these in.",r="warn"),{note:l,tone:r,rows:a,notes:Array.isArray(t.notes)?t.notes:[]}}const c=(e,t,o=`${t}s`)=>`${e} ${e===1?t:o}`;export function customTypeRows(e){return(e?.summary?.customTypes||[]).map(t=>{const o=TYPE_AS[t.as]?t.as:"collection",i=TYPE_AS[o],n=t.rewrite?`was at /${t.rewrite}/\u2026`:"its old addresses are not in the export";let s;if(o==="skip")s="Nothing - left out.";else if(o==="pages")s=`${c(t.importing||0,"page")} under ${t.prefix}${t.drafts?` (${t.drafts} as draft${t.drafts===1?"":"s"})`:""}.`;else{const a=t.collection||{},l=a.action==="skip"?`The collection "${a.slug}" was deleted here - choose Import again on it below.`:`${a.action==="update"?"The collection":"A new collection"} "${a.slug}" with ${c(t.fields?.length||0,"field")}: ${c(t.importing||0,"entry","entries")}${t.drafts?` (${t.drafts} draft${t.drafts===1?"":"s"}, not shown on the site)`:""}.`,r=t.listPage?t.listAt?` A list page at ${t.listAt}${t.listAction==="keep"?" (edited here - left as it is)":""}; the old addresses lead there.`:"":" No list page - the old addresses are not redirected.";s=`${l}${r}`}return{type:t.type,label:t.label||t.type,meta:[c(t.count||0,"item"),`"${t.type}"`,n].join(" \xB7 "),samples:(t.samples||[]).length?`For example: ${t.samples.join(", ")}`:"",creates:s,pill:i.text,pillClass:`is-${i.tone}`,as:o,listOn:o==="collection"&&t.listPage!==!1,canList:o==="collection",listLabel:`List page at ${t.prefix||`/${t.type}`}`,segCollection:o==="collection"?"is-on":"",segPages:o==="pages"?"is-on":"",segSkip:o==="skip"?"is-on":""}})}export function internalTypeRows(e){return(e?.summary?.internalTypes||[]).map(t=>({type:t.type,text:`${t.type} (${t.count}) - ${t.reason}`}))}export function typeMenuItems(e){if(!e)return[];const t=[];return e.as!=="collection"&&t.push({id:"collection",label:"Import as a collection",icon:"database"}),e.as!=="pages"&&t.push({id:"pages",label:"Import as pages",icon:"file-text"}),e.as!=="skip"&&t.push({id:"skip",label:"Leave this type out",icon:"x-circle"}),e.as==="collection"&&t.push(e.listOn?{id:"nolist",label:"No list page",icon:"eye-off"}:{id:"list",label:"Make a list page",icon:"list"}),t.push({id:"show",label:"Show its items",icon:"filter"}),t}export function widgetAreaRows(e){return(e?.summary?.widgets?.areas||[]).map(t=>{const o=ACTION[t.action]||ACTION.skip,i=t.action==="skip"?"Not imported.":`${t.action==="update"?"Updates the block":t.action==="keep"?"The block (edited here - left as it is)":"A new block"} "${t.block}"${t.coming<t.count?` with ${c(t.coming,"widget")}`:""}.`;return{area:t.id,label:t.label||t.id,meta:[c(t.count||0,"widget"),`"${t.id}"`,(t.types||[]).join(", ")].filter(Boolean).join(" \xB7 "),creates:i,placeText:t.footer?PLACE_TEXT[t.place]||"":t.action==="skip"?"":"A sidebar - a block only; put it on a page with [block].",rebuild:(t.rebuild||[]).length?`Needs rebuilding: ${t.rebuild.join("; ")}`:"",pill:o.text,pillClass:`is-${o.tone}`,canPlace:!!t.footer&&t.action!=="skip"&&t.place!=="there"&&t.place!=="removed",placeOn:t.place==="add"}})}export function widgetsFileText(e,t=0){const o=e?.widgets;return o?`${o.fileName} - ${c(o.areas||0,"widget area")}, ${c(o.widgets||0,"widget")}.`:`Optional: the widgets, as the .wie file the free "Widget Importer & Exporter" WordPress plugin exports${t?` (up to ${Math.max(1,Math.round(t/1048576))} MB)`:""}.`}
1
+ export const KIND_LABEL={page:"Page",post:"Post",product:"Product",custom:"Custom item",collection:"Collection",media:"Media",menu:"Menu",category:"Category",widgets:"Widget area"},KIND_PLURAL={page:"Pages",post:"Posts",product:"Products",custom:"Custom items",collection:"Collections",media:"Media",menu:"Menus",category:"Categories",widgets:"Widgets"},KIND_ICON={page:"file-text",post:"edit-3",product:"shopping-bag",custom:"box",collection:"database",media:"image",menu:"menu",category:"tag",widgets:"layout"},PLACE_TEXT={add:"Goes in the footer, after what is there now",there:"Already in the footer",off:"Not put in the footer (you chose this)",removed:"Not put back in the footer - it was taken out here since the last import",full:"Not put in the footer - it already has 8 blocks; make room in Site Settings \u203A Footer"},TYPE_AS={collection:{text:"Collection",tone:"ok"},pages:{text:"Pages",tone:"info"},skip:{text:"Skipped",tone:"muted"}},PRODUCT_TYPE_LABEL={simple:"Simple",variable:"With options",grouped:"Grouped",external:"External"},ACTION={create:{text:"New",tone:"ok"},rename:{text:"New, renamed",tone:"warn"},update:{text:"Update",tone:"info"},keep:{text:"Left as is",tone:"muted"},skip:{text:"Skipped",tone:"muted"}},RESULT={created:{text:"Made",tone:"ok"},updated:{text:"Updated",tone:"info"},kept:{text:"Left as is",tone:"muted"},skipped:{text:"Skipped",tone:"muted"},failed:{text:"Failed",tone:"bad"}},PLAN_FILTERS=[{id:"all",label:"All"},{id:"new",label:"New"},{id:"update",label:"Updates"},{id:"renamed",label:"Renamed"},{id:"keep",label:"Left as is"},{id:"skip",label:"Skipped"},{id:"rebuild",label:"Needs rebuilding"}],REPORT_FILTERS=[{id:"all",label:"All"},{id:"created",label:"Made"},{id:"updated",label:"Updated"},{id:"kept",label:"Left as is"},{id:"skipped",label:"Skipped"},{id:"failed",label:"Failed"},{id:"rebuild",label:"Needs rebuilding"}];const f=e=>!!(e.builder||e.needs||e.shortcodes?.length),h=e=>[e.builder?`Built with ${e.builder}`:"",e.needs||"",...(e.shortcodes||[]).map(t=>`[${t}]`)].filter(Boolean).join(" ");export function planMatches(e,t){switch(t){case"new":return e.action==="create"||e.action==="rename";case"update":return e.action==="update";case"renamed":return e.action==="rename"||!!e.replaces;case"keep":return e.action==="keep";case"skip":return e.action==="skip";case"rebuild":return f(e);default:return!0}}export function reportMatches(e,t){return t==="rebuild"?f(e):t==="all"||!t||e.result===t}export function counts(e,t,o,s=""){const n=e.filter(i=>!s||i.kind===s);return Object.fromEntries(t.map(i=>[i.id,n.filter(a=>o(a,i.id)).length]))}export function kindTabs(e){const t=["page","post","product","custom","collection","media","menu","category","widgets"],o={};for(const s of e)o[s.kind]=(o[s.kind]||0)+1;return[{id:"",label:"Everything",count:e.length},...t.filter(s=>o[s]).map(s=>({id:s,label:KIND_PLURAL[s],count:o[s]}))]}export function planRow(e){const t=ACTION[e.action]||ACTION.skip;let o=e.target?e.kind==="menu"?`Menu "${e.target}"`:e.kind==="collection"?`Collection "${e.target}"`:e.kind==="widgets"?`Block "${e.target}"`:e.target:"";e.kind==="widgets"&&e.place&&e.action!=="skip"&&(o=`${o}${o?" \xB7 ":""}${PLACE_TEXT[e.place]||""}`),e.kind==="custom"&&e.as==="collection"&&e.collection&&e.action!=="skip"&&(o=`In the collection "${e.collection}"${e.listAt?`, listed at ${e.listAt}`:""}`);const s=[e.kind==="custom"&&e.postType?`${KIND_LABEL.custom} (${e.postType})`:KIND_LABEL[e.kind]||e.kind];return e.listFor&&s.push("list page"),e.status&&(e.kind==="page"||e.kind==="post"||e.kind==="custom")&&s.push(e.status),e.kind==="collection"&&e.fields&&s.push(`${e.fields} fields`),e.kind==="product"&&(e.productType&&s.push(PRODUCT_TYPE_LABEL[e.productType]||e.productType),e.variants&&s.push(`${e.variants} option${e.variants===1?"":"s"}`),e.action!=="skip"&&e.status&&s.push(e.status==="active"?"on sale":"draft")),e.comments&&s.push(`${e.comments} comment${e.comments===1?"":"s"}`),e.count&&s.push(e.kind==="widgets"?`${e.count} widget${e.count===1?"":"s"}`:`${e.count} item${e.count===1?"":"s"}`),e.kind==="widgets"&&e.widgetTypes?.length&&s.push(e.widgetTypes.join(", ")),{key:e.key,kind:e.kind,icon:KIND_ICON[e.kind]||"file",title:e.title||"(untitled)",pill:e.replaces?"Replaces a page":t.text,pillClass:`is-${e.replaces?"warn":t.tone}`,where:o,meta:s.join(" \xB7 "),reason:e.reason||"",rebuild:h(e),chosen:!!e.choice}}export function reportRow(e){const t=RESULT[e.result]||RESULT.skipped;return{key:e.key,kind:e.kind,icon:KIND_ICON[e.kind]||"file",title:e.title||"(untitled)",pill:t.text,pillClass:`is-${t.tone}`,where:e.target||"",meta:[KIND_LABEL[e.kind]||e.kind,e.format==="html"?"kept as HTML":"",e.kind==="widgets"&&e.placed?"added to the footer":""].filter(Boolean).join(" \xB7 "),reason:[e.reason,...e.notes||[]].filter(Boolean).join(" "),rebuild:h(e)}}export function choiceItems(e,t){const o=[];return t&&o.push({id:"reset",label:"Undo my choice",icon:"rotate-ccw"}),t!=="skip"&&e.action!=="skip"&&e.action!=="keep"&&o.push({id:"skip",label:"Skip this",icon:"x-circle"}),t!=="overwrite"&&e.action==="keep"&&o.push({id:"overwrite",label:"Overwrite my changes here",icon:"refresh-cw"}),t!=="overwrite"&&e.action==="rename"&&(e.kind==="page"||e.kind==="post")&&e.wanted&&e.kind==="page"&&o.push({id:"overwrite",label:`Replace the page at ${e.wanted}`,icon:"refresh-cw"}),t!=="import"&&e.action==="skip"&&/Deleted (on this site|from the Blog|from the Shop|from the collection)/.test(e.reason||"")&&o.push({id:"import",label:"Import again",icon:"download"}),o}export function applyChoice(e,t,o){const s={...e||{}};return!o||o==="reset"?delete s[t]:s[t]=o,s}export function defaultOptions(e=!1){return{posts:e?"blog":"pages",media:"download",comments:!0,menus:!0,redirects:!0,drafts:!0,products:!0,types:{},listPages:{},choices:{},users:{},userRole:"",invite:!1,footer:{},home:!0,postsPage:0,latestPosts:!1}}export function cleanOptions(e={},t=!1){const o=defaultOptions(t),s=(n,i,a)=>i.includes(n)?n:a;return{posts:s(e.posts,t?["blog","pages","skip"]:["pages","skip"],o.posts),media:s(e.media,["download","zip","skip"],o.media),comments:e.comments!==!1,menus:e.menus!==!1,redirects:e.redirects!==!1,drafts:e.drafts!==!1,products:e.products!==!1,types:Object.fromEntries(Object.entries(e.types||{}).filter(([n,i])=>m.test(n)&&(i==="pages"||i==="skip"))),listPages:Object.fromEntries(Object.entries(e.listPages||{}).filter(([n,i])=>m.test(n)&&i===!1)),choices:Object.fromEntries(Object.entries(e.choices||{}).filter(([n,i])=>typeof n=="string"&&["skip","overwrite","import"].includes(i))),users:Object.fromEntries(Object.entries(e.users||{}).filter(([n,i])=>w.test(n)&&(i==="create"||i==="skip"))),userRole:typeof e.userRole=="string"&&b.test(e.userRole)?e.userRole:"",invite:e.invite===!0,footer:Object.fromEntries(Object.entries(e.footer||{}).filter(([n,i])=>g.test(n)&&i===!1)),home:e.home!==!1,postsPage:Number.isInteger(e.postsPage)&&e.postsPage>0?e.postsPage:0,latestPosts:e.latestPosts===!0}}const g=/^[A-Za-z0-9_-]{1,100}$/;export function withFooterChoice(e,t,o){const s={...e.footer||{}};return o?delete s[t]:s[t]=!1,{...e,footer:s}}export function optionsSnapshot(e){const t=cleanOptions(e,!0),o=Object.keys(t.choices).sort().map(l=>`${l}=${t.choices[l]}`).join(","),s=Object.keys(t.types).sort().map(l=>`${l}=${t.types[l]}`).join(","),n=Object.keys(t.listPages).sort().join(","),i=Object.keys(t.users).sort().map(l=>`${l}=${t.users[l]}`).join(","),a=Object.keys(t.footer).sort().join(",");return[t.posts,t.media,t.comments,t.menus,t.redirects,t.drafts,t.products,o,s,n,i,t.userRole,t.invite,a,t.home,t.postsPage,t.latestPosts].join("|")}const m=/^[a-z0-9_-]{1,40}$/;export function withTypeChoice(e,t,o){const s={...e.types||{}};return o==="pages"||o==="skip"?s[t]=o:delete s[t],{...e,types:s}}export function withListPage(e,t,o){const s={...e.listPages||{}};return o?delete s[t]:s[t]=!1,{...e,listPages:s}}export const isDirty=(e,t)=>optionsSnapshot(e)!==optionsSnapshot(defaultOptions(t)),draftKey=e=>e?`wordpress-import.draft.${e}`:"";export function liveDrafts(e,t,o){if(!e||typeof e!="object"||!e.items||typeof e.items!="object")return{items:{},pruned:!1};if(e.owner&&o&&e.owner!==o)return{items:{},pruned:!1};const s={};let n=!1;for(const[i,a]of Object.entries(e.items))t.includes(i)&&a?.options&&a.was===i?s[i]=a:n=!0;return{items:s,pruned:n}}export function withDraft(e,t,o,s){const n={...e&&e.owner===t&&e.items||{}};return s?n[o]=s:delete n[o],{owner:t,items:n}}export function draftQuestion({when:e="",file:t=""}={}){return{title:"Choices you left last time",text:`You changed the import options${t?` for ${t}`:""}${e?` (${e})`:""} and left before running it. Throw away those choices and start from the defaults?`,confirmText:"Throw away",cancelText:"Keep them"}}export function progressText(e){if(!e)return"";const t=e.total?` \xB7 ${e.done} of ${e.total}`:"";return`${e.phase||"Working"}${t}${e.current?` \xB7 ${String(e.current).slice(0,80)}`:""}`}export const progressPercent=e=>e?.total?Math.min(100,Math.round(e.done/e.total*100)):0;export function planTiles(e){const t=e?.summary?.counts||{},o=s=>t[s]?t[s].total-t[s].skip:0;return[{id:"page",label:"Pages",value:o("page"),of:t.page?.total||0},{id:"post",label:"Posts",value:o("post"),of:t.post?.total||0},...t.product?[{id:"product",label:"Products",value:o("product"),of:t.product.total}]:[],...t.custom?[{id:"custom",label:"Custom items",value:o("custom"),of:t.custom.total}]:[],{id:"media",label:"Media",value:o("media"),of:t.media?.total||0},{id:"menu",label:"Menus",value:o("menu"),of:t.menu?.total||0},...e?.people?.rows?.length?[{id:"people",label:"New accounts",value:e.people.counts?.create||0,of:0}]:[],...t.widgets?[{id:"widgets",label:"Widget areas",value:o("widgets"),of:t.widgets.total}]:[],{id:"comments",label:"Comments",value:e?.summary?.comments||0,of:0},{id:"redirects",label:"Redirects",value:e?.summary?.redirects||0,of:0}].map(s=>({...s,sub:s.of&&s.of!==s.value?`of ${s.of}`:""}))}const w=/^author:(\d{1,12}|login-[\w.-]{1,60})$/,b=/^[a-z0-9][a-z0-9_-]{0,59}$/i;export const PERSON={create:{text:"New account",tone:"ok"},match:{text:"Matched",tone:"info"},linked:{text:"Linked before",tone:"info"},same:{text:"Same email",tone:"muted"},skip:{text:"No account",tone:"muted"}};export function creditText(e={}){const t=(o,s,n)=>e[o]?`${e[o]} ${e[o]===1?s:n}`:"";return[t("post","post","posts"),t("page","page","pages"),t("product","product","products"),t("custom","item","items")].filter(Boolean).join(", ")||"nothing in this import"}export function personRow(e){const t=PERSON[e.action]||PERSON.skip,o=e.problem==="email";return{key:e.key,name:e.name||"Author",meta:[e.email||(o?"no usable email":""),creditText(e.items)].filter(Boolean).join(" \xB7 "),reason:e.reason||"",pill:o?"No email":t.text,pillClass:`is-${o?"warn":t.tone}${e.choice?" is-chosen":""}`,choosable:!!e.choosable,segCreate:e.action==="create"?"is-on":"",segSkip:e.action==="skip"?"is-on":""}}export function chooseAllPeople(e,t){const o={};for(const s of e?.rows||[])s.choosable&&(o[s.key]=t);return o}export function choosePerson(e,t,o){const s={...e||{}},n=t.total?"create":"skip";return!o||o==="reset"||o===n?delete s[t.key]:s[t.key]=o,s}export function personMenuItems(e){const t=[];return e.choosable&&e.action!=="create"&&t.push({id:"create",label:"Make a new account",icon:"user-plus"}),e.choosable&&e.action!=="skip"&&t.push({id:"skip",label:"Do not make an account",icon:"user-x"}),e.choice&&t.push({id:"reset",label:"Undo my choice",icon:"rotate-ccw"}),e.email&&e.problem!=="email"&&t.push({id:"copy",label:"Copy the email address",icon:"copy"}),t.length||t.push({id:"none",label:"Nothing to choose for them",icon:"info",disabled:!0}),t}export function peopleSummary(e){const t=e?.counts||{};if(!e?.rows?.length)return{text:"This export names no authors.",tone:"info"};const o=[];t.create&&o.push(`${t.create} new account${t.create===1?"":"s"}`),t.match+t.linked&&o.push(`${t.match+t.linked} matched to ${t.match+t.linked===1?"a user":"users"} here`),t.skip&&o.push(`${t.skip} without an account`);let s=`${e.rows.length} author${e.rows.length===1?"":"s"}: ${o.join(", ")||"nothing to do"}.`;return t.create&&(s+=" New accounts have no password - nobody can sign in to one until a password is set."),!e.canCreate&&e.why&&(s+=` ${e.why}`),{text:s,tone:t.problems||!e.canCreate?"warn":"info"}}export function inviteNote(e){return e?.canCreate?e.mailReady?e.counts?.create?"Sent once the import has finished: an invitation saying an account has been made for them, with a link to set their password that works for 7 days.":"Nobody new to email.":"No mail server is set up (Settings \u203A Email), so this cannot be sent. After the import, use Users \u203A \u22EE \u203A Copy a reset link instead.":""}export function reportPersonRow(e){const t={created:{text:"Account made",tone:"ok"},matched:{text:"Matched",tone:"info"},linked:{text:"Linked before",tone:"info"},same:{text:"Same email",tone:"muted"},skipped:{text:"No account",tone:"muted"},failed:{text:"Failed",tone:"bad"}},o=t[e.result]||t.skipped,s=[creditText(e.items)];return e.result==="created"&&s.push(e.invited?"invited by email":e.inviteFailed?"the invitation was not sent":"no password set yet"),{key:e.key,name:e.name||"Author",meta:s.join(" \xB7 "),reason:e.reason||"",pill:o.text,pillClass:`is-${o.tone}`,userId:e.userId||""}}export function historyRow(e,t=o=>o){const o=e.summary||{},s=Object.values(o.kinds||{}).reduce((a,l)=>a+(l.created||0),0),n=Object.values(o.kinds||{}).reduce((a,l)=>a+(l.updated||0),0),i=[];return s&&i.push(`${s} made`),o.people?.created&&i.push(`${o.people.created} account${o.people.created===1?"":"s"}`),n&&i.push(`${n} updated`),o.redirects?.created&&i.push(`${o.redirects.created} redirect${o.redirects.created===1?"":"s"}`),o.rebuild&&i.push(`${o.rebuild} to rebuild`),o.failed&&i.push(`${o.failed} failed`),e.status==="failed"&&i.splice(0,i.length,"Stopped before the end - run it again to finish"),{id:e.id,title:e.siteTitle||e.source||"WordPress site",meta:[t(e.at),e.by,e.file].filter(Boolean).join(" \xB7 "),summary:i.join(", ")||"Nothing changed",pill:e.status==="failed"?"Stopped":e.status==="partial"?"Some failed":"Done",pillClass:e.status==="failed"||e.status==="partial"?"is-bad":"is-ok"}}export function stagedRow(e,t=o=>o){const o=e.counts||{},s=[o.page&&`${o.page} pages`,o.post&&`${o.post} posts`,o.product&&`${o.product} products`,o.attachment&&`${o.attachment} media`,e.widgets&&`${e.widgets.widgets} widget${e.widgets.widgets===1?"":"s"}`].filter(Boolean);return{id:e.id,title:e.siteTitle||e.fileName||"WordPress export",meta:[e.fileName,t(e.at),s.join(", ")].filter(Boolean).join(" \xB7 "),pill:e.uploads?"Uploads zip added":e.widgets?"Widgets added":"Waiting",pillClass:e.uploads||e.widgets?"is-ok":"is-muted"}}export function productSummary(e){const t=e?.summary?.products;if(!t?.total)return null;const o=t.shop||e.shop||{},s=e.options?.products!==!1,n=Object.entries(t.types||{}).map(([d,u])=>`${u} ${(PRODUCT_TYPE_LABEL[d]||d).toLowerCase()}`).join(", "),i=e.summary.counts?.product||{},a=[{label:"In the export",value:`${t.total}${n?` (${n})`:""}`}];o.available&&s&&(a.push({label:"On sale in the Shop",value:String(t.onSale||0)}),a.push({label:"As drafts",value:String(t.drafts||0)}),i.keep&&a.push({label:"Left as is",value:String(i.keep)}),a.push({label:"Skipped",value:String(i.skip||0)})),t.rebuild&&a.push({label:"Need rebuilding",value:String(t.rebuild)});let l,r="info";return o.available?s?l=`Products go to the Shop at ${o.basePath||"/shop"}.`:l='Products are not being imported - switch on "WooCommerce products" above to bring them in.':(l=o.reason||"Switch on the Shop to bring these in.",r="warn"),{note:l,tone:r,rows:a,notes:Array.isArray(t.notes)?t.notes:[]}}const c=(e,t,o=`${t}s`)=>`${e} ${e===1?t:o}`;export function customTypeRows(e){return(e?.summary?.customTypes||[]).map(t=>{const o=TYPE_AS[t.as]?t.as:"collection",s=TYPE_AS[o],n=t.rewrite?`was at /${t.rewrite}/\u2026`:"its old addresses are not in the export";let i;if(o==="skip")i="Nothing - left out.";else if(o==="pages")i=`${c(t.importing||0,"page")} under ${t.prefix}${t.drafts?` (${t.drafts} as draft${t.drafts===1?"":"s"})`:""}.`;else{const a=t.collection||{},l=a.action==="skip"?`The collection "${a.slug}" was deleted here - choose Import again on it below.`:`${a.action==="update"?"The collection":"A new collection"} "${a.slug}" with ${c(t.fields?.length||0,"field")}: ${c(t.importing||0,"entry","entries")}${t.drafts?` (${t.drafts} draft${t.drafts===1?"":"s"}, not shown on the site)`:""}.`,r=t.listPage?t.listAt?` A list page at ${t.listAt}${t.listAction==="keep"?" (edited here - left as it is)":""}; the old addresses lead there.`:"":" No list page - the old addresses are not redirected.";i=`${l}${r}`}return{type:t.type,label:t.label||t.type,meta:[c(t.count||0,"item"),`"${t.type}"`,n].join(" \xB7 "),samples:(t.samples||[]).length?`For example: ${t.samples.join(", ")}`:"",creates:i,pill:s.text,pillClass:`is-${s.tone}`,as:o,listOn:o==="collection"&&t.listPage!==!1,canList:o==="collection",listLabel:`List page at ${t.prefix||`/${t.type}`}`,segCollection:o==="collection"?"is-on":"",segPages:o==="pages"?"is-on":"",segSkip:o==="skip"?"is-on":""}})}export function internalTypeRows(e){return(e?.summary?.internalTypes||[]).map(t=>({type:t.type,text:`${t.type} (${t.count}) - ${t.reason}`}))}export function typeMenuItems(e){if(!e)return[];const t=[];return e.as!=="collection"&&t.push({id:"collection",label:"Import as a collection",icon:"database"}),e.as!=="pages"&&t.push({id:"pages",label:"Import as pages",icon:"file-text"}),e.as!=="skip"&&t.push({id:"skip",label:"Leave this type out",icon:"x-circle"}),e.as==="collection"&&t.push(e.listOn?{id:"nolist",label:"No list page",icon:"eye-off"}:{id:"list",label:"Make a list page",icon:"list"}),t.push({id:"show",label:"Show its items",icon:"filter"}),t}export function widgetAreaRows(e){return(e?.summary?.widgets?.areas||[]).map(t=>{const o=ACTION[t.action]||ACTION.skip,s=t.action==="skip"?"Not imported.":`${t.action==="update"?"Updates the block":t.action==="keep"?"The block (edited here - left as it is)":"A new block"} "${t.block}"${t.coming<t.count?` with ${c(t.coming,"widget")}`:""}.`;return{area:t.id,label:t.label||t.id,meta:[c(t.count||0,"widget"),`"${t.id}"`,(t.types||[]).join(", ")].filter(Boolean).join(" \xB7 "),creates:s,placeText:t.footer?PLACE_TEXT[t.place]||"":t.action==="skip"?"":"A sidebar - a block only; put it on a page with [block].",rebuild:(t.rebuild||[]).length?`Needs rebuilding: ${t.rebuild.join("; ")}`:"",pill:o.text,pillClass:`is-${o.tone}`,canPlace:!!t.footer&&t.action!=="skip"&&t.place!=="there"&&t.place!=="removed",placeOn:t.place==="add"}})}export function widgetsFileText(e,t=0){const o=e?.widgets;return o?`${o.fileName} - ${c(o.areas||0,"widget area")}, ${c(o.widgets||0,"widget")}.`:`Optional: the widgets, as the .wie file the free "Widget Importer & Exporter" WordPress plugin exports${t?` (up to ${Math.max(1,Math.round(t/1048576))} MB)`:""}.`}const p=e=>`"${String(e||"").trim()||"Untitled"}"`;export function homeCard(e){const t=e?.home;if(!t)return null;const o=e.options||{},s=[];let n;t.how==="page"&&t.page?n=`WordPress's home page was the page ${p(t.page.title)}${t.page.oldUrl?` (${t.page.oldUrl})`:""}.`:t.how==="unclear"?n=`${t.matches} pages had the home page's address in WordPress, so it cannot be told which one was the home page - none of them is made this site's home page.`:n="WordPress showed its latest posts on the home page (no page was set as the home page).";const i=t.now?`This site's home page now: ${p(t.now.title)}.`:"This site has no home page now.";let a="",l="info";t.done?.source?a=`An import of another WordPress site (${t.done.source}) set this site's home page - it is not changed.`:t.done?(a=t.done.how==="latest"?"An earlier import put the latest posts on the home page - it is not moved again.":"An earlier import made it this site's home page - it is not moved again.",t.done.previous&&(a+=` The home page from before is at ${t.done.previous}.`)):t.move?(a=`${t.how==="page"?p(t.page?.title):"A page showing the latest Blog posts"} becomes the home page. ${t.now?p(t.now.title):"The page at /"} moves to ${t.move.to} as a draft - nothing is deleted.`,l="warn"):t.page?.isHome||t.latest?.on?a=`${t.how==="page"?p(t.page?.title):"A page showing the latest Blog posts"} becomes the home page.`:t.how==="page"&&t.page?a=o.home===!1?`It is imported as an ordinary page${t.page.target?` at ${t.page.target}`:""} - this site's home page stays as it is.`:"":t.how==="latest"&&(a=t.latest?.offered?"This site's home page stays as it is unless you choose to show the latest posts on it.":"This site's home page stays as it is."),t.changed&&!t.done?.source&&(s.push(t.changed),l="warn");const r=t.postsPage;let d="";return r&&(d=r.confirmed?`${p(r.title)} was WordPress's posts page: it is not made, and its old address (and links and menus that pointed to it) lead to ${r.to}.`:`${p(r.title)} has nothing in it and is called like a blog - it was probably WordPress's posts page. If it was, it is not needed here: its address can lead to ${r.to} instead.`),{wp:n,now:i,after:a,tone:l,notes:s,frontSwitch:t.how==="page"&&!!t.page&&!t.done,frontLabel:"Make it this site's home page",latestSwitch:!!t.latest?.offered&&!t.done,postsSwitch:!!r,postsText:d,postsLabel:"It was the posts page - lead its address to the posts",postsId:r?.wpId||0,editNow:t.move&&t.now?"#/pages/edit/":""}}
@@ -156,6 +156,14 @@
156
156
  descriptions carry over. Shortcodes and page-builder layouts this site has no equivalent for are
157
157
  listed as <em>needs rebuilding</em>. Run it again later and it updates what it made, leaving alone
158
158
  anything you have edited here since unless you choose Overwrite. Each run's report is kept.</p>
159
+ <p>The page WordPress showed as its home page becomes this site's home page (switch that off in the
160
+ preview's Home page card). The page this site had at <code>/</code> is never deleted: it moves to
161
+ <code>/previous-home</code> as a draft, menus that lead to <code>/</code> keep doing so, and the old
162
+ home page's WordPress address leads to <code>/</code>. That happens once - a later run updates the
163
+ home page where it is, and leaves it alone if you changed it. An empty page called Blog, News or
164
+ similar is offered as WordPress's posts page: confirm it and its address leads to the posts instead
165
+ of an empty page being made. When WordPress showed its latest posts on the home page, you can choose
166
+ to do the same here.</p>
159
167
 
160
168
  <h3>Notifications</h3>
161
169
  <p>The bell in the admin, and <strong>System &gt; Notifications</strong>. Opening the screen marks
@@ -130,6 +130,21 @@
130
130
  <div class="wpx-note-list" data-each="notes.value key=text"></div>
131
131
  </div>
132
132
 
133
+ <div class="wpx-card wpx-home" hidden data-bind-hidden="!hasHome.value">
134
+ <div class="wpx-card-title"><span data-icon="home" data-icon-size="16"></span> Home page
135
+ <span data-help-title="The home page" data-help="A WordPress export does not say which page was the home page, but each page's address is in it - and the home page's address is the site's own. That page is the one offered here; a page called &quot;Home&quot; with another address is just a page. Made this site's home page, it goes at / and the page there now moves to /previous-home as a draft - nothing is deleted, menus that lead to / keep doing so, and nothing redirects to the old one. Its WordPress address (/home/, say) and ?page_id= links lead to /. That happens once: run the import again and the home page is updated where it is, left as it is if you edited it here (unless you choose Overwrite), and left alone altogether if you changed the home page yourself since. The posts page cannot be told from the export, so an empty page with a blog's name is only suggested: switch it on if it was, and its address leads to the posts instead of an empty page being made. When WordPress showed its latest posts on the home page, you can do the same here with the Blog's list."></span></div>
136
+ <p class="wpx-note" data-bind-text="homeWp.value"></p>
137
+ <p class="wpx-note"><span data-bind-text="homeNow.value"></span> <a class="wpx-home-edit" hidden data-bind-hidden="!homeEdit.value" href="#/pages/edit/">Open it</a></p>
138
+ <label class="wpx-toggle" hidden data-bind-hidden="!homeFrontSwitch.value"><input type="checkbox" class="form-switch" data-model="optHome.value"> Make it this site's home page</label>
139
+ <label class="wpx-toggle" hidden data-bind-hidden="!homeLatestSwitch.value"><input type="checkbox" class="form-switch" data-model="optLatest.value"> Show the latest posts on the home page</label>
140
+ <p class="wpx-alert" hidden data-bind-hidden="!homeAfter.value" data-bind-class="homeAfterCls.value"><span data-bind-text="homeAfter.value"></span></p>
141
+ <div class="wpx-note-list wpx-home-notes" data-each="homeNotes.value key=text"></div>
142
+ <div class="wpx-home-posts" hidden data-bind-hidden="!homePostsSwitch.value">
143
+ <small class="wpx-note" data-bind-text="homePostsText.value"></small>
144
+ <label class="wpx-toggle"><input type="checkbox" class="form-switch" data-model="optPostsPage.value"> It was the posts page - lead its address to the posts</label>
145
+ </div>
146
+ </div>
147
+
133
148
  <div class="wpx-card wpx-products" hidden data-bind-hidden="!hasProducts.value">
134
149
  <div class="wpx-card-title"><span data-icon="shopping-bag" data-icon-size="16"></span> Products
135
150
  <span data-help-title="WooCommerce products" data-help="What the export's WooCommerce products come to here. The currency is not in a WordPress export: prices are read as the Shop's own currency. Weights are read as kilograms, WooCommerce's usual unit. Orders, customers, coupons, reviews and tax and shipping settings never come across - they hold people's personal details, or settings a person should check; set tax and delivery in the Shop's settings. Product reviews may come across in a later version."></span>
@@ -291,6 +306,8 @@
291
306
  .wpx-role-pick { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin: 0; }
292
307
  .wpx-role { width: auto; max-width: 100%; }
293
308
  .wpx-people-ctl .wpx-note { flex-basis: 100%; }
309
+ .wpx-home-posts { display: flex; flex-direction: column; gap: .4rem; padding-top: .5rem; border-top: 1px solid var(--dm-border); }
310
+ .wpx-home-edit { font-size: var(--dm-text-sm); }
294
311
  .wpx-report { display: flex; flex-direction: column; gap: .75rem; padding: .25rem 0; }
295
312
  @media (max-width: 720px) { .wpx-toggles { grid-template-columns: 1fr; } .wpx-row { flex-wrap: wrap; } .wpx-chips { justify-content: flex-start; } }
296
313
  </style>
@@ -1,9 +1,9 @@
1
- import{apiRequest as C,getToken as Za}from"../api.js";import{addBannerAction as Va}from"../lib/plugin-chrome.js";import{helpPopovers as yt}from"../lib/help-popover.js";import{onceClosed as Ga,openMenuAt as O,sidebarCount as R,viewLifetime as Ja}from"../lib/tool-kit.js";import{SHAPES as xt,whileLoading as kt}from"../lib/loading.js";import{fmtDateTime as Ya}from"../lib/dates.js";import{applyChoice as Xa,chooseAllPeople as Mt,choosePerson as Qa,choiceItems as Tt,cleanOptions as ye,counts as Et,customTypeRows as es,inviteNote as ts,peopleSummary as as,personMenuItems as ss,personRow as os,reportPersonRow as ns,defaultOptions as He,draftKey as ls,draftQuestion as rs,historyRow as is,internalTypeRows as cs,isDirty as ps,kindTabs as ds,liveDrafts as $t,optionsSnapshot as Lt,PLAN_FILTERS as St,planMatches as Pt,planRow as us,planTiles as Ct,productSummary as ms,progressPercent as hs,progressText as ws,REPORT_FILTERS as Rt,reportMatches as qt,reportRow as vs,stagedRow as fs,typeMenuItems as bs,widgetAreaRows as gs,widgetsFileText as ys,withDraft as At,withFooterChoice as xs,withListPage as ks,withTypeChoice as Ms}from"../lib/wordpress-import-arrange.js";const It=/^#\/wordpress-import\/?(\?.*)?$/,re="#/wordpress-import",$="/import/wordpress",xe=400,Ts=`
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=`
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>`,Nt=`
6
+ </div>`,Dt=`
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 C,getToken as Za}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>`,Ot=`
17
+ </div>`,Ht=`
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>`,Es=`
22
+ </div>`,zo=`
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>`,$s=`
27
+ </div>`,jo=`
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 C,getToken as Za}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>`,Ls=`
45
+ </div>`,Wo=`
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 C,getToken as Za}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>`,Ss=`
60
+ </div>`,Fo=`
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>`,Ps=`
65
+ </div>`,Ko=`
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,11 +76,13 @@ import{apiRequest as C,getToken as Za}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>`,Dt='<div class="wpx-tile"><strong>{{value}}</strong><small>{{label}} <span data-bind-hidden="!sub">{{sub}}</span></small></div>',Ut='<p class="wpx-alert"><span>{{text}}</span></p>',Ht="<li>{{text}}</li>",Cs='<div class="wpx-fact"><dt>{{label}}</dt><dd>{{value}}</dd></div>',Be='<button type="button" data-on-click="$root.pickSeg($data)" data-bind-class="cls">{{label}} <b>{{count}}</b></button>',f=Ja(It);function _e(u,l,i){return new Promise((b,k)=>{const w=new XMLHttpRequest,q=new FormData;q.append("file",l),w.open("POST",`/api${u}`);const v=Za();v&&w.setRequestHeader("Authorization",`Bearer ${v}`),w.upload.onprogress=m=>{m.lengthComputable&&i(m.loaded/m.total)},w.onload=()=>{let m=null;try{m=JSON.parse(w.responseText)}catch{}w.status>=200&&w.status<300?b(m):k(new Error(m?.error||`The upload failed (${w.status}).`))},w.onerror=()=>k(new Error("The upload failed - the connection dropped.")),w.send(q),f.add(()=>{w.readyState!==4&&w.abort()})})}const ke=u=>u>=1048576?`${Math.round(u/1048576)} MB`:`${Math.max(1,Math.round(u/1024))} KB`,V=u=>String(u??"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function Rs(u){const l=document.createElement("template");return l.innerHTML=u.trim(),l.content.firstElementChild}async function Me(u,l){const i=E.confirm(u,l);let b=null;try{b=[...document.querySelectorAll(".dm-dialog-overlay")].at(-1)??null}catch{}const k=f.add(()=>{if(!b)return;const w=b;b=null;try{w.querySelector(".dm-dialog-cancel")?.click(),w.remove()}catch{}});try{return await i}finally{b=null,k()}}function ze(u){navigator.clipboard?.writeText(u).then(()=>E.toast("Copied.",{type:"success"}),()=>E.toast(u,{type:"info",duration:8e3}))}function H(u,l,i,b){const k=E.contextMenu(u,{cursor:"glyph",match:l,inherit:!1,density:"compact",items:i});f.add(()=>k?.destroy?.());const w=v=>{const m=v.target.closest(l);if(!m||!u.contains(m))return;const p=v.target.closest(".wpx-kebab");if(p){O(p);return}b?.(m)},q=v=>{const m=v.target.closest(l);m&&(v.key==="F10"&&v.shiftKey?(v.preventDefault(),O(m)):v.key==="Enter"&&v.target===m&&b&&(v.preventDefault(),v.stopPropagation(),b(m)))};u.addEventListener("click",w),u.addEventListener("keydown",q),f.add(()=>{u.removeEventListener("click",w),u.removeEventListener("keydown",q)})}export const wordpressImportView={templateUrl:"/admin/js/templates/wordpress-import.html",async onMount(u){f.reset();const l=u.get(0).querySelector(".wpx")||u.get(0),i=()=>It.test(location.hash)&&l.isConnected;let b="";try{b=S.get("auth_user")?.id||""}catch{}const k=ls(b),w=()=>{try{return k?S.get(k):null}catch{return null}},q=e=>{try{k&&S.set(k,e)}catch{}},v=l.querySelector(".wpx-loading");let m;try{m=await kt(v,C(`${$}/status`),xt.rows)}catch(e){if(!l.isConnected)return;v&&(v.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(!i())return;v&&(v.hidden=!0),l.querySelector(".wpx-body").hidden=!1;const p=M.observable(m.staged||[]),B=M.observable(m.history||[]),G=M.observable(m.blog||{available:!1}),je=M.observable(m.shop||{available:!1}),_=m.limits||{},We=m.people?.defaultRole||"",y=M.observable(m.running||null),h=M.observable(""),g=M.observable(null),ie=M.observable(!1),Te=M.observable(!1),Ee=M.observable(""),c=M.observable(He(G.peek().available)),z=M.observable("all"),D=M.observable(""),J=M.observable(""),ce=M.observable(""),Y=M.observable(""),pe=M.observable(""),de=M.observable(!1);let ue=!0;const qs=()=>p.peek().find(e=>e.id===h.peek())||null,X=M.computed(()=>p.value.find(e=>e.id===h.value)||null),Bt=M.computed(()=>h.value?"preview":"start"),U=M.computed(()=>!!G.value.available),_t=M.computed(()=>!!(g.value?.shop||je.value).available),Fe=M.computed(()=>!!h.value&&ps(c.value,U.value)),Q=e=>M.computed({read:()=>c.value[e]!==!1,write:t=>{c.value={...c.peek(),[e]:!!t}}}),P=e=>{c.value={...c.peek(),...e}},ee=e=>{try{return Ya(e)}catch{return e}},zt=M.computed(()=>p.value.map(e=>fs(e,ee))),jt=M.computed(()=>B.value.map(e=>is(e,ee))),j=M.computed(()=>(g.value?.items||[]).map(e=>({...e,choice:c.value.choices?.[e.key]}))),Wt=M.computed(()=>ds(j.value).map(e=>({...e,cls:D.value===e.id?"is-on":"",seg:"kind"}))),Ft=M.computed(()=>Et(j.value,St,Pt,D.value)),Kt=M.computed(()=>St.map(e=>({...e,count:Ft.value[e.id],cls:z.value===e.id?"is-on":"",seg:"filter"}))),$e=M.computed(()=>{const e=J.value.trim().toLowerCase();return j.value.filter(t=>(!D.value||t.kind===D.value)&&Pt(t,z.value)&&(!e||`${t.title} ${t.target} ${t.oldUrl} ${t.postType||""}`.toLowerCase().includes(e)))}),Zt=M.computed(()=>$e.value.slice(0,xe).map(e=>{const t=us(e);return{...t,pillCls:`${t.pillClass}${e.choice?" is-chosen":""}`}})),Vt=M.computed(()=>$e.value.length>xe?`Showing the first ${xe} of ${$e.value.length} - search or filter to find the rest.`:""),Gt=M.computed(()=>j.value.length?"Nothing matches.":"This export has nothing to import."),Jt=M.computed(()=>Ct(g.value)),Yt=M.computed(()=>(g.value?.notes||[]).map(e=>({text:e}))),Xt=M.computed(()=>(g.value?.redirects||[]).slice(0,xe).map(e=>({...e,pill:e.action==="skip"?"Skipped":e.action==="update"?"Update":"New",pillClass:e.action==="skip"?"is-muted":"is-ok"}))),Qt=M.computed(()=>String((g.value?.redirects||[]).filter(e=>e.action!=="skip").length)),ea=M.computed(()=>g.value?.site?.title||X.value?.siteTitle||"WordPress export"),ta=M.computed(()=>{const e=X.value,t=g.value?.site;return[t?.url||e?.siteUrl,e?.fileName,t?.wxrVersion&&`WXR ${t.wxrVersion}`,e?.at&&`uploaded ${ee(e.at)}`].filter(Boolean).join(" \xB7 ")}),te=M.computed(()=>ms(g.value)),aa=M.computed(()=>te.value?.note||""),sa=M.computed(()=>te.value?.tone==="warn"?"is-warn":"is-info"),oa=M.computed(()=>(te.value?.rows||[]).map(e=>({...e,id:e.label}))),na=M.computed(()=>(te.value?.notes||[]).map(e=>({text:e}))),la=M.computed(()=>!!te.value),Le=M.computed(()=>es(g.value)),Se=M.computed(()=>cs(g.value)),ra=M.computed(()=>!!(Le.value.length||Se.value.length)),ia=M.computed(()=>{const e=Se.value.length;return e?`Not imported: ${e} kind${e===1?"":"s"} of WordPress or plugin housekeeping`:""}),T=M.computed(()=>g.value?.people||null),ca=M.computed(()=>!!T.value?.rows?.length),pa=M.computed(()=>(T.value?.rows||[]).map(os)),da=M.computed(()=>!!T.value?.canCreate),ua=M.computed(()=>(T.value?.rows||[]).some(e=>e.choosable)),Ke=M.computed(()=>as(T.value)),ma=M.computed(()=>Ke.value.text),ha=M.computed(()=>Ke.value.tone==="warn"?"is-warn":"is-info"),wa=M.computed(()=>!!(T.value?.canCreate&&T.value?.mailReady&&T.value?.counts?.create)),va=M.computed(()=>ts(T.value)),fa=M.computed({read:()=>c.value.invite===!0,write:e=>{c.value={...c.peek(),invite:!!e}}}),ba=M.computed(()=>U.value?`Posts go to ${G.value.basePath||"/blog"}.`:G.value.reason||"No Blog is switched on here."),me=M.computed(()=>X.value?.uploads||null),ga=M.computed(()=>{const e=me.value;return e?`${e.fileName} - ${e.files} file${e.files===1?"":"s"}, ${ke(e.size)}${e.unsafe?`. ${e.unsafe} with unsafe names (like ../) were ignored`:""}.`:`Zip the wp-content/uploads folder and upload it here (up to ${ke(_.uploads||0)}).`}),ya=M.computed(()=>me.value?"Replace the zip":"Upload the uploads zip"),Ze=M.computed(()=>X.value?.widgets||null),xa=M.computed(()=>ys(X.value,_.widgets||0)),ka=M.computed(()=>Ze.value?"Replace the widgets file":"Upload the widgets file"),Pe=M.observable(""),Ve=M.computed(()=>gs(g.value)),Ge=M.computed(()=>!!g.value&&!y.value&&!ie.value&&!(c.value.media==="zip"&&!me.value)),Ma=M.computed(()=>y.value?`Importing${y.value.siteTitle?` ${y.value.siteTitle}`:""}\u2026`:""),Ta=M.computed(()=>ws(y.value)),Je=M.computed(()=>hs(y.value)),Ea=M.observable(ke(_.wxr||0)),Ye=l.querySelector(".wpx-running .wpx-bar-fill");f.add(M.effect(()=>{Ye&&(Ye.style.width=`${Je.value}%`)}));async function Ce(){const e=await C(`${$}/status`);return i()?(p.value=e.staged||[],B.value=e.history||[],G.value=e.blog||{available:!1},je.value=e.shop||{available:!1},e.running&&!y.peek()&&(y.value=e.running,se()),e):null}let he=0,Re=null;async function W({first:e=!1}={}){const t=h.peek();if(!t)return;const a=++he;Ee.value="";const s=C(`${$}/staged/${t}/preview`,{method:"POST",body:JSON.stringify({options:ye(c.peek(),U.peek())})});try{let r;if(e?(ie.value=!0,r=await kt(l.querySelector(".wpx-plan-loading"),s,xt.table)):(Te.value=!0,r=await s),a!==he||!i()||h.peek()!==t)return;g.value=r.plan}catch(r){if(a!==he||!i())return;if(r.status===404){E.toast("That upload has gone - upload the export again.",{type:"warning"}),ae(),Ce().catch(()=>{});return}Ee.value=`Could not read the export: ${r.message||r}`}finally{a===he&&(ie.value=!1,Te.value=!1)}}let we="";f.add(M.effect(()=>{const e=`${h.value}|${Lt(c.value)}`;if(e===we)return;const t=we.split("|")[0];we=e,!(!h.value||t!==h.value)&&(clearTimeout(Re),Re=setTimeout(()=>W(),250))})),f.add(()=>clearTimeout(Re)),f.add(M.effect(()=>{const e=h.value,t=c.value,a=Fe.value;if(!e||!ue||!k)return;const s=w();q(At(s,b,e,a?{options:ye(t,!0),at:new Date().toISOString(),was:e}:null))}));const qe=e=>{k&&q(At(w(),b,e,null))};async function ve(e){if(!p.peek().some(d=>d.id===e))return;const t=p.peek().find(d=>d.id===e),a=$t(w(),p.peek().map(d=>d.id),b).items[e];if(ue=!a,g.value=null,z.value="all",D.value="",J.value="",c.value=He(U.peek()),we=`${e}|${Lt(c.peek())}`,h.value=e,await W({first:!0}),!i()||h.peek()!==e||!a)return;await new Promise(d=>setTimeout(d,0));const s=rs({when:a.at?ee(a.at):"",file:t?.fileName||""}),r=await Me(V(s.text),{title:s.title,confirmText:s.confirmText,cancelText:s.cancelText});!i()||h.peek()!==e||(ue=!0,r?qe(e):c.value=ye(a.options,U.peek()))}function ae(){h.value="",g.value=null,ue=!0}async function Xe(e=h.peek()){const t=p.peek().find(a=>a.id===e);if(t&&await Me(`Discard the upload of <strong>${V(t.siteTitle||t.fileName)}</strong>? Nothing has been imported from it.`,{title:"Discard upload",confirmText:"Discard",danger:!0}))try{if(await C(`${$}/staged/${e}`,{method:"DELETE"}),qe(e),!i())return;h.peek()===e&&ae(),p.value=p.peek().filter(a=>a.id!==e),R(re)}catch(a){E.toast(`Not discarded: ${a.message||a}`,{type:"error"})}}async function Qe(e){if(!e)return;if(Y.value="",!/\.(xml|zip)$/i.test(e.name)){Y.value="Upload the .xml file WordPress exports, or a .zip holding it.";return}if(_.wxr&&e.size>_.wxr*1.2&&/\.xml$/i.test(e.name)){Y.value=`That file is over ${ke(_.wxr)}.`;return}const t=l.querySelector(".wpx-start .wpx-upload .wpx-bar-fill");ce.value=`Uploading ${e.name}\u2026`;try{const a=await _e(`${$}/upload`,e,s=>{t&&(t.style.width=`${Math.round(s*100)}%`),ce.value=s<1?`Uploading ${e.name} - ${Math.round(s*100)}%`:`Reading ${e.name}\u2026`});if(!i())return;p.value=[a,...p.peek().filter(s=>s.id!==a.id)],R(re),await ve(a.id)}catch(a){i()&&(Y.value=a.message||String(a))}finally{i()&&(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");pe.value=`Uploading ${e.name}\u2026`;try{const s=await _e(`${$}/staged/${t}/uploads`,e,r=>{a&&(a.style.width=`${Math.round(r*100)}%`),pe.value=r<1?`Uploading ${e.name} - ${Math.round(r*100)}%`:`Checking ${e.name}\u2026`});if(!i())return;p.value=p.peek().map(r=>r.id===s.id?s:r),h.peek()===t&&W()}catch(s){i()&&E.toast(s.message||String(s),{type:"error"})}finally{i()&&(pe.value="",a&&(a.style.width="0"))}}async function La(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}Pe.value=`Reading ${e.name}\u2026`;try{const a=await _e(`${$}/staged/${t}/widgets`,e,()=>{});if(!i())return;p.value=p.peek().map(s=>s.id===a.id?a:s),h.peek()===t&&W()}catch(a){i()&&E.toast(a.message||String(a),{type:"error"})}finally{i()&&(Pe.value="")}}}async function Sa(){const e=h.peek();try{const t=await C(`${$}/staged/${e}/widgets`,{method:"DELETE"});if(!i())return;p.value=p.peek().map(a=>a.id===t.id?t:a),W()}catch(t){E.toast(`Not removed: ${t.message||t}`,{type:"error"})}}async function Pa(){const e=h.peek();try{const t=await C(`${$}/staged/${e}/uploads`,{method:"DELETE"});if(!i())return;p.value=p.peek().map(a=>a.id===t.id?t:a),W()}catch(t){E.toast(`Not removed: ${t.message||t}`,{type:"error"})}}async function Ca(){const e=h.peek(),t=g.peek();if(!e||!t||!Ge.peek())return;const s=Ct(t).filter(d=>d.value&&d.id!=="comments").map(d=>`${d.value} ${d.label.toLowerCase()}`);if(!(!await Me(`Import <strong>${V(t.site?.title||"this export")}</strong>: ${V(s.join(", ")||"nothing new")}? Pages edited here since an earlier import are left alone unless you chose Overwrite.`,{title:"Run the import",confirmText:"Run import"})||!i()||h.peek()!==e))try{const d=await C(`${$}/staged/${e}/run`,{method:"POST",body:JSON.stringify({options:ye(c.peek(),U.peek())})});if(qe(e),!i())return;y.value={...d.job,siteTitle:t.site?.title||""},ae(),R(re),se()}catch(d){E.toast(`The import did not start: ${d.message||d}`,{type:"error"})}}let Ae=null;function se(){clearTimeout(Ae);const e=y.peek();e&&(Ae=setTimeout(async()=>{try{const t=await C(`${$}/jobs/${e.id}`);if(!i())return;if(y.value={...t.job,siteTitle:e.siteTitle},t.job.status==="running")return se();if(y.value=null,await Ce().catch(()=>{}),R(re),R("#/pages"),R("#/media"),R("#/plugins/shopping-cart"),R("#/collections"),R("#/blocks"),!i())return;t.job.status==="done"?(E.toast("The import has finished - here is its report.",{type:"success"}),fe(t.job.reportId)):(E.toast(`The import stopped: ${t.job.error||"an error"}`,{type:"error"}),t.job.reportId&&fe(t.job.reportId))}catch(t){if(!i())return;if(t.status===404){y.value=null,Ce().catch(()=>{});return}se()}},700))}f.add(()=>clearTimeout(Ae)),y.peek()&&se();let F=!1;async function fe(e){if(F||!e)return;F=!0;let t;try{t=await C(`${$}/history/${e}`)}catch(o){F=!1,E.toast(`Could not open the report: ${o.message||o}`,{type:"error"});return}if(!i()){F=!1;return}let a;try{a=E.slideover({title:V(`Report - ${t.run?.siteTitle||"WordPress import"}`),size:"lg",position:"right"})}catch(o){throw F=!1,o}const s=Rs(`
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(`
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>
83
83
  <div class="wpx-note-list" data-each="notes.value key=text"></div>
84
+ <p class="wpx-alert is-info" hidden data-bind-hidden="!homePrevious.value"><span data-bind-text="homePreviousText.value"></span>
85
+ <button type="button" class="btn btn-sm btn-ghost wpx-open-previous">Open it</button></p>
84
86
  <div class="wpx-bar-row"><div class="wpx-seg" role="group" aria-label="Show" data-each="filters.value key=id"></div>
85
87
  <span data-help-title="The report" data-help="Every item of the export and what happened to it. Kept as HTML: the page had tables, columns or other layout Markdown cannot say - it shows as it did, and edits as HTML. Needs rebuilding: shortcodes and page builders this site has no equivalent for. Right-click (or \u22EE, or Shift+F10) an item to open it."></span></div>
86
88
  <p class="wpx-note" data-bind-hidden="rows.value.length">Nothing matches.</p>
@@ -90,4 +92,4 @@ import{apiRequest as C,getToken as Za}from"../api.js";import{addBannerAction as
90
92
  <div class="wpx-list wpx-report-people" data-each="people.value key=key"></div>
91
93
  <div class="wpx-card-title" data-bind-hidden="!redirects.value.length">Redirects</div>
92
94
  <div class="wpx-list wpx-report-redirects" data-each="redirects.value key=from"></div>
93
- </div>`);s.querySelector(".wpx-tiles").innerHTML=Dt,s.querySelector(".wpx-note-list").innerHTML=Ut,s.querySelector(".wpx-seg").innerHTML=Be,s.querySelector(".wpx-report-items").innerHTML=Nt,s.querySelector(".wpx-report-redirects").innerHTML=Ot,s.querySelector(".wpx-report-people").innerHTML=Ss;const r=M.observable(t.run?.summary?.failed?"failed":"all"),d=t.items||[],ft=Et(d,Rt,qt),N=t.run?.summary||{},De=o=>Object.values(N.kinds||{}).reduce((n,x)=>n+(x[o]||0),0),Na={meta:M.observable([ee(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:De("created"),sub:""},{id:"updated",label:"Updated",value:De("updated"),sub:""},{id:"kept",label:"Left as is",value:De("kept"),sub:""},{id:"redirects",label:"Redirects",value:(N.redirects?.created||0)+(N.redirects?.updated||0),sub:""},...N.people?[{id:"people",label:"Accounts made",value:N.people.created||0,sub:N.people.invited?`${N.people.invited} emailed`:""}]:[],{id:"comments",label:"Comments",value:N.comments||0,sub:""},{id:"failed",label:"Failed",value:N.failed||0,sub:""}]),notes:M.observable((t.notes||[]).map(o=>({text:o}))),people:M.observable((t.people||[]).map(ns)),filters:M.computed(()=>Rt.filter(o=>o.id==="all"||ft[o.id]).map(o=>({...o,count:ft[o.id],cls:r.value===o.id?"is-on":""}))),rows:M.computed(()=>d.filter(o=>qt(o,r.value)).slice(0,1e3).map(o=>{const n=vs(o);return{...n,pillCls:n.pillClass}})),redirects:M.observable((t.redirects||[]).filter(o=>o.result!=="skipped").slice(0,1e3).map(o=>({...o,reason:o.error||"",pill:o.result==="failed"?"Failed":o.result==="updated"?"Updated":"Added",pillClass:o.result==="failed"?"is-bad":"is-ok"})))};a.element.appendChild(s);const Oa=M.applyBindings(Na,s,{methods:{pickSeg:o=>{r.value=o.id}}}),Da=yt(s),bt=new Map(d.map(o=>[o.key,o])),Ua=s.querySelector(".wpx-report-items"),Ha=o=>{const n=bt.get(o.dataset.key);if(!n)return[];const x=typeof n.target=="string"&&n.target.startsWith("/")?n.target:"",L=[{header:n.title}];if(x&&n.result!=="failed"&&n.result!=="skipped"&&L.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")&&L.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)&&(L.push({label:"Edit the block",icon:"edit",action:()=>{a.close(),location.hash=`#/blocks/edit/${n.target}`}}),n.footer&&L.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 Z=n.kind==="collection"?n.target:n.collection;L.push({label:"Open the collection",icon:"database",action:()=>{a.close(),location.hash=`#/collections/${Z}/entries`}})}return n.kind==="product"&&n.entryId&&/^[\w-]{1,64}$/.test(n.entryId)&&L.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)&&L.push({label:"Open the old address",icon:"link",action:()=>window.open(n.oldUrl,"_blank","noopener,noreferrer")}),x&&L.push({label:"Copy the new address",icon:"copy",action:()=>ze(new URL(x,location.origin).href)}),L},Ba=E.contextMenu(Ua,{cursor:"glyph",match:".wpx-row",inherit:!1,density:"compact",items:Ha}),_a=s.querySelector(".wpx-report-people"),za=new Map((t.people||[]).map(o=>[o.key,o])),ja=E.contextMenu(_a,{cursor:"glyph",match:".wpx-row",inherit:!1,density:"compact",items:o=>{const n=za.get(o.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"}}]}}),Wa=s.querySelector(".wpx-report-redirects"),Fa=E.contextMenu(Wa,{cursor:"glyph",match:".wpx-row",inherit:!1,density:"compact",items:o=>[{header:o.dataset.from},...o.dataset.from.includes("*")?[]:[{label:"Try it on the site",icon:"external-link",action:()=>window.open(o.dataset.from,"_blank","noopener")}],{label:"Open SEO \u203A Redirects",icon:"corner-up-right",action:()=>{a.close(),location.hash="#/seo/redirects"}}]}),Ue=o=>{const n=o.target.closest(".wpx-kebab");if(n){O(n);return}const x=o.target.closest(".wpx-report-people .wpx-row");if(x){O(x.querySelector(".wpx-kebab")||x);return}const L=o.target.closest(".wpx-report-items .wpx-row");if(!L)return;const Z=bt.get(L.dataset.key);Z?.target?.startsWith("/")&&Z.result!=="failed"&&Z.result!=="skipped"&&window.open(Z.target,"_blank","noopener")},gt=o=>{const n=o.target.closest(".wpx-row");n&&(o.key==="F10"&&o.shiftKey?(o.preventDefault(),O(n)):o.key==="Enter"&&o.target===n&&(o.preventDefault(),o.stopPropagation(),Ue({target:n})))};s.addEventListener("click",Ue),s.addEventListener("keydown",gt);const Ka=f.add(()=>a.close());Ga(a,()=>{F=!1,Oa.dispose(),Da(),Ba?.destroy?.(),Fa?.destroy?.(),ja?.destroy?.(),s.removeEventListener("click",Ue),s.removeEventListener("keydown",gt),Ka()}),a.open(),I.scan(s)}async function Ra(e){const t=B.peek().find(a=>a.id===e);if(t&&await Me(`Remove the import of <strong>${V(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 C(`${$}/history/${e}`,{method:"DELETE"}),!i())return;B.value=B.peek().filter(a=>a.id!==e),R(re)}catch(a){E.toast(`Not removed: ${a.message||a}`,{type:"error"})}}l.querySelector(".wpx-staged").innerHTML=Ts,l.querySelector(".wpx-items").innerHTML=Nt,l.querySelector(".wpx-redirects").innerHTML=Ot,l.querySelector(".wpx-history").innerHTML=Es,l.querySelector(".wpx-tiles").innerHTML=Dt,l.querySelector(".wpx-note-list").innerHTML=Ut,l.querySelector(".wpx-kinds").innerHTML=Be,l.querySelector(".wpx-filters").innerHTML=Be,l.querySelector(".wpx-facts").innerHTML=Cs,l.querySelector(".wpx-product-notes").innerHTML=Ht,l.querySelector(".wpx-types").innerHTML=$s,l.querySelector(".wpx-internal").innerHTML=Ht,l.querySelector(".wpx-people-list").innerHTML=Ls,l.querySelector(".wpx-widget-areas").innerHTML=Ps;const qa={mode:Bt,job:y,jobTitle:Ma,jobText:Ta,jobPct:Je,staged:p,stagedRows:zt,historyRows:jt,plan:g,planLoading:ie,planError:Ee,refreshing:Te,opt:c,blogAvailable:U,blogNote:ba,dirty:Fe,canRun:Ge,siteTitle:ea,siteMeta:ta,tiles:Jt,notes:Yt,kinds:Wt,filters:Kt,rows:Zt,moreText:Vt,emptyText:Gt,redirectRows:Xt,redirectCount:Qt,query:J,uploadText:ce,uploadError:Y,dropping:de,wxrLimit:Ea,zipInfo:me,zipText:ga,zipButton:ya,zipUploadText:pe,hasPeople:ca,peopleRows:pa,peopleCanCreate:da,peopleChoosable:ua,peopleNote:ma,peopleNoteCls:ha,inviteAvailable:wa,inviteText:va,optInvite:fa,shopAvailable:_t,hasProducts:la,productNote:aa,productNoteCls:sa,productFacts:oa,productNotes:na,typeRows:Le,internalRows:Se,hasCustom:ra,internalSummary:ia,widgetsInfo:Ze,widgetsText:xa,widgetsButton:ka,widgetsUploadText:Pe,widgetRows:Ve,optComments:Q("comments"),optMenus:Q("menus"),optRedirects:Q("redirects"),optDrafts:Q("drafts"),optProducts:Q("products")},Aa=M.applyBindings(qa,l,{methods:{back:()=>ae(),discard:()=>Xe(),run:()=>Ca(),resetChoices:()=>{c.value=He(U.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:()=>Pa(),removeWidgets:()=>Sa(),peopleAll:()=>P({users:Mt(T.peek(),"create")}),peopleNone:()=>P({users:Mt(T.peek(),"skip")}),pickSeg:e=>{e.seg==="kind"?D.value=e.id:z.value=e.id},showProducts:()=>{D.value="product",z.value="all",J.value="",l.querySelector(".wpx-items")?.closest(".wpx-card")?.scrollIntoView({behavior:"smooth",block:"start"})}}});f.add(()=>Aa.dispose()),f.add(yt(l,{position:"bottom"}));const oe=l.querySelector(".wpx-file"),et=()=>{const e=oe.files?.[0];oe.value="",Qe(e)};oe.addEventListener("change",et);const K=l.querySelector(".wpx-drop"),tt=e=>{e.preventDefault(),de.value=!0},at=()=>{de.value=!1},st=e=>{e.preventDefault(),de.value=!1,Qe(e.dataTransfer?.files?.[0])};K.addEventListener("dragover",tt),K.addEventListener("dragleave",at),K.addEventListener("drop",st);const be=l.querySelector(".wpx-zip-file"),ot=()=>{const e=be.files?.[0];be.value="",$a(e)};be.addEventListener("change",ot);const ge=l.querySelector(".wpx-wie-file"),nt=()=>{const e=ge.files?.[0];ge.value="",La(e)};ge.addEventListener("change",nt),f.add(()=>ge.removeEventListener("change",nt)),f.add(()=>{oe.removeEventListener("change",et),K.removeEventListener("dragover",tt),K.removeEventListener("dragleave",at),K.removeEventListener("drop",st),be.removeEventListener("change",ot)}),H(l.querySelector(".wpx-staged"),".wpx-row",e=>[{header:p.peek().find(t=>t.id===e.dataset.id)?.siteTitle||"Upload"},{label:"Preview the import",icon:"eye",action:()=>ve(e.dataset.id)},{label:"Discard the upload",icon:"trash-2",action:()=>Xe(e.dataset.id)}],e=>ve(e.dataset.id));const lt=(e,t)=>{c.value={...c.peek(),choices:Xa(c.peek().choices,e,t)}};H(l.querySelector(".wpx-items"),".wpx-row",e=>{const t=j.peek().find(s=>s.key===e.dataset.key);if(!t)return[];const a=[{header:t.title}];for(const s of Tt(t,t.choice))a.push({label:s.label,icon:s.icon,action:()=>lt(t.key,s.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=>O(e.querySelector(".wpx-kebab")||e));const rt=(e,t)=>{c.value=Ms(c.peek(),e,t)},it=(e,t)=>{c.value=ks(c.peek(),e,t)},Ia=e=>{D.value="",z.value="all",J.value=e,l.querySelector(".wpx-items")?.closest(".wpx-card")?.scrollIntoView({behavior:"smooth",block:"start"})},ne=l.querySelector(".wpx-types"),ct=e=>{if(!e.target.closest(".wpx-type-ctl"))return;e.stopPropagation();const a=e.target.closest("button[data-as]"),s=e.target.closest(".wpx-type-row");a&&s&&rt(s.dataset.type,a.dataset.as)},pt=e=>{const t=e.target.closest("input[data-list-page]"),a=e.target.closest(".wpx-type-row");t&&a&&it(a.dataset.type,t.checked)};ne.addEventListener("click",ct,!0),ne.addEventListener("change",pt),f.add(()=>{ne.removeEventListener("click",ct,!0),ne.removeEventListener("change",pt)}),H(ne,".wpx-type-row",e=>{const t=Le.peek().find(a=>a.type===e.dataset.type);return t?[{header:`${t.label} ("${t.type}")`},...bs(t).map(a=>({label:a.label,icon:a.icon,action:()=>{a.id==="show"?Ia(t.type):a.id==="list"||a.id==="nolist"?it(t.type,a.id==="list"):rt(t.type,a.id)}}))]:[]},e=>O(e.querySelector(".wpx-kebab")||e));const Ie=(e,t)=>{c.value=xs(c.peek(),e,t)},le=l.querySelector(".wpx-widget-areas"),dt=e=>{e.target.closest(".wpx-type-ctl")&&e.stopPropagation()},ut=e=>{const t=e.target.closest("input[data-footer]"),a=e.target.closest(".wpx-widget-row");t&&a&&Ie(a.dataset.area,t.checked)};le.addEventListener("click",dt,!0),le.addEventListener("change",ut),f.add(()=>{le.removeEventListener("click",dt,!0),le.removeEventListener("change",ut)}),H(le,".wpx-widget-row",e=>{const t=Ve.peek().find(r=>r.area===e.dataset.area),a=j.peek().find(r=>r.key===`widgets:${e.dataset.area}`);if(!t)return[];const s=[{header:`${t.label} ("${t.area}")`}];if(t.canPlace&&s.push(t.placeOn?{label:"Leave it out of the footer",icon:"eye-off",action:()=>Ie(t.area,!1)}:{label:"Place in the footer",icon:"layout",action:()=>Ie(t.area,!0)}),a)for(const r of Tt(a,a.choice))s.push({label:r.label,icon:r.icon,action:()=>lt(a.key,r.id)});return s.length===1&&s.push({label:"Nothing to change here",icon:"info",disabled:!0,action:()=>{}}),s},e=>O(e.querySelector(".wpx-kebab")||e));const mt=(e,t)=>{const a=T.peek()?.rows?.find(s=>s.key===e);a&&P({users:Qa(c.peek().users,a,t)})},Ne=l.querySelector(".wpx-people-list"),ht=e=>{if(!e.target.closest(".wpx-type-ctl"))return;e.stopPropagation();const a=e.target.closest("button[data-user]"),s=e.target.closest(".wpx-person");a&&s&&mt(s.dataset.key,a.dataset.user)};Ne.addEventListener("click",ht,!0),f.add(()=>Ne.removeEventListener("click",ht,!0)),H(Ne,".wpx-person",e=>{const t=T.peek()?.rows?.find(a=>a.key===e.dataset.key);return t?[{header:t.name},...ss(t).map(a=>({label:a.label,icon:a.icon,disabled:!!a.disabled,action:()=>{a.disabled||(a.id==="copy"?ze(t.email):mt(t.key,a.id))}}))]:[]},e=>O(e.querySelector(".wpx-kebab")||e));const A=l.querySelector(".wpx-role");f.add(M.effect(()=>{const e=T.value;if(!A)return;const t=e?.roles||[],a=e?.role||"",s=`${t.map(r=>r.name).join(",")}|${a}`;A.dataset.sig!==s&&(A.dataset.sig=s,A.replaceChildren(...t.map(r=>{const d=document.createElement("option");return d.value=r.name,d.textContent=r.name===We?`${r.label} (default)`:r.label,d})),A.value=a)}));const wt=()=>P({userRole:A.value===We?"":A.value});A?.addEventListener("change",wt),f.add(()=>A?.removeEventListener("change",wt)),H(l.querySelector(".wpx-redirects"),".wpx-row",e=>[{header:e.dataset.from},{label:"Copy the old address",icon:"copy",action:()=>ze(e.dataset.from)}],null),H(l.querySelector(".wpx-history"),".wpx-row",e=>[{header:B.peek().find(t=>t.id===e.dataset.id)?.siteTitle||"Import"},{label:"Open the report",icon:"file-text",action:()=>fe(e.dataset.id)},{label:"Remove from the history",icon:"trash-2",action:()=>Ra(e.dataset.id)}],e=>fe(e.dataset.id)),Va(u,{id:"wpx-upload",icon:"upload",title:"Upload an export",onClick:()=>{h.peek()&&ae(),setTimeout(()=>oe.click(),0)}}),I.scan(l);const Oe=$t(w(),p.peek().map(e=>e.id),b);Oe.pruned&&k&&q({owner:b,items:Oe.items});const vt=Object.entries(Oe.items).sort((e,t)=>String(t[1].at).localeCompare(String(e[1].at)))[0];vt&&!y.peek()&&await ve(vt[0])}};
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])}};
package/docs/cache.md CHANGED
@@ -37,6 +37,8 @@ page:<urlPath>:m<fileMtime>:role<roles>:o<origin>
37
37
 
38
38
  For example: `page:/about:m1727430000000:roleanon:ohttps://example.com`.
39
39
 
40
+ Domma Docs pages (`/domma-docs/...`) are cached the same way under `docs:<path>:role<roles>:o<origin>`, because their navbar, footers and overlays filter by role too. Both keys build the role part with `viewerRoleKey()` in `server/services/userRoles.js` - use it for any new cached render whose output depends on the viewer, and never key on the user id.
41
+
40
42
  Admin API responses, login pages and error pages are not cached. Drafts (including draft previews) are never cached, and a page containing a row-level `[view]` is rendered fresh for each signed-in viewer.
41
43
 
42
44
  Cache keys are versioned with a `v1:` prefix. Bumping the version (in `server/services/cache/index.js`) auto-invalidates the entire cache without needing `clear()` - useful if the cached value shape ever changes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.8.9",
3
+ "version": "1.8.11",
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",
@@ -128,6 +128,7 @@ export function planForScreen(plan, blog, shop = {}) {
128
128
  options: plan.options,
129
129
  summary: plan.summary,
130
130
  people: peopleForScreen(plan.people),
131
+ home: plan.home || null,
131
132
  notes: plan.notes,
132
133
  items: plan.items.map(r => ({
133
134
  key: r.key, kind: r.kind, wpId: r.wpId, title: r.title, action: r.action, reason: r.reason || '',
@@ -138,7 +139,9 @@ export function planForScreen(plan, blog, shop = {}) {
138
139
  ...(r.kind === 'custom' && {as: r.as || '', postType: r.postType || '', collection: r.collection || '', listAt: r.listAt || ''}),
139
140
  ...(r.kind === 'collection' && {postType: r.postType || '', fields: r.fields || 0}),
140
141
  ...(r.kind === 'widgets' && {area: r.area || '', footer: Boolean(r.footer), place: r.place || '', needs: r.needs || '', widgetTypes: r.widgetTypes || []}),
141
- ...(r.listFor && {listFor: r.listFor})
142
+ ...(r.listFor && {listFor: r.listFor}),
143
+ ...(r.front && {front: true}), ...(r.moveFrom && {moveFrom: r.moveFrom}),
144
+ ...(r.homeLatest && {homeLatest: true}), ...(r.postsPage && {postsPage: true}), ...(r.postsIndex && {postsIndex: true})
142
145
  })),
143
146
  redirects: plan.redirects.map(r => ({from: r.from, to: r.to, action: r.action, reason: r.reason || ''}))
144
147
  };
@@ -17,7 +17,8 @@
17
17
  */
18
18
  import {isProjectEnabled} from '../services/projects.js';
19
19
  import {renderDocPage} from '../services/docs.js';
20
- import {getBaseUrl, render404} from './public.js';
20
+ import {getBaseUrl, render404, sendHtml} from './public.js';
21
+ import {resolveViewer} from '../services/viewerSession.js';
21
22
 
22
23
  const DOCS_PROJECT_SLUG = 'domma-docs';
23
24
 
@@ -30,12 +31,17 @@ export async function docsPublicRoutes(fastify) {
30
31
  }
31
32
 
32
33
  const docPath = String(request.params['*'] || '');
33
- const html = await renderDocPage(docPath, {user: null, baseUrl: getBaseUrl(request)});
34
+ // Resolved like any public page: the navbar, footers and overlays in
35
+ // the docs shell filter by role, and renderDocPage keys its cache on
36
+ // the viewer's role set, so anonymous traffic still shares one entry.
37
+ const user = await resolveViewer(request);
38
+ const html = await renderDocPage(docPath, {user, baseUrl: getBaseUrl(request)});
34
39
  if (html == null) {
35
40
  reply.status(404);
36
41
  return reply.type('text/html; charset=utf-8').send(await render404(`/domma-docs${docPath ? '/' + docPath : ''}`));
37
42
  }
38
- return reply.type('text/html; charset=utf-8').send(html);
43
+ // private + ETag: the response is role-dependent, so no shared proxy may reuse it.
44
+ return sendHtml(request, reply, html);
39
45
  };
40
46
 
41
47
  fastify.get('/domma-docs', handle); // exact - params['*'] === '' (home landing)
@@ -14,7 +14,7 @@ import {buildRobotsTxt, generate as generateSitemap} from '../services/sitemap.j
14
14
  import {resolveViewer} from '../services/viewerSession.js';
15
15
  import {validSessionCookie, newSessionCookie} from '../services/shortcodeExpiry.js';
16
16
  import {touchPreviewLink, verifyPreviewToken} from '../services/previewLinks.js';
17
- import {getEffectiveRoles} from '../services/userRoles.js';
17
+ import {viewerRoleKey} from '../services/userRoles.js';
18
18
  import {hooks} from '../services/hooks.js';
19
19
  import {getConfig} from '../config.js';
20
20
  import * as cache from '../services/cache/index.js';
@@ -73,7 +73,7 @@ function escapeHtml(str) {
73
73
  * keys on role for exactly that reason - and a shared proxy must never hand one
74
74
  * visitor a page rendered for another's permissions.
75
75
  */
76
- function sendHtml(request, reply, html) {
76
+ export function sendHtml(request, reply, html) {
77
77
  const etag = `"${createHash('sha1').update(html).digest('base64')}"`;
78
78
  reply.header('cache-control', 'private, no-cache');
79
79
  reply.header('etag', etag);
@@ -196,7 +196,6 @@ export async function publicRoutes(fastify) {
196
196
  // what renders. Real sites hold a handful of role combinations, so
197
197
  // the entry count stays bounded by what users actually have.
198
198
  const userObj = await resolveViewer(request);
199
- const userRole = userObj ? [...getEffectiveRoles(userObj)].sort().join('+') : null;
200
199
 
201
200
  // The access decision - the directory-index fallback, the project
202
201
  // kill-switch, drafts and visibility - is resolvePublishedPage(), shared
@@ -274,7 +273,7 @@ export async function publicRoutes(fastify) {
274
273
  // script, git pull - yields a fresh entry even if no invalidation
275
274
  // hook fired. Stale-mtime entries age out via TTL/LRU.
276
275
  const pageMtime = await getPageMtime(page.urlPath || urlPath);
277
- const cacheKey = `page:${urlPath}:m${pageMtime}:role${userRole ?? 'anon'}:o${baseUrl}`;
276
+ const cacheKey = `page:${urlPath}:m${pageMtime}:role${viewerRoleKey(userObj)}:o${baseUrl}`;
278
277
  // The footer's blocks (site.json footer.blocks) are on every page: editing one must reach the cached pages.
279
278
  const cacheTags = [`page:${urlPath}`, ...(page.cacheTags || []), 'nav', 'site', ...footerBlockCacheTags(getConfig('site'))];
280
279
  const html = await cache.wrap(cacheKey, render, {tags: cacheTags});