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

Template Parts: Show existing template parts and a list of block patterns at creation flow. #38814

Merged
merged 12 commits into from
Feb 18, 2022
1 change: 1 addition & 0 deletions packages/block-editor/src/components/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export { default as WritingFlow } from './writing-flow';
export { default as useBlockDisplayInformation } from './use-block-display-information';
export { default as __unstableIframe } from './iframe';
export { default as __experimentalUseNoRecursiveRenders } from './use-no-recursive-renders';
export { default as __experimentalBlockPatternsList } from './block-patterns-list';

/*
* State Related Components
Expand Down
132 changes: 61 additions & 71 deletions packages/block-library/src/template-part/edit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,28 @@ import {
store as blockEditorStore,
} from '@wordpress/block-editor';
import {
Dropdown,
ToolbarGroup,
ToolbarButton,
Spinner,
Modal,
} from '@wordpress/components';
import { __, sprintf } from '@wordpress/i18n';
import { store as coreStore } from '@wordpress/core-data';
import { useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import TemplatePartPlaceholder from './placeholder';
import TemplatePartSelection from './selection';
import TemplatePartSelectionModal from './selection-modal';
import { TemplatePartAdvancedControls } from './advanced-controls';
import TemplatePartInnerBlocks from './inner-blocks';
import { createTemplatePartId } from './utils/create-template-part-id';
import {
useAlternativeBlockPatterns,
useAlternativeTemplateParts,
useTemplatePartArea,
} from './utils/hooks';

export default function TemplatePartEdit( {
attributes,
Expand All @@ -39,29 +45,22 @@ export default function TemplatePartEdit( {
} ) {
const { slug, theme, tagName, layout = {} } = attributes;
const templatePartId = createTemplatePartId( theme, slug );

const [ hasAlreadyRendered, RecursionProvider ] = useNoRecursiveRenders(
templatePartId
);
const [
isTemplatePartSelectionOpen,
setIsTemplatePartSelectionOpen,
] = useState( false );

// Set the postId block attribute if it did not exist,
// but wait until the inner blocks have loaded to allow
// new edits to trigger this.
const {
isResolved,
innerBlocks,
isMissing,
defaultWrapper,
area,
enableSelection,
youknowriad marked this conversation as resolved.
Show resolved Hide resolved
hasResolvedReplacements,
} = useSelect(
const { isResolved, innerBlocks, isMissing, area } = useSelect(
( select ) => {
const {
getEditedEntityRecord,
getEntityRecords,
hasFinishedResolution,
} = select( coreStore );
const { getEditedEntityRecord, hasFinishedResolution } = select(
coreStore
);
const { getBlocks } = select( blockEditorStore );

const getEntityArgs = [
Expand All @@ -73,54 +72,33 @@ export default function TemplatePartEdit( {
? getEditedEntityRecord( ...getEntityArgs )
: null;
const _area = entityRecord?.area || attributes.area;

// Check whether other entities exist for switching/selection.
const availableReplacementArgs = [
'postType',
'wp_template_part',
_area && 'uncategorized' !== _area && { area: _area },
];
const matchingReplacements = getEntityRecords(
...availableReplacementArgs
);
const _enableSelection = templatePartId
? matchingReplacements?.length > 1
: matchingReplacements?.length > 0;

const hasResolvedEntity = templatePartId
? hasFinishedResolution(
'getEditedEntityRecord',
getEntityArgs
)
: false;

// FIXME: @wordpress/block-library should not depend on @wordpress/editor.
// Blocks can be loaded into a *non-post* block editor.
// eslint-disable-next-line @wordpress/data-no-store-string-literals
const defaultWrapperElement = select( 'core/editor' )
.__experimentalGetDefaultTemplatePartAreas()
.find( ( { area: value } ) => value === _area )?.area_tag;

return {
innerBlocks: getBlocks( clientId ),
isResolved: hasResolvedEntity,
isMissing: hasResolvedEntity && isEmpty( entityRecord ),
defaultWrapper: defaultWrapperElement || 'div',
area: _area,
enableSelection: _enableSelection,
hasResolvedReplacements: hasFinishedResolution(
'getEntityRecords',
availableReplacementArgs
),
};
},
[ templatePartId, clientId ]
);

const { templateParts } = useAlternativeTemplateParts(
area,
templatePartId
);
const blockPatterns = useAlternativeBlockPatterns( area, clientId );
const hasReplacements = !! templateParts.length || !! blockPatterns.length;
const areaObject = useTemplatePartArea( area );
const blockProps = useBlockProps();
const isPlaceholder = ! slug;
const isEntityAvailable = ! isPlaceholder && ! isMissing && isResolved;
const TagName = tagName || defaultWrapper;
const TagName = tagName || areaObject.tagName;

// We don't want to render a missing state if we have any inner blocks.
// A new template part is automatically created if we have any inner blocks but no entity.
Expand Down Expand Up @@ -160,43 +138,31 @@ export default function TemplatePartEdit( {
setAttributes={ setAttributes }
isEntityAvailable={ isEntityAvailable }
templatePartId={ templatePartId }
defaultWrapper={ defaultWrapper }
defaultWrapper={ areaObject.tagName }
/>
{ isPlaceholder && (
<TagName { ...blockProps }>
<TemplatePartPlaceholder
area={ attributes.area }
templatePartId={ templatePartId }
clientId={ clientId }
setAttributes={ setAttributes }
enableSelection={ enableSelection }
hasResolvedReplacements={ hasResolvedReplacements }
onOpenSelectionModal={ () =>
setIsTemplatePartSelectionOpen( true )
}
/>
</TagName>
) }
{ isEntityAvailable && enableSelection && (
{ isEntityAvailable && hasReplacements && (
<BlockControls>
<ToolbarGroup className="wp-block-template-part__block-control-group">
<Dropdown
className="wp-block-template-part__preview-dropdown-button"
contentClassName="wp-block-template-part__preview-dropdown-content"
position="bottom right left"
renderToggle={ ( { isOpen, onToggle } ) => (
<ToolbarButton
aria-expanded={ isOpen }
onClick={ onToggle }
>
{ __( 'Replace' ) }
</ToolbarButton>
) }
renderContent={ ( { onClose } ) => (
<TemplatePartSelection
setAttributes={ setAttributes }
onClose={ onClose }
area={ area }
templatePartId={ templatePartId }
/>
) }
/>
<ToolbarButton
onClick={ () =>
setIsTemplatePartSelectionOpen( true )
}
>
{ __( 'Replace' ) }
</ToolbarButton>
</ToolbarGroup>
</BlockControls>
) }
Expand All @@ -215,6 +181,30 @@ export default function TemplatePartEdit( {
<Spinner />
</TagName>
) }
{ isTemplatePartSelectionOpen && (
<Modal
className="block-editor-template-part__selection-modal"
title={ sprintf(
// Translators: %s as template part area title ("Header", "Footer", etc.).
__( 'Choose a %s' ),
areaObject.label.toLowerCase()
) }
closeLabel={ __( 'Cancel' ) }
onRequestClose={ () =>
setIsTemplatePartSelectionOpen( false )
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
}
}
isFullScreen

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried this but the behavior is a bit weird, the width of the modal changes as the patterns load.

>
<TemplatePartSelectionModal
templatePartId={ templatePartId }
clientId={ clientId }
area={ area }
setAttributes={ setAttributes }
onClose={ () =>
setIsTemplatePartSelectionOpen( false )
}
/>
</Modal>
) }
</RecursionProvider>
);
}
78 changes: 78 additions & 0 deletions packages/block-library/src/template-part/edit/placeholder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**
* WordPress dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Placeholder, Button, Spinner } from '@wordpress/components';
import { useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import {
useAlternativeBlockPatterns,
useAlternativeTemplateParts,
useCreateTemplatePartFromBlocks,
useTemplatePartArea,
} from './utils/hooks';
import TitleModal from './title-modal';

export default function TemplatePartPlaceholder( {
area,
clientId,
templatePartId,
onOpenSelectionModal,
setAttributes,
} ) {
const { templateParts, isResolving } = useAlternativeTemplateParts(
area,
templatePartId
);
const blockPatterns = useAlternativeBlockPatterns( area, clientId );
const [ showTitleModal, setShowTitleModal ] = useState( false );
const areaObject = useTemplatePartArea( area );
const createFromBlocks = useCreateTemplatePartFromBlocks(
area,
setAttributes
);

return (
<Placeholder
icon={ areaObject.icon }
label={ areaObject.label }
instructions={ sprintf(
// Translators: %s as template part area title ("Header", "Footer", etc.).
__( 'Choose an existing %s or create a new one.' ),
areaObject.label.toLowerCase()
) }
>
{ isResolving && <Spinner /> }

{ ! isResolving &&
!! ( templateParts.length || blockPatterns.length ) && (
<Button variant="primary" onClick={ onOpenSelectionModal }>
{ __( 'Choose' ) }
</Button>
) }

{ ! isResolving && (
<Button
variant="secondary"
onClick={ () => {
setShowTitleModal( true );
} }
>
{ __( 'Start blank' ) }
</Button>
) }
{ showTitleModal && (
<TitleModal
areaLabel={ areaObject.label }
onClose={ () => setShowTitleModal( false ) }
onSubmit={ ( title ) => {
createFromBlocks( [], title );
} }
/>
) }
</Placeholder>
);
}
Loading