Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[AMP Stories] Add template Inserter #2029

Merged
merged 25 commits into from
Apr 1, 2019
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
bf424fc
Start adding template inserter.
miina Mar 25, 2019
7e0b682
Make template inserter (kind of) functional.
miina Mar 26, 2019
738d975
Improve style.
miina Mar 26, 2019
1932e3f
Improve API request.
miina Mar 26, 2019
9273dc0
Remove unused params.
miina Mar 26, 2019
f0b70f9
Rework getting reusable blocks.
miina Mar 27, 2019
1306f9c
Remove unused code.
miina Mar 27, 2019
8ae5c64
Style adjustments.
miina Mar 27, 2019
3c43aeb
Add icons for the template inserter and reordering.
miina Mar 28, 2019
7eb3a3e
Allow saving custom templates.
miina Mar 28, 2019
24bf37c
Update dashicon.
miina Mar 28, 2019
364a61f
Prevent endless loading.
miina Mar 28, 2019
3a53207
Merge remote-tracking branch 'origin/amp-stories-redux' into amp-stor…
miina Mar 28, 2019
788d22d
Style fix.
miina Mar 28, 2019
46834cf
Add todo notice.
miina Mar 29, 2019
4f3baf5
Use pre-existing pageIcon file
swissspidy Apr 1, 2019
9f4d919
Use pre-existing BlockPreview component
swissspidy Apr 1, 2019
de77363
Extract all SVGs into separate files
swissspidy Apr 1, 2019
379d51d
Fix reorder cancel button styling
swissspidy Apr 1, 2019
6859bfd
ESLint fix
swissspidy Apr 1, 2019
cdec4ae
Merge branch 'amp-stories-redux' into amp-story/1902-template_inserter
swissspidy Apr 1, 2019
abd0879
Remove unused import after merge
swissspidy Apr 1, 2019
d72d6ee
Add role to PluginBlockSettingsMenuItem
swissspidy Apr 1, 2019
53def88
Remove allowedBlockNames todo
swissspidy Apr 1, 2019
a5ee722
Rename allowedBlockNames to allowedBlocks as per upstream discussion
swissspidy Apr 1, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions assets/css/amp-editor-story-blocks.css
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,80 @@ div[data-type="amp/amp-story-page"] .wp-block-image {
* 10. Custom Components
*/

/*
* Template Inserter Component.
*/
.amp-stories__template-inserter__popover.components-popover .components-popover__content:not(.is-mobile) {
width: 386px;
height: 400px;
padding: 2px;
transform: translateX(-90%);
}

@media (min-width: 782px) {
.amp-stories__template-inserter__popover.block-editor-inserter__popover:not(.is-mobile) > .components-popover__content {
overflow-y: scroll;
}
}

.edit-post-layout:not(.is-sidebar-opened) .amp-stories__template-inserter__popover.components-popover .components-popover__content:not(.is-mobile) {
left: initial;
transform: none;
}

.amp-stories__template-inserter__popover.block-editor-inserter__popover .block-editor-block-types-list {
margin: 0;
padding: 5px;
}

.amp-stories__template-inserter__popover.components-popover.is-top .components-popover__content {
bottom: initial;
}

.amp-stories__editor-inserter__results li {
display: block;
list-style-type: none;
width: 160px;
height: 268px;
float: left;
margin: 15px;
}

.amp-stories__editor-inserter__results li amp-story-page,
.amp-stories__editor-inserter__results li amp-story-grid-layer {
display: block;
width: 100%;
height: 100%;
}

.amp-stories__editor-inserter__results .block-editor-block-preview {
width: 160px;
height: 268px;
margin: 10px;
padding: 0;
}

.amp-stories__editor-inserter__results .block-editor-block-preview .block-editor-block-preview__content,
.amo-stories__editor-inserter__results .components-placeholder {
padding: 0;
width: 100%;
height: 100%;
}

.amp-stories__blank-page-inserter {
height: 100%;
width: 100%;
}

.amp-stories__blank-page-inserter svg {
margin: 0 auto;
}

.amp-stories__editor-inserter__results .block-editor-block-preview {
pointer-events: initial;
}


/*
* Preview Picker component
*
Expand Down
1 change: 1 addition & 0 deletions assets/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export { default as StoryControls } from './story-controls';
export { default as Shortcuts } from './shortcuts';
export { default as StoryBlockDropZone } from './story-block-drop-zone';
export { default as StoryBlockMover } from './block-mover';
export { default as TemplateInserter } from './template-inserter';
export { default as FontFamilyPicker } from './font-family-picker';
export { default as withAmpStorySettings } from './with-amp-story-settings';
export { default as withAnimationControls } from './with-animation-controls';
Expand Down
27 changes: 6 additions & 21 deletions assets/src/components/story-controls.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@
* to add new pages and start/stop reordering pages.
*/

/**
* Internal dependencies
*/
import { TemplateInserter } from './';

/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { IconButton, Button } from '@wordpress/components';
import { Fragment } from '@wordpress/element';
import { Inserter } from '@wordpress/editor';
import { withDispatch, withSelect } from '@wordpress/data';
import { compose } from '@wordpress/compose';

Expand Down Expand Up @@ -38,26 +42,7 @@ function StoryControls( { isReordering, startReordering, saveOrder, resetOrder }

return (
<Fragment>
<Inserter
rootClientId=""
clientId=""
isAppender={ false }
position="bottom left"
title={ __( 'Add New Page', 'amp' ) }
style={ { position: 'relative' } }
renderToggle={ ( { onToggle, disabled, isOpen } ) => (
<IconButton
icon="insert"
label={ __( 'Add New Page', 'amp' ) }
labelPosition="bottom left"
onClick={ onToggle }
className="editor-inserter__toggle"
aria-haspopup="true"
aria-expanded={ isOpen }
disabled={ disabled }
/>
) }
/>
<TemplateInserter />
<IconButton
className="amp-story-controls-reorder"
icon="sort"
Expand Down
44 changes: 44 additions & 0 deletions assets/src/components/template-inserter/block-preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/**
* External dependencies
*/
import { noop } from 'lodash';

/**
* WordPress dependencies
*/
import { createBlock } from '@wordpress/blocks';
import { Disabled } from '@wordpress/components';
// import { BlockEdit } from '@wordpress/block-editor';

/**
* Block Preview Component: It renders a preview given a block name and attributes.
*
* @param {Object} props Component props.
*
* @return {WPElement} Rendered element.
*/
function BlockPreview( props ) {
return (
<button onClick={ props.onClick } className="components-button editor-block-preview block-editor-block-preview">
<BlockPreviewContent { ...props } />
</button>
);
}

export function BlockPreviewContent( { name, attributes } ) {
// @todo Importing this outside of the function causes error for some reason.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is because the new block-editor package is not yet listed in wpDependencies in webpack.config.js nor in package.json.

You can still use @wordpress/editor for now. It just proxies to the new package behind the scenes for BC.

Alternatively, since I am probably going to merge #2000 today and doing some rewriting there from @wordpress/editor to @wordpress/block-editor, you could then just merge in the latest changes from the amp-stories-redux branch.

const BlockEdit = wp.blockEditor.BlockEdit;
const block = createBlock( name, attributes );
return (
<Disabled className="editor-block-preview__content block-editor-block-preview__content editor-styles-wrapper" aria-hidden>
<BlockEdit
name={ block.name }
focus={ false }
attributes={ block.attributes }
setAttributes={ noop }
/>
</Disabled>
);
}

export default BlockPreview;
20 changes: 20 additions & 0 deletions assets/src/components/template-inserter/icon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const pageIcon = (
<svg width="86" height="96" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#a)">
<path d="M71.115 91.034H1.654V1.655h52.923l16.538 16.552v72.828z" fill="#fff" />
<path d="M54.577 1.655v16.552h16.538L54.577 1.655z" fill="#A9A9A9" />
<path d="M71.115 19.862H54.577a1.66 1.66 0 0 1-1.654-1.655V1.655c0-.91.744-1.655 1.654-1.655.447 0 .86.182 1.174.48L72.29 17.032a1.65 1.65 0 0 1 0 2.334 1.652 1.652 0 0 1-1.175.496zm-14.884-3.31H67.13L56.23 5.644v10.908z" fill="#686868" />
<path d="M38.038 92.69H1.654A1.66 1.66 0 0 1 0 91.034V1.655C0 .745.744 0 1.654 0h52.923c.447 0 .86.182 1.174.48L72.29 17.032c.297.314.48.728.48 1.175V48c0 .91-.745 1.655-1.655 1.655S69.462 48.91 69.462 48V18.886L53.898 3.31H3.308v86.07h34.73c.91 0 1.654.744 1.654 1.655a1.66 1.66 0 0 1-1.654 1.655z" fill="#686868" />
<path d="M64.5 94.345c10.96 0 19.846-8.893 19.846-19.862 0-10.97-8.885-19.862-19.846-19.862-10.96 0-19.846 8.892-19.846 19.862 0 10.97 8.885 19.862 19.846 19.862z" fill="#A9A9A9" />
<path d="M64.5 96C52.625 96 43 86.367 43 74.483s9.625-21.517 21.5-21.517S86 62.599 86 74.483c-.017 11.884-9.625 21.5-21.5 21.517zm0-39.724c-10.055 0-18.192 8.143-18.192 18.207 0 10.063 8.137 18.207 18.192 18.207s18.192-8.144 18.192-18.207c-.016-10.047-8.153-18.19-18.192-18.207z" fill="#686868" />
<path d="M64.5 86.069a1.66 1.66 0 0 1-1.654-1.655V64.552c0-.91.744-1.655 1.654-1.655.91 0 1.654.744 1.654 1.655v19.862a1.66 1.66 0 0 1-1.654 1.655z" fill="#fff" /><path d="M74.423 76.138H54.577a1.66 1.66 0 0 1-1.654-1.655c0-.91.744-1.655 1.654-1.655h19.846c.91 0 1.654.745 1.654 1.655a1.66 1.66 0 0 1-1.654 1.655z" fill="#fff" />
</g>
<defs>
<clipPath id="a">
<path fill="#fff" d="M0 0h86v96H0z" />
</clipPath>
</defs>
</svg>
);

export default pageIcon;
163 changes: 163 additions & 0 deletions assets/src/components/template-inserter/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Dropdown, IconButton, Spinner } from '@wordpress/components';
import { Component } from '@wordpress/element';
import { dispatch } from '@wordpress/data';
import { parse, createBlock } from '@wordpress/blocks';
const { apiFetch } = wp;

const blocksRestBase = 'blocks';

/**
* Internal dependencies
*/
import BlockPreview from './block-preview';
import pageIcon from './icon';

class TemplateInserter extends Component {
constructor() {
super( ...arguments );

this.onToggle = this.onToggle.bind( this );

this.state = {
reusableBlocks: null,
};
}

componentDidMount() {
// This is used for making sure that once the response finishes the component actually still exists.
this.isComponentMounted = true;
this.getReusableBlocks();
}

componentWillUnmount() {
this.isComponentMounted = false;
}

onToggle( isOpen ) {
const { onToggle } = this.props;

// Surface toggle callback to parent component
if ( onToggle ) {
onToggle( isOpen );
}
}

getReusableBlocks() {
if ( ! this.isComponentMounted ) {
return;
}

if ( null !== this.state.reusableBlocks ) {
this.setState( { reusableBlocks: null } );
}

// @todo We only need reusable blocks that can be used by AMP Stories.
const blocksRequest = this.lastRequest = apiFetch( {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking out loud here

One downside of doing the API request in the component is that we can't easily use the component twice without duplicating requests.

I was looking a bit at the Inserter and InserterMenu components in Gutenberg and how they use fetchReusableBlocks and some other functions to do the data fetching. Are there perhaps some commonalities that we could re-use (maybe in combination with an api-fetch middleware to do the filtering) or copy over to our own data store?

We only need reusable blocks that can be used by AMP Stories.

This goes the other way around too: In all other editors we don't want to show AMP Stories templates. ?search doesn't work for that. An easy solution could be using a taxonomy behind the scenes. Post meta could work too, but doesn't scale well.

Copy link
Contributor Author

@miina miina Mar 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?search doesn't work for that. An easy solution could be using a taxonomy behind the scenes. Post meta could work too, but doesn't scale well.

Agreed, ?search was added as more of a placeholder for preventing fetching all the reusable blocks which would then just cause errors on insertion or due to unregistered block types. I was thinking that implementing taxonomy / meta could be done either in #2010 / #2011 since those PR-s involve creating template posts.

Are there perhaps some commonalities that we could re-use (maybe in combination with an api-fetch middleware to do the filtering) or copy over to our own data store?

Actually, we could also use the default experimental dispatch/selector for getting all reusable blocks and filter those. I thought initially that this might break the logic due to potentially including unregistered blocks, however, it looks like allowing core/template as an allowed block should resolve that issue. I'll make the change to test it out and then if it seems like it would make sense to manage the reusable blocks in our own data store then we can rework that later. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would mean though that we'd need to rework it anyway once taxonomy comes in. So yes, perhaps it does make sense to move it to our own data store.

Copy link
Contributor Author

@miina miina Mar 27, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could just filter by having amp-story-page as the root block of the template, and not allow anything else, maybe just that could make sense for now, too. Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me. Let's stick with the current approach and iterate in follow-up PRs.

path: `/wp/v2/${ blocksRestBase }?search=amp-story-page`,
} )
.then( ( response ) => {
// Check if it's the result of the last request.
if ( this.isComponentMounted && blocksRequest === this.lastRequest && response ) {
this.setState( { reusableBlocks: response } );
}
} )
.catch( ( error ) => {
if ( this.isComponentMounted && blocksRequest === this.lastRequest ) {
this.setState( { reusableBlocks: {
error: true,
message: error.message,
} } );
}
} );
}

render() {
const { insertBlocks, insertBlock } = dispatch( 'core/block-editor' );
return (
<Dropdown
className="editor-inserter block-editor-inserter"
contentClassName="amp-stories__template-inserter__popover is-bottom editor-inserter__popover block-editor-inserter__popover"
onToggle={ this.onToggle }
expandOnMobile
renderToggle={ ( { onToggle, isOpen } ) => (
<IconButton
icon="insert"
label={ __( 'Insert Template', 'amp' ) }
onClick={ onToggle }
className="editor-inserter__amp-inserter"
aria-haspopup="true"
aria-expanded={ isOpen }
/>
) }
renderContent={ ( { onClose } ) => {
const onSelect = ( name, content ) => {
if ( 'core/block' === name ) {
const blocks = parse( content );
insertBlocks( blocks );
} else {
const block = createBlock( name );
insertBlock( block );
}
onClose();
};

const reusableBlocks = this.state.reusableBlocks;
if ( ! reusableBlocks ) {
return (
<Spinner />
);
}

if ( reusableBlocks.error ) {
const errorMessage = sprintf( __( 'Loading templates failed: %s', 'amp' ), reusableBlocks.error.message );
return (
<div>
{ errorMessage }
</div>
);
}

return (
<div key="template-list" className="amp-stories__editor-inserter__menu">
<div
className="amp-stories__editor-inserter__results"
tabIndex="0"
role="region"
aria-label={ __( 'Available templates', 'amp' ) }
>
<div role="list" className="editor-block-types-list block-editor-block-types-list">
<div className="editor-block-preview block-editor-block-preview">
<IconButton
icon={ pageIcon }
label={ __( 'Blank Page', 'amp' ) }
onClick={ () => {
onSelect( 'amp/amp-story-page' );
} }
className="amp-stories__blank-page-inserter editor-block-preview__content block-editor-block-preview__content editor-styles-wrapper"
/>
</div>
{ reusableBlocks && reusableBlocks.map( ( item ) =>
<BlockPreview
key="template-preview"
name="core/block"
attributes={ { ref: item.id } }
onClick={ () => {
onSelect( 'core/block', item.content.raw );
} }
/>
) }
</div>
</div>
</div>
);
} }
/>
);
}
}

export default TemplateInserter;
1 change: 1 addition & 0 deletions assets/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const ALLOWED_CHILD_BLOCKS = [
'core/verse',
'core/video',
'amp/amp-story-text',
'core/block', // Reusable blocks.
];

export const ALLOWED_BLOCKS = [
Expand Down