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 +5 -0
- package/CLAUDE.md +15 -0
- package/admin/js/lib/wordpress-import-arrange.js +1 -1
- package/admin/js/templates/docs/usage-tools.html +8 -0
- package/admin/js/templates/wordpress-import.html +17 -0
- package/admin/js/views/wordpress-import.js +12 -10
- package/package.json +1 -1
- package/server/routes/api/wordpress-import.js +4 -1
- package/server/services/content.js +6 -2
- package/server/services/seo/index.js +3 -2
- package/server/services/wpImport/front.js +82 -0
- package/server/services/wpImport/index.js +171 -17
- package/server/services/wpImport/plan.js +152 -1
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 > 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 "Home" 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
|
|
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>`,
|
|
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>`,
|
|
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>`,
|
|
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
|
|
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>`,
|
|
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>`,
|
|
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>`,
|
|
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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");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
|
@@ -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
|
-
|
|
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, 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(/ | | /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
|
-
|
|
620
|
-
const
|
|
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') {
|
|
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
|
-
|
|
693
|
-
|
|
694
|
-
if (
|
|
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, '[').replace(/\]/g, ']');
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|