domma-cms 1.8.10 → 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,10 @@
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
+
3
8
  ## [1.8.10] - 2026-10-16
4
9
 
5
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.
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "domma-cms",
3
- "version": "1.8.10",
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
  };
@@ -282,9 +282,13 @@ export async function setPageFrontmatter(urlPath, patch) {
282
282
  *
283
283
  * @param {string} oldUrlPath
284
284
  * @param {string} newUrlPath
285
+ * @param {object} [opts]
286
+ * @param {boolean} [opts.redirect=true] - false: the old address is not to lead to the new one (the home
287
+ * page moved aside - "/" stays the home page), so the SEO Tool adds no redirect and re-aims none
288
+ * @param {string} [opts.source] - who moved it, passed on to listeners
285
289
  * @returns {Promise<void>}
286
290
  */
287
- export async function renamePage(oldUrlPath, newUrlPath) {
291
+ export async function renamePage(oldUrlPath, newUrlPath, {redirect = true, source = ''} = {}) {
288
292
  const oldFile = await resolveExistingFilePath(oldUrlPath);
289
293
  const newFile = urlPathToFilePath(newUrlPath);
290
294
  await fs.mkdir(path.dirname(newFile), { recursive: true });
@@ -296,7 +300,7 @@ export async function renamePage(oldUrlPath, newUrlPath) {
296
300
  // Version directory move failure is non-fatal
297
301
  }
298
302
  // The old URL may be linked from anywhere - the SEO plugin offers a redirect.
299
- hooks.emit('content:pageRenamed', {from: oldUrlPath, to: newUrlPath});
303
+ hooks.emit('content:pageRenamed', {from: oldUrlPath, to: newUrlPath, ...(redirect === false && {redirect: false}), ...(source && {source})});
300
304
  }
301
305
 
302
306
  /**
@@ -129,8 +129,9 @@ async function seo(fastify, {enabled = true, getBaseUrl = null, rolesService = n
129
129
  const saveHits = setInterval(() => store.saveHits(), 60_000);
130
130
  saveHits.unref?.();
131
131
 
132
- const onRenamed = ({from, to} = {}) => {
133
- if (!cfg.redirects.onRename || !from || !to) return;
132
+ const onRenamed = ({from, to, redirect} = {}) => {
133
+ // redirect: false - the old address is not to lead to the new one (the home page moved aside).
134
+ if (!cfg.redirects.onRename || !from || !to || redirect === false) return;
134
135
  try {
135
136
  store.setRules(onMoved(store.rulesWithHits(), from, to, {id: newId()}));
136
137
  log.info(`[seo] ${from} moved to ${to}: redirect added.`);
@@ -0,0 +1,82 @@
1
+ /**
2
+ * WordPress's home page setting (Settings › Reading) - read from what the
3
+ * export does carry, since its options (show_on_front, page_on_front,
4
+ * page_for_posts) are not in a WXR file. PURE: no I/O.
5
+ *
6
+ * front page WordPress writes each item's <link> with get_permalink(), and
7
+ * the static front page's permalink is the site's home address.
8
+ * So: the one published page whose link IS the home address
9
+ * (http/https, www and a trailing slash aside). Never by title -
10
+ * a page called "Home" is just a page. Two such pages, or none,
11
+ * and there is no front page. A password-protected one is not
12
+ * offered: it would arrive as a draft and leave "/" empty.
13
+ * posts page Cannot be known for certain: only offered as a suggestion -
14
+ * a published page with nothing in it whose slug is a usual
15
+ * name for a blog (or the Blog's own address here). The person
16
+ * confirms it.
17
+ * no front WordPress showed its latest posts on the home page.
18
+ *
19
+ * @module services/wpImport/front
20
+ */
21
+
22
+ /** Slugs a WordPress posts page usually has. */
23
+ export const POSTS_PAGE_SLUGS = ['blog', 'news', 'posts', 'articles', 'journal'];
24
+
25
+ /** An address reduced to what identifies it: host without www, path without its trailing slash. Null when not a web address. */
26
+ export function addressKey(url) {
27
+ let u;
28
+ try { u = new URL(String(url || '').trim()); } catch { return null; }
29
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') return null;
30
+ let p = u.pathname;
31
+ try { p = decodeURI(p); } catch { /* keep */ }
32
+ p = `/${p}`.replace(/\/{2,}/g, '/').replace(/\/+$/, '').toLowerCase();
33
+ // A query or a port is part of the address: /?page_id=5 is not the home page.
34
+ return `${u.hostname.toLowerCase().replace(/^www\./, '')}${u.port ? `:${u.port}` : ''}${p}${u.search}`;
35
+ }
36
+
37
+ /** The home address(es) the export names: the channel link and wp:base_blog_url (both home_url()); wp:base_site_url only without them. */
38
+ export function homeKeys(site = {}) {
39
+ const keys = new Set([site.link, site.baseBlogUrl].map(addressKey).filter(Boolean));
40
+ if (!keys.size) { const k = addressKey(site.baseSiteUrl); if (k) keys.add(k); }
41
+ return keys;
42
+ }
43
+
44
+ /**
45
+ * The page WordPress used as its home page.
46
+ *
47
+ * @param {object} wxr - from readWxr()
48
+ * @returns {{page: object|null, matches: number}} matches: how many published pages had the home address
49
+ */
50
+ export function detectFrontPage(wxr) {
51
+ const homes = homeKeys(wxr?.site);
52
+ if (!homes.size) return {page: null, matches: 0};
53
+ const found = (wxr.items || []).filter(it => it.type === 'page' && it.status === 'publish' && !it.password && homes.has(addressKey(it.link)));
54
+ return {page: found.length === 1 ? found[0] : null, matches: found.length};
55
+ }
56
+
57
+ /** Does this WordPress content have nothing a reader would see? (Block comments, empty tags, &nbsp; and spaces only.) */
58
+ export function isEmptyContent(html) {
59
+ const s = String(html || '')
60
+ .replace(/<!--[\s\S]*?-->/g, '')
61
+ .replace(/<(img|video|audio|iframe|embed|object|svg|canvas|form|input|table|hr)\b/gi, 'X')
62
+ .replace(/<[^>]*>/g, '')
63
+ .replace(/&nbsp;|&#160;|&#xa0;/gi, ' ');
64
+ return !/\[[a-z]/i.test(s) && !s.trim();
65
+ }
66
+
67
+ /**
68
+ * The page that was probably WordPress's posts page - a suggestion only.
69
+ *
70
+ * @param {object} wxr
71
+ * @param {{front?: object|null, blogBase?: string}} [o] - the front page (never suggested); the Blog's address here
72
+ * @returns {object|null} the WordPress page
73
+ */
74
+ export function suggestPostsPage(wxr, {front = null, blogBase = ''} = {}) {
75
+ const base = String(blogBase || '').split('/').filter(Boolean).pop() || '';
76
+ const slugOf = (it) => String(it.slug || '').toLowerCase();
77
+ const candidates = (wxr?.items || []).filter(it => it.type === 'page' && it.status === 'publish' && !it.password
78
+ && it.id !== front?.id && isEmptyContent(it.content) && (POSTS_PAGE_SLUGS.includes(slugOf(it)) || (base && slugOf(it) === base)));
79
+ // The Blog's own address first, then the usual names in order.
80
+ const rank = (it) => (base && slugOf(it) === base ? -1 : POSTS_PAGE_SLUGS.indexOf(slugOf(it)));
81
+ return candidates.sort((a, b) => rank(a) - rank(b) || a.id - b.id)[0] || null;
82
+ }
@@ -42,6 +42,12 @@
42
42
  * services/renderer.js) - added once, never taken out, and not put back once
43
43
  * someone removed it here.
44
44
  *
45
+ * The home page (./front.js): the page WordPress showed at its home address
46
+ * becomes this site's home page. The page at "/" before is moved to
47
+ * /previous-home as a draft - never deleted, never overwritten - by the first
48
+ * run that does it, and never again (mapping `home`). That move leaves the
49
+ * menus' "/" alone and adds no redirect (renamePage's `redirect: false`).
50
+ *
45
51
  * The one request hook here answers WordPress's query addresses (/?p=12,
46
52
  * /?page_id=7, /?attachment_id=3) from the mapping; path redirects go into
47
53
  * SEO's redirect store.
@@ -60,14 +66,14 @@ import {convertContent} from './convert.js';
60
66
  import {currencyDecimals, mergeForUpdate, productFingerprintParts, readWooProducts, shopProductInput} from './woo.js';
61
67
  import {fetchMedia, testHostsAllowed} from './fetch.js';
62
68
  import {
63
- addHistory, clearRunning, createStaging, dropStaging, fingerprint, markRunning, recordFailedRun, recoverInterrupted, followRename, forgetMapCache, mappingFor, newId, queryTarget, saveMapping,
69
+ addHistory, clearRunning, readMap, createStaging, dropStaging, fingerprint, markRunning, recordFailedRun, recoverInterrupted, followRename, forgetMapCache, mappingFor, newId, queryTarget, saveMapping,
64
70
  stagingMeta, stagingPath, updateStagingMeta
65
71
  } from './store.js';
66
- import {createPage, getPage, listMedia, listPages, resolveExistingFilePath, saveMedia, updatePage} from '../content.js';
72
+ import {createPage, getPage, listMedia, listPages, renamePage, resolveExistingFilePath, saveMedia, updatePage} from '../content.js';
67
73
  import {createCollection, createEntry, getCollection, getEntry, listCollections, listEntries, updateCollection, updateEntry} from '../collections.js';
68
74
  import {customEntryData, customFields, customTypesOf, entryFingerprintText, listPageBody, mediaContext, mergeFields, schemaFields, stripShortcodes} from './custom.js';
69
75
  import {getPluginStates} from '../plugins.js';
70
- import {createMenu, getMenu, listMenus, updateMenu} from '../menus.js';
76
+ import {createMenu, getMenu, listMenus, relinkMenus, updateMenu} from '../menus.js';
71
77
  import {createUser, getUserByEmail, getUserById, listUsers} from '../users.js';
72
78
  import {adminInvite} from '../passwordReset.js';
73
79
  import {adminRightsGiven} from '../notification-sources.js';
@@ -228,9 +234,10 @@ export function footerBlockList(site = null) {
228
234
  }
229
235
 
230
236
  /** The site's state as the planner needs it. */
231
- export async function siteState(mapping = {}) {
237
+ export async function siteState(mapping = {}, source = '') {
232
238
  const pages = await listPages();
233
239
  const pagePaths = new Set(pages.map(p => String(p.urlPath || '/').toLowerCase()));
240
+ const homePage = pages.find(p => String(p.urlPath || '/') === '/');
234
241
  const pageHashes = {};
235
242
  for (const m of Object.values(mapping.items || {})) {
236
243
  if (m.path && pagePaths.has(m.path)) pageHashes[m.path] = await pageFingerprint(m.path);
@@ -288,9 +295,16 @@ export async function siteState(mapping = {}) {
288
295
  try { searchOn = getSearchSettings().enabled !== false; } catch { /* no search settings */ }
289
296
  const seo = seoEngine();
290
297
  // The users here - only what matching an author needs (never a password or a token).
298
+ // The home page set by an import of ANOTHER WordPress site: once per site, not once per export.
299
+ let homeSetBy = null;
300
+ try {
301
+ for (const [key, src] of Object.entries((await readMap()).sources || {})) {
302
+ if (key !== source && src?.items?.home?.kind === 'home') { homeSetBy = {...src.items.home, source: key}; break; }
303
+ }
304
+ } catch { /* no imports yet */ }
291
305
  const users = (await listUsers().catch(() => [])).map(u => ({id: u.id, email: String(u.email || '').toLowerCase(), name: u.name || ''}));
292
306
  return {
293
- pagePaths, pageHashes, shop, collections, customEntries, users,
307
+ pagePaths, pageHashes, homeSetBy, homeTitle: homePage ? String(homePage.title || '') : null, shop, collections, customEntries, users,
294
308
  blockNames, blockHashes, footerBlocks: footerBlockList(), searchOn,
295
309
  shortcodes: new Set(getShortcodeProcessors().map(p => p.name)),
296
310
  mediaNames: new Set(media.map(m => String(m.name).toLowerCase())),
@@ -407,7 +421,7 @@ export async function previewImport(id, options = {}) {
407
421
  const wxr = await loadExport(id);
408
422
  const {sourceKey} = await import('./plan.js');
409
423
  const mapping = await mappingFor(sourceKey(wxr.site));
410
- const state = await siteState(mapping);
424
+ const state = await siteState(mapping, sourceKey(wxr.site));
411
425
  const zipPaths = options.media === 'zip' ? await uploadsIndex(id) : null;
412
426
  const wie = await loadWidgets(id);
413
427
  // `people` (what the person may do with users) comes from the route, never from the request body.
@@ -435,7 +449,12 @@ export function cleanOptions(o = {}) {
435
449
  choices,
436
450
  users: cleanUserChoices(o.users),
437
451
  userRole: typeof o.userRole === 'string' && /^[a-z0-9][a-z0-9_-]{0,59}$/i.test(o.userRole) ? o.userRole : '',
438
- invite: o.invite === true
452
+ invite: o.invite === true,
453
+ // The home page: WordPress's front page made this site's (on unless refused), the suggested posts page
454
+ // confirmed (its WordPress id - the plan accepts only the one it suggests), the latest posts instead.
455
+ home: o.home !== false,
456
+ postsPage: Number.isInteger(o.postsPage) && o.postsPage > 0 && o.postsPage < 1e12 ? o.postsPage : 0,
457
+ latestPosts: o.latestPosts === true
439
458
  };
440
459
  }
441
460
 
@@ -616,8 +635,10 @@ async function runPhases(job, {plan, wxr, oldMapping, meta, inviteOrigin, wie},
616
635
  // Address lookups for content and menus.
617
636
  // A custom item kept in a collection is reached through its type's list page.
618
637
  const addressOf = (r) => (r.kind === 'custom' && r.as === 'collection' ? r.listAt : r.target);
619
- const targetOf = new Map(plan.items.filter(r => (r.kind === 'page' || r.kind === 'post' || r.kind === 'product' || r.kind === 'custom') && !r.listFor && addressOf(r) && r.action !== 'skip').map(r => [r.wpId, addressOf(r)]));
620
- const oldPathTarget = new Map(plan.items.filter(r => r.oldPath && addressOf(r) && r.kind !== 'collection' && r.action !== 'skip').map(r => [r.oldPath, addressOf(r)]));
638
+ // A confirmed posts page is not made, but links and menu items that pointed to it lead to the Blog.
639
+ const reached = (r) => r.action !== 'skip' || r.postsPage;
640
+ const targetOf = new Map(plan.items.filter(r => (r.kind === 'page' || r.kind === 'post' || r.kind === 'product' || r.kind === 'custom') && !r.listFor && r.wpId && addressOf(r) && reached(r)).map(r => [r.wpId, addressOf(r)]));
641
+ const oldPathTarget = new Map(plan.items.filter(r => r.oldPath && addressOf(r) && r.kind !== 'collection' && reached(r)).map(r => [r.oldPath, addressOf(r)]));
621
642
  const listPageOf = new Map(plan.items.filter(r => r.listFor && r.action !== 'skip' && r.target).map(r => [r.listFor, r.target]));
622
643
  const {oldPathOf, basePathOf} = await import('./plan.js');
623
644
  const mediaFor = (url) => {
@@ -674,6 +695,10 @@ async function runPhases(job, {plan, wxr, oldMapping, meta, inviteOrigin, wie},
674
695
  }
675
696
 
676
697
  await checkpoint(true);
698
+ // ---- 2b. The home page: what is at "/" now moves aside - once, never on a re-run ----
699
+ const homeOutcome = await runHome({plan, mapping, oldMapping, user, results});
700
+ if (homeOutcome) await checkpoint(true);
701
+
677
702
  // ---- 3. Pages (and posts as pages) ----
678
703
  job.phase = 'Pages';
679
704
  const users = await listUsers().catch(() => []);
@@ -682,16 +707,35 @@ async function runPhases(job, {plan, wxr, oldMapping, meta, inviteOrigin, wie},
682
707
  // Pages, posts as pages, and custom items as pages; a type's list page is made with its collection.
683
708
  const docRows = plan.items.filter(r => !r.listFor && (r.kind === 'page' || (r.kind === 'post' && !blogMode) || (r.kind === 'custom' && r.as === 'pages')));
684
709
  for (const r of docRows) {
710
+ if (!works(r) && r.moveFrom && homeOutcome?.error) { set(r.key, {result: 'failed', error: homeOutcome.error}); continue; }
685
711
  if (!works(r)) {
686
- if (r.action === 'keep') { mapping.items[r.key] = oldMapping.items[r.key]; recordQuery(mapping, r); }
712
+ if (r.action === 'keep') {
713
+ // Moved to "/" by this run (the home page), its edits here left as they are.
714
+ mapping.items[r.key] = r.moveFrom && homeOutcome?.moved ? {...oldMapping.items[r.key], path: r.target} : oldMapping.items[r.key];
715
+ recordQuery(mapping, r);
716
+ }
717
+ if (r.postsPage) { mapping.query.page_id[r.wpId] = r.target; mapping.query.p[r.wpId] = r.target; }
687
718
  set(r.key, {result: r.action === 'keep' ? 'kept' : 'skipped', target: r.target});
688
719
  continue;
689
720
  }
690
721
  const it = byId.get(r.wpId);
691
722
  try {
692
- const conv = convert(it);
693
- const fm = pageFrontmatter(it, r, plan, wxr);
694
- if (creditOf(it)) fm.createdBy = creditOf(it);
723
+ // The home page could not be made room for: nothing is put at "/" over the page there.
724
+ if ((r.front || r.homeLatest) && r.target === '/' && homeOutcome?.error) throw new Error(homeOutcome.error);
725
+ if (r.moveFrom && !homeOutcome?.moved) throw new Error('It could not be moved to the home page.');
726
+ let conv;
727
+ let fm;
728
+ if (r.homeLatest) {
729
+ // No front page in WordPress: its home page showed the latest posts - the Blog's own list does here.
730
+ conv = {body: `[blog-list count="10" /]\n`, notes: [], format: ''};
731
+ fm = {title: clip(titleText(plan.site.title), 200) || 'Home', description: '', status: 'published', sortOrder: 1,
732
+ importedFrom: {source: 'wordpress', home: 'latest-posts', url: clip(plan.site.url, 500)}};
733
+ } else {
734
+ conv = convert(it);
735
+ if (r.postsIndex) conv = {...conv, body: postsIndexBody(conv.body, plan, byId)};
736
+ fm = pageFrontmatter(it, r, plan, wxr);
737
+ }
738
+ if (it && creditOf(it)) fm.createdBy = creditOf(it);
695
739
  const exists = await getPage(r.target).catch(() => null);
696
740
  if (exists && (r.action === 'update')) await updatePage(r.target, fm, conv.body, {author: user?.email || 'WordPress import'});
697
741
  else if (exists) throw new Error(`${r.target} was taken while the import ran.`);
@@ -705,6 +749,12 @@ async function runPhases(job, {plan, wxr, oldMapping, meta, inviteOrigin, wie},
705
749
  await step(r.title);
706
750
  }
707
751
 
752
+ // The new home page was not made: the one moved aside for it goes back.
753
+ if (homeOutcome?.moved && homeOutcome.previous && results.get(homeOutcome.key)?.result === 'failed') {
754
+ await putHomeBack(homeOutcome, mapping, user?.email || 'WordPress import');
755
+ if (homeOutcome.restored) (results.get(homeOutcome.key).notes ||= []).push('The home page from before was put back at /.');
756
+ }
757
+
708
758
  await checkpoint(true);
709
759
  // ---- 4. Blog posts and comments ----
710
760
  let commentsMade = 0;
@@ -1043,7 +1093,7 @@ async function runPhases(job, {plan, wxr, oldMapping, meta, inviteOrigin, wie},
1043
1093
  job.phase = 'Finishing';
1044
1094
  await saveMapping(plan.source, mapping);
1045
1095
  forgetMapCache();
1046
- const report = buildReport({job, plan, results, redirectResults, meta, user, started, commentsMade, people});
1096
+ const report = buildReport({job, plan, results, redirectResults, meta, user, started, commentsMade, people, homeOutcome});
1047
1097
  await addHistory(report.run, report);
1048
1098
  await discardStaged(job.staged);
1049
1099
  job.reportId = report.run.id;
@@ -1053,6 +1103,102 @@ async function runPhases(job, {plan, wxr, oldMapping, meta, inviteOrigin, wie},
1053
1103
  log.info?.(`[wordpress-import] ${plan.site.title || plan.source}: ${JSON.stringify(report.run.summary)}`);
1054
1104
  }
1055
1105
 
1106
+ // ---- The home page ---------------------------------------------------------------
1107
+
1108
+ /**
1109
+ * Make room at "/" for the page this run makes the home page - the first time
1110
+ * only: the mapping's `home` says an earlier run did it, and then nothing here
1111
+ * moves again. The page at "/" now goes to the plan's /previous-home (the next
1112
+ * free -2, -3… if that was taken meanwhile), as a draft: never deleted, never
1113
+ * overwritten. No redirect is added for it and menus keep pointing at "/" -
1114
+ * "/" is still the home page, with the new page on it. A WordPress front page
1115
+ * an earlier run imported at another address moves to "/" itself.
1116
+ *
1117
+ * @returns {Promise<{moved: boolean, previous: string, title: string, error?: string}|null>} null when nothing was claimed
1118
+ */
1119
+ async function runHome({plan, mapping, oldMapping, user, results}) {
1120
+ const claim = plan.items.find(r => (r.front || r.homeLatest) && r.target === '/' && r.action !== 'skip');
1121
+ if (!claim || oldMapping.items?.home?.kind === 'home') return null;
1122
+ const out = {moved: false, previous: '', title: '', status: '', from: claim.moveFrom || '', key: claim.key};
1123
+ const author = user?.email || 'WordPress import';
1124
+ try {
1125
+ const move = plan.home?.move;
1126
+ // A page put at "/" since the preview is never taken over: preview again.
1127
+ if (!move && !claim.moveFrom && claim.action === 'create' && await getPage('/').catch(() => null)) throw new Error('a page was put at / since the preview - preview the import again.');
1128
+ if (move) {
1129
+ const now = await getPage('/').catch(() => null);
1130
+ if (now) {
1131
+ let to = move.to;
1132
+ for (let n = 2; await getPage(to).catch(() => null); n++) to = `${move.to}-${n}`;
1133
+ await renamePage('/', to, {redirect: false, source: 'wordpress-import'});
1134
+ out.previous = to;
1135
+ out.title = String(now.title || '');
1136
+ out.status = String(now.status || 'published');
1137
+ await followRename('/', to);
1138
+ followInMapping(mapping, '/', to);
1139
+ await updatePage(to, {status: 'draft', slug: to.split('/').pop()}, undefined, {author});
1140
+ }
1141
+ }
1142
+ if (claim.moveFrom) {
1143
+ // Never onto a page: whatever is at "/" must have moved first.
1144
+ if (await getPage('/').catch(() => null)) throw new Error('There is still a page at / - it was not moved.');
1145
+ await renamePage(claim.moveFrom, '/', {redirect: false, source: 'wordpress-import'});
1146
+ await followRename(claim.moveFrom, '/');
1147
+ followInMapping(mapping, claim.moveFrom, '/');
1148
+ await relinkMenus(claim.moveFrom, '/').catch(() => {});
1149
+ }
1150
+ out.moved = true;
1151
+ mapping.items.home = {kind: 'home', how: claim.homeLatest ? 'latest' : 'page', wpId: claim.wpId || 0, previous: out.previous, at: new Date().toISOString()};
1152
+ } catch (err) {
1153
+ out.error = `The home page was not changed: ${err.message || err}`;
1154
+ results.set(claim.key, {result: 'failed', error: out.error});
1155
+ await putHomeBack(out, mapping, author);
1156
+ }
1157
+ return out;
1158
+ }
1159
+
1160
+ /**
1161
+ * The new home page could not be put at "/": the page that was there goes back, as it was (published
1162
+ * again if it was), and nothing records the home page as set - so the site is never left without one,
1163
+ * and the next run tries again.
1164
+ */
1165
+ async function putHomeBack(out, mapping, author) {
1166
+ if (!out?.previous) return;
1167
+ try {
1168
+ if (await getPage('/').catch(() => null)) return;
1169
+ await renamePage(out.previous, '/', {redirect: false, source: 'wordpress-import'});
1170
+ await followRename(out.previous, '/');
1171
+ followInMapping(mapping, out.previous, '/');
1172
+ await updatePage('/', {status: out.status || 'published', slug: 'index'}, undefined, {author});
1173
+ delete mapping.items.home;
1174
+ out.restored = true;
1175
+ out.moved = false;
1176
+ out.previous = '';
1177
+ } catch { /* it stays a draft where it is - the report says where */ }
1178
+ }
1179
+
1180
+ /** The mapping this run holds, following a page it moved (store.followRename does the file; this, the copy in hand). */
1181
+ function followInMapping(mapping, from, to) {
1182
+ // Entries are replaced, not changed: they are shared with the earlier mapping the run compares against.
1183
+ for (const [k, it] of Object.entries(mapping.items || {})) if (it?.path === from) mapping.items[k] = {...it, path: to};
1184
+ for (const list of Object.values(mapping.query || {})) {
1185
+ for (const [id, t] of Object.entries(list || {})) if (t === from) list[id] = to;
1186
+ }
1187
+ }
1188
+
1189
+ /** WordPress's posts page, when posts become pages: its text, then a list of them, newest first. */
1190
+ export function postsIndexBody(intro, plan, byId) {
1191
+ const esc = (s) => String(s).replace(/\[/g, '&#91;').replace(/\]/g, '&#93;');
1192
+ const rows = plan.items.filter(r => r.kind === 'post' && r.target && r.action !== 'skip' && r.status === 'published')
1193
+ .map(r => ({r, it: byId.get(r.wpId)}))
1194
+ .sort((a, b) => String(b.it?.date || '').localeCompare(String(a.it?.date || '')));
1195
+ const lines = rows.map(({r, it}) => {
1196
+ const day = String(it?.date || '').slice(0, 10);
1197
+ return `- [${esc(clip(titleText(it?.title || r.title), 200) || 'Untitled')}](${r.target})${/^\d{4}-\d{2}-\d{2}$/.test(day) ? ` - ${day}` : ''}`;
1198
+ });
1199
+ return [String(intro || '').trim(), lines.join('\n') || '_No posts yet._'].filter(Boolean).join('\n\n') + '\n';
1200
+ }
1201
+
1056
1202
  // ---- People ------------------------------------------------------------------
1057
1203
 
1058
1204
  /** Between two "set your password" emails - a big export must not burst the mail server. Tests shorten it. */
@@ -1348,7 +1494,7 @@ async function importComments(it, postEntryId, mapping, oldMapping, user) {
1348
1494
  return made;
1349
1495
  }
1350
1496
 
1351
- function buildReport({job, plan, results, redirectResults, meta, user, started, commentsMade, people}) {
1497
+ function buildReport({job, plan, results, redirectResults, meta, user, started, commentsMade, people, homeOutcome}) {
1352
1498
  const items = plan.items.map(r => {
1353
1499
  const res = results.get(r.key) || {result: r.action === 'skip' ? 'skipped' : r.action === 'keep' ? 'kept' : 'skipped'};
1354
1500
  return {
@@ -1408,7 +1554,14 @@ function buildReport({job, plan, results, redirectResults, meta, user, started,
1408
1554
  durationMs: Date.now() - started,
1409
1555
  summary
1410
1556
  };
1411
- return {run, options: plan.options, items, people: peopleRows, redirects: redirectResults, notes: [...peopleNotes, ...plan.notes, ...(plan.summary.products?.notes || []), ...commentErrors.slice(0, 50)], blogBase: plan.blogBase, shopBase: plan.shopBase};
1557
+ const homeNotes = [];
1558
+ if (homeOutcome?.error) homeNotes.push(homeOutcome.error);
1559
+ if (plan.home?.changed) homeNotes.push(plan.home.changed);
1560
+ const home = plan.home ? {
1561
+ how: plan.home.how, page: plan.home.page, latest: plan.home.latest, postsPage: plan.home.postsPage,
1562
+ previous: homeOutcome?.previous || '', previousTitle: clip(homeOutcome?.title, 120), moved: Boolean(homeOutcome?.moved), error: homeOutcome?.error || '', changed: plan.home.changed || ''
1563
+ } : null;
1564
+ return {run, options: plan.options, home, items, people: peopleRows, redirects: redirectResults, notes: [...homeNotes, ...peopleNotes, ...plan.notes, ...(plan.summary.products?.notes || []), ...commentErrors.slice(0, 50)], blogBase: plan.blogBase, shopBase: plan.shopBase};
1412
1565
  }
1413
1566
 
1414
1567
  // ---- The request hook: WordPress's query addresses ------------------------
@@ -1430,7 +1583,8 @@ async function wordpressQueryRedirects(fastify) {
1430
1583
  } catch { /* nothing to recover */ }
1431
1584
  // The redirects below belong to the migrated site, not to the Tool: they answer
1432
1585
  // whether or not the importer is switched on. Cheap: only "/" or "/index.php" with a query is looked at.
1433
- const onRenamed = ({from, to} = {}) => { followRename(from, to).catch(() => {}); };
1586
+ // A move the import makes itself (the home page) it follows itself, in step with its own mapping.
1587
+ const onRenamed = ({from, to, source} = {}) => { if (source !== 'wordpress-import') followRename(from, to).catch(() => {}); };
1434
1588
  coreHooks.on?.('content:pageRenamed', onRenamed);
1435
1589
  fastify.addHook('onClose', (_i, done) => { coreHooks.off?.('content:pageRenamed', onRenamed); done(); });
1436
1590
  fastify.addHook('onRequest', async (request, reply) => {
@@ -25,6 +25,14 @@
25
25
  * beside the export (./widgets.js), become one block per widget area, and a
26
26
  * footer area's block joins the footer (summary.widgets).
27
27
  *
28
+ * The home page (./front.js, plan.home): the page WordPress showed at its home
29
+ * address becomes this site's home page (options.home, on by default when one
30
+ * is found); the page there now moves to /previous-home as a draft - once, on
31
+ * the first run that does it (mapping `home`), never again. A suggested posts
32
+ * page, once confirmed (options.postsPage), is not made: its old address leads
33
+ * to the Blog. With no front page, the latest posts can be the home page
34
+ * (options.latestPosts, a page holding the Blog's [blog-list]).
35
+ *
28
36
  * @module services/wpImport/plan
29
37
  */
30
38
  import {detectBuilder, findShortcodes} from './convert.js';
@@ -34,6 +42,7 @@ import {readWooProducts, shopProductInput, wooPageRole} from './woo.js';
34
42
  import {customFields, customOldPath, customTypesOf, mediaContext, rewriteSlugOf, typeLabel} from './custom.js';
35
43
  import {planPeople} from './people.js';
36
44
  import {planWidgets} from './widgets.js';
45
+ import {detectFrontPage, suggestPostsPage} from './front.js';
37
46
 
38
47
  /** Addresses under the Shop's that are its own pages, never a product's. */
39
48
  const SHOP_RESERVED = ['cart', 'checkout', 'basket', 'account', 'order'];
@@ -219,6 +228,10 @@ export function isoDate(gmt, local) {
219
228
  * @param {object} [options.people] - what the person running it may do with users (never from a request)
220
229
  * @param {object|null} [options.wie] - the widgets file, from readWie() (./widgets.js)
221
230
  * @param {Object<string,false>} [options.footer] - per widget area: false = not placed in the footer
231
+ * @param {boolean} [options.home=true] - the WordPress front page becomes this site's home page
232
+ * @param {number} [options.postsPage] - the suggested posts page, confirmed (its WordPress id)
233
+ * @param {boolean} [options.latestPosts=false] - no front page: the latest posts on the home page
234
+ * @param {string|null} [site.homeTitle] - the title of the page at / now (null: none)
222
235
  * @returns {object}
223
236
  */
224
237
  export function planImport(wxr, site, mapping = {}, options = {}) {
@@ -226,6 +239,7 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
226
239
  posts: site.blog?.available ? 'blog' : 'pages',
227
240
  media: 'download', comments: true, menus: true, redirects: true, drafts: true, products: true, choices: {}, zipPaths: null,
228
241
  types: {}, listPages: {}, users: {}, userRole: '', invite: false, footer: {}, wie: null,
242
+ home: true, postsPage: 0, latestPosts: false,
229
243
  ...options
230
244
  };
231
245
  opt.types ||= {};
@@ -247,7 +261,9 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
247
261
  const notes = [];
248
262
  const byId = new Map(wxr.items.map(it => [it.id, it]));
249
263
 
250
- const takenPaths = new Set(site.pagePaths);
264
+ // An address this site already redirects (and not by this import) would hide a page made there: taken too.
265
+ const ownRedirectFroms = new Set(Object.values(mapping.redirects || {}).map(r => r?.from));
266
+ const takenPaths = new Set([...site.pagePaths, ...[...(site.redirectFroms || [])].filter(f => typeof f === 'string' && !f.includes('*') && !ownRedirectFroms.has(f))]);
251
267
  const takenMedia = new Set(site.mediaNames);
252
268
  const takenMenus = new Set(site.menuSlugs);
253
269
  const takenSlugs = new Set(site.blog?.postSlugs || []);
@@ -294,6 +310,41 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
294
310
  // Parents before children, so a child's address can follow its parent's.
295
311
  const depthOf = (it, seen = new Set()) => (it.parent && byId.has(it.parent) && !seen.has(it.id) ? 1 + depthOf(byId.get(it.parent), seen.add(it.id)) : 0);
296
312
  const orderedPages = [...pages].sort((a, b) => depthOf(a) - depthOf(b) || a.order - b.order || a.id - b.id);
313
+ // ---- The home page: WordPress's front page, its posts page (./front.js) ----
314
+ // `home` in the mapping: an earlier run set this site's home page - it is never moved again.
315
+ // An import of another WordPress site that set it counts too (site.homeSetBy): once per site.
316
+ const homeRec = mapped.home?.kind === 'home' ? mapped.home : site.homeSetBy?.kind === 'home' ? site.homeSetBy : null;
317
+ const leftAsIs = homeRec?.source
318
+ ? `An earlier import (of ${homeRec.source}) set this site's home page - it is left as it is.`
319
+ : 'This site\'s home page was changed here since the last import - it is left as it is.';
320
+ const {page: front, matches: frontMatches} = detectFrontPage(wxr);
321
+ const suggested = opt.posts !== 'skip' ? suggestPostsPage(wxr, {front, blogBase}) : null;
322
+ // Imported before as a page that is still here: the person's page now - never hidden behind a redirect.
323
+ const suggestedAt = suggested && mapped[`page:${suggested.id}`]?.path;
324
+ const postsCandidate = suggested && !(suggestedAt && site.pagePaths.has(suggestedAt)) ? suggested : null;
325
+ const postsPageId = postsCandidate && Number(opt.postsPage) === postsCandidate.id ? postsCandidate.id : 0;
326
+ const latestOffered = !front && !frontMatches && opt.posts === 'blog' && Boolean(site.shortcodes?.has('blog-list'));
327
+ let homeMove = null;
328
+ let homeChanged = '';
329
+ /** Something of this run's takes "/" for the first time: the page there now moves to /previous-home (-2…), never deleted. */
330
+ const claimHome = () => {
331
+ if (site.pagePaths.has('/') && !ownPaths.has('/')) {
332
+ const to = freePath('/previous-home', takenPaths);
333
+ takenPaths.add(to);
334
+ takenPaths.delete('/');
335
+ homeMove = {from: '/', to};
336
+ }
337
+ };
338
+ const homeAddress = (it) => {
339
+ const p = oldPathOf(it.link, wxr.site);
340
+ return p !== null && relative(p, base) === '/';
341
+ };
342
+ /** WordPress's own address for a page by its slugs (/about/team) - the front page answered there too. */
343
+ const slugAddress = (it) => {
344
+ const segs = [];
345
+ for (let c = it, n = 0; c && n < 10; c = c.parent ? byId.get(c.parent) : null, n++) segs.unshift(slugify(c.slug));
346
+ return segs.every(Boolean) ? `${base}/${segs.join('/')}` : '';
347
+ };
297
348
  const WOO_PAGE = {
298
349
  cart: {to: '/cart', what: 'cart'}, checkout: {to: '/cart', what: 'checkout'},
299
350
  account: {to: '/account', what: 'account page'}, shop: {to: '', what: 'shop page'}
@@ -309,8 +360,30 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
309
360
  if (oldPath) extraRedirects.push({from: oldPath, to, key});
310
361
  continue;
311
362
  }
363
+ if (postsPageId === it.id) {
364
+ if (opt.posts === 'blog') {
365
+ // The Blog lists the posts itself: no empty page, and the old address leads there.
366
+ push({...docRow(it, key, 'page', oldPath), status: 'published', action: 'skip', postsPage: true, target: blogBase,
367
+ reason: `WordPress's posts page - the Blog lists the posts at ${blogBase}, so no empty page is made${oldPath ? ' and the old address leads there' : ''}.`});
368
+ if (oldPath) extraRedirects.push({from: oldPath, to: blogBase, key: `${key}/posts`});
369
+ continue;
370
+ }
371
+ // Posts as pages: it becomes their index, where they are put.
372
+ const r = planDocument({it, key, kind: 'page', wanted: pagesBase, oldPath, statusMap: PAGE_STATUS});
373
+ r.postsIndex = true;
374
+ if (r.action === 'create' && !r.reason) r.reason = `WordPress's posts page - it lists the posts imported as pages under ${pagesBase}.`;
375
+ if (r.target) pageTarget.set(it.id, r.target);
376
+ continue;
377
+ }
378
+ if (front?.id === it.id && opt.home && choices[key] !== 'skip') {
379
+ const r = planFront(it, key, oldPath);
380
+ if (r.target) pageTarget.set(it.id, r.target);
381
+ continue;
382
+ }
312
383
  let wanted = oldPath ? cleanPath(relative(oldPath, base)) : '';
313
384
  if (!wanted || wanted === '/' && oldPath !== null && relative(oldPath, base) !== '/') wanted = '';
385
+ // Only the one front page takes "/" (and only when chosen): another page at the home address keeps its slug.
386
+ if (wanted === '/' && homeAddress(it)) wanted = '';
314
387
  if (!wanted) {
315
388
  const parentPath = it.parent && pageTarget.get(it.parent);
316
389
  const slug = slugify(it.slug) || slugify(it.title) || `page-${it.id}`;
@@ -320,6 +393,25 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
320
393
  if (row.target) pageTarget.set(it.id, row.target);
321
394
  }
322
395
 
396
+ // ---- No front page: the latest posts on the home page, when chosen ----
397
+ if (latestOffered && opt.latestPosts) {
398
+ const key = 'home:latest';
399
+ const was = mapped[key];
400
+ let wanted = '/';
401
+ if (!was?.path && choices[key] !== 'skip') {
402
+ if (!homeRec) claimHome();
403
+ else if (takenPaths.has('/')) { wanted = ''; homeChanged = leftAsIs; }
404
+ }
405
+ if (wanted) {
406
+ const r = planDocument({it: {id: 0, title: wxr.site.title || 'Latest posts', link: '', status: 'publish', content: '', meta: {}, terms: [], password: ''},
407
+ key, kind: 'page', wanted, oldPath: null, statusMap: PAGE_STATUS});
408
+ r.homeLatest = true;
409
+ r.wpId = 0;
410
+ r.title = 'Home page: the latest posts';
411
+ if (r.action === 'create' && !r.reason) r.reason = 'Shows the latest Blog posts, as WordPress did on its home page.';
412
+ }
413
+ }
414
+
323
415
  // ---- Posts ----
324
416
  const posts = wxr.items.filter(i => i.type === 'post');
325
417
  for (const it of posts) {
@@ -552,6 +644,8 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
552
644
  const query = {p: {}, page_id: {}, attachment_id: {}};
553
645
  const live = (r) => r.target && r.action !== 'skip';
554
646
  for (const r of items) {
647
+ // A confirmed posts page is not made: ?page_id= for it leads to the Blog.
648
+ if (r.postsPage) { query.page_id[r.wpId] = r.target; query.p[r.wpId] = r.target; continue; }
555
649
  // An entry in a collection has no address of its own: its old one leads to the type's list page.
556
650
  if (r.kind === 'custom' && r.as === 'collection' && r.action !== 'skip') {
557
651
  if (r.listAt) query.p[r.wpId] = r.listAt;
@@ -662,6 +756,7 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
662
756
  const productRows = items.filter(r => r.kind === 'product');
663
757
  const people = planPeople(wxr, items, site.users || [], mapping, opt, opt.people || {});
664
758
  const shortcodeCount = items.reduce((n, r) => n + (r.shortcodes?.length || 0), 0);
759
+ const home = homeSummary();
665
760
 
666
761
  const summary = {
667
762
  counts,
@@ -696,11 +791,67 @@ export function planImport(wxr, site, mapping = {}, options = {}) {
696
791
  query,
697
792
  mediaByRel,
698
793
  people,
794
+ home,
699
795
  notes: [...new Set(notes)],
700
796
  summary
701
797
  };
702
798
 
703
799
  // ---- helpers sharing the plan's state ----
800
+ /**
801
+ * WordPress's front page, chosen as the home page. The first run that does it moves the page at "/"
802
+ * away (claimHome); a re-run updates it where it is, and if the home page was changed here by hand
803
+ * since, leaves the home page alone and says so.
804
+ */
805
+ function planFront(it, key, oldPath) {
806
+ const was = mapped[key];
807
+ const here = was?.path && site.pagePaths.has(was.path) ? was.path : '';
808
+ const slugPath = slugAddress(it);
809
+ let row;
810
+ if (homeRec) {
811
+ // Set by an earlier run: never moved again.
812
+ if (here && here !== '/') homeChanged = `This site's home page was changed here since the last import - "${it.title || 'the home page'}" stays at ${here}.`;
813
+ let wanted = '/';
814
+ if (!here && takenPaths.has('/')) {
815
+ wanted = `/${slugify(it.slug) || slugify(it.title) || 'home'}`;
816
+ homeChanged = leftAsIs;
817
+ }
818
+ row = planDocument({it, key, kind: 'page', wanted, oldPath, statusMap: PAGE_STATUS});
819
+ } else {
820
+ // Deleted here since an earlier run made it (not as the home page): that is the person's call, as for any page.
821
+ if (!was?.path || here) claimHome();
822
+ row = planDocument({it, key, kind: 'page', wanted: '/', oldPath, statusMap: PAGE_STATUS});
823
+ // Imported before at another address (not as the home page then): it moves to "/".
824
+ if (here && here !== '/' && row.action !== 'skip') {
825
+ row.moveFrom = here;
826
+ row.target = '/';
827
+ takenPaths.add('/');
828
+ extraRedirects.push({from: here, to: '/', key: `${key}/moved`});
829
+ }
830
+ }
831
+ row.front = true;
832
+ if (row.target === '/' && slugPath && slugPath !== '/') extraRedirects.push({from: slugPath, to: '/', key: `${key}/slug`});
833
+ return row;
834
+ }
835
+ function homeSummary() {
836
+ const frontRow = front ? items.find(r => r.key === `page:${front.id}`) : null;
837
+ const latestRow = items.find(r => r.key === 'home:latest');
838
+ const postsRow = postsCandidate ? items.find(r => r.key === `page:${postsCandidate.id}`) : null;
839
+ return {
840
+ how: front ? 'page' : frontMatches > 1 ? 'unclear' : 'latest',
841
+ matches: frontMatches,
842
+ on: opt.home !== false,
843
+ page: front ? {wpId: front.id, title: front.title || '(untitled)', oldUrl: front.link, target: frontRow?.target || '', action: frontRow?.action || 'skip', isHome: frontRow?.target === '/' && frontRow.action !== 'skip'} : null,
844
+ now: site.pagePaths.has('/') ? {title: site.homeTitle ?? ''} : null,
845
+ move: homeMove,
846
+ done: homeRec ? {how: homeRec.how || 'page', previous: homeRec.previous || '', source: homeRec.source || ''} : null,
847
+ changed: homeChanged,
848
+ latest: {offered: latestOffered, on: Boolean(latestOffered && opt.latestPosts), target: latestRow?.target || '', action: latestRow?.action || ''},
849
+ postsPage: postsCandidate ? {
850
+ wpId: postsCandidate.id, title: postsCandidate.title || '(untitled)', oldUrl: postsCandidate.link,
851
+ confirmed: Boolean(postsPageId), to: opt.posts === 'blog' ? blogBase : (postsRow?.postsIndex ? postsRow.target : pagesBase)
852
+ } : null
853
+ };
854
+ }
704
855
  function docRow(it, key, kind, oldPath) {
705
856
  return {
706
857
  key, kind, wpId: it.id, title: it.title || `(untitled ${kind} ${it.id})`, oldUrl: it.link, oldPath,