domma-cms 1.8.8 → 1.8.10
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 +10 -0
- package/CLAUDE.md +22 -0
- package/admin/js/lib/blocks-arrange.js +1 -1
- package/admin/js/lib/site-settings-arrange.js +1 -1
- package/admin/js/lib/wordpress-import-arrange.js +1 -1
- package/admin/js/templates/settings.html +5 -0
- package/admin/js/templates/wordpress-import.html +20 -1
- package/admin/js/views/blocks.js +4 -4
- package/admin/js/views/wordpress-import.js +23 -9
- package/docs/cache.md +2 -0
- package/docs/configuration.md +1 -0
- package/package.json +1 -1
- package/public/css/site.css +1 -1
- package/public/js/search.js +1 -1
- package/public/js/site.js +1 -1
- package/server/routes/api/blocks.js +8 -3
- package/server/routes/api/settings.js +8 -0
- package/server/routes/api/wordpress-import.js +38 -2
- package/server/routes/docs-public.js +9 -3
- package/server/routes/public.js +6 -6
- package/server/services/docs.js +11 -5
- package/server/services/renderer.js +56 -3
- package/server/services/userRoles.js +15 -0
- package/server/services/wpImport/convert.js +2 -1
- package/server/services/wpImport/index.js +167 -7
- package/server/services/wpImport/plan.js +35 -3
- package/server/services/wpImport/widgets.js +355 -0
- package/server/templates/page.html +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.8.10] - 2026-10-16
|
|
4
|
+
|
|
5
|
+
- 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.
|
|
6
|
+
|
|
7
|
+
## [1.8.9] - 2026-10-16
|
|
8
|
+
|
|
9
|
+
- **Import from WordPress brings the widgets across.** A WordPress export has no widgets in it, so the import screen now takes a second, optional file: the `.wie` file the free "Widget Importer & Exporter" WordPress plugin exports (or put it in the same zip as the export). Each widget area becomes a block named wp-… (Content › Blocks), its widgets in order under their titles. Text and custom HTML come across cleaned - no scripts, WordPress shortcodes left as text to rebuild - with links and pictures pointing at their new addresses; an image widget brings its picture with the media; a navigation menu shows the menu imported from it; recent posts and categories show the Blog's own lists, and archives a link to the Blog; a search widget becomes a link that opens the site search. Anything else (a calendar, an RSS feed, a plugin's widget) is listed to rebuild - nothing is guessed. A footer widget area is also added to the footer of every page, after whatever is there, unless you switch that off in the preview's new Widgets card; other areas are blocks you can put on a page with `[block]`. Run it again and a block you edited here is left alone unless you choose Overwrite, nothing is added to the footer twice, and an area you took out of the footer is not put back.
|
|
10
|
+
- **The footer can show blocks.** Site Settings › Footer has a new Footer blocks field: name one or more blocks and they appear side by side at the top of the footer on every page (stacked on a phone), drawn by the server - so they show even before the page's script runs. Saving one of those blocks updates the footer straight away.
|
|
11
|
+
- A link to `#site-search` anywhere on a page now opens the site search.
|
|
12
|
+
|
|
3
13
|
## [1.8.8] - 2026-10-16
|
|
4
14
|
|
|
5
15
|
- **Your emails with a contact, on their card.** With Email (Pro), the foot of a contact's card in Contacts lists your ten newest emails to or from any of the contact's addresses - Received or Sent, unread, and the mailbox when you have several - each opening the message in Email. Only your own mailboxes are searched. This release carries the engine side: a whole-address mail query (`listByAddresses`) and links that open the reader at one message (`#/plugins/<mail>?open=<uid>&account=<id>&folder=<path>`).
|
package/CLAUDE.md
CHANGED
|
@@ -401,6 +401,28 @@ A built-in Tool (`wordpress-import` in `services/tools.js`, permission `import.w
|
|
|
401
401
|
redirect an earlier run added from an address this run gave a page is removed (report result `removed`). Excerpts lose
|
|
402
402
|
their shortcode tags (`stripShortcodes`) - the list page's display rows go through the later shortcode stages.
|
|
403
403
|
|
|
404
|
+
- **Widgets** (`widgets.js`, pure): a WXR has none, so they come from a `.wie` file (the "Widget Importer & Exporter"
|
|
405
|
+
plugin's JSON) - `POST /staged/:id/widgets`, or a `.wie` inside the export's zip, staged as `widgets.wie`. Untrusted:
|
|
406
|
+
`readWie()` caps size (`WIE_MAX_BYTES`) and nesting (`jsonDepth()` runs BEFORE `JSON.parse`), drops
|
|
407
|
+
`__proto__`/`constructor`/`prototype` keys and keeps only plain scalar settings. Each area (not `wp_inactive_widgets`,
|
|
408
|
+
not empty) is a row `widgets:<area>` -> block `wp-<area>` (`freePath` on a clash), built by `widgetAreaHtml()`:
|
|
409
|
+
text/custom_html/block through `convertContent(..., {html: true})` (never Markdown - it is a block template), titles
|
|
410
|
+
escaped, every `{` as `{` (no `{{placeholder}}` survives), no blank lines (gotcha 11). `decideWidget()` is the ONE
|
|
411
|
+
decision, shared by the plan and the run: image -> the imported media only; nav_menu -> `[menu]` of the menu this run
|
|
412
|
+
made; recent-posts/categories -> the Blog's `[blog-list]`/`[blog-categories]` (by registered shortcode name, and only
|
|
413
|
+
when posts go to the Blog); archives -> a link to the Blog; search -> `<a href="#site-search">` (public/js/search.js
|
|
414
|
+
opens it); anything else -> `needs`, nothing guessed. A footer area (id matches /footer/) is appended to
|
|
415
|
+
`site.json` footer.blocks (`place: add`; never removes; `there` = already in; `off` = the person's `options.footer`
|
|
416
|
+
choice; `removed` = an earlier run placed it and it is gone now - not put back; `full` = the footer already has
|
|
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
|
+
placed}`; an edited block is "Left as is" unless Overwrite.
|
|
419
|
+
- **Footer blocks** (`site.json` footer.blocks, Site Settings > Footer): `buildFooterBlocks()` in `renderer.js` renders
|
|
420
|
+
each as `[block template="…" /]` through `parseMarkdown` into `<div class="cms-footer-blocks">` INSIDE
|
|
421
|
+
`#site-footer` - on BOTH render paths (`footerBlocksHtml`). site.js lifts that node out before `$footer.html()` (which
|
|
422
|
+
would wipe and re-sanitise it) and prepends it back. Public page cache entries carry `block:<name>` for each footer
|
|
423
|
+
block (`footerBlockCacheTags()` - routes/public.js and the docs pages), so saving one reaches cached pages. A block
|
|
424
|
+
template's [collection]/[view]/expiry shortcodes never run (those stages precede [block]), so names are all it depends on.
|
|
425
|
+
|
|
404
426
|
## Site search
|
|
405
427
|
|
|
406
428
|
Search is part of the CMS, not a plugin - `server/services/search.js` (settings, `stripMarkdown`, `scorePage`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fmtDate as u}from"./dates.js";import{matchesTag as l,tagsOf as c,tagsText as
|
|
1
|
+
import{fmtDate as u}from"./dates.js";import{matchesTag as l,tagsOf as c,tagsText as g}from"./list-tags-arrange.js";export const NAME_RE=/^[a-z0-9][a-z0-9-]*$/,FILTERS=["all","used","unused"];const i=(e,t,n=`${t}s`)=>`${e} ${e===1?t:n}`;export function formatSize(e){const t=Number(e)||0;return t<1024?`${t} B`:t<1024*1024?`${(t/1024).toFixed(t<10*1024?1:0)} KB`:`${(t/1024/1024).toFixed(1)} MB`}export function usageCount(e){return e?.usage?(e.usage.pages?.length||0)+(e.usage.views?.length||0)+(e.usage.footer?1:0):null}export function usageText(e){const t=usageCount(e);if(t===null)return"";if(!t)return"Not used on any page or view";const n=e.usage.pages?.length||0,o=e.usage.views?.length||0,s=[e.usage.footer&&"the footer of every page",n&&i(n,"page"),o&&i(o,"view")].filter(Boolean);return`Used on ${s.length>2?`${s.slice(0,-1).join(", ")} and ${s.at(-1)}`:s.join(" and ")}`}export function shortcodeFor(e){return e?.example||`[block template="${e?.name??""}" /]`}export function arrangeBlocks(e,{filter:t="all",query:n="",tag:o=""}={}){const s=String(n).trim().toLowerCase();return[...e||[]].filter(r=>t==="used"?usageCount(r)>0:t==="unused"?usageCount(r)===0:!0).filter(r=>l(r,o)).filter(r=>!s||[r.name,r.description,r.meta?.project,...c(r)].some(a=>String(a||"").toLowerCase().includes(s))).sort((r,a)=>String(r.name).localeCompare(String(a.name)))}export function blockRow(e,t=u){const n=usageCount(e);return{name:e.name,description:e.description||"",meta:[`${e.name}.html`,formatSize(e.size),e.updatedAt?`changed ${t(e.updatedAt)}`:"",usageText(e)].filter(Boolean).join(" \xB7 "),hasCss:!!e.hasCss,bundled:!!e.bundled,project:e.meta?.project||"",listTags:g(e),unused:n===0,uses:n===null?"":String(n)}}export function checkNewName(e,t=[]){const n=String(e??"").trim();return n?NAME_RE.test(n)?t.includes(n)?`"${n}" already exists.`:"":"Lowercase letters, digits and hyphens; starting with a letter or digit.":"A block needs a name."}export function copyName(e,t=[]){const n=`${e}-copy`;if(!t.includes(n))return n;for(let o=2;o<1e3;o++)if(!t.includes(`${n}-${o}`))return`${n}-${o}`;return n}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const TAB_FIELDS={general:["title","tagline","siteUrl","adminHome","spacerSize","bodyClass"],branding:["brandText","brandLogo","brandIcon","brandUrl","adminBrandTitle","adminBrandLogo","adminBrandIcon"],favicon:["favicon"],seo:["seoTitle","seoSeparator","seoDescription"],footer:["footerCopyright","footerLayout","footerBlurb","footerMotionToggle","footerMotionDefault","footerMotionControl","footerClass","footerSocialClass","socialTwitter","socialFacebook","socialInstagram","socialLinkedin","socialGithub","socialYoutube"],email:["smtpHost","smtpPort","smtpUser","smtpPass","smtpSecure","smtpFromAddress","smtpFromName"],backToTop:["bttEnabled","bttSmooth","bttLabel","bttThreshold","bttPosition","bttOffset","bttBottomOffset"],cookies:["ccEnabled","ccMessage","ccAcceptAll","ccRejectAll","ccCustomize","ccSavePrefs","ccPrivacyText","ccPrivacyUrl","ccCookieText","ccCookieUrl","ccPosition","ccLayout","ccTheme","ccShowFunctional","ccShowAnalytics","ccShowMarketing","ccVersion"],breadcrumbs:["bcEnabled","bcHomeLabel","bcPosition","bcOffsetX","bcOffsetY"],css:["css"]},SECRET_FIELDS=["smtpPass"],BREADCRUMB_POSITIONS=["TL","TR","BL","BR"],FOOTER_LAYOUTS=["auto","columns","row"];const t=(o,r="")=>o==null?r:String(o),
|
|
1
|
+
export const TAB_FIELDS={general:["title","tagline","siteUrl","adminHome","spacerSize","bodyClass"],branding:["brandText","brandLogo","brandIcon","brandUrl","adminBrandTitle","adminBrandLogo","adminBrandIcon"],favicon:["favicon"],seo:["seoTitle","seoSeparator","seoDescription"],footer:["footerCopyright","footerLayout","footerBlurb","footerBlocks","footerMotionToggle","footerMotionDefault","footerMotionControl","footerClass","footerSocialClass","socialTwitter","socialFacebook","socialInstagram","socialLinkedin","socialGithub","socialYoutube"],email:["smtpHost","smtpPort","smtpUser","smtpPass","smtpSecure","smtpFromAddress","smtpFromName"],backToTop:["bttEnabled","bttSmooth","bttLabel","bttThreshold","bttPosition","bttOffset","bttBottomOffset"],cookies:["ccEnabled","ccMessage","ccAcceptAll","ccRejectAll","ccCustomize","ccSavePrefs","ccPrivacyText","ccPrivacyUrl","ccCookieText","ccCookieUrl","ccPosition","ccLayout","ccTheme","ccShowFunctional","ccShowAnalytics","ccShowMarketing","ccVersion"],breadcrumbs:["bcEnabled","bcHomeLabel","bcPosition","bcOffsetX","bcOffsetY"],css:["css"]},SECRET_FIELDS=["smtpPass"],BREADCRUMB_POSITIONS=["TL","TR","BL","BR"];export function footerBlockNames(o){return[...new Set(String(o??"").split(/[\s,]+/).map(r=>r.trim().toLowerCase()).filter(Boolean))]}export const FOOTER_LAYOUTS=["auto","columns","row"];const t=(o,r="")=>o==null?r:String(o),s=(o,r)=>{const c=Number.parseInt(o,10);return Number.isFinite(c)?c:r};export function homeValue(o){return/^\/[A-Za-z0-9\-_/]*$/.test(o||"")&&String(o).replace(/\/+$/,"")||"/"}export function formFromSettings(o={},r=""){const c=o.backToTop||{},e=o.cookieConsent||{},i=o.breadcrumbs||{};return{title:t(o.title),tagline:t(o.tagline),siteUrl:t(o.baseUrl),adminHome:homeValue(o.adminHome),spacerSize:t(o.layoutOptions?.spacerSize??40),bodyClass:t(o.bodyClass),brandText:t(o.brand?.text),brandLogo:t(o.brand?.logo),brandIcon:t(o.brand?.icon),brandUrl:t(o.brand?.url,"/"),favicon:t(o.favicon),adminBrandTitle:t(o.adminBrand?.title),adminBrandLogo:t(o.adminBrand?.logo),adminBrandIcon:t(o.adminBrand?.icon),seoTitle:t(o.seo?.defaultTitle),seoSeparator:t(o.seo?.titleSeparator," | "),seoDescription:t(o.seo?.defaultDescription),footerCopyright:t(o.footer?.copyright),footerLayout:FOOTER_LAYOUTS.includes(o.footer?.layout)?o.footer.layout:"auto",footerBlurb:t(o.footer?.blurb),footerBlocks:Array.isArray(o.footer?.blocks)?o.footer.blocks.filter(a=>typeof a=="string").join(", "):"",footerMotionToggle:o.footer?.showMotionToggle!==!1,footerMotionDefault:["off","on"].includes(o.footer?.motionDefault)?o.footer.motionDefault:"system",footerMotionControl:o.footer?.motionControl==="checkbox"?"checkbox":"switch",footerClass:t(o.footer?.class),footerSocialClass:t(o.footer?.socialClass),socialTwitter:t(o.social?.twitter),socialFacebook:t(o.social?.facebook),socialInstagram:t(o.social?.instagram),socialLinkedin:t(o.social?.linkedin),socialGithub:t(o.social?.github),socialYoutube:t(o.social?.youtube),smtpHost:t(o.smtp?.host),smtpPort:t(o.smtp?.port||587),smtpUser:t(o.smtp?.user),smtpPass:"",smtpSecure:o.smtp?.secure===!0,smtpFromAddress:t(o.smtp?.fromAddress),smtpFromName:t(o.smtp?.fromName),bttEnabled:c.enabled!==!1,bttSmooth:c.smooth!==!1,bttLabel:t(c.label),bttThreshold:t(c.scrollThreshold??300),bttPosition:t(c.position,"bottom-right"),bttOffset:t(c.offset??32),bttBottomOffset:t(c.bottomOffset??c.offset??32),ccEnabled:e.enabled!==!1,ccMessage:t(e.message),ccAcceptAll:t(e.acceptAllText,"Accept All"),ccRejectAll:t(e.rejectAllText,"Reject All"),ccCustomize:t(e.customizeText,"Customize"),ccSavePrefs:t(e.savePreferencesText,"Save Preferences"),ccPrivacyText:t(e.privacyPolicyText,"Privacy Policy"),ccPrivacyUrl:t(e.privacyPolicyUrl),ccCookieText:t(e.cookiePolicyText,"Cookie Policy"),ccCookieUrl:t(e.cookiePolicyUrl),ccPosition:t(e.position,"bottom"),ccLayout:t(e.layout,"bar"),ccTheme:t(e.theme,"dark"),ccShowFunctional:e.showFunctional!==!1,ccShowAnalytics:e.showAnalytics!==!1,ccShowMarketing:e.showMarketing!==!1,ccVersion:t(e.consentVersion,"1.0"),bcEnabled:i.enabled===!0,bcHomeLabel:t(i.homeLabel,"Home"),bcPosition:BREADCRUMB_POSITIONS.includes(i.position)?i.position:"TL",bcOffsetX:t(i.offsetX??8),bcOffsetY:t(i.offsetY??8),css:t(r)}}export function payloadFromForm(o,r={},{adminHomeReady:c=!0}={}){const e=i=>t(i).trim();return{title:e(o.title),tagline:e(o.tagline),baseUrl:e(o.siteUrl).replace(/\/+$/,""),...c&&{adminHome:e(o.adminHome)||"/"},layoutOptions:{spacerSize:s(o.spacerSize,40)||40},bodyClass:e(o.bodyClass),brand:{...r.brand,text:e(o.brandText),logo:e(o.brandLogo),icon:e(o.brandIcon),url:e(o.brandUrl)||"/"},favicon:e(o.favicon),adminBrand:{...r.adminBrand,title:e(o.adminBrandTitle),logo:e(o.adminBrandLogo),icon:e(o.adminBrandIcon)},seo:{defaultTitle:e(o.seoTitle),titleSeparator:t(o.seoSeparator)||" | ",defaultDescription:e(o.seoDescription)},footer:{copyright:e(o.footerCopyright),layout:FOOTER_LAYOUTS.includes(o.footerLayout)?o.footerLayout:"auto",blurb:e(o.footerBlurb),blocks:footerBlockNames(o.footerBlocks),showMotionToggle:o.footerMotionToggle===!0,links:r.footer?.links||[],motionDefault:["off","on"].includes(o.footerMotionDefault)?o.footerMotionDefault:"system",motionControl:o.footerMotionControl==="checkbox"?"checkbox":"switch",class:e(o.footerClass),socialClass:e(o.footerSocialClass)},social:{twitter:e(o.socialTwitter),facebook:e(o.socialFacebook),instagram:e(o.socialInstagram),linkedin:e(o.socialLinkedin),github:e(o.socialGithub),youtube:e(o.socialYoutube)},smtp:{host:e(o.smtpHost),port:s(o.smtpPort,587)||587,user:e(o.smtpUser),pass:t(o.smtpPass),secure:o.smtpSecure===!0,fromAddress:e(o.smtpFromAddress),fromName:e(o.smtpFromName)},backToTop:{enabled:o.bttEnabled===!0,scrollThreshold:s(o.bttThreshold,300),position:t(o.bttPosition)||"bottom-right",offset:s(o.bttOffset,32),bottomOffset:s(o.bttBottomOffset,32),label:e(o.bttLabel),smooth:o.bttSmooth===!0},cookieConsent:{enabled:o.ccEnabled===!0,message:e(o.ccMessage),acceptAllText:e(o.ccAcceptAll),rejectAllText:e(o.ccRejectAll),customizeText:e(o.ccCustomize),savePreferencesText:e(o.ccSavePrefs),privacyPolicyText:e(o.ccPrivacyText),privacyPolicyUrl:e(o.ccPrivacyUrl),cookiePolicyText:e(o.ccCookieText),cookiePolicyUrl:e(o.ccCookieUrl),position:t(o.ccPosition)||"bottom",layout:t(o.ccLayout)||"bar",theme:t(o.ccTheme)||"dark",showFunctional:o.ccShowFunctional===!0,showAnalytics:o.ccShowAnalytics===!0,showMarketing:o.ccShowMarketing===!0,consentVersion:e(o.ccVersion)||"1.0"},breadcrumbs:{enabled:o.bcEnabled===!0,homeLabel:e(o.bcHomeLabel)||"Home",position:BREADCRUMB_POSITIONS.includes(o.bcPosition)?o.bcPosition:"TL",offsetX:s(o.bcOffsetX,8),offsetY:s(o.bcOffsetY,8)}}}export function siteUrlError(o){const r=t(o).trim().replace(/\/+$/,"");return r?/^https?:\/\//i.test(r)?/^https?:\/\/([a-z0-9.\-]+|\[[0-9a-f:.]+\])(:\d{1,5})?$/i.test(r)?"":"Just the address, like https://example.com - no page path after it.":"Start with https:// (or http://).":""}const n=o=>typeof o=="boolean"?o:t(o);export function snapshot(o){return JSON.stringify(Object.keys(o).sort().map(r=>[r,n(o[r])]))}export function changedTabs(o,r){const c=[];for(const[e,i]of Object.entries(TAB_FIELDS))i.some(a=>n(o[a])!==n(r[a]))&&c.push(e);return c}export function draftOf(o){return Object.fromEntries(Object.entries(o).filter(([r])=>!SECRET_FIELDS.includes(r)))}export function settingsProblems(o){const r=[];return t(o.smtpHost).trim()?t(o.smtpFromAddress).trim()||r.push({tone:"warn",tab:"email",text:"No From address - mail has no sender and many servers refuse it."}):r.push({tone:"danger",tab:"email",text:"No SMTP host - mail goes to a throwaway test inbox and reaches no one (password resets, form notifications)."}),o.ccEnabled===!0&&!t(o.ccPrivacyUrl).trim()&&r.push({tone:"warn",tab:"cookies",text:"The cookie banner is on but links to no privacy policy."}),r}export function describeCacheKey(o){const r=/^page:(.*?):m\d+:role([^:]*):/.exec(String(o||""));return r?{path:r[1]||"/",role:r[2]||"anon",page:!0}:{path:String(o||""),role:"",page:!1}}export function expiresIn(o,r=Date.now()){if(o==null)return"no expiry";const c=o-r;return c<=0?"expired":c<6e4?`${Math.round(c/1e3)}s`:c<36e5?`${Math.round(c/6e4)}m`:`${Math.round(c/36e5)}h`}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const KIND_LABEL={page:"Page",post:"Post",product:"Product",custom:"Custom item",collection:"Collection",media:"Media",menu:"Menu",category:"Category"},KIND_PLURAL={page:"Pages",post:"Posts",product:"Products",custom:"Custom items",collection:"Collections",media:"Media",menu:"Menus",category:"Categories"},KIND_ICON={page:"file-text",post:"edit-3",product:"shopping-bag",custom:"box",collection:"database",media:"image",menu:"menu",category:"tag"},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 d=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 d(e);default:return!0}}export function reportMatches(e,t){return t==="rebuild"?d(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"],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.target:"";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.count} item${e.count===1?"":"s"}`),{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: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":""].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 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}}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])=>f.test(n)&&(i==="pages"||i==="skip"))),listPages:Object.fromEntries(Object.entries(e.listPages||{}).filter(([n,i])=>f.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])=>h.test(n)&&(i==="create"||i==="skip"))),userRole:typeof e.userRole=="string"&&g.test(e.userRole)?e.userRole:"",invite:e.invite===!0}}export function optionsSnapshot(e){const t=cleanOptions(e,!0),o=Object.keys(t.choices).sort().map(a=>`${a}=${t.choices[a]}`).join(","),s=Object.keys(t.types).sort().map(a=>`${a}=${t.types[a]}`).join(","),n=Object.keys(t.listPages).sort().join(","),i=Object.keys(t.users).sort().map(a=>`${a}=${t.users[a]}`).join(",");return[t.posts,t.media,t.comments,t.menus,t.redirects,t.drafts,t.products,o,s,n,i,t.userRole,t.invite].join("|")}const f=/^[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}]:[],{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 h=/^author:(\d{1,12}|login-[\w.-]{1,60})$/,g=/^[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`].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":"Waiting",pillClass:e.uploads?"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(([p,m])=>`${m} ${(PRODUCT_TYPE_LABEL[p]||p).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,c="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.",c="warn"),{note:l,tone:c,rows:a,notes:Array.isArray(t.notes)?t.notes:[]}}const r=(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=`${r(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 ${r(t.fields?.length||0,"field")}: ${r(t.importing||0,"entry","entries")}${t.drafts?` (${t.drafts} draft${t.drafts===1?"":"s"}, not shown on the site)`:""}.`,c=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}${c}`}return{type:t.type,label:t.label||t.type,meta:[r(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}
|
|
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)`:""}.`}
|
|
@@ -191,6 +191,11 @@
|
|
|
191
191
|
<span data-help="A sentence or two under your brand in the columns layout - what the site is, in plain words. Leave it empty for none."></span></label>
|
|
192
192
|
<textarea id="stx-footer-blurb" class="form-input" rows="2" maxlength="300" placeholder="What your site is about, in a sentence." data-model="footerBlurb.value"></textarea>
|
|
193
193
|
</div>
|
|
194
|
+
<div class="stx-wide">
|
|
195
|
+
<label class="form-label" for="stx-footer-blocks">Footer blocks
|
|
196
|
+
<span data-help="Blocks (Content › Blocks) shown side by side at the top of the footer on every page, by name, in order - for example wp-footer-1, wp-footer-2. Import from WordPress puts a site's footer widget areas here. Leave it empty for none; a name that is not a block is left out."></span></label>
|
|
197
|
+
<input id="stx-footer-blocks" type="text" class="form-input" maxlength="400" placeholder="wp-footer-1, wp-footer-2" data-model="footerBlocks.value">
|
|
198
|
+
</div>
|
|
194
199
|
<div class="stx-wide stx-setting">
|
|
195
200
|
<label class="form-label" for="stx-motion">Show a “Reduce motion” switch
|
|
196
201
|
<span data-help="An accessibility switch in the footer that lets visitors turn animations off. Turning this off only hides the switch - visitors who already chose it, and anyone whose browser asks for reduced motion, still get a calmer site."></span></label>
|
|
@@ -93,6 +93,19 @@
|
|
|
93
93
|
</div>
|
|
94
94
|
</div>
|
|
95
95
|
</div>
|
|
96
|
+
<div class="wpx-option">
|
|
97
|
+
<span class="form-label">Widgets
|
|
98
|
+
<span data-help-title="Widgets" data-help="A WordPress export has no widgets in it. To bring them across, install the free "Widget Importer & Exporter" plugin in WordPress, go to Tools › Widget Importer & Exporter, press Export Widgets and upload the .wie file it gives you here (or put it in the same zip as the export). Each widget area becomes a block named wp-…; a footer area's block is also added to the footer of every page."></span></span>
|
|
99
|
+
<div class="wpx-zip">
|
|
100
|
+
<small class="wpx-note" data-bind-text="widgetsText.value"></small>
|
|
101
|
+
<div class="wpx-zip-actions">
|
|
102
|
+
<label class="btn btn-sm btn-outline wpx-zip-pick"><input type="file" class="wpx-wie-file" accept=".wie,.json,application/json" hidden>
|
|
103
|
+
<span data-bind-text="widgetsButton.value"></span></label>
|
|
104
|
+
<button type="button" class="btn btn-sm btn-ghost" hidden data-bind-hidden="!widgetsInfo.value" data-on-click="removeWidgets">Remove</button>
|
|
105
|
+
</div>
|
|
106
|
+
<small class="wpx-note" hidden data-bind-hidden="!widgetsUploadText.value" data-bind-text="widgetsUploadText.value"></small>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
96
109
|
<div class="wpx-toggles">
|
|
97
110
|
<label class="wpx-toggle"><input type="checkbox" class="form-switch" data-model="optComments.value" data-bind-disabled="opt.value.posts !== 'blog'"> Approved comments
|
|
98
111
|
<span data-help="Approved comments go to the Blog's comments, replies under the comment they answer. Spam, unapproved comments, pingbacks and trackbacks are never imported."></span></label>
|
|
@@ -155,6 +168,12 @@
|
|
|
155
168
|
</details>
|
|
156
169
|
</div>
|
|
157
170
|
|
|
171
|
+
<div class="wpx-card wpx-widgets" hidden data-bind-hidden="!widgetRows.value.length">
|
|
172
|
+
<div class="wpx-card-title"><span data-icon="layout" data-icon-size="16"></span> Widgets
|
|
173
|
+
<span data-help-title="Widgets" data-help="Each WordPress widget area becomes one block (Content › Blocks), its widgets in order under their titles. Text and custom HTML come across cleaned (no scripts, WordPress shortcodes left as text to rebuild); an image comes with the media; a navigation menu shows the menu imported from it; recent posts and categories show the Blog's own lists; a search widget becomes a link that opens the site search. Anything else (a calendar, an RSS feed, a plugin's widget) is listed to rebuild - nothing is guessed. A footer area is added to the footer of every page, after whatever is there - switch that off here, or change it later in Site Settings › Footer. Run it again and a block you edited here is left alone unless you choose Overwrite, and nothing is added to the footer twice."></span></div>
|
|
174
|
+
<div class="wpx-list wpx-widget-areas" data-each="widgetRows.value key=area"></div>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
158
177
|
<div class="wpx-card">
|
|
159
178
|
<div class="wpx-card-title">Item by item
|
|
160
179
|
<span data-help-title="Item by item" data-help="What happens to each page, post, product, custom item, collection, file and menu. New: made here. Renamed: its address is taken here, so it gets the next free one (the old address redirects to it). Update: made by an earlier import and unchanged here since - refreshed. Left as is: made by an earlier import but edited here since - not touched unless you choose Overwrite. Right-click (or ⋮, or Shift+F10) an item to skip it or change what happens. Needs rebuilding: shortcodes and page builders this site has no equivalent for - the text stays, the layout is yours to rebuild."></span>
|
|
@@ -265,7 +284,7 @@
|
|
|
265
284
|
.wpx-kebab { border: 0; background: none; cursor: pointer; font-size: var(--dm-text-lg); color: var(--dm-text-muted); border-radius: var(--dm-radius-sm); padding: 0 .3rem; }
|
|
266
285
|
.wpx-kebab:hover { color: var(--dm-primary); }
|
|
267
286
|
.wpx-type-ctl { display: flex; flex-wrap: wrap; gap: .4rem .9rem; align-items: center; margin: .3rem 0 .15rem; cursor: default; }
|
|
268
|
-
.wpx-type-row .wpx-where { font-family: inherit; font-size: var(--dm-text-sm); }
|
|
287
|
+
.wpx-type-row .wpx-where, .wpx-widget-row .wpx-where { font-family: inherit; font-size: var(--dm-text-sm); }
|
|
269
288
|
.wpx-internal-box summary { cursor: pointer; color: var(--dm-text-muted); font-size: var(--dm-text-sm); }
|
|
270
289
|
.wpx-internal { padding-left: 1.1rem; margin: .4rem 0 0; display: flex; flex-direction: column; gap: .25rem; }
|
|
271
290
|
.wpx-people-ctl { display: flex; flex-wrap: wrap; gap: .6rem 1.2rem; align-items: center; }
|
package/admin/js/views/blocks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{api as h,apiRequest as
|
|
1
|
+
import{api as h,apiRequest as Re}from"../api.js";import{addBannerAction as A}from"../lib/plugin-chrome.js";import{helpPopovers as re}from"../lib/help-popover.js";import{onceClosed as de,openMenuAt as ue,sidebarCount as qe,viewLifetime as Ue}from"../lib/tool-kit.js";import{filterByProject as He,getProjectFromHash as Oe,renderProjectFilterNotice as Pe}from"../lib/project-context.js";import{FILTERS as Ke,arrangeBlocks as We,blockRow as _e,checkNewName as ze,copyName as pe,shortcodeFor as me,usageCount as _,usageText as be}from"../lib/blocks-arrange.js";import{fmtDate as Ge}from"../lib/dates.js";import{UNTAGGED as Ye,activeNote as Je,askTagName as Ve,normaliseActive as Qe,openTagSettings as Xe,same as he,tagCounts as Ze,tagMenuItems as et,tagsOf as D}from"../lib/list-tags.js";const fe=/^#\/(blocks|projects\/[^/?#]+\/blocks)\/?(\?.*)?$/,tt="#/blocks",F="blocks.draft",z="blocks.filter",ge="blocks.tag",at=`
|
|
2
2
|
<div class="blx-row" data-name="{{name}}" tabindex="0" data-bind-class="unused && 'is-unused'">
|
|
3
3
|
<span class="blx-icon" aria-hidden="true"><span data-icon="box" data-icon-size="16"></span></span>
|
|
4
4
|
<span class="blx-main"><strong>{{name}}</strong><span data-bind-hidden="!description">{{description}}</span><small>{{meta}}</small></span>
|
|
@@ -11,13 +11,13 @@ import{api as h,apiRequest as Fe}from"../api.js";import{addBannerAction as A}fro
|
|
|
11
11
|
</span>
|
|
12
12
|
<span class="blx-uses" title="Pages and views that use it">{{uses}}</span>
|
|
13
13
|
<button type="button" class="blx-kebab" aria-label="Actions for {{name}}" title="Actions">\u22EE</button>
|
|
14
|
-
</div>`,k=l=>String(l??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function
|
|
14
|
+
</div>`,k=l=>String(l??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");function ve(l){const n=document.createElement("template");return n.innerHTML=l.trim(),n.content.firstElementChild}const i=Ue(fe);async function st(l){try{return await navigator.clipboard.writeText(l),!0}catch{const n=document.createElement("textarea");n.value=l,n.style.cssText="position:fixed;opacity:0;",document.body.appendChild(n),n.select();let f=!1;try{f=document.execCommand("copy")}catch{}return n.remove(),f}}export const blocksView={templateUrl:"/admin/js/templates/blocks.html",async onMount(l){i.reset();const n=l.get(0).querySelector(".blx")||l.get(0),f=()=>fe.test(location.hash)&&n.isConnected,w=Oe(),u=M.observable([]),G=M.observable(!0),R=M.observable("");let Y="all";try{Y=Ke.includes(S.get(z))?S.get(z):"all"}catch{}const y=M.observable(Y),J=M.observable("");i.add(M.effect(()=>{const e=y.value;try{S.set(z,e)}catch{}}));let V="";try{V=Qe(S.get(ge))}catch{}const g=M.observable(V),x=M.observable([]);i.add(M.effect(()=>{const e=g.value;try{S.set(ge,e)}catch{}}));const ye=Ge,q=M.observable([]);i.add(M.effect(()=>{q.value=We(u.value,{filter:y.value,query:J.value,tag:g.value})}));const U=M.observable([]);i.add(M.effect(()=>{U.value=q.value.map(e=>_e(e,ye))}));const H=M.computed(()=>Ze(x.value,u.value)),ke=M.computed(()=>Je(g.value,q.value.length,"blocks")),we=M.computed(()=>{const e=u.value,t=e.filter(a=>_(a)>0).length;return{all:e.length,used:t,unused:e.length-t}}),xe=M.computed(()=>u.value.length?"No blocks match.":"No blocks yet - New block in the banner makes one, or Import brings one in."),O=e=>u.peek().find(t=>t.name===e),Ee=w?`#/blocks/new?project=${encodeURIComponent(w)}`:"#/blocks/new",Q=e=>`#/blocks/edit/${encodeURIComponent(e)}`;async function P(){try{let e=await Re("/blocks?usage=1",{method:"GET"});if(!f())return;if(e=Array.isArray(e)?e:[],w){const t=e.length;e=He(e,w),Pe(n,{total:t,shown:e.length,slug:w,allHref:"#/blocks",noun:"blocks"})}u.value=e,R.value="",await Se()}catch(e){R.value=`Could not load the blocks: ${e.message||e}`}finally{G.value=!1}}async function Se(){try{x.value=(await h.listTags.list("blocks")).map(t=>t.name)}catch{}const e=g.peek();e&&e!==Ye&&!H.peek().some(t=>he(t.name,e))&&(g.value="")}async function X(e,t){try{const a=await h.listTags.changeItem("blocks",e.name,t);u.value=u.peek().map(s=>s.name===e.name?{...s,meta:{...s.meta||{},adminTags:a}}:s),a.some(s=>!x.peek().some(p=>he(p,s)))&&(x.value=[...x.peek(),...a])}catch(a){E.toast(`Tags not saved: ${a.message||a}`,{type:"error"})}}const Ce=()=>Xe({kind:"blocks",noun:"blocks",title:"Blocks settings - tags",tags:H,active:g,reload:P}),j=async()=>{qe(tt),f()&&await P()};async function Te(e){const a=_(e)||0?` It is ${k(be(e).toLowerCase())} - they will show nothing where it was.`:"";if(await E.confirm(`Delete the block "${k(e.name)}"?${a} This cannot be undone.`))try{await h.blocks.delete(e.name),E.toast(`Deleted "${e.name}".`,{type:"success"}),await j()}catch(s){E.toast(s.message||"Failed to delete block.",{type:"error"})}}async function Me(e){try{await h.blocks.exportBundle(e.name)}catch(t){E.toast(t.message||"Export failed.",{type:"error"})}}async function Ne(e){const t=me(e),a=await st(t);E.toast(a?`Copied ${t}`:`Could not copy - your browser refused. The shortcode is ${t}`,{type:a?"success":"error"})}const C=n.querySelector(".blx-import-file");async function Z(){const e=C.files?.[0];if(!e)return;C.value="";let t;try{t=JSON.parse(await e.text())}catch{E.toast("Not a valid .dmblock.json file (could not parse JSON).",{type:"error"});return}if(!t||typeof t!="object"||!t.name||typeof t.html!="string"){E.toast("Bundle is missing a name or html field.",{type:"error"});return}try{const a=await h.blocks.importBundle(t);E.toast(`Imported "${a.name}".`,{type:"success"}),await j()}catch(a){if(a.code==="CONFLICT"){if(!await E.confirm(`A block named "${k(a.name)}" already exists. Overwrite it with the imported version?`)){E.toast("Import cancelled.",{type:"info"});return}try{const s=await h.blocks.importBundle(t,{overwrite:!0});E.toast(`Overwrote "${s.name}".`,{type:"success"}),await j()}catch(s){E.toast(s.message||"Import failed.",{type:"error"})}return}E.toast(a.message||"Import failed.",{type:"error"})}}C.addEventListener("change",Z),i.add(()=>C.removeEventListener("change",Z));const T=new Set;let v=null,ee=!1;function Ie(e){const t=E.slideover({title:`Where ${k(e.name)} is used`,size:"sm",position:"right"}),a=ve(`
|
|
15
15
|
<div class="blx-form">
|
|
16
16
|
<p class="blx-note blx-where-sum"></p>
|
|
17
17
|
<ul class="blx-where"></ul>
|
|
18
18
|
<div><div class="form-label">Shortcode</div><div class="blx-code"></div>
|
|
19
19
|
<small class="blx-hint">Right-click the block in the list and Copy shortcode to paste it into a page.</small></div>
|
|
20
|
-
</div>`);a.querySelector(".blx-where-sum").textContent=
|
|
20
|
+
</div>`);a.querySelector(".blx-where-sum").textContent=be(e)+".",a.querySelector(".blx-code").textContent=me(e);const s=a.querySelector(".blx-where"),p=(r,B,$)=>{const L=document.createElement("li"),o=document.createElement("a");o.href=r,o.textContent=B,o.addEventListener("click",()=>t.close());const m=document.createElement("small");m.textContent=$,L.append(o,m),s.appendChild(L)};e.usage?.footer&&p("#/settings","Site Settings \u203A Footer","footer");for(const r of e.usage?.pages||[])p(`#/pages/edit${r}`,r,"page");for(const r of e.usage?.views||[])p(`#/views/edit/${encodeURIComponent(r)}`,r,"view");t.element.appendChild(a),T.add(t),de(t,()=>T.delete(t)),t.open()}function te(e,t=null){const a=()=>u.peek().map(d=>d.name),s=t?.name??pe(e.name,a()),p=M.observable(s),r=M.observable(!1),B=M.computed(()=>ze(p.value,u.value.map(d=>d.name))),$={name:p,saving:r,err:B,cannot:M.computed(()=>r.value||!!B.value)},L=()=>p.peek()!==pe(e.name,a())||!!t,o=E.slideover({title:`Duplicate ${k(e.name)}`,size:"sm",position:"right"}),m=ve(`
|
|
21
21
|
<div class="blx-form">
|
|
22
22
|
<div>
|
|
23
23
|
<label class="form-label" for="blx-dup-name">Name of the copy
|
|
@@ -32,4 +32,4 @@ import{api as h,apiRequest as Fe}from"../api.js";import{addBannerAction as A}fro
|
|
|
32
32
|
<button type="button" class="btn btn-sm btn-primary" data-on-click="save" data-bind-disabled="cannot.value">
|
|
33
33
|
<span data-icon="copy" data-icon-size="13"></span> Duplicate</button>
|
|
34
34
|
</div>
|
|
35
|
-
</div>`);let K=!1;async function
|
|
35
|
+
</div>`);let K=!1;async function le(){if($.cannot.peek())return;r.value=!0;const d=p.peek().trim();try{const c=await h.blocks.get(e.name);await h.blocks.put(d,{content:c.content,css:c.css||"",...(c.meta?.project||D(c).length)&&{meta:{...c.meta?.project&&{project:c.meta.project},...D(c).length&&{adminTags:D(c)}}}}),E.toast(`Made "${d}".`,{type:"success"}),K=!0,o.close(),await j()}catch(c){E.toast(`Not duplicated: ${c.message||c}`,{type:"error"})}finally{r.value=!1}}o.element.appendChild(m);const $e=re(m),Ae=M.applyBindings($,m,{methods:{save:le,cancel:()=>o.close()}}),De=d=>{d.key==="Enter"&&d.target.matches("input")&&(d.preventDefault(),le())};m.addEventListener("keydown",De);const ce={dirty:L,values:()=>({name:p.peek()}),source:e.name};v=ce,T.add(o),de(o,()=>{Ae.dispose(),$e(),T.delete(o),v===ce&&(v=null)});const Fe=o.close.bind(o);let W=!1;o.close=async d=>{if(!K&&!ee&&d!==!0&&L()){if(W)return;W=!0;const c=await E.confirm("Discard the name you typed?");if(W=!1,!c)return}return K=!0,Fe()},I.scan(m),o.open();const ie=m.querySelector("#blx-dup-name");ie?.focus(),ie?.select()}function Le(e){const t=O(e);if(!t)return[];const a=_(t)||0;return[{header:t.name},{label:"Edit",icon:"edit",action:()=>{location.hash=Q(t.name)}},{label:"Copy shortcode",icon:"copy",action:()=>Ne(t)},{label:a?`Where it is used (${a})\u2026`:"Where it is used\u2026",icon:"link",action:()=>Ie(t)},{type:"divider"},...et({all:H.peek().map(s=>s.name),current:D(t),change:s=>X(t,s),newTag:async()=>{const s=await Ve();s&&await X(t,{add:[s]})}}),{type:"divider"},{label:"Duplicate\u2026",icon:"layers",action:()=>te(t)},{label:"Export (.dmblock.json)",icon:"download",action:()=>Me(t)},{type:"divider"},{label:"Delete\u2026",icon:"trash",danger:!0,action:()=>Te(t)}]}const b=n.querySelector(".blx-list");b.innerHTML=at;const je=M.applyBindings({blocks:u,loading:G,loadError:R,filter:y,query:J,rows:U,counts:we,emptyText:xe,tagNote:ke},n,{methods:{showAllTags:()=>{g.value=""},showAll:()=>{y.value="all"},showUsed:()=>{y.value="used"},showUnused:()=>{y.value="unused"}}});i.add(()=>je.dispose()),i.add(re(n,{position:"bottom"})),i.add(M.effect(()=>{U.value,requestAnimationFrame(()=>{b.isConnected&&I.scan(b)})}));const Be=E.contextMenu(b,{cursor:"glyph",match:".blx-row",inherit:!1,density:"compact",items:e=>Le(e.dataset.name)});i.add(()=>Be?.destroy?.());const ae=e=>{O(e)&&(location.hash=Q(e))},se=e=>{const t=e.target.closest(".blx-row");if(!t)return;const a=e.target.closest(".blx-kebab");if(a){ue(a);return}ae(t.dataset.name)},ne=e=>{const t=e.target.closest(".blx-row");t&&(e.key==="F10"&&e.shiftKey?(e.preventDefault(),ue(t)):e.key==="Enter"&&e.target===t&&ae(t.dataset.name))};b.addEventListener("click",se),b.addEventListener("keydown",ne),i.add(()=>{b.removeEventListener("click",se),b.removeEventListener("keydown",ne)}),A(l,{id:"blocks-settings",icon:"settings",title:"Blocks settings - tags",onClick:Ce}),A(l,{id:"blocks-guide",icon:"book",title:"Blocks guide",onClick:()=>{location.hash="#/docs/usage/blocks"}}),A(l,{id:"blocks-import",icon:"upload",title:"Import a .dmblock.json bundle",onClick:()=>C.click()}),A(l,{id:"blocks-new",icon:"plus",title:"New block",onClick:()=>{location.hash=Ee}});const oe=e=>{v?.dirty()&&(e.preventDefault(),e.returnValue="")};if(window.addEventListener("beforeunload",oe),i.add(()=>window.removeEventListener("beforeunload",oe)),i.add(()=>{if(v?.dirty())try{S.set(F,{source:v.source,values:v.values(),at:new Date().toISOString()})}catch{}ee=!0,[...T].forEach(e=>{try{e.close(!0)}catch{}})}),I.scan(n),await P(),!f())return;let N=null;try{N=S.get(F)}catch{}if(N?.values&&N.source){try{S.remove?S.remove(F):S.set(F,null)}catch{}const e=O(N.source);e&&await E.confirm(`You were duplicating "${k(e.name)}" and left it unfinished. Pick up where you left off?`)&&f()&&te(e,N.values)}}};
|