From d51fa3806cfc7eca8376906765e0be9f5e6c6cb9 Mon Sep 17 00:00:00 2001 From: roo2 Date: Wed, 3 Feb 2021 21:54:30 +1000 Subject: [PATCH 01/13] move page template modal into its own package --- packages/page-template-modal/.eslintrc.js | 8 + packages/page-template-modal/README.md | 8 + packages/page-template-modal/package.json | 59 ++ .../src/components/block-iframe-preview.js | 259 +++++++++ .../src/components/block-preview.js | 36 ++ .../src/components/page-template-modal.js | 503 ++++++++++++++++++ .../components/template-selector-control.js | 77 +++ .../src/components/template-selector-item.js | 80 +++ .../components/template-selector-preview.js | 33 ++ packages/page-template-modal/src/index.js | 84 +++ .../styles/starter-page-templates-editor.scss | 484 +++++++++++++++++ .../src/utils/contains-missing-block.js | 28 + .../src/utils/ensure-assets.js | 219 ++++++++ .../src/utils/map-blocks-recursively.js | 34 ++ .../src/utils/replace-placeholders.js | 32 ++ .../page-template-modal/src/utils/tracking.js | 77 +++ 16 files changed, 2021 insertions(+) create mode 100644 packages/page-template-modal/.eslintrc.js create mode 100644 packages/page-template-modal/README.md create mode 100644 packages/page-template-modal/package.json create mode 100644 packages/page-template-modal/src/components/block-iframe-preview.js create mode 100644 packages/page-template-modal/src/components/block-preview.js create mode 100644 packages/page-template-modal/src/components/page-template-modal.js create mode 100644 packages/page-template-modal/src/components/template-selector-control.js create mode 100644 packages/page-template-modal/src/components/template-selector-item.js create mode 100644 packages/page-template-modal/src/components/template-selector-preview.js create mode 100644 packages/page-template-modal/src/index.js create mode 100644 packages/page-template-modal/src/styles/starter-page-templates-editor.scss create mode 100644 packages/page-template-modal/src/utils/contains-missing-block.js create mode 100644 packages/page-template-modal/src/utils/ensure-assets.js create mode 100644 packages/page-template-modal/src/utils/map-blocks-recursively.js create mode 100644 packages/page-template-modal/src/utils/replace-placeholders.js create mode 100644 packages/page-template-modal/src/utils/tracking.js diff --git a/packages/page-template-modal/.eslintrc.js b/packages/page-template-modal/.eslintrc.js new file mode 100644 index 0000000000000..cd9efe6ade839 --- /dev/null +++ b/packages/page-template-modal/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + rules: { + 'react/react-in-jsx-scope': 0, + // page-template-modal renders in a Gutenberg environment and should + // conform to those naming conventions instead of Calypso's. + 'wpcalypso/jsx-classname-namespace': 0, + }, +}; diff --git a/packages/page-template-modal/README.md b/packages/page-template-modal/README.md new file mode 100644 index 0000000000000..0c326a4bb6db0 --- /dev/null +++ b/packages/page-template-modal/README.md @@ -0,0 +1,8 @@ +# Page Template Modal + +A modal for choosing a starting template for a new page, extracted from the editing toolkit + +## Development Workflow + +This package is developed as part of the Calypso monorepo. Run `yarn` +in the root of the repository to get the required `devDependencies`. \ No newline at end of file diff --git a/packages/page-template-modal/package.json b/packages/page-template-modal/package.json new file mode 100644 index 0000000000000..c388c2f8369fc --- /dev/null +++ b/packages/page-template-modal/package.json @@ -0,0 +1,59 @@ +{ + "name": "@automattic/page-template-modal", + "version": "1.0.0", + "description": "Automattic Page Template Modal", + "homepage": "https://github.com/Automattic/wp-calypso", + "license": "GPL-2.0-or-later", + "author": "Automattic Inc.", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "calypso:src": "src/index.js", + "sideEffects": [ + "*.css", + "*.scss" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/Automattic/wp-calypso.git", + "directory": "packages/page-template-modal" + }, + "publishConfig": { + "access": "public" + }, + "bugs": { + "url": "https://github.com/Automattic/wp-calypso/issues" + }, + "files": [ + "dist", + "src" + ], + "dependencies": { + "@wordpress/nux": "^3.24.1", + "@wordpress/i18n": "*", + "@wordpress/compose": "*", + "@wordpress/components": "*", + "@wordpress/data": "*", + "@wordpress/element": "*", + "@wordpress/blocks": "*", + "@wordpress/hooks": "*", + "@wordpress/api-fetch": "*", + "@wordpress/url": "*", + "lodash": "*", + "classnames": "^2.2.6", + "@wordpress/block-editor": "*", + "@wordpress/editor": "*" + }, + "devDependencies": { + "react": "^16.12.0", + "react-dom": "^16.12.0", + "react-test-renderer": "^16.12.0" + }, + "peerDependencies": { + "react": "^16.8" + }, + "scripts": { + "clean": "npx rimraf dist", + "build": "transpile", + "prepack": "yarn run clean && yarn run build" + } +} diff --git a/packages/page-template-modal/src/components/block-iframe-preview.js b/packages/page-template-modal/src/components/block-iframe-preview.js new file mode 100644 index 0000000000000..7a0729a1a7d7f --- /dev/null +++ b/packages/page-template-modal/src/components/block-iframe-preview.js @@ -0,0 +1,259 @@ +/** + * External dependencies + */ +import { each, filter, get, castArray, debounce, noop } from 'lodash'; +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + createPortal, + useRef, + useEffect, + useState, + useMemo, + useReducer, + useLayoutEffect, + useCallback, +} from '@wordpress/element'; +import { withSelect } from '@wordpress/data'; +import { compose, withSafeTimeout } from '@wordpress/compose'; + +import { __ } from '@wordpress/i18n'; + +import CustomBlockPreview from './block-preview'; + +// Debounce time applied to the on resize window event. +const DEBOUNCE_TIMEOUT = 300; + +/** + * Copies the styles from the provided src document + * to the given iFrame head and body DOM references. + * + * @param {object} srcDocument the src document from which to copy the + * `link` and `style` Nodes from the `head` and `body` + * @param {object} targetiFrameDocument the target iframe's + * `contentDocument` where the `link` and `style` Nodes from the `head` and + * `body` will be copied + */ +const copyStylesToIframe = ( srcDocument, targetiFrameDocument ) => { + const styleNodes = [ 'link', 'style' ]; + + // See https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment + const targetDOMFragment = { + head: document.createDocumentFragment(), // eslint-disable-line no-undef + body: document.createDocumentFragment(), // eslint-disable-line no-undef + }; + + each( Object.keys( targetDOMFragment ), ( domReference ) => { + return each( + filter( srcDocument[ domReference ].children, ( { localName } ) => + // Only return specific style-related Nodes + styleNodes.includes( localName ) + ), + ( targetNode ) => { + // Clone the original node and append to the appropriate Fragement + const deep = true; + targetDOMFragment[ domReference ].appendChild( targetNode.cloneNode( deep ) ); + } + ); + } ); + + // Consolidate updates to iframe DOM + targetiFrameDocument.head.appendChild( targetDOMFragment.head ); + targetiFrameDocument.body.appendChild( targetDOMFragment.body ); +}; + +/** + * Performs a blocks preview using an iFrame. + * + * @param {object} props component's props + * @param {object} props.className CSS class to apply to component + * @param {string} props.bodyClassName CSS class to apply to the iframe's `` tag + * @param {number} props.viewportWidth pixel width of the viewable size of the preview + * @param {Array} props.blocks array of Gutenberg Block objects + * @param {object} props.settings block Editor settings object + * @param {Function} props.setTimeout safe version of window.setTimeout via `withSafeTimeout` + * @param {string} props.title Template Title - see #39831 for details. + */ +const BlockFramePreview = ( { + className = 'block-iframe-preview', + bodyClassName = 'block-iframe-preview-body', + viewportWidth, + blocks, + settings, + setTimeout = noop, + title, +} ) => { + const frameContainerRef = useRef(); + const iframeRef = useRef(); + + // Set the initial scale factor. + const [ style, setStyle ] = useState( { + transform: `scale( 1 )`, + } ); + + // Rendering blocks list. + const renderedBlocks = useMemo( () => castArray( blocks ), [ blocks ] ); + const [ recomputeBlockListKey, triggerRecomputeBlockList ] = useReducer( + ( state ) => state + 1, + 0 + ); + useLayoutEffect( triggerRecomputeBlockList, [ blocks ] ); + + /** + * This function re scales the viewport depending on + * the wrapper and the iframe width. + */ + const rescale = useCallback( () => { + const parentNode = get( frameContainerRef, [ 'current', 'parentNode' ] ); + if ( ! parentNode ) { + return; + } + + // Scaling iFrame. + const width = viewportWidth || frameContainerRef.current.offsetWidth; + const scale = parentNode.offsetWidth / viewportWidth; + const height = parentNode.offsetHeight / scale; + + setStyle( { + width, + height, + transform: `scale( ${ scale } )`, + } ); + }, [ viewportWidth ] ); + + /* + * Temporarily manually set the PostTitle from DOM. + * It isn't currently possible to manually force the `` component + * to render a title provided as a prop. A Core PR will rectify this (see below). + * Until then we use direct DOM manipulation to set the post title. + * + * See: https://github.com/WordPress/gutenberg/pull/20609/ + */ + useEffect( () => { + if ( ! title ) return; + + const iframeBody = get( iframeRef, [ 'current', 'contentDocument', 'body' ] ); + if ( ! iframeBody ) { + return; + } + + const templateTitle = iframeBody.querySelector( + '.editor-post-title .editor-post-title__input' + ); + + if ( ! templateTitle ) { + return; + } + + templateTitle.value = title; + }, [ recomputeBlockListKey ] ); + + // Populate iFrame styles. + useEffect( () => { + setTimeout( () => { + copyStylesToIframe( window.document, iframeRef.current.contentDocument ); + iframeRef.current.contentDocument.body.classList.add( + bodyClassName, + 'editor-styles-wrapper', + 'block-editor__container' + ); + /* + * Temporarly override height of the Post Title. + * Post Title component doesn't resize correctly, + * this quick CSS fix overrides the height to be auto + * A Core PR will rectify this (see below). + * + * See: https://github.com/WordPress/gutenberg/pull/20609/ + */ + iframeRef.current.contentDocument.head.innerHTML += + ''; + + // Prevent links and buttons from being clicked. This is applied within + // the iframe, because if we targeted the iframe itself it would prevent + // scrolling the iframe in Firefox. + iframeRef.current.contentDocument.head.innerHTML += + ''; + + rescale(); + }, 0 ); + }, [ setTimeout, bodyClassName, rescale ] ); + + // Scroll the preview to the top when the blocks change. + useEffect( () => { + const body = get( iframeRef, [ 'current', 'contentDocument', 'body' ] ); + if ( ! body ) { + return; + } + + // scroll to top when blocks changes. + body.scrollTop = 0; + }, [ recomputeBlockListKey ] ); + + // Handling windows resize event. + useEffect( () => { + const refreshPreview = debounce( rescale, DEBOUNCE_TIMEOUT ); + window.addEventListener( 'resize', refreshPreview ); + + return () => { + window.removeEventListener( 'resize', refreshPreview ); + }; + }, [ rescale ] ); + + // Handle wp-admin specific `wp-collapse-menu` event to refresh the preview on sidebar toggle. + useEffect( () => { + if ( window.jQuery ) { + window.jQuery( window.document ).on( 'wp-collapse-menu', rescale ); + } + return () => { + if ( window.jQuery ) { + window.jQuery( window.document ).off( 'wp-collapse-menu', rescale ); + } + }; + }, [ rescale ] ); + + /* eslint-disable wpcalypso/jsx-classname-namespace */ + return ( +
+ +
+ ); + /* eslint-enable wpcalypso/jsx-classname-namespace */ +}; + +export default compose( + withSafeTimeout, + withSelect( ( select ) => { + const blockEditorStore = select( 'core/block-editor' ); + return { + settings: blockEditorStore ? blockEditorStore.getSettings() : {}, + }; + } ) +)( BlockFramePreview ); diff --git a/packages/page-template-modal/src/components/block-preview.js b/packages/page-template-modal/src/components/block-preview.js new file mode 100644 index 0000000000000..438fd16ca6777 --- /dev/null +++ b/packages/page-template-modal/src/components/block-preview.js @@ -0,0 +1,36 @@ +/** + * External dependencies + */ + +/** + * Internal dependencies + */ + +/** + * WordPress dependencies + */ +import { BlockEditorProvider, BlockList } from '@wordpress/block-editor'; +import { Disabled } from '@wordpress/components'; +import { PostTitle } from '@wordpress/editor'; + +// Exists as a pass through component to simplify automatted testing of +// components which need to `BlockEditorProvider`. Setting up JSDom to handle +// and mock the entire Block Editor isn't useful and is difficult for testing. +// Therefore this component exists to simplify mocking out the Block Editor +// when under test conditions. +export default function ( { blocks, settings, hidePageTitle, recomputeBlockListKey } ) { + /* eslint-disable wpcalypso/jsx-classname-namespace */ + return ( + + + { ! hidePageTitle && ( +
+ +
+ ) } + +
+
+ ); + /* eslint-enable wpcalypso/jsx-classname-namespace */ +} diff --git a/packages/page-template-modal/src/components/page-template-modal.js b/packages/page-template-modal/src/components/page-template-modal.js new file mode 100644 index 0000000000000..29622f2a10f5a --- /dev/null +++ b/packages/page-template-modal/src/components/page-template-modal.js @@ -0,0 +1,503 @@ +/** + * External dependencies + */ +import { find, isEmpty, reduce, get, keyBy, mapValues, memoize, omit } from 'lodash'; +import { __, sprintf } from '@wordpress/i18n'; +import classnames from 'classnames'; +import { Button, Modal, Spinner, IconButton } from '@wordpress/components'; +import { Component } from '@wordpress/element'; +import { parse as parseBlocks } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import TemplateSelectorControl from './template-selector-control'; +import TemplateSelectorPreview from './template-selector-preview'; +import { trackDismiss, trackSelection, trackView } from '../utils/tracking'; +import replacePlaceholders from '../utils/replace-placeholders'; +import ensureAssets from '../utils/ensure-assets'; +import mapBlocksRecursively from '../utils/map-blocks-recursively'; +import containsMissingBlock from '../utils/contains-missing-block'; + +export default class PageTemplateModal extends Component { + state = { + isLoading: false, + previewedTemplate: null, + error: null, + }; + + // Extract titles for faster lookup. + getTitlesByTemplateSlugs = memoize( ( templates ) => + mapValues( keyBy( templates, 'name' ), 'title' ) + ); + + // Parse templates blocks and memoize them. + getBlocksByTemplateSlugs = memoize( ( templates ) => { + const blocksByTemplateSlugs = reduce( + templates, + ( prev, { name, html } ) => { + prev[ name ] = html + ? parseBlocks( replacePlaceholders( html, this.props.siteInformation ) ) + : []; + return prev; + }, + {} + ); + + // Remove templates that include a missing block + return this.filterTemplatesWithMissingBlocks( blocksByTemplateSlugs ); + } ); + + filterTemplatesWithMissingBlocks( templates ) { + return reduce( + templates, + ( acc, templateBlocks, name ) => { + // Does the template contain any missing blocks? + const templateHasMissingBlocks = containsMissingBlock( templateBlocks ); + + // Only retain the template in the collection if: + // 1. It does not contain any missing blocks + // 2. There are no blocks at all (likely the "blank" template placeholder) + if ( ! templateHasMissingBlocks || ! templateBlocks.length ) { + acc[ name ] = templateBlocks; + } + + return acc; + }, + {} + ); + } + + getBlocksForPreview = memoize( ( previewedTemplate ) => { + const blocks = this.getBlocksByTemplateSlug( previewedTemplate ); + + // Modify the existing blocks returning new block object references. + return mapBlocksRecursively( blocks, function modifyBlocksForPreview( block ) { + // `jetpack/contact-form` has a placeholder to configure form settings + // we need to disable this to show the full form in the preview + if ( + 'jetpack/contact-form' === block.name && + undefined !== block.attributes.hasFormSettingsSet + ) { + block.attributes.hasFormSettingsSet = true; + } + + return block; + } ); + } ); + + getBlocksForSelection = ( selectedTemplate ) => { + const blocks = this.getBlocksByTemplateSlug( selectedTemplate ); + // Modify the existing blocks returning new block object references. + return mapBlocksRecursively( blocks, function modifyBlocksForSelection( block ) { + // Ensure that core/button doesn't link to external template site + if ( 'core/button' === block.name && undefined !== block.attributes.url ) { + block.attributes.url = '#'; + } + + return block; + } ); + }; + + static getDerivedStateFromProps( props, state ) { + // The only time `state.previewedTemplate` isn't set is before `templates` + // are loaded. As soon as we have our `templates`, we set it using + // `this.getDefaultSelectedTemplate`. Afterwards, the user can select a + // different template, but can never un-select it. + // This makes it a reliable indicator for whether the modal has just been launched. + // It's also possible that `templates` are present during initial mount, in which + // case this will be called before `componentDidMount`, which is also fine. + if ( ! state.previewedTemplate && ! isEmpty( props.templates ) ) { + // Show the modal, and select the first template automatically. + return { + previewedTemplate: PageTemplateModal.getDefaultSelectedTemplate( props ), + }; + } + return null; + } + + componentDidMount() { + if ( this.props.isOpen ) { + this.trackCurrentView(); + } + } + + componentDidUpdate( prevProps ) { + // Only track when the modal is first displayed + // and if it didn't already happen during componentDidMount. + if ( ! prevProps.isOpen && this.props.isOpen ) { + this.trackCurrentView(); + } + + // Disable welcome guide right away as it collides with the modal window. + if ( this.props.isWelcomeGuideActive || this.props.areTipsEnabled ) { + this.props.hideWelcomeGuide(); + } + } + + trackCurrentView() { + trackView( 'add-page' ); + } + + static getDefaultSelectedTemplate = ( props ) => { + const blankTemplate = get( props.templates, [ 0, 'name' ] ); + const previouslyChosenTemplate = props._starter_page_template; + + // Usually the "new page" case + if ( ! props.isFrontPage && ! previouslyChosenTemplate ) { + return blankTemplate; + } + + // if the page isn't new, select "Current" as the default template + return 'current'; + }; + + setTemplate = ( name ) => { + // Track selection and mark post as using a template in its postmeta. + trackSelection( name ); + this.props.saveTemplateChoice( name ); + + // Skip setting template if user selects current layout + if ( 'current' === name ) { + this.props.setOpenState( false ); + return; + } + + // Check to see if this is a blank template selection + // and reset the template if so. + if ( 'blank' === name ) { + this.props.insertTemplate( '', [] ); + this.props.setOpenState( false ); + return; + } + + const isHomepageTemplate = find( this.props.templates, { name, category: 'home' } ); + + // Load content. + const blocks = this.getBlocksForSelection( name ); + + // Only overwrite the page title if the template is not one of the Homepage Layouts + const title = isHomepageTemplate ? null : this.getTitleByTemplateSlug( name ); + + // Skip inserting if this is not a blank template + // and there's nothing to insert. + if ( ! blocks || ! blocks.length ) { + this.props.setOpenState( false ); + return; + } + + // Show loading state. + this.setState( { + error: null, + isLoading: true, + } ); + + // Make sure all blocks use local assets before inserting. + this.maybePrefetchAssets( blocks ) + .then( ( blocksWithAssets ) => { + this.setState( { isLoading: false } ); + // Don't insert anything if the user clicked Cancel/Close + // before we loaded everything. + if ( ! this.props.isOpen ) { + return; + } + + this.props.insertTemplate( title, blocksWithAssets ); + this.props.setOpenState( false ); + } ) + .catch( ( error ) => { + this.setState( { + isLoading: false, + error, + } ); + } ); + }; + + maybePrefetchAssets = ( blocks ) => { + return this.props.shouldPrefetchAssets ? ensureAssets( blocks ) : Promise.resolve( blocks ); + }; + + handleConfirmation = ( name ) => { + if ( typeof name !== 'string' ) { + name = this.state.previewedTemplate; + } + + this.setTemplate( name ); + }; + + previewTemplate = ( name ) => { + this.setState( { previewedTemplate: name } ); + + /** + * Determines (based on whether the large preview is able to be visible at the + * current breakpoint) whether or not the Template selection UI interaction model + * should be select _and_ confirm or simply a single "tap to confirm". + */ + const largeTplPreviewVisible = window.matchMedia( '(min-width: 660px)' ).matches; + // Confirm the template when large preview isn't visible + if ( ! largeTplPreviewVisible ) { + this.handleConfirmation( name ); + } + }; + + closeModal = ( event ) => { + // Check to see if the Blur event occurred on the buttons inside of the Modal. + // If it did then we don't want to dismiss the Modal for this type of Blur. + if ( event.target.matches( 'button.template-selector-item__label' ) ) { + return false; + } + + trackDismiss(); + + // Try if we have specific URL to go back to, otherwise go to the page list. + const calypsoifyCloseUrl = get( window, [ 'calypsoifyGutenberg', 'closeUrl' ] ); + window.top.location = calypsoifyCloseUrl || 'edit.php?post_type=page'; + }; + + getBlocksByTemplateSlug( name ) { + if ( name === 'current' ) { + return this.props.currentBlocks; + } + return get( this.getBlocksByTemplateSlugs( this.props.templates ), [ name ], [] ); + } + + getTitleByTemplateSlug( name ) { + return get( this.getTitlesByTemplateSlugs( this.props.templates ), [ name ], '' ); + } + + getTemplateGroups = () => { + if ( ! this.props.templates.length ) { + return null; + } + + const templateGroups = {}; + for ( const template of this.props.templates ) { + for ( const key in template.categories ) { + if ( ! ( key in templateGroups ) ) { + templateGroups[ key ] = template.categories[ key ]; + } + } + } + + return this.sortGroupsNames( templateGroups ); + }; + + sortGroupsNames = ( groups ) => { + return Object.keys( groups ) + .sort() + .reduce( ( result, key ) => { + result[ key ] = groups[ key ]; + return result; + }, {} ); + }; + + getTemplatesForGroup = ( groupName ) => { + if ( ! this.props.templates.length ) { + return null; + } + + if ( 'blank' === groupName ) { + return [ { name: 'blank', title: 'Blank', html: '' } ]; + } + + if ( 'current' === groupName && '' !== this.props._starter_page_template ) { + for ( const template of this.props.templates ) { + if ( this.props._starter_page_template === template.name ) { + return [ template ]; + } + } + } + + const templates = []; + for ( const template of this.props.templates ) { + for ( const key in template.categories ) { + if ( key === groupName ) { + templates.push( template ); + } + } + } + + return templates; + }; + + renderTemplateGroups = () => { + const unfilteredGroups = this.getTemplateGroups(); + const groups = ! this.props.isFrontPage + ? unfilteredGroups + : omit( unfilteredGroups, 'home-page' ); + + if ( ! groups ) { + return null; + } + + const currentGroup = + 'blank' !== this.props._starter_page_template + ? this.renderTemplateGroup( 'current', __( 'Current', 'full-site-editing' ) ) + : null; + + const blankGroup = this.renderTemplateGroup( 'blank', __( 'Blank', 'full-site-editing' ) ); + + const homePageGroup = this.props.isFrontPage + ? this.renderTemplateGroup( 'home-page', __( 'Home Page', 'full-site-editing' ) ) + : null; + + const renderedGroups = []; + for ( const key in groups ) { + renderedGroups.push( this.renderTemplateGroup( key, groups[ key ].title ) ); + } + + return ( + <> + { currentGroup } + { blankGroup } + { homePageGroup } + { renderedGroups } + + ); + }; + + renderTemplateGroup = ( groupName, groupTitle ) => { + const templates = this.getTemplatesForGroup( groupName ); + + if ( ! templates.length ) { + return null; + } + + return this.renderTemplatesList( templates, groupName, groupTitle ); + }; + + renderTemplatesList = ( templatesList, groupName, groupTitle ) => { + if ( ! templatesList.length ) { + return null; + } + + const isCurrentPreview = templatesList[ 0 ]?.name === 'current'; + + const blocksByTemplateSlug = isCurrentPreview + ? { current: this.props.currentBlocks } + : // The raw `templates` prop is not filtered to remove Templates that + // contain missing Blocks. Therefore we compare with the keys of the + // filtered templates from `getBlocksByTemplateSlugs()` and filter this + // list to match. This ensures that the list of Template thumbnails is + // filtered so that it does not include Templates that have missing Blocks. + this.getBlocksByTemplateSlugs( this.props.templates ); + + const templatesWithoutMissingBlocks = Object.keys( blocksByTemplateSlug ); + + const filterOutTemplatesWithMissingBlocks = ( templatesToFilter, filterIn ) => { + return templatesToFilter.filter( ( template ) => filterIn.includes( template.name ) ); + }; + + const filteredTemplatesList = filterOutTemplatesWithMissingBlocks( + templatesList, + templatesWithoutMissingBlocks + ); + + if ( ! filteredTemplatesList.length ) { + return null; + } + + // Skip rendering current preview if there is no page content. + if ( isCurrentPreview && ! blocksByTemplateSlug.current?.length ) { + return null; + } + + return ( +
+ { groupTitle } + + +
+ ); + }; + + render() { + const { previewedTemplate, isLoading } = this.state; + const { hidePageTitle, isOpen, currentBlocks } = this.props; + + if ( ! isOpen ) { + return null; + } + + // Sometimes currentBlocks is not loaded before getBlocksForPreview is called + // getBlocksForPreview memoizes the function call which causes it to always + // call it with an empty array. We delete the the cache for the function + // to allow it to memoize the loaded currentBlocks. + const currentBlocksPreviewCache = this.getBlocksForPreview.cache.get( 'current' ); + if ( + currentBlocksPreviewCache && + currentBlocks && + currentBlocksPreviewCache.length !== currentBlocks.length + ) { + this.getBlocksForPreview.cache.delete( 'current' ); + } + + return ( + + + +
+ { isLoading ? ( +
+ + { __( 'Adding layout…', 'full-site-editing' ) } +
+ ) : ( + <> +
{ this.renderTemplateGroups() }
+ + + ) } +
+
+ +
+
+ ); + } +} diff --git a/packages/page-template-modal/src/components/template-selector-control.js b/packages/page-template-modal/src/components/template-selector-control.js new file mode 100644 index 0000000000000..757c43b9580fc --- /dev/null +++ b/packages/page-template-modal/src/components/template-selector-control.js @@ -0,0 +1,77 @@ +/** + * External dependencies + */ +import { isEmpty, isArray, noop, map } from 'lodash'; +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { withInstanceId, compose } from '@wordpress/compose'; +import { BaseControl } from '@wordpress/components'; +import { memo } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import TemplateSelectorItem from './template-selector-item'; +import replacePlaceholders from '../utils/replace-placeholders'; + +export const TemplateSelectorControl = ( { + label, + legendLabel, + className, + help, + instanceId, + templates = [], + blocksByTemplates = {}, + theme = 'maywood', + locale = 'en', + onTemplateSelect = noop, + siteInformation = {}, + selectedTemplate, +} ) => { + if ( isEmpty( templates ) || ! isArray( templates ) ) { + return null; + } + + const id = `template-selector-control-${ instanceId }`; + + return ( + +
    + { map( templates, ( { ID, name, title, description } ) => ( +
  • + +
  • + ) ) } +
+
+ ); +}; + +export default compose( memo, withInstanceId )( TemplateSelectorControl ); diff --git a/packages/page-template-modal/src/components/template-selector-item.js b/packages/page-template-modal/src/components/template-selector-item.js new file mode 100644 index 0000000000000..9c720b0a8251c --- /dev/null +++ b/packages/page-template-modal/src/components/template-selector-item.js @@ -0,0 +1,80 @@ +/** + * External dependencies + */ +import { isNil } from 'lodash'; +import classnames from 'classnames'; + +const TemplateSelectorItem = ( props ) => { + const { + id, + value, + onSelect, + title, + description, + theme, + locale, + templatePostID = null, + isSelected, + } = props; + + if ( isNil( id ) || isNil( title ) || isNil( value ) ) { + return null; + } + + const mshotsUrl = 'https://s0.wordpress.com/mshots/v1/'; + const designsEndpoint = 'https://public-api.wordpress.com/rest/v1/template/demo/'; + const sourceSiteUrl = 'dotcompatterns.wordpress.com'; + + const previewUrl = `${ designsEndpoint }${ encodeURIComponent( theme ) }/${ encodeURIComponent( + sourceSiteUrl + ) }/?post_id=${ encodeURIComponent( templatePostID ) }&language=${ encodeURIComponent( + locale + ) }`; + + const staticPreviewImg = + 'blank' === value + ? null + : mshotsUrl + encodeURIComponent( previewUrl ) + '?vpw=1024&vph=1024&w=500&h=500'; + + const refreshSourceImg = ( e ) => { + const img = e.target; + + if ( -1 !== img.src.indexOf( 'reload=1' ) ) { + return; + } + + setTimeout( () => { + img.src = img.src + '&reload=1'; + }, 10000 ); + }; + + const innerPreview = + 'blank' === value ? null : ( + { + ); + + const handleClick = () => { + onSelect( value ); + }; + + return ( + + ); +}; + +export default TemplateSelectorItem; diff --git a/packages/page-template-modal/src/components/template-selector-preview.js b/packages/page-template-modal/src/components/template-selector-preview.js new file mode 100644 index 0000000000000..a2328d36db8f4 --- /dev/null +++ b/packages/page-template-modal/src/components/template-selector-preview.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import BlockIframePreview from './block-iframe-preview'; + +const TemplateSelectorPreview = ( { blocks = [], viewportWidth, title } ) => { + const noBlocks = ! blocks.length; + return ( + /* eslint-disable wpcalypso/jsx-classname-namespace */ +
+ { noBlocks && ( +
+
+ { __( 'Select a layout to preview.', 'full-site-editing' ) } +
+
+ ) } + + { /* Always render preview iframe to ensure it's ready to populate with Blocks. */ + /* Without this some browsers will experience a noticavle delay + /* before Blocks are populated into the iframe. */ } + +
+ /* eslint-enable wpcalypso/jsx-classname-namespace */ + ); +}; + +export default TemplateSelectorPreview; diff --git a/packages/page-template-modal/src/index.js b/packages/page-template-modal/src/index.js new file mode 100644 index 0000000000000..0bb0573cb4da9 --- /dev/null +++ b/packages/page-template-modal/src/index.js @@ -0,0 +1,84 @@ +/** + * External dependencies + */ +import { stubTrue } from 'lodash'; +import '@wordpress/nux'; +import { compose } from '@wordpress/compose'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { addFilter, removeFilter } from '@wordpress/hooks'; + +/** + * Internal dependencies + */ +import PageTemplateModal from './components/page-template-modal'; +import './styles/starter-page-templates-editor.scss'; + +const INSERTING_HOOK_NAME = 'isInsertingPageTemplate'; +const INSERTING_HOOK_NAMESPACE = 'automattic/full-site-editing/inserting-template'; + +export { initializeWithIdentity as initializeTracksWithIdentity } from './utils/tracking'; + +export const PageTemplatesPlugin = compose( + withSelect( ( select ) => { + const getMeta = () => select( 'core/editor' ).getEditedPostAttribute( 'meta' ); + const { _starter_page_template } = getMeta(); + const { isOpen } = select( 'automattic/starter-page-layouts' ); + const currentBlocks = select( 'core/editor' ).getBlocks(); + return { + isOpen: isOpen(), + getMeta, + _starter_page_template, + currentBlocks, + currentPostTitle: select( 'core/editor' ).getCurrentPost().title, + postContentBlock: currentBlocks.find( ( block ) => block.name === 'a8c/post-content' ), + isWelcomeGuideActive: select( 'core/edit-post' ).isFeatureActive( 'welcomeGuide' ), // Gutenberg 7.2.0 or higher + areTipsEnabled: select( 'core/nux' ) ? select( 'core/nux' ).areTipsEnabled() : false, // Gutenberg 7.1.0 or lower + }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const editorDispatcher = dispatch( 'core/editor' ); + const { setOpenState } = dispatch( 'automattic/starter-page-layouts' ); + return { + setOpenState, + saveTemplateChoice: ( name ) => { + // Save selected template slug in meta. + const currentMeta = ownProps.getMeta(); + editorDispatcher.editPost( { + meta: { + ...currentMeta, + _starter_page_template: name, + }, + } ); + }, + insertTemplate: ( title, blocks ) => { + // Add filter to let the tracking library know we are inserting a template. + addFilter( INSERTING_HOOK_NAME, INSERTING_HOOK_NAMESPACE, stubTrue ); + + // Set post title. + if ( title ) { + editorDispatcher.editPost( { title } ); + } + + // Replace blocks. + const postContentBlock = ownProps.postContentBlock; + dispatch( 'core/block-editor' ).replaceInnerBlocks( + postContentBlock ? postContentBlock.clientId : '', + blocks, + false + ); + + // Remove filter. + removeFilter( INSERTING_HOOK_NAME, INSERTING_HOOK_NAMESPACE ); + }, + hideWelcomeGuide: () => { + if ( ownProps.isWelcomeGuideActive ) { + // Gutenberg 7.2.0 or higher. + dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' ); + } else if ( ownProps.areTipsEnabled ) { + // Gutenberg 7.1.0 or lower. + dispatch( 'core/nux' ).disableTips(); + } + }, + }; + } ) +)( PageTemplateModal ); diff --git a/packages/page-template-modal/src/styles/starter-page-templates-editor.scss b/packages/page-template-modal/src/styles/starter-page-templates-editor.scss new file mode 100644 index 0000000000000..4b1820b52e158 --- /dev/null +++ b/packages/page-template-modal/src/styles/starter-page-templates-editor.scss @@ -0,0 +1,484 @@ +@mixin screen-reader-text() { + border: 0; + clip: rect( 1px, 1px, 1px, 1px ); + clip-path: inset( 50% ); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + word-wrap: normal !important; +} + +$template-modal-background-color: #eeeeee; +$template-selector-border-color: #e2e4e7; +$template-selector-border-color-selected: #555d66; +$template-selector-border-color-active: #00a0d2; +$template-selector-border-color-hover: #c9c9ca; +$template-selector-empty-background: #fff; +$template-selector-modal-offset-bottom: 25px; +$template-selector-modal-offset-right: 32px; +$template-selector-blank-template-mobile-height: 70px; +$template-large-preview-title-height: 120px; + +// Preview positioning +$preview-right-margin: 24px; + +// Breakpoints +$breakpoint-mobile: 660px; +$breakpoint-tablet: 783px; +$breakpoint-desktop: 961px; +$breakpoint-huge: 1648px; + +// WP.org sidebar and admin bar sizes +$wp-org-sidebar-full: 160px; +$wp-org-sidebar-collapsed: 36px; +$wp-org-admin-bar-full: 32px; +$wp-org-admin-bar-mobile: 46px; + +// Modal Overlay +.page-template-modal-screen-overlay { + animation: none; + background-color: transparent; // hide the overlay visually + z-index: 99; // Right below the wp-admin admin bar and sidebar. +} + +// When not in fullscreen mode allow space for WP.org sidebar +body:not( .is-fullscreen-mode ) { + .page-template-modal-screen-overlay { + @media screen and ( min-width: $breakpoint-tablet ) { + left: $wp-org-sidebar-collapsed; + } + + @media screen and ( min-width: $breakpoint-desktop ) { + left: $wp-org-sidebar-full; + } + } + @media screen and ( min-width: $breakpoint-tablet ) { + &.folded .page-template-modal-screen-overlay { + left: $wp-org-sidebar-collapsed; + } + &:not( .folded ):not( .auto-fold ) .page-template-modal-screen-overlay { + left: $wp-org-sidebar-full; + } + } +} + +// Allow space for admin bar if present and not in full screen mode +body.admin-bar:not( .is-fullscreen-mode ) .page-template-modal-screen-overlay { + top: $wp-org-admin-bar-mobile; + + @media screen and ( min-width: $breakpoint-tablet ) { + top: $wp-org-admin-bar-full; + } +} + +// Full screen modal +.page-template-modal { + width: 100%; + height: 100vh; + animation: none; + box-shadow: none; // cancel "modal" appearance + border: none; // cancel "modal" appearance + top: 0; // overlay the Block Editor toolbar + left: 0; + right: 0; + bottom: 0; + transform: none; + max-width: none; + max-height: none; + background-color: $template-modal-background-color; +} + +.page-template-modal .components-modal__header-heading-container { + @include screen-reader-text(); +} + +// Show close button in all modes. +.page-template-modal__close-button { + display: block; + position: absolute; + z-index: 20; + top: 9px; + width: 36px; + height: 36px; + left: 10px; +} + +.page-template-modal .components-modal__header::after { + display: block; + position: absolute; + content: ' '; + border-right: 1px solid $template-selector-border-color; + height: 100%; + left: 56px; +} + +.page-template-modal .components-modal__content { + overflow-y: scroll; + -webkit-overflow-scrolling: touch; +} +.page-template-modal__inner { + position: relative; + margin: 0 auto; + padding: 0 20px 40px; +} + +.page-template-modal__list { + margin-bottom: 20px; + + .components-base-control__label { + @include screen-reader-text(); + } +} + +.template-selector-control__options { + display: grid; + grid-template-columns: 1fr; + grid-gap: 0.75em; + + @media screen and ( min-width: $breakpoint-mobile ) { + margin-top: 0; + grid-template-columns: repeat( + auto-fill, + minmax( 110px, 1fr ) + ); // allow grid to take over number of cols on large screens + } +} + +.template-selector-item__label { + display: block; + width: 100%; + font-size: 14px; + text-align: center; + border: solid 2px $template-selector-border-color; + border-radius: 6px; + cursor: pointer; + appearance: none; + padding: 0; + overflow: hidden; + background-color: $template-selector-empty-background; + position: relative; + transform: translateZ( 0 ); // Fix for Safari rounded border overflow (1/2). + + &:focus { + box-shadow: 0 0 0 1px $template-selector-empty-background, + 0 0 0 3px $template-selector-border-color-active; + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 2px solid transparent; + } + + &:hover { + border: solid 2px $template-selector-border-color-hover; + } + + &.is-selected { + border: solid 2px $template-selector-border-color-selected; + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 2px solid transparent; + outline-offset: -2px; + + &:focus { + box-shadow: 0 0 0 1px $template-selector-empty-background, + 0 0 0 3px $template-selector-border-color-active; + border: solid 2px $template-selector-border-color-selected; + + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 4px solid transparent; + outline-offset: -4px; + } + } +} + +.template-selector-item__preview-wrap { + width: 100%; + display: block; + margin: 0 auto; + background: $template-selector-empty-background; + border-radius: 0; + overflow: hidden; + height: 0; + padding-top: 120%; + box-sizing: content-box; + position: relative; + pointer-events: none; + opacity: 1; + transform: translateZ( 0 ); // Fix for Safari rounded border overflow (2/2). + + @media screen and ( min-width: $breakpoint-mobile ) { + padding-top: 100%; // Aspect radio boxes. It will take the 100% of width. + } + + &.is-rendering { + opacity: 0.5; + } +} + +.template-selector-item__media { + width: 100%; + display: block; + position: absolute; + top: 0; + left: 0; +} + +.page-template-modal__form { + @media screen and ( min-width: $breakpoint-mobile ) { + max-width: 20%; + } + + @media screen and ( min-width: $breakpoint-tablet ) { + max-width: 30%; + } +} + +.page-template-modal__form-title { + font-weight: bold; + margin-bottom: 1em; + text-align: center; + @media screen and ( min-width: $breakpoint-mobile ) { + text-align: left; + } +} + +.page-template-modal__buttons { + position: absolute; + right: 0; + top: 0; + z-index: 10; + height: 56px; + display: flex; + align-items: center; + padding-right: 24px; + + @media screen and ( min-width: $breakpoint-mobile ) { + display: flex; + } + + &.is-visually-hidden { + @include screen-reader-text(); + } + + .components-button { + height: 33px; // match to Gutenberg toolbar styles + } +} + +// Template Selector Preview +.template-selector-preview { + display: none; + position: fixed; + top: 111px + $wp-org-admin-bar-mobile; + bottom: 24px; + left: calc( 20% + #{$preview-right-margin * 2} ); + right: $preview-right-margin; + background: $template-selector-empty-background; + border-radius: 2px; + overflow: hidden; + box-shadow: 0 2px 2px 0 rgba( 0, 0, 0, 0.14 ), + 0 3px 1px -2px rgba( 0, 0, 0, 0.12 ), + 0 1px 5px 0 rgba( 0, 0, 0, 0.2 ); + + @media screen and ( min-width: $breakpoint-mobile ) { + display: block; + &.is-blank-preview { + align-items: center; + display: flex; + justify-content: center; + } + } + + @media screen and ( min-width: $breakpoint-tablet ) { + top: 111px + $wp-org-admin-bar-full; + left: calc( 30% + #{$preview-right-margin * 1.5 + $wp-org-sidebar-collapsed} ); + body:not( .auto-fold ):not( .folded ) & { + left: calc( 30% + #{$preview-right-margin / 2 + $wp-org-sidebar-full} ); + } + } + @media screen and ( min-width: $breakpoint-desktop ) { + left: calc( 30% + #{$preview-right-margin / 2 + $wp-org-sidebar-full} ); + body.folded & { + left: calc( 30% + #{$preview-right-margin * 1.5 + $wp-org-sidebar-collapsed} ); + } + } + + body.is-fullscreen-mode & { + top: 111px; + @media screen and ( min-width: $breakpoint-tablet ) { + left: calc( 30% + #{$preview-right-margin * 2} ) !important; + } + } + + .edit-post-visual-editor { + margin: 0; + padding: 0; + } + + // not-selected template + &.not-selected { + .editor-styles-wrapper { + position: relative; + width: 100%; + height: 100%; + + .template-selector-preview__empty-state { + position: absolute; + width: 100%; + text-align: center; + height: 50px; + line-height: 50px; + top: 50%; + margin: -25px 0 0; + } + } + } +} + +.page-template-modal__loading { + position: absolute; + top: 50%; + left: 50%; + transform: translate( -50%, -50% ); + display: flex; + align-items: flex-end; + + .components-spinner { + float: none; + } +} + +// Sidebar modal opener goo. +.sidebar-modal-opener { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + .template-selector-item__label { + max-width: 300px; + } +} + +.sidebar-modal-opener__warning-modal { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.sidebar-modal-opener__warning-text { + max-width: 300px; + font-size: 1rem; + line-height: 1.5rem; +} + +.sidebar-modal-opener__warning-options { + float: right; + margin-top: 20px; + + .components-button { + margin-left: 12px; + } +} + +.block-iframe-preview { + position: absolute; + top: 0; + left: 0; + transform-origin: top left; + text-align: initial; + margin: 0; + overflow: visible; + min-height: auto; + + // Fallback viewport width. + // Used when BlockFramePreview's viewportWidth prop is undefined. + // Can be overridden through more specific CSS. + width: 1440px; +} + +.block-iframe-preview-body { + margin: 0; + padding: 0; + overflow-x: hidden; + overflow-y: auto; + + > .block-editor, + > .block-editor > .edit-post-visual-editor { + padding: 0; + margin: 0; + } + + // Hide inserter/appender. + .block-list-appender, + .block-editor-inserter { + display: none !important; + visibility: hidden; + position: absolute; + left: -9999vw; + } + + // Preview adjustments. + .editor-styles-wrapper { + // core/cover. + .wp-block-cover { + height: auto; + } + + // TODO: fix the embed iframe blocks properly. + .wp-block-embed__wrapper iframe { + height: auto; + min-height: 400px; + } + } + + // Manual CSS Overrides. Remove after better solutions are in place. + + // Removes empty paragraph placeholders, i.e. "Write Title..." + [data-type='core/paragraph'] [data-rich-text-placeholder] { + display: none; + } + + /* + * Fixes jetpack .wp-block-jetpack-slideshow styles, as the /wp-content/plugins/jetpack/_inc/blocks/vendors~swiper.[hash].css + * file is loaded on block insert, not on page load. After the iframe is grabbing these styles, we can remove this code. + */ + .swiper-wrapper { + display: flex; + } + + .swiper-button-prev, + .swiper-button-next { + display: none; + } + + .swiper-pagination { + text-align: center; + } + + .swiper-pagination-bullet { + border-radius: 100%; + } + + // Fixes cover image spacing and full-width group spacing + .editor-styles-wrapper [data-block] { + &[data-type='core/group'], + &[data-type='core/cover'][data-align='full'] { + margin-top: 0; + margin-bottom: 0; + } + } + + // Tweak template title (post-title) component. + .block-iframe-preview__template-title { + padding-top: 20px; + } + + // flex: column; on the parent and flex-basis: 0; on the child in Safari causes a lot of weird overlapping layout + // issues in the iframe preview: https://github.com/Automattic/wp-calypso/issues/39874 + // Using flex-basis: auto; on the child allows the height to be calculated properly + .wp-block-columns > .block-editor-inner-blocks > .block-editor-block-list__layout > [data-type='core/column'] .block-core-columns { + flex-basis: auto; + } +} diff --git a/packages/page-template-modal/src/utils/contains-missing-block.js b/packages/page-template-modal/src/utils/contains-missing-block.js new file mode 100644 index 0000000000000..5458c43fabe7f --- /dev/null +++ b/packages/page-template-modal/src/utils/contains-missing-block.js @@ -0,0 +1,28 @@ +// Once parsed, missing Blocks have a name prop of `core/missing`. +// see: https://github.com/WordPress/gutenberg/tree/742dbf2ef0e37481a3c14c29f3688aa0cd3cf887/packages/block-library/src/missing +const MISSING_BLOCK_NAME = 'core/missing'; + +/** + * Determines whether the provided collection of Blocks contains any "missing" + * blocks as determined by the presence of the `core/missing` block type. + * + * @param {Array} blocks the collection of block objects to check for "missing" block . + * @returns {boolean} whether the collection blocks contains any missing blocks. + */ +function containsMissingBlock( blocks ) { + return !! blocks.find( ( block ) => { + // If we found a missing block the bale out immediately + if ( block.name === MISSING_BLOCK_NAME ) { + return true; + } + + // If there are innerblocks then recurse down into them... + if ( block.innerBlocks && block.innerBlocks.length ) { + return containsMissingBlock( block.innerBlocks ); + } + + return false; + } ); +} + +export default containsMissingBlock; diff --git a/packages/page-template-modal/src/utils/ensure-assets.js b/packages/page-template-modal/src/utils/ensure-assets.js new file mode 100644 index 0000000000000..de7cfb3cdb917 --- /dev/null +++ b/packages/page-template-modal/src/utils/ensure-assets.js @@ -0,0 +1,219 @@ +/** + * External dependencies + */ +import { reduce, isEmpty, forEach, set, map } from 'lodash'; + +/** + * WordPress dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { removeQueryArgs } from '@wordpress/url'; + +/** + * A full asset URL. + * + * @typedef {string} URL + */ + +/** + * Gutenberg Block. + * + * @typedef {object} GutenbergBlock + * @property {string} clientId A unique id of the block. + * @property {string} name A block name, like "core/paragraph". + * @property {Array} innerBlocks Nested blocks. + * @property {object} attributes An object with attributes, different for each block type. + */ + +/** + * Usage object contains an info that certain property is used inside another object. + * + * @typedef {object} Usage + * @property {string} prop Name of the property. + * @property {Array} path A path inside an object where prop is, defined as list of keys. + */ + +/** + * An asset file that is referenced in blocks. + * + * @typedef {object} Asset + * @property {URL} url A full URL of the asset. + * @property {Array} usages A list of {@link Usage} objects. + */ + +/** + * A collection of {@link Asset} objects, keyed by their URLs. + * + * @typedef {object.} Assets URLs as keys, {@link Asset}.as a values. + */ + +/** + * FetchSession describes a set of blocks and their assets. + * + * @typedef {object} FetchSession + * @property {Array} blocks List of Gutenberg blocks. + * @property {object} blocksByClientId Blocks, keyed by their `clientId` + * @property {Assets} assets A list of assets detected in blocks. + */ + +/** + * Extends an {@link Assets} object with a new asset and updates its usages. + * + * @param {Assets} assets Object containing assets. + * @param {URL} url A full URL of the asset. + * @param {Array} usages A list of {@link Usage} objects. + * @returns {Assets} assets object with the new {@link Asset} included + */ +const addAssetToLoad = ( assets, url, usages ) => { + // Remove resizing query arguments from the URL. + url = removeQueryArgs( url, 'w', 's' ); + + // Use an existing asset for the URL or make a new one. + const asset = assets[ url ] || { + url, + usages: [], + }; + + // Return new result object, extended with the new/updated asset. + return { + ...assets, + [ url ]: { + ...asset, + // Store where exactly block uses id/url so we can update it later. + usages: [ ...asset.usages, ...usages ], + }, + }; +}; + +/** + * This function is used as a reducer iteratee. It checks if the block + * contains any image and if so, enqueues it to be downloaded later. + * + * @param {FetchSession} session Session object. + * @param {GutenbergBlock} block Gutenberg Block object. + * @returns {FetchSession} Updated session object + */ +const findAssetsInBlock = ( session, block ) => { + // Save a reference for the block so we can later easily + // find it without any loops and recursion. + session.blocksByClientId[ block.clientId ] = block; + + // Identify assets in blocks where we expect them. + switch ( block.name ) { + // Both of these blocks use same attribute names for image id and url + // and thus we can share the implementation. + case 'core/cover': + case 'core/image': { + const url = block.attributes.url; + if ( url ) { + session.assets = addAssetToLoad( session.assets, url, [ + { prop: 'url', path: [ block.clientId, 'attributes', 'url' ] }, + { prop: 'id', path: [ block.clientId, 'attributes', 'id' ] }, + ] ); + } + } + case 'core/media-text': { + const url = block.attributes.mediaUrl; + if ( url && block.attributes.mediaType === 'image' ) { + session.assets = addAssetToLoad( session.assets, url, [ + { prop: 'url', path: [ block.clientId, 'attributes', 'mediaUrl' ] }, + { prop: 'id', path: [ block.clientId, 'attributes', 'mediaId' ] }, + ] ); + } + } + case 'core/gallery': { + forEach( block.attributes.images, ( image, i ) => { + session.assets = addAssetToLoad( session.assets, image.url, [ + { prop: 'url', path: [ block.clientId, 'attributes', 'images', i, 'url' ] }, + { prop: 'url', path: [ block.clientId, 'attributes', 'images', i, 'link' ] }, + { prop: 'id', path: [ block.clientId, 'attributes', 'images', i, 'id' ] }, + { prop: 'id', path: [ block.clientId, 'attributes', 'ids', i ] }, + ] ); + } ); + } + } + + // Recursively process all inner blocks. + if ( ! isEmpty( block.innerBlocks ) ) { + return reduce( block.innerBlocks, findAssetsInBlock, session ); + } + + return session; +}; + +/** + * Calls an API that fetches assets and saves the result into the DetectedAssets object. + * + * @param {Assets} assets Assets that were detected from blocks. + * @returns {Promise} Promise that resoves into an object with URLs as keys and fetch results as values. + */ +const fetchAssets = async ( assets ) => { + return await apiFetch( { + method: 'POST', + path: '/fse/v1/sideload/image/batch', + data: { resources: map( assets ) }, + } ).then( ( response ) => + reduce( + assets, + ( fetched, asset ) => { + const { id, source_url } = response.shift(); + return { + ...fetched, + [ asset.url ]: { id, url: source_url }, + }; + }, + {} + ) + ); +}; + +/** + * Takes fetched assets and makes sure all their usages will be changed into + * their new local copies. + * + * @param {FetchSession} session A current session. + * @param {object} fetchedAssets Fetched assets. + * @returns {Array} A promise resolving into an array of blocks. + */ +const getBlocksWithAppliedAssets = ( session, fetchedAssets ) => { + forEach( session.assets, ( asset ) => { + const newAsset = fetchedAssets[ asset.url ]; + if ( ! newAsset ) { + return; + } + forEach( asset.usages, ( usage ) => { + set( session.blocksByClientId, usage.path, newAsset[ usage.prop ] ); + } ); + } ); + + return session.blocks; +}; + +/** + * Analyzes blocks and if they use any external assets, ensures they are + * copied into a local site and are used in blocks instead of the remote ones. + * + * @param {Array} blocks Blocks, as returned by `wp.block.parse` + * @returns {Promise} A promise that resolves into an array of {@link GutenbergBlock} with updated assets + */ +const ensureAssetsInBlocks = async ( blocks ) => { + // Create a FetchSession object by reducing blocks. + const session = reduce( blocks, findAssetsInBlock, { + assets: {}, + blocksByClientId: {}, + blocks, + } ); + + // No assets found. Proceed with insertion right away. + if ( isEmpty( session.assets ) ) { + return blocks; + } + + // Ensure assets are available on the site and replace originals + // with local copies before inserting the template. + return fetchAssets( session.assets ).then( ( fetchedAssets ) => { + return getBlocksWithAppliedAssets( session, fetchedAssets ); + } ); +}; + +export default ensureAssetsInBlocks; diff --git a/packages/page-template-modal/src/utils/map-blocks-recursively.js b/packages/page-template-modal/src/utils/map-blocks-recursively.js new file mode 100644 index 0000000000000..d813f2d202c58 --- /dev/null +++ b/packages/page-template-modal/src/utils/map-blocks-recursively.js @@ -0,0 +1,34 @@ +/** + * External dependencies + */ +import { identity } from 'lodash'; +import { cloneBlock } from '@wordpress/blocks'; + +/** + * Recursively maps over a collection of blocks calling the modifier function on + * each to modify it and returning a collection of new block references. + * + * @param {Array} blocks an array of block objects + * @param {Function} modifier a callback function used to modify the blocks + */ +function mapBlocksRecursively( blocks, modifier = identity ) { + return blocks.map( ( block ) => { + // `blocks` is an object. Therefore any changes made here will + // be reflected across all references to the blocks object. To ensure we + // only modify the blocks when needed, we return a new object reference + // for any blocks we modify. This allows us to modify blocks for + // particular contexts. For example we may wish to show blocks + // differently in the preview than we do when they are inserted into the + // editor itself. + block = modifier( cloneBlock( block ) ); + + // Recurse into nested Blocks + if ( block.innerBlocks && block.innerBlocks.length ) { + block.innerBlocks = mapBlocksRecursively( block.innerBlocks, modifier ); + } + + return block; + } ); +} + +export default mapBlocksRecursively; diff --git a/packages/page-template-modal/src/utils/replace-placeholders.js b/packages/page-template-modal/src/utils/replace-placeholders.js new file mode 100644 index 0000000000000..3d4a309added9 --- /dev/null +++ b/packages/page-template-modal/src/utils/replace-placeholders.js @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import { _x } from '@wordpress/i18n'; + +const PLACEHOLDER_DEFAULTS = { + Address: _x( '123 Main St', 'default address', 'full-site-editing' ), + Phone: _x( '555-555-5555', 'default phone number', 'full-site-editing' ), + CompanyName: _x( 'Your Company Name', 'default company name', 'full-site-editing' ), + Vertical: _x( 'Business', 'default vertical name', 'full-site-editing' ), +}; + +const KEY_MAP = { + CompanyName: 'title', + Address: 'address', + Phone: 'phone', + Vertical: 'vertical', +}; + +const replacePlaceholders = ( pageContent, siteInformation = {} ) => { + if ( ! pageContent ) { + return ''; + } + + return pageContent.replace( /{{(\w+)}}/g, ( match, placeholder ) => { + const defaultValue = PLACEHOLDER_DEFAULTS[ placeholder ]; + const key = KEY_MAP[ placeholder ]; + return siteInformation[ key ] || defaultValue || placeholder; + } ); +}; + +export default replacePlaceholders; diff --git a/packages/page-template-modal/src/utils/tracking.js b/packages/page-template-modal/src/utils/tracking.js new file mode 100644 index 0000000000000..13c01c21e7fc3 --- /dev/null +++ b/packages/page-template-modal/src/utils/tracking.js @@ -0,0 +1,77 @@ +// Ensure Tracks Library +window._tkq = window._tkq || []; + +let tracksIdentity = null; + +/** + * Populate `identity` on WPCOM and ATOMIC to enable tracking. + * Always disabled for regular self-hosted installations. + * + * @param {object} identity Info about identity. + * @param {number} identity.userid User ID. + * @param {string} identity.username Username. + * @param {number} identity.blogid Blog ID. + * @returns {void} + */ +export const initializeWithIdentity = ( identity ) => { + tracksIdentity = identity; + window._tkq.push( [ 'identifyUser', identity.userid, identity.username ] ); +}; + +/** + * Track a view of the layout selector. + * + * @param {string} source Source triggering the view. + * @returns {void} + */ +export const trackView = ( source ) => { + if ( ! tracksIdentity ) { + return; + } + window._tkq.push( [ + 'recordEvent', + 'a8c_full_site_editing_template_selector_view', + { + blog_id: tracksIdentity.blogid, + source, + }, + ] ); +}; + +/** + * Track closing of the layout selector. + * + * @returns {void} + */ +export const trackDismiss = () => { + if ( ! tracksIdentity ) { + return; + } + window._tkq.push( [ + 'recordEvent', + 'a8c_full_site_editing_template_selector_dismiss', + { + blog_id: tracksIdentity.blogid, + }, + ] ); +}; + +/** + * Track layout selection. + * + * @param {string} template Template slug. + * @returns {void} + */ +export const trackSelection = ( template ) => { + if ( ! tracksIdentity ) { + return; + } + window._tkq.push( [ + 'recordEvent', + 'a8c_full_site_editing_template_selector_template_selected', + { + blog_id: tracksIdentity.blogid, + template, + }, + ] ); +}; From 7cc0c1a847c309da71382fed3c852af2f5bfd274 Mon Sep 17 00:00:00 2001 From: roo2 Date: Tue, 9 Feb 2021 15:00:11 +1000 Subject: [PATCH 02/13] pr feedback, specify dependency versions --- packages/page-template-modal/.eslintrc.js | 3 --- packages/page-template-modal/package.json | 29 ++++++++++------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/packages/page-template-modal/.eslintrc.js b/packages/page-template-modal/.eslintrc.js index cd9efe6ade839..e4b3f3e8cdfcd 100644 --- a/packages/page-template-modal/.eslintrc.js +++ b/packages/page-template-modal/.eslintrc.js @@ -1,8 +1,5 @@ module.exports = { rules: { 'react/react-in-jsx-scope': 0, - // page-template-modal renders in a Gutenberg environment and should - // conform to those naming conventions instead of Calypso's. - 'wpcalypso/jsx-classname-namespace': 0, }, }; diff --git a/packages/page-template-modal/package.json b/packages/page-template-modal/package.json index c388c2f8369fc..6210326b809ec 100644 --- a/packages/page-template-modal/package.json +++ b/packages/page-template-modal/package.json @@ -29,24 +29,19 @@ ], "dependencies": { "@wordpress/nux": "^3.24.1", - "@wordpress/i18n": "*", - "@wordpress/compose": "*", - "@wordpress/components": "*", - "@wordpress/data": "*", - "@wordpress/element": "*", - "@wordpress/blocks": "*", - "@wordpress/hooks": "*", - "@wordpress/api-fetch": "*", - "@wordpress/url": "*", - "lodash": "*", + "@wordpress/i18n": "^3.17.0", + "@wordpress/compose": "^3.23.1", + "@wordpress/components": "^12.0.1", + "@wordpress/data": "^4.26.1", + "@wordpress/element": "^2.19.01", + "@wordpress/blocks": "^6.25.1", + "@wordpress/hooks": "^2.11.0", + "@wordpress/api-fetch": "^3.3.0", + "@wordpress/url": "^2.21.0", + "lodash": "^4.17.19", "classnames": "^2.2.6", - "@wordpress/block-editor": "*", - "@wordpress/editor": "*" - }, - "devDependencies": { - "react": "^16.12.0", - "react-dom": "^16.12.0", - "react-test-renderer": "^16.12.0" + "@wordpress/block-editor": "^5.2.1", + "@wordpress/editor": "^9.25" }, "peerDependencies": { "react": "^16.8" From b47858bb8152a35991e8a4a3bdd3990d892ed295 Mon Sep 17 00:00:00 2001 From: roo2 Date: Mon, 8 Feb 2021 14:05:56 +1000 Subject: [PATCH 03/13] use the page template modal package --- .../starter-page-templates/index.js | 5 +- .../components/block-iframe-preview.js | 259 ---------- .../components/block-preview.js | 36 -- .../components/template-selector-control.js | 77 --- .../components/template-selector-item.js | 80 --- .../components/template-selector-preview.js | 33 -- .../styles/starter-page-templates-editor.scss | 484 ------------------ .../utils/contains-missing-block.js | 28 - .../utils/ensure-assets.js | 219 -------- .../utils/map-blocks-recursively.js | 34 -- .../utils/replace-placeholders.js | 32 -- .../page-template-modal/utils/tracking.js | 77 --- apps/editing-toolkit/package.json | 1 + 13 files changed, 3 insertions(+), 1362 deletions(-) delete mode 100644 apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/block-iframe-preview.js delete mode 100644 apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/block-preview.js delete mode 100644 apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/template-selector-control.js delete mode 100644 apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/template-selector-item.js delete mode 100644 apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/template-selector-preview.js delete mode 100644 apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/styles/starter-page-templates-editor.scss delete mode 100644 apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/contains-missing-block.js delete mode 100644 apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/ensure-assets.js delete mode 100644 apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/map-blocks-recursively.js delete mode 100644 apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/replace-placeholders.js delete mode 100644 apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/tracking.js diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.js index 2c513abbab7b2..a08124f20d6d1 100644 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.js +++ b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/index.js @@ -7,8 +7,7 @@ import { dispatch } from '@wordpress/data'; /** * Internal dependencies */ -import { PageTemplatesPlugin } from './page-template-modal'; -import { initializeWithIdentity } from './page-template-modal/utils/tracking'; +import { PageTemplatesPlugin, initializeTracksWithIdentity } from '@automattic/page-template-modal'; import './store'; // Load config passed from backend. @@ -25,7 +24,7 @@ const { } = window.starterPageTemplatesConfig; if ( tracksUserData ) { - initializeWithIdentity( tracksUserData ); + initializeTracksWithIdentity( tracksUserData ); } const templatesPluginSharedProps = { diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/block-iframe-preview.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/block-iframe-preview.js deleted file mode 100644 index 7a0729a1a7d7f..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/block-iframe-preview.js +++ /dev/null @@ -1,259 +0,0 @@ -/** - * External dependencies - */ -import { each, filter, get, castArray, debounce, noop } from 'lodash'; -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { - createPortal, - useRef, - useEffect, - useState, - useMemo, - useReducer, - useLayoutEffect, - useCallback, -} from '@wordpress/element'; -import { withSelect } from '@wordpress/data'; -import { compose, withSafeTimeout } from '@wordpress/compose'; - -import { __ } from '@wordpress/i18n'; - -import CustomBlockPreview from './block-preview'; - -// Debounce time applied to the on resize window event. -const DEBOUNCE_TIMEOUT = 300; - -/** - * Copies the styles from the provided src document - * to the given iFrame head and body DOM references. - * - * @param {object} srcDocument the src document from which to copy the - * `link` and `style` Nodes from the `head` and `body` - * @param {object} targetiFrameDocument the target iframe's - * `contentDocument` where the `link` and `style` Nodes from the `head` and - * `body` will be copied - */ -const copyStylesToIframe = ( srcDocument, targetiFrameDocument ) => { - const styleNodes = [ 'link', 'style' ]; - - // See https://developer.mozilla.org/en-US/docs/Web/API/DocumentFragment - const targetDOMFragment = { - head: document.createDocumentFragment(), // eslint-disable-line no-undef - body: document.createDocumentFragment(), // eslint-disable-line no-undef - }; - - each( Object.keys( targetDOMFragment ), ( domReference ) => { - return each( - filter( srcDocument[ domReference ].children, ( { localName } ) => - // Only return specific style-related Nodes - styleNodes.includes( localName ) - ), - ( targetNode ) => { - // Clone the original node and append to the appropriate Fragement - const deep = true; - targetDOMFragment[ domReference ].appendChild( targetNode.cloneNode( deep ) ); - } - ); - } ); - - // Consolidate updates to iframe DOM - targetiFrameDocument.head.appendChild( targetDOMFragment.head ); - targetiFrameDocument.body.appendChild( targetDOMFragment.body ); -}; - -/** - * Performs a blocks preview using an iFrame. - * - * @param {object} props component's props - * @param {object} props.className CSS class to apply to component - * @param {string} props.bodyClassName CSS class to apply to the iframe's `` tag - * @param {number} props.viewportWidth pixel width of the viewable size of the preview - * @param {Array} props.blocks array of Gutenberg Block objects - * @param {object} props.settings block Editor settings object - * @param {Function} props.setTimeout safe version of window.setTimeout via `withSafeTimeout` - * @param {string} props.title Template Title - see #39831 for details. - */ -const BlockFramePreview = ( { - className = 'block-iframe-preview', - bodyClassName = 'block-iframe-preview-body', - viewportWidth, - blocks, - settings, - setTimeout = noop, - title, -} ) => { - const frameContainerRef = useRef(); - const iframeRef = useRef(); - - // Set the initial scale factor. - const [ style, setStyle ] = useState( { - transform: `scale( 1 )`, - } ); - - // Rendering blocks list. - const renderedBlocks = useMemo( () => castArray( blocks ), [ blocks ] ); - const [ recomputeBlockListKey, triggerRecomputeBlockList ] = useReducer( - ( state ) => state + 1, - 0 - ); - useLayoutEffect( triggerRecomputeBlockList, [ blocks ] ); - - /** - * This function re scales the viewport depending on - * the wrapper and the iframe width. - */ - const rescale = useCallback( () => { - const parentNode = get( frameContainerRef, [ 'current', 'parentNode' ] ); - if ( ! parentNode ) { - return; - } - - // Scaling iFrame. - const width = viewportWidth || frameContainerRef.current.offsetWidth; - const scale = parentNode.offsetWidth / viewportWidth; - const height = parentNode.offsetHeight / scale; - - setStyle( { - width, - height, - transform: `scale( ${ scale } )`, - } ); - }, [ viewportWidth ] ); - - /* - * Temporarily manually set the PostTitle from DOM. - * It isn't currently possible to manually force the `` component - * to render a title provided as a prop. A Core PR will rectify this (see below). - * Until then we use direct DOM manipulation to set the post title. - * - * See: https://github.com/WordPress/gutenberg/pull/20609/ - */ - useEffect( () => { - if ( ! title ) return; - - const iframeBody = get( iframeRef, [ 'current', 'contentDocument', 'body' ] ); - if ( ! iframeBody ) { - return; - } - - const templateTitle = iframeBody.querySelector( - '.editor-post-title .editor-post-title__input' - ); - - if ( ! templateTitle ) { - return; - } - - templateTitle.value = title; - }, [ recomputeBlockListKey ] ); - - // Populate iFrame styles. - useEffect( () => { - setTimeout( () => { - copyStylesToIframe( window.document, iframeRef.current.contentDocument ); - iframeRef.current.contentDocument.body.classList.add( - bodyClassName, - 'editor-styles-wrapper', - 'block-editor__container' - ); - /* - * Temporarly override height of the Post Title. - * Post Title component doesn't resize correctly, - * this quick CSS fix overrides the height to be auto - * A Core PR will rectify this (see below). - * - * See: https://github.com/WordPress/gutenberg/pull/20609/ - */ - iframeRef.current.contentDocument.head.innerHTML += - ''; - - // Prevent links and buttons from being clicked. This is applied within - // the iframe, because if we targeted the iframe itself it would prevent - // scrolling the iframe in Firefox. - iframeRef.current.contentDocument.head.innerHTML += - ''; - - rescale(); - }, 0 ); - }, [ setTimeout, bodyClassName, rescale ] ); - - // Scroll the preview to the top when the blocks change. - useEffect( () => { - const body = get( iframeRef, [ 'current', 'contentDocument', 'body' ] ); - if ( ! body ) { - return; - } - - // scroll to top when blocks changes. - body.scrollTop = 0; - }, [ recomputeBlockListKey ] ); - - // Handling windows resize event. - useEffect( () => { - const refreshPreview = debounce( rescale, DEBOUNCE_TIMEOUT ); - window.addEventListener( 'resize', refreshPreview ); - - return () => { - window.removeEventListener( 'resize', refreshPreview ); - }; - }, [ rescale ] ); - - // Handle wp-admin specific `wp-collapse-menu` event to refresh the preview on sidebar toggle. - useEffect( () => { - if ( window.jQuery ) { - window.jQuery( window.document ).on( 'wp-collapse-menu', rescale ); - } - return () => { - if ( window.jQuery ) { - window.jQuery( window.document ).off( 'wp-collapse-menu', rescale ); - } - }; - }, [ rescale ] ); - - /* eslint-disable wpcalypso/jsx-classname-namespace */ - return ( -
- -
- ); - /* eslint-enable wpcalypso/jsx-classname-namespace */ -}; - -export default compose( - withSafeTimeout, - withSelect( ( select ) => { - const blockEditorStore = select( 'core/block-editor' ); - return { - settings: blockEditorStore ? blockEditorStore.getSettings() : {}, - }; - } ) -)( BlockFramePreview ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/block-preview.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/block-preview.js deleted file mode 100644 index 438fd16ca6777..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/block-preview.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * External dependencies - */ - -/** - * Internal dependencies - */ - -/** - * WordPress dependencies - */ -import { BlockEditorProvider, BlockList } from '@wordpress/block-editor'; -import { Disabled } from '@wordpress/components'; -import { PostTitle } from '@wordpress/editor'; - -// Exists as a pass through component to simplify automatted testing of -// components which need to `BlockEditorProvider`. Setting up JSDom to handle -// and mock the entire Block Editor isn't useful and is difficult for testing. -// Therefore this component exists to simplify mocking out the Block Editor -// when under test conditions. -export default function ( { blocks, settings, hidePageTitle, recomputeBlockListKey } ) { - /* eslint-disable wpcalypso/jsx-classname-namespace */ - return ( - - - { ! hidePageTitle && ( -
- -
- ) } - -
-
- ); - /* eslint-enable wpcalypso/jsx-classname-namespace */ -} diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/template-selector-control.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/template-selector-control.js deleted file mode 100644 index 757c43b9580fc..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/template-selector-control.js +++ /dev/null @@ -1,77 +0,0 @@ -/** - * External dependencies - */ -import { isEmpty, isArray, noop, map } from 'lodash'; -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { withInstanceId, compose } from '@wordpress/compose'; -import { BaseControl } from '@wordpress/components'; -import { memo } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import TemplateSelectorItem from './template-selector-item'; -import replacePlaceholders from '../utils/replace-placeholders'; - -export const TemplateSelectorControl = ( { - label, - legendLabel, - className, - help, - instanceId, - templates = [], - blocksByTemplates = {}, - theme = 'maywood', - locale = 'en', - onTemplateSelect = noop, - siteInformation = {}, - selectedTemplate, -} ) => { - if ( isEmpty( templates ) || ! isArray( templates ) ) { - return null; - } - - const id = `template-selector-control-${ instanceId }`; - - return ( - -
    - { map( templates, ( { ID, name, title, description } ) => ( -
  • - -
  • - ) ) } -
-
- ); -}; - -export default compose( memo, withInstanceId )( TemplateSelectorControl ); diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/template-selector-item.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/template-selector-item.js deleted file mode 100644 index 9c720b0a8251c..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/template-selector-item.js +++ /dev/null @@ -1,80 +0,0 @@ -/** - * External dependencies - */ -import { isNil } from 'lodash'; -import classnames from 'classnames'; - -const TemplateSelectorItem = ( props ) => { - const { - id, - value, - onSelect, - title, - description, - theme, - locale, - templatePostID = null, - isSelected, - } = props; - - if ( isNil( id ) || isNil( title ) || isNil( value ) ) { - return null; - } - - const mshotsUrl = 'https://s0.wordpress.com/mshots/v1/'; - const designsEndpoint = 'https://public-api.wordpress.com/rest/v1/template/demo/'; - const sourceSiteUrl = 'dotcompatterns.wordpress.com'; - - const previewUrl = `${ designsEndpoint }${ encodeURIComponent( theme ) }/${ encodeURIComponent( - sourceSiteUrl - ) }/?post_id=${ encodeURIComponent( templatePostID ) }&language=${ encodeURIComponent( - locale - ) }`; - - const staticPreviewImg = - 'blank' === value - ? null - : mshotsUrl + encodeURIComponent( previewUrl ) + '?vpw=1024&vph=1024&w=500&h=500'; - - const refreshSourceImg = ( e ) => { - const img = e.target; - - if ( -1 !== img.src.indexOf( 'reload=1' ) ) { - return; - } - - setTimeout( () => { - img.src = img.src + '&reload=1'; - }, 10000 ); - }; - - const innerPreview = - 'blank' === value ? null : ( - { - ); - - const handleClick = () => { - onSelect( value ); - }; - - return ( - - ); -}; - -export default TemplateSelectorItem; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/template-selector-preview.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/template-selector-preview.js deleted file mode 100644 index a2328d36db8f4..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/components/template-selector-preview.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import BlockIframePreview from './block-iframe-preview'; - -const TemplateSelectorPreview = ( { blocks = [], viewportWidth, title } ) => { - const noBlocks = ! blocks.length; - return ( - /* eslint-disable wpcalypso/jsx-classname-namespace */ -
- { noBlocks && ( -
-
- { __( 'Select a layout to preview.', 'full-site-editing' ) } -
-
- ) } - - { /* Always render preview iframe to ensure it's ready to populate with Blocks. */ - /* Without this some browsers will experience a noticavle delay - /* before Blocks are populated into the iframe. */ } - -
- /* eslint-enable wpcalypso/jsx-classname-namespace */ - ); -}; - -export default TemplateSelectorPreview; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/styles/starter-page-templates-editor.scss b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/styles/starter-page-templates-editor.scss deleted file mode 100644 index 4b1820b52e158..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/styles/starter-page-templates-editor.scss +++ /dev/null @@ -1,484 +0,0 @@ -@mixin screen-reader-text() { - border: 0; - clip: rect( 1px, 1px, 1px, 1px ); - clip-path: inset( 50% ); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; - word-wrap: normal !important; -} - -$template-modal-background-color: #eeeeee; -$template-selector-border-color: #e2e4e7; -$template-selector-border-color-selected: #555d66; -$template-selector-border-color-active: #00a0d2; -$template-selector-border-color-hover: #c9c9ca; -$template-selector-empty-background: #fff; -$template-selector-modal-offset-bottom: 25px; -$template-selector-modal-offset-right: 32px; -$template-selector-blank-template-mobile-height: 70px; -$template-large-preview-title-height: 120px; - -// Preview positioning -$preview-right-margin: 24px; - -// Breakpoints -$breakpoint-mobile: 660px; -$breakpoint-tablet: 783px; -$breakpoint-desktop: 961px; -$breakpoint-huge: 1648px; - -// WP.org sidebar and admin bar sizes -$wp-org-sidebar-full: 160px; -$wp-org-sidebar-collapsed: 36px; -$wp-org-admin-bar-full: 32px; -$wp-org-admin-bar-mobile: 46px; - -// Modal Overlay -.page-template-modal-screen-overlay { - animation: none; - background-color: transparent; // hide the overlay visually - z-index: 99; // Right below the wp-admin admin bar and sidebar. -} - -// When not in fullscreen mode allow space for WP.org sidebar -body:not( .is-fullscreen-mode ) { - .page-template-modal-screen-overlay { - @media screen and ( min-width: $breakpoint-tablet ) { - left: $wp-org-sidebar-collapsed; - } - - @media screen and ( min-width: $breakpoint-desktop ) { - left: $wp-org-sidebar-full; - } - } - @media screen and ( min-width: $breakpoint-tablet ) { - &.folded .page-template-modal-screen-overlay { - left: $wp-org-sidebar-collapsed; - } - &:not( .folded ):not( .auto-fold ) .page-template-modal-screen-overlay { - left: $wp-org-sidebar-full; - } - } -} - -// Allow space for admin bar if present and not in full screen mode -body.admin-bar:not( .is-fullscreen-mode ) .page-template-modal-screen-overlay { - top: $wp-org-admin-bar-mobile; - - @media screen and ( min-width: $breakpoint-tablet ) { - top: $wp-org-admin-bar-full; - } -} - -// Full screen modal -.page-template-modal { - width: 100%; - height: 100vh; - animation: none; - box-shadow: none; // cancel "modal" appearance - border: none; // cancel "modal" appearance - top: 0; // overlay the Block Editor toolbar - left: 0; - right: 0; - bottom: 0; - transform: none; - max-width: none; - max-height: none; - background-color: $template-modal-background-color; -} - -.page-template-modal .components-modal__header-heading-container { - @include screen-reader-text(); -} - -// Show close button in all modes. -.page-template-modal__close-button { - display: block; - position: absolute; - z-index: 20; - top: 9px; - width: 36px; - height: 36px; - left: 10px; -} - -.page-template-modal .components-modal__header::after { - display: block; - position: absolute; - content: ' '; - border-right: 1px solid $template-selector-border-color; - height: 100%; - left: 56px; -} - -.page-template-modal .components-modal__content { - overflow-y: scroll; - -webkit-overflow-scrolling: touch; -} -.page-template-modal__inner { - position: relative; - margin: 0 auto; - padding: 0 20px 40px; -} - -.page-template-modal__list { - margin-bottom: 20px; - - .components-base-control__label { - @include screen-reader-text(); - } -} - -.template-selector-control__options { - display: grid; - grid-template-columns: 1fr; - grid-gap: 0.75em; - - @media screen and ( min-width: $breakpoint-mobile ) { - margin-top: 0; - grid-template-columns: repeat( - auto-fill, - minmax( 110px, 1fr ) - ); // allow grid to take over number of cols on large screens - } -} - -.template-selector-item__label { - display: block; - width: 100%; - font-size: 14px; - text-align: center; - border: solid 2px $template-selector-border-color; - border-radius: 6px; - cursor: pointer; - appearance: none; - padding: 0; - overflow: hidden; - background-color: $template-selector-empty-background; - position: relative; - transform: translateZ( 0 ); // Fix for Safari rounded border overflow (1/2). - - &:focus { - box-shadow: 0 0 0 1px $template-selector-empty-background, - 0 0 0 3px $template-selector-border-color-active; - // Windows High Contrast mode will show this outline, but not the box-shadow. - outline: 2px solid transparent; - } - - &:hover { - border: solid 2px $template-selector-border-color-hover; - } - - &.is-selected { - border: solid 2px $template-selector-border-color-selected; - // Windows High Contrast mode will show this outline, but not the box-shadow. - outline: 2px solid transparent; - outline-offset: -2px; - - &:focus { - box-shadow: 0 0 0 1px $template-selector-empty-background, - 0 0 0 3px $template-selector-border-color-active; - border: solid 2px $template-selector-border-color-selected; - - // Windows High Contrast mode will show this outline, but not the box-shadow. - outline: 4px solid transparent; - outline-offset: -4px; - } - } -} - -.template-selector-item__preview-wrap { - width: 100%; - display: block; - margin: 0 auto; - background: $template-selector-empty-background; - border-radius: 0; - overflow: hidden; - height: 0; - padding-top: 120%; - box-sizing: content-box; - position: relative; - pointer-events: none; - opacity: 1; - transform: translateZ( 0 ); // Fix for Safari rounded border overflow (2/2). - - @media screen and ( min-width: $breakpoint-mobile ) { - padding-top: 100%; // Aspect radio boxes. It will take the 100% of width. - } - - &.is-rendering { - opacity: 0.5; - } -} - -.template-selector-item__media { - width: 100%; - display: block; - position: absolute; - top: 0; - left: 0; -} - -.page-template-modal__form { - @media screen and ( min-width: $breakpoint-mobile ) { - max-width: 20%; - } - - @media screen and ( min-width: $breakpoint-tablet ) { - max-width: 30%; - } -} - -.page-template-modal__form-title { - font-weight: bold; - margin-bottom: 1em; - text-align: center; - @media screen and ( min-width: $breakpoint-mobile ) { - text-align: left; - } -} - -.page-template-modal__buttons { - position: absolute; - right: 0; - top: 0; - z-index: 10; - height: 56px; - display: flex; - align-items: center; - padding-right: 24px; - - @media screen and ( min-width: $breakpoint-mobile ) { - display: flex; - } - - &.is-visually-hidden { - @include screen-reader-text(); - } - - .components-button { - height: 33px; // match to Gutenberg toolbar styles - } -} - -// Template Selector Preview -.template-selector-preview { - display: none; - position: fixed; - top: 111px + $wp-org-admin-bar-mobile; - bottom: 24px; - left: calc( 20% + #{$preview-right-margin * 2} ); - right: $preview-right-margin; - background: $template-selector-empty-background; - border-radius: 2px; - overflow: hidden; - box-shadow: 0 2px 2px 0 rgba( 0, 0, 0, 0.14 ), - 0 3px 1px -2px rgba( 0, 0, 0, 0.12 ), - 0 1px 5px 0 rgba( 0, 0, 0, 0.2 ); - - @media screen and ( min-width: $breakpoint-mobile ) { - display: block; - &.is-blank-preview { - align-items: center; - display: flex; - justify-content: center; - } - } - - @media screen and ( min-width: $breakpoint-tablet ) { - top: 111px + $wp-org-admin-bar-full; - left: calc( 30% + #{$preview-right-margin * 1.5 + $wp-org-sidebar-collapsed} ); - body:not( .auto-fold ):not( .folded ) & { - left: calc( 30% + #{$preview-right-margin / 2 + $wp-org-sidebar-full} ); - } - } - @media screen and ( min-width: $breakpoint-desktop ) { - left: calc( 30% + #{$preview-right-margin / 2 + $wp-org-sidebar-full} ); - body.folded & { - left: calc( 30% + #{$preview-right-margin * 1.5 + $wp-org-sidebar-collapsed} ); - } - } - - body.is-fullscreen-mode & { - top: 111px; - @media screen and ( min-width: $breakpoint-tablet ) { - left: calc( 30% + #{$preview-right-margin * 2} ) !important; - } - } - - .edit-post-visual-editor { - margin: 0; - padding: 0; - } - - // not-selected template - &.not-selected { - .editor-styles-wrapper { - position: relative; - width: 100%; - height: 100%; - - .template-selector-preview__empty-state { - position: absolute; - width: 100%; - text-align: center; - height: 50px; - line-height: 50px; - top: 50%; - margin: -25px 0 0; - } - } - } -} - -.page-template-modal__loading { - position: absolute; - top: 50%; - left: 50%; - transform: translate( -50%, -50% ); - display: flex; - align-items: flex-end; - - .components-spinner { - float: none; - } -} - -// Sidebar modal opener goo. -.sidebar-modal-opener { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - - .template-selector-item__label { - max-width: 300px; - } -} - -.sidebar-modal-opener__warning-modal { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.sidebar-modal-opener__warning-text { - max-width: 300px; - font-size: 1rem; - line-height: 1.5rem; -} - -.sidebar-modal-opener__warning-options { - float: right; - margin-top: 20px; - - .components-button { - margin-left: 12px; - } -} - -.block-iframe-preview { - position: absolute; - top: 0; - left: 0; - transform-origin: top left; - text-align: initial; - margin: 0; - overflow: visible; - min-height: auto; - - // Fallback viewport width. - // Used when BlockFramePreview's viewportWidth prop is undefined. - // Can be overridden through more specific CSS. - width: 1440px; -} - -.block-iframe-preview-body { - margin: 0; - padding: 0; - overflow-x: hidden; - overflow-y: auto; - - > .block-editor, - > .block-editor > .edit-post-visual-editor { - padding: 0; - margin: 0; - } - - // Hide inserter/appender. - .block-list-appender, - .block-editor-inserter { - display: none !important; - visibility: hidden; - position: absolute; - left: -9999vw; - } - - // Preview adjustments. - .editor-styles-wrapper { - // core/cover. - .wp-block-cover { - height: auto; - } - - // TODO: fix the embed iframe blocks properly. - .wp-block-embed__wrapper iframe { - height: auto; - min-height: 400px; - } - } - - // Manual CSS Overrides. Remove after better solutions are in place. - - // Removes empty paragraph placeholders, i.e. "Write Title..." - [data-type='core/paragraph'] [data-rich-text-placeholder] { - display: none; - } - - /* - * Fixes jetpack .wp-block-jetpack-slideshow styles, as the /wp-content/plugins/jetpack/_inc/blocks/vendors~swiper.[hash].css - * file is loaded on block insert, not on page load. After the iframe is grabbing these styles, we can remove this code. - */ - .swiper-wrapper { - display: flex; - } - - .swiper-button-prev, - .swiper-button-next { - display: none; - } - - .swiper-pagination { - text-align: center; - } - - .swiper-pagination-bullet { - border-radius: 100%; - } - - // Fixes cover image spacing and full-width group spacing - .editor-styles-wrapper [data-block] { - &[data-type='core/group'], - &[data-type='core/cover'][data-align='full'] { - margin-top: 0; - margin-bottom: 0; - } - } - - // Tweak template title (post-title) component. - .block-iframe-preview__template-title { - padding-top: 20px; - } - - // flex: column; on the parent and flex-basis: 0; on the child in Safari causes a lot of weird overlapping layout - // issues in the iframe preview: https://github.com/Automattic/wp-calypso/issues/39874 - // Using flex-basis: auto; on the child allows the height to be calculated properly - .wp-block-columns > .block-editor-inner-blocks > .block-editor-block-list__layout > [data-type='core/column'] .block-core-columns { - flex-basis: auto; - } -} diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/contains-missing-block.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/contains-missing-block.js deleted file mode 100644 index 5458c43fabe7f..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/contains-missing-block.js +++ /dev/null @@ -1,28 +0,0 @@ -// Once parsed, missing Blocks have a name prop of `core/missing`. -// see: https://github.com/WordPress/gutenberg/tree/742dbf2ef0e37481a3c14c29f3688aa0cd3cf887/packages/block-library/src/missing -const MISSING_BLOCK_NAME = 'core/missing'; - -/** - * Determines whether the provided collection of Blocks contains any "missing" - * blocks as determined by the presence of the `core/missing` block type. - * - * @param {Array} blocks the collection of block objects to check for "missing" block . - * @returns {boolean} whether the collection blocks contains any missing blocks. - */ -function containsMissingBlock( blocks ) { - return !! blocks.find( ( block ) => { - // If we found a missing block the bale out immediately - if ( block.name === MISSING_BLOCK_NAME ) { - return true; - } - - // If there are innerblocks then recurse down into them... - if ( block.innerBlocks && block.innerBlocks.length ) { - return containsMissingBlock( block.innerBlocks ); - } - - return false; - } ); -} - -export default containsMissingBlock; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/ensure-assets.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/ensure-assets.js deleted file mode 100644 index de7cfb3cdb917..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/ensure-assets.js +++ /dev/null @@ -1,219 +0,0 @@ -/** - * External dependencies - */ -import { reduce, isEmpty, forEach, set, map } from 'lodash'; - -/** - * WordPress dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { removeQueryArgs } from '@wordpress/url'; - -/** - * A full asset URL. - * - * @typedef {string} URL - */ - -/** - * Gutenberg Block. - * - * @typedef {object} GutenbergBlock - * @property {string} clientId A unique id of the block. - * @property {string} name A block name, like "core/paragraph". - * @property {Array} innerBlocks Nested blocks. - * @property {object} attributes An object with attributes, different for each block type. - */ - -/** - * Usage object contains an info that certain property is used inside another object. - * - * @typedef {object} Usage - * @property {string} prop Name of the property. - * @property {Array} path A path inside an object where prop is, defined as list of keys. - */ - -/** - * An asset file that is referenced in blocks. - * - * @typedef {object} Asset - * @property {URL} url A full URL of the asset. - * @property {Array} usages A list of {@link Usage} objects. - */ - -/** - * A collection of {@link Asset} objects, keyed by their URLs. - * - * @typedef {object.} Assets URLs as keys, {@link Asset}.as a values. - */ - -/** - * FetchSession describes a set of blocks and their assets. - * - * @typedef {object} FetchSession - * @property {Array} blocks List of Gutenberg blocks. - * @property {object} blocksByClientId Blocks, keyed by their `clientId` - * @property {Assets} assets A list of assets detected in blocks. - */ - -/** - * Extends an {@link Assets} object with a new asset and updates its usages. - * - * @param {Assets} assets Object containing assets. - * @param {URL} url A full URL of the asset. - * @param {Array} usages A list of {@link Usage} objects. - * @returns {Assets} assets object with the new {@link Asset} included - */ -const addAssetToLoad = ( assets, url, usages ) => { - // Remove resizing query arguments from the URL. - url = removeQueryArgs( url, 'w', 's' ); - - // Use an existing asset for the URL or make a new one. - const asset = assets[ url ] || { - url, - usages: [], - }; - - // Return new result object, extended with the new/updated asset. - return { - ...assets, - [ url ]: { - ...asset, - // Store where exactly block uses id/url so we can update it later. - usages: [ ...asset.usages, ...usages ], - }, - }; -}; - -/** - * This function is used as a reducer iteratee. It checks if the block - * contains any image and if so, enqueues it to be downloaded later. - * - * @param {FetchSession} session Session object. - * @param {GutenbergBlock} block Gutenberg Block object. - * @returns {FetchSession} Updated session object - */ -const findAssetsInBlock = ( session, block ) => { - // Save a reference for the block so we can later easily - // find it without any loops and recursion. - session.blocksByClientId[ block.clientId ] = block; - - // Identify assets in blocks where we expect them. - switch ( block.name ) { - // Both of these blocks use same attribute names for image id and url - // and thus we can share the implementation. - case 'core/cover': - case 'core/image': { - const url = block.attributes.url; - if ( url ) { - session.assets = addAssetToLoad( session.assets, url, [ - { prop: 'url', path: [ block.clientId, 'attributes', 'url' ] }, - { prop: 'id', path: [ block.clientId, 'attributes', 'id' ] }, - ] ); - } - } - case 'core/media-text': { - const url = block.attributes.mediaUrl; - if ( url && block.attributes.mediaType === 'image' ) { - session.assets = addAssetToLoad( session.assets, url, [ - { prop: 'url', path: [ block.clientId, 'attributes', 'mediaUrl' ] }, - { prop: 'id', path: [ block.clientId, 'attributes', 'mediaId' ] }, - ] ); - } - } - case 'core/gallery': { - forEach( block.attributes.images, ( image, i ) => { - session.assets = addAssetToLoad( session.assets, image.url, [ - { prop: 'url', path: [ block.clientId, 'attributes', 'images', i, 'url' ] }, - { prop: 'url', path: [ block.clientId, 'attributes', 'images', i, 'link' ] }, - { prop: 'id', path: [ block.clientId, 'attributes', 'images', i, 'id' ] }, - { prop: 'id', path: [ block.clientId, 'attributes', 'ids', i ] }, - ] ); - } ); - } - } - - // Recursively process all inner blocks. - if ( ! isEmpty( block.innerBlocks ) ) { - return reduce( block.innerBlocks, findAssetsInBlock, session ); - } - - return session; -}; - -/** - * Calls an API that fetches assets and saves the result into the DetectedAssets object. - * - * @param {Assets} assets Assets that were detected from blocks. - * @returns {Promise} Promise that resoves into an object with URLs as keys and fetch results as values. - */ -const fetchAssets = async ( assets ) => { - return await apiFetch( { - method: 'POST', - path: '/fse/v1/sideload/image/batch', - data: { resources: map( assets ) }, - } ).then( ( response ) => - reduce( - assets, - ( fetched, asset ) => { - const { id, source_url } = response.shift(); - return { - ...fetched, - [ asset.url ]: { id, url: source_url }, - }; - }, - {} - ) - ); -}; - -/** - * Takes fetched assets and makes sure all their usages will be changed into - * their new local copies. - * - * @param {FetchSession} session A current session. - * @param {object} fetchedAssets Fetched assets. - * @returns {Array} A promise resolving into an array of blocks. - */ -const getBlocksWithAppliedAssets = ( session, fetchedAssets ) => { - forEach( session.assets, ( asset ) => { - const newAsset = fetchedAssets[ asset.url ]; - if ( ! newAsset ) { - return; - } - forEach( asset.usages, ( usage ) => { - set( session.blocksByClientId, usage.path, newAsset[ usage.prop ] ); - } ); - } ); - - return session.blocks; -}; - -/** - * Analyzes blocks and if they use any external assets, ensures they are - * copied into a local site and are used in blocks instead of the remote ones. - * - * @param {Array} blocks Blocks, as returned by `wp.block.parse` - * @returns {Promise} A promise that resolves into an array of {@link GutenbergBlock} with updated assets - */ -const ensureAssetsInBlocks = async ( blocks ) => { - // Create a FetchSession object by reducing blocks. - const session = reduce( blocks, findAssetsInBlock, { - assets: {}, - blocksByClientId: {}, - blocks, - } ); - - // No assets found. Proceed with insertion right away. - if ( isEmpty( session.assets ) ) { - return blocks; - } - - // Ensure assets are available on the site and replace originals - // with local copies before inserting the template. - return fetchAssets( session.assets ).then( ( fetchedAssets ) => { - return getBlocksWithAppliedAssets( session, fetchedAssets ); - } ); -}; - -export default ensureAssetsInBlocks; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/map-blocks-recursively.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/map-blocks-recursively.js deleted file mode 100644 index d813f2d202c58..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/map-blocks-recursively.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * External dependencies - */ -import { identity } from 'lodash'; -import { cloneBlock } from '@wordpress/blocks'; - -/** - * Recursively maps over a collection of blocks calling the modifier function on - * each to modify it and returning a collection of new block references. - * - * @param {Array} blocks an array of block objects - * @param {Function} modifier a callback function used to modify the blocks - */ -function mapBlocksRecursively( blocks, modifier = identity ) { - return blocks.map( ( block ) => { - // `blocks` is an object. Therefore any changes made here will - // be reflected across all references to the blocks object. To ensure we - // only modify the blocks when needed, we return a new object reference - // for any blocks we modify. This allows us to modify blocks for - // particular contexts. For example we may wish to show blocks - // differently in the preview than we do when they are inserted into the - // editor itself. - block = modifier( cloneBlock( block ) ); - - // Recurse into nested Blocks - if ( block.innerBlocks && block.innerBlocks.length ) { - block.innerBlocks = mapBlocksRecursively( block.innerBlocks, modifier ); - } - - return block; - } ); -} - -export default mapBlocksRecursively; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/replace-placeholders.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/replace-placeholders.js deleted file mode 100644 index 3d4a309added9..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/replace-placeholders.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * External dependencies - */ -import { _x } from '@wordpress/i18n'; - -const PLACEHOLDER_DEFAULTS = { - Address: _x( '123 Main St', 'default address', 'full-site-editing' ), - Phone: _x( '555-555-5555', 'default phone number', 'full-site-editing' ), - CompanyName: _x( 'Your Company Name', 'default company name', 'full-site-editing' ), - Vertical: _x( 'Business', 'default vertical name', 'full-site-editing' ), -}; - -const KEY_MAP = { - CompanyName: 'title', - Address: 'address', - Phone: 'phone', - Vertical: 'vertical', -}; - -const replacePlaceholders = ( pageContent, siteInformation = {} ) => { - if ( ! pageContent ) { - return ''; - } - - return pageContent.replace( /{{(\w+)}}/g, ( match, placeholder ) => { - const defaultValue = PLACEHOLDER_DEFAULTS[ placeholder ]; - const key = KEY_MAP[ placeholder ]; - return siteInformation[ key ] || defaultValue || placeholder; - } ); -}; - -export default replacePlaceholders; diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/tracking.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/tracking.js deleted file mode 100644 index 13c01c21e7fc3..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/utils/tracking.js +++ /dev/null @@ -1,77 +0,0 @@ -// Ensure Tracks Library -window._tkq = window._tkq || []; - -let tracksIdentity = null; - -/** - * Populate `identity` on WPCOM and ATOMIC to enable tracking. - * Always disabled for regular self-hosted installations. - * - * @param {object} identity Info about identity. - * @param {number} identity.userid User ID. - * @param {string} identity.username Username. - * @param {number} identity.blogid Blog ID. - * @returns {void} - */ -export const initializeWithIdentity = ( identity ) => { - tracksIdentity = identity; - window._tkq.push( [ 'identifyUser', identity.userid, identity.username ] ); -}; - -/** - * Track a view of the layout selector. - * - * @param {string} source Source triggering the view. - * @returns {void} - */ -export const trackView = ( source ) => { - if ( ! tracksIdentity ) { - return; - } - window._tkq.push( [ - 'recordEvent', - 'a8c_full_site_editing_template_selector_view', - { - blog_id: tracksIdentity.blogid, - source, - }, - ] ); -}; - -/** - * Track closing of the layout selector. - * - * @returns {void} - */ -export const trackDismiss = () => { - if ( ! tracksIdentity ) { - return; - } - window._tkq.push( [ - 'recordEvent', - 'a8c_full_site_editing_template_selector_dismiss', - { - blog_id: tracksIdentity.blogid, - }, - ] ); -}; - -/** - * Track layout selection. - * - * @param {string} template Template slug. - * @returns {void} - */ -export const trackSelection = ( template ) => { - if ( ! tracksIdentity ) { - return; - } - window._tkq.push( [ - 'recordEvent', - 'a8c_full_site_editing_template_selector_template_selected', - { - blog_id: tracksIdentity.blogid, - template, - }, - ] ); -}; diff --git a/apps/editing-toolkit/package.json b/apps/editing-toolkit/package.json index f2bd38fc5f368..6c85c2b960002 100644 --- a/apps/editing-toolkit/package.json +++ b/apps/editing-toolkit/package.json @@ -94,6 +94,7 @@ "@automattic/composite-checkout": "*", "@automattic/data-stores": "^1.0.0-alpha.1", "@automattic/domain-picker": "^1.0.0-alpha.0", + "@automattic/page-template-modal": "^1.0.0-alpha.0", "@automattic/format-currency": "^1.0.0-alpha.0", "@automattic/i18n-utils": "^1.0.0", "@automattic/launch": "^1.0.0", From 08f86c14c614c977ee2ac72c49401f1effed60c4 Mon Sep 17 00:00:00 2001 From: roo2 Date: Tue, 9 Feb 2021 15:58:48 +1000 Subject: [PATCH 04/13] fix package.json --- apps/editing-toolkit/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/editing-toolkit/package.json b/apps/editing-toolkit/package.json index 6c85c2b960002..49ad291d7688a 100644 --- a/apps/editing-toolkit/package.json +++ b/apps/editing-toolkit/package.json @@ -94,11 +94,11 @@ "@automattic/composite-checkout": "*", "@automattic/data-stores": "^1.0.0-alpha.1", "@automattic/domain-picker": "^1.0.0-alpha.0", - "@automattic/page-template-modal": "^1.0.0-alpha.0", "@automattic/format-currency": "^1.0.0-alpha.0", "@automattic/i18n-utils": "^1.0.0", "@automattic/launch": "^1.0.0", "@automattic/onboarding": "^1.0.0", + "@automattic/page-template-modal": "^1.0.0-alpha.0", "@automattic/plans-grid": "^1.0.0-alpha.0", "@automattic/react-i18n": "^1.0.0-alpha.0", "@automattic/typography": "^1.0.0", From 7ed0642c5b31c20c715f2c95a02c41d442d3086b Mon Sep 17 00:00:00 2001 From: roo2 Date: Tue, 9 Feb 2021 15:59:14 +1000 Subject: [PATCH 05/13] import change from https://github.com/Automattic/wp-calypso/pull/49695 --- .../page-template-modal/index.js | 577 ------------------ .../src/components/page-template-modal.js | 3 +- 2 files changed, 2 insertions(+), 578 deletions(-) delete mode 100644 apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/index.js diff --git a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/index.js b/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/index.js deleted file mode 100644 index e803e29f0881f..0000000000000 --- a/apps/editing-toolkit/editing-toolkit-plugin/starter-page-templates/page-template-modal/index.js +++ /dev/null @@ -1,577 +0,0 @@ -/** - * External dependencies - */ -import { find, isEmpty, reduce, get, keyBy, mapValues, memoize, stubTrue, omit } from 'lodash'; -import classnames from 'classnames'; -import '@wordpress/nux'; -import { __, sprintf } from '@wordpress/i18n'; -import { compose } from '@wordpress/compose'; -import { Button, Modal, Spinner, IconButton } from '@wordpress/components'; -import { withDispatch, withSelect } from '@wordpress/data'; -import { Component } from '@wordpress/element'; -import { parse as parseBlocks } from '@wordpress/blocks'; -import { addFilter, removeFilter } from '@wordpress/hooks'; - -/** - * Internal dependencies - */ -import './styles/starter-page-templates-editor.scss'; -import TemplateSelectorControl from './components/template-selector-control'; -import TemplateSelectorPreview from './components/template-selector-preview'; -import { trackDismiss, trackSelection, trackView } from './utils/tracking'; -import replacePlaceholders from './utils/replace-placeholders'; -import ensureAssets from './utils/ensure-assets'; -import mapBlocksRecursively from './utils/map-blocks-recursively'; -import containsMissingBlock from './utils/contains-missing-block'; - -const INSERTING_HOOK_NAME = 'isInsertingPageTemplate'; -const INSERTING_HOOK_NAMESPACE = 'automattic/full-site-editing/inserting-template'; - -class PageTemplateModal extends Component { - state = { - isLoading: false, - previewedTemplate: null, - error: null, - }; - - // Extract titles for faster lookup. - getTitlesByTemplateSlugs = memoize( ( templates ) => - mapValues( keyBy( templates, 'name' ), 'title' ) - ); - - // Parse templates blocks and memoize them. - getBlocksByTemplateSlugs = memoize( ( templates ) => { - const blocksByTemplateSlugs = reduce( - templates, - ( prev, { name, html } ) => { - prev[ name ] = html - ? parseBlocks( replacePlaceholders( html, this.props.siteInformation ) ) - : []; - return prev; - }, - {} - ); - - // Remove templates that include a missing block - return this.filterTemplatesWithMissingBlocks( blocksByTemplateSlugs ); - } ); - - filterTemplatesWithMissingBlocks( templates ) { - return reduce( - templates, - ( acc, templateBlocks, name ) => { - // Does the template contain any missing blocks? - const templateHasMissingBlocks = containsMissingBlock( templateBlocks ); - - // Only retain the template in the collection if: - // 1. It does not contain any missing blocks - // 2. There are no blocks at all (likely the "blank" template placeholder) - if ( ! templateHasMissingBlocks || ! templateBlocks.length ) { - acc[ name ] = templateBlocks; - } - - return acc; - }, - {} - ); - } - - getBlocksForPreview = memoize( ( previewedTemplate ) => { - const blocks = this.getBlocksByTemplateSlug( previewedTemplate ); - - // Modify the existing blocks returning new block object references. - return mapBlocksRecursively( blocks, function modifyBlocksForPreview( block ) { - // `jetpack/contact-form` has a placeholder to configure form settings - // we need to disable this to show the full form in the preview - if ( - 'jetpack/contact-form' === block.name && - undefined !== block.attributes.hasFormSettingsSet - ) { - block.attributes.hasFormSettingsSet = true; - } - - return block; - } ); - } ); - - getBlocksForSelection = ( selectedTemplate ) => { - const blocks = this.getBlocksByTemplateSlug( selectedTemplate ); - // Modify the existing blocks returning new block object references. - return mapBlocksRecursively( blocks, function modifyBlocksForSelection( block ) { - // Ensure that core/button doesn't link to external template site - if ( 'core/button' === block.name && undefined !== block.attributes.url ) { - block.attributes.url = '#'; - } - - return block; - } ); - }; - - static getDerivedStateFromProps( props, state ) { - // The only time `state.previewedTemplate` isn't set is before `templates` - // are loaded. As soon as we have our `templates`, we set it using - // `this.getDefaultSelectedTemplate`. Afterwards, the user can select a - // different template, but can never un-select it. - // This makes it a reliable indicator for whether the modal has just been launched. - // It's also possible that `templates` are present during initial mount, in which - // case this will be called before `componentDidMount`, which is also fine. - if ( ! state.previewedTemplate && ! isEmpty( props.templates ) ) { - // Show the modal, and select the first template automatically. - return { - previewedTemplate: PageTemplateModal.getDefaultSelectedTemplate( props ), - }; - } - return null; - } - - componentDidMount() { - if ( this.props.isOpen ) { - this.trackCurrentView(); - } - } - - componentDidUpdate( prevProps ) { - // Only track when the modal is first displayed - // and if it didn't already happen during componentDidMount. - if ( ! prevProps.isOpen && this.props.isOpen ) { - this.trackCurrentView(); - } - - // Disable welcome guide right away as it collides with the modal window. - if ( this.props.isWelcomeGuideActive || this.props.areTipsEnabled ) { - this.props.hideWelcomeGuide(); - } - } - - trackCurrentView() { - trackView( 'add-page' ); - } - - static getDefaultSelectedTemplate = ( props ) => { - const blankTemplate = get( props.templates, [ 0, 'name' ] ); - const previouslyChosenTemplate = props._starter_page_template; - - // Usually the "new page" case - if ( ! props.isFrontPage && ! previouslyChosenTemplate ) { - return blankTemplate; - } - - // if the page isn't new, select "Current" as the default template - return 'current'; - }; - - setTemplate = ( name ) => { - // Track selection and mark post as using a template in its postmeta. - trackSelection( name ); - this.props.saveTemplateChoice( name ); - - // Skip setting template if user selects current layout - if ( 'current' === name ) { - this.props.setOpenState( false ); - return; - } - - // Check to see if this is a blank template selection - // and reset the template if so. - if ( 'blank' === name ) { - this.props.insertTemplate( '', [] ); - this.props.setOpenState( false ); - return; - } - - const isHomepageTemplate = find( this.props.templates, { name, category: 'home' } ); - - // Load content. - const blocks = this.getBlocksForSelection( name ); - - // Only overwrite the page title if the template is not one of the Homepage Layouts - const title = isHomepageTemplate ? null : this.getTitleByTemplateSlug( name ); - - // Skip inserting if this is not a blank template - // and there's nothing to insert. - if ( ! blocks || ! blocks.length ) { - this.props.setOpenState( false ); - return; - } - - // Show loading state. - this.setState( { - error: null, - isLoading: true, - } ); - - // Make sure all blocks use local assets before inserting. - this.maybePrefetchAssets( blocks ) - .then( ( blocksWithAssets ) => { - this.setState( { isLoading: false } ); - // Don't insert anything if the user clicked Cancel/Close - // before we loaded everything. - if ( ! this.props.isOpen ) { - return; - } - - this.props.insertTemplate( title, blocksWithAssets ); - this.props.setOpenState( false ); - } ) - .catch( ( error ) => { - this.setState( { - isLoading: false, - error, - } ); - } ); - }; - - maybePrefetchAssets = ( blocks ) => { - return this.props.shouldPrefetchAssets ? ensureAssets( blocks ) : Promise.resolve( blocks ); - }; - - handleConfirmation = ( name ) => { - if ( typeof name !== 'string' ) { - name = this.state.previewedTemplate; - } - - this.setTemplate( name ); - }; - - previewTemplate = ( name ) => { - this.setState( { previewedTemplate: name } ); - - /** - * Determines (based on whether the large preview is able to be visible at the - * current breakpoint) whether or not the Template selection UI interaction model - * should be select _and_ confirm or simply a single "tap to confirm". - */ - const largeTplPreviewVisible = window.matchMedia( '(min-width: 660px)' ).matches; - // Confirm the template when large preview isn't visible - if ( ! largeTplPreviewVisible ) { - this.handleConfirmation( name ); - } - }; - - closeModal = ( event ) => { - // Check to see if the Blur event occurred on the buttons inside of the Modal. - // If it did then we don't want to dismiss the Modal for this type of Blur. - if ( event.target.matches( 'button.template-selector-item__label' ) ) { - return false; - } - - trackDismiss(); - - // Try if we have specific URL to go back to, otherwise go to the page list. - const calypsoifyCloseUrl = get( window, [ 'calypsoifyGutenberg', 'closeUrl' ] ); - window.top.location = calypsoifyCloseUrl || 'edit.php?post_type=page'; - }; - - getBlocksByTemplateSlug( name ) { - if ( name === 'current' ) { - return this.props.currentBlocks; - } - return get( this.getBlocksByTemplateSlugs( this.props.templates ), [ name ], [] ); - } - - getTitleByTemplateSlug( name ) { - return get( this.getTitlesByTemplateSlugs( this.props.templates ), [ name ], '' ); - } - - getTemplateGroups = () => { - if ( ! this.props.templates.length ) { - return null; - } - - const templateGroups = {}; - for ( const template of this.props.templates ) { - for ( const key in template.categories ) { - // Temporarily skip the 'featured' category so that we can expose it at another time. - if ( key !== 'featured' && ! ( key in templateGroups ) ) { - templateGroups[ key ] = template.categories[ key ]; - } - } - } - - return this.sortGroupsNames( templateGroups ); - }; - - sortGroupsNames = ( groups ) => { - return Object.keys( groups ) - .sort() - .reduce( ( result, key ) => { - result[ key ] = groups[ key ]; - return result; - }, {} ); - }; - - getTemplatesForGroup = ( groupName ) => { - if ( ! this.props.templates.length ) { - return null; - } - - if ( 'blank' === groupName ) { - return [ { name: 'blank', title: 'Blank', html: '' } ]; - } - - if ( 'current' === groupName && '' !== this.props._starter_page_template ) { - for ( const template of this.props.templates ) { - if ( this.props._starter_page_template === template.name ) { - return [ template ]; - } - } - } - - const templates = []; - for ( const template of this.props.templates ) { - for ( const key in template.categories ) { - if ( key === groupName ) { - templates.push( template ); - } - } - } - - return templates; - }; - - renderTemplateGroups = () => { - const unfilteredGroups = this.getTemplateGroups(); - const groups = ! this.props.isFrontPage - ? unfilteredGroups - : omit( unfilteredGroups, 'home-page' ); - - if ( ! groups ) { - return null; - } - - const currentGroup = - 'blank' !== this.props._starter_page_template - ? this.renderTemplateGroup( 'current', __( 'Current', 'full-site-editing' ) ) - : null; - - const blankGroup = this.renderTemplateGroup( 'blank', __( 'Blank', 'full-site-editing' ) ); - - const homePageGroup = this.props.isFrontPage - ? this.renderTemplateGroup( 'home-page', __( 'Home Page', 'full-site-editing' ) ) - : null; - - const renderedGroups = []; - for ( const key in groups ) { - renderedGroups.push( this.renderTemplateGroup( key, groups[ key ].title ) ); - } - - return ( - <> - { currentGroup } - { blankGroup } - { homePageGroup } - { renderedGroups } - - ); - }; - - renderTemplateGroup = ( groupName, groupTitle ) => { - const templates = this.getTemplatesForGroup( groupName ); - - if ( ! templates.length ) { - return null; - } - - return this.renderTemplatesList( templates, groupName, groupTitle ); - }; - - renderTemplatesList = ( templatesList, groupName, groupTitle ) => { - if ( ! templatesList.length ) { - return null; - } - - const isCurrentPreview = templatesList[ 0 ]?.name === 'current'; - - const blocksByTemplateSlug = isCurrentPreview - ? { current: this.props.currentBlocks } - : // The raw `templates` prop is not filtered to remove Templates that - // contain missing Blocks. Therefore we compare with the keys of the - // filtered templates from `getBlocksByTemplateSlugs()` and filter this - // list to match. This ensures that the list of Template thumbnails is - // filtered so that it does not include Templates that have missing Blocks. - this.getBlocksByTemplateSlugs( this.props.templates ); - - const templatesWithoutMissingBlocks = Object.keys( blocksByTemplateSlug ); - - const filterOutTemplatesWithMissingBlocks = ( templatesToFilter, filterIn ) => { - return templatesToFilter.filter( ( template ) => filterIn.includes( template.name ) ); - }; - - const filteredTemplatesList = filterOutTemplatesWithMissingBlocks( - templatesList, - templatesWithoutMissingBlocks - ); - - if ( ! filteredTemplatesList.length ) { - return null; - } - - // Skip rendering current preview if there is no page content. - if ( isCurrentPreview && ! blocksByTemplateSlug.current?.length ) { - return null; - } - - return ( -
- { groupTitle } - - -
- ); - }; - - render() { - const { previewedTemplate, isLoading } = this.state; - const { hidePageTitle, isOpen, currentBlocks } = this.props; - - if ( ! isOpen ) { - return null; - } - - // Sometimes currentBlocks is not loaded before getBlocksForPreview is called - // getBlocksForPreview memoizes the function call which causes it to always - // call it with an empty array. We delete the the cache for the function - // to allow it to memoize the loaded currentBlocks. - const currentBlocksPreviewCache = this.getBlocksForPreview.cache.get( 'current' ); - if ( - currentBlocksPreviewCache && - currentBlocks && - currentBlocksPreviewCache.length !== currentBlocks.length - ) { - this.getBlocksForPreview.cache.delete( 'current' ); - } - - return ( - - - -
- { isLoading ? ( -
- - { __( 'Adding layout…', 'full-site-editing' ) } -
- ) : ( - <> -
{ this.renderTemplateGroups() }
- - - ) } -
-
- -
-
- ); - } -} - -export const PageTemplatesPlugin = compose( - withSelect( ( select ) => { - const getMeta = () => select( 'core/editor' ).getEditedPostAttribute( 'meta' ); - const { _starter_page_template } = getMeta(); - const { isOpen } = select( 'automattic/starter-page-layouts' ); - const currentBlocks = select( 'core/editor' ).getBlocks(); - return { - isOpen: isOpen(), - getMeta, - _starter_page_template, - currentBlocks, - currentPostTitle: select( 'core/editor' ).getCurrentPost().title, - postContentBlock: currentBlocks.find( ( block ) => block.name === 'a8c/post-content' ), - isWelcomeGuideActive: select( 'core/edit-post' ).isFeatureActive( 'welcomeGuide' ), // Gutenberg 7.2.0 or higher - areTipsEnabled: select( 'core/nux' ) ? select( 'core/nux' ).areTipsEnabled() : false, // Gutenberg 7.1.0 or lower - }; - } ), - withDispatch( ( dispatch, ownProps ) => { - const editorDispatcher = dispatch( 'core/editor' ); - const { setOpenState } = dispatch( 'automattic/starter-page-layouts' ); - return { - setOpenState, - saveTemplateChoice: ( name ) => { - // Save selected template slug in meta. - const currentMeta = ownProps.getMeta(); - editorDispatcher.editPost( { - meta: { - ...currentMeta, - _starter_page_template: name, - }, - } ); - }, - insertTemplate: ( title, blocks ) => { - // Add filter to let the tracking library know we are inserting a template. - addFilter( INSERTING_HOOK_NAME, INSERTING_HOOK_NAMESPACE, stubTrue ); - - // Set post title. - if ( title ) { - editorDispatcher.editPost( { title } ); - } - - // Replace blocks. - const postContentBlock = ownProps.postContentBlock; - dispatch( 'core/block-editor' ).replaceInnerBlocks( - postContentBlock ? postContentBlock.clientId : '', - blocks, - false - ); - - // Remove filter. - removeFilter( INSERTING_HOOK_NAME, INSERTING_HOOK_NAMESPACE ); - }, - hideWelcomeGuide: () => { - if ( ownProps.isWelcomeGuideActive ) { - // Gutenberg 7.2.0 or higher. - dispatch( 'core/edit-post' ).toggleFeature( 'welcomeGuide' ); - } else if ( ownProps.areTipsEnabled ) { - // Gutenberg 7.1.0 or lower. - dispatch( 'core/nux' ).disableTips(); - } - }, - }; - } ) -)( PageTemplateModal ); diff --git a/packages/page-template-modal/src/components/page-template-modal.js b/packages/page-template-modal/src/components/page-template-modal.js index 29622f2a10f5a..b6f770406496a 100644 --- a/packages/page-template-modal/src/components/page-template-modal.js +++ b/packages/page-template-modal/src/components/page-template-modal.js @@ -273,7 +273,8 @@ export default class PageTemplateModal extends Component { const templateGroups = {}; for ( const template of this.props.templates ) { for ( const key in template.categories ) { - if ( ! ( key in templateGroups ) ) { + // Temporarily skip the 'featured' category so that we can expose it at another time. + if ( key !== 'featured' && ! ( key in templateGroups ) ) { templateGroups[ key ] = template.categories[ key ]; } } From 965eaaa7d5279e717036d97bd2862ce681131491 Mon Sep 17 00:00:00 2001 From: roo2 Date: Tue, 9 Feb 2021 16:24:51 +1000 Subject: [PATCH 06/13] add i18n_text_domain global --- packages/page-template-modal/.eslintrc.js | 3 +++ .../src/components/block-iframe-preview.js | 2 +- .../src/components/page-template-modal.js | 16 ++++++++-------- .../src/components/template-selector-preview.js | 2 +- .../src/utils/replace-placeholders.js | 8 ++++---- 5 files changed, 17 insertions(+), 14 deletions(-) diff --git a/packages/page-template-modal/.eslintrc.js b/packages/page-template-modal/.eslintrc.js index e4b3f3e8cdfcd..0353e59a707d1 100644 --- a/packages/page-template-modal/.eslintrc.js +++ b/packages/page-template-modal/.eslintrc.js @@ -2,4 +2,7 @@ module.exports = { rules: { 'react/react-in-jsx-scope': 0, }, + globals: { + __i18n_text_domain__: true, + }, }; diff --git a/packages/page-template-modal/src/components/block-iframe-preview.js b/packages/page-template-modal/src/components/block-iframe-preview.js index 7a0729a1a7d7f..db380c96151db 100644 --- a/packages/page-template-modal/src/components/block-iframe-preview.js +++ b/packages/page-template-modal/src/components/block-iframe-preview.js @@ -219,7 +219,7 @@ const BlockFramePreview = ( {