diff --git a/package-lock.json b/package-lock.json index e831f80ec58887..9daff0e044507a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10256,7 +10256,6 @@ "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/keycodes": "file:packages/keycodes", - "@wordpress/priority-queue": "file:packages/priority-queue", "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/shortcode": "file:packages/shortcode", "@wordpress/token-list": "file:packages/token-list", @@ -10405,6 +10404,7 @@ "@babel/runtime": "^7.9.2", "@wordpress/element": "file:packages/element", "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", + "@wordpress/priority-queue": "file:packages/priority-queue", "clipboard": "^2.0.1", "lodash": "^4.17.15", "mousetrap": "^1.6.5", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 873921a8791098..93528e43f6db47 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -43,7 +43,6 @@ "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/keycodes": "file:../keycodes", - "@wordpress/priority-queue": "file:../priority-queue", "@wordpress/rich-text": "file:../rich-text", "@wordpress/shortcode": "file:../shortcode", "@wordpress/token-list": "file:../token-list", diff --git a/packages/block-editor/src/components/inserter/block-patterns.js b/packages/block-editor/src/components/inserter/block-patterns.js index 9fc7f67413469b..2da6a210fd0346 100644 --- a/packages/block-editor/src/components/inserter/block-patterns.js +++ b/packages/block-editor/src/components/inserter/block-patterns.js @@ -10,12 +10,12 @@ import { useMemo, useCallback } from '@wordpress/element'; import { parse } from '@wordpress/blocks'; import { ENTER, SPACE } from '@wordpress/keycodes'; import { __, _x } from '@wordpress/i18n'; +import { useAsyncList } from '@wordpress/compose'; /** * Internal dependencies */ import BlockPreview from '../block-preview'; -import useAsyncList from './use-async-list'; import InserterPanel from './panel'; import { searchItems } from './search-items'; import InserterNoResults from './no-results'; diff --git a/packages/block-library/src/template-part/edit/placeholder.js b/packages/block-library/src/template-part/edit/placeholder.js deleted file mode 100644 index f3b3f7bc43e48d..00000000000000 --- a/packages/block-library/src/template-part/edit/placeholder.js +++ /dev/null @@ -1,131 +0,0 @@ -/** - * WordPress dependencies - */ -import { useEntityBlockEditor, EntityProvider } from '@wordpress/core-data'; -import { __ } from '@wordpress/i18n'; -import { BlockPreview } from '@wordpress/block-editor'; -import { useState, useCallback } from '@wordpress/element'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { cleanForSlug } from '@wordpress/url'; -import { Placeholder, TextControl, Button } from '@wordpress/components'; -import { layout } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import useTemplatePartPost from './use-template-part-post'; - -function TemplatePartPreview() { - const [ blocks ] = useEntityBlockEditor( 'postType', 'wp_template_part' ); - return ( -
-
- { __( 'Preview' ) } -
- -
- ); -} - -export default function TemplatePartPlaceholder( { setAttributes } ) { - const [ slug, _setSlug ] = useState( '' ); - const [ theme, setTheme ] = useState( '' ); - const [ help, setHelp ] = useState(); - - // Try to find an existing template part. - const postId = useTemplatePartPost( null, slug, theme ); - - // If found, get its preview. - const preview = useSelect( - ( select ) => { - if ( ! postId ) { - return; - } - const templatePart = select( 'core' ).getEntityRecord( - 'postType', - 'wp_template_part', - postId - ); - if ( templatePart ) { - return ( - - - - ); - } - }, - [ postId ] - ); - - const setSlug = useCallback( ( nextSlug ) => { - _setSlug( nextSlug ); - setHelp( cleanForSlug( nextSlug ) ); - }, [] ); - - const { saveEntityRecord } = useDispatch( 'core' ); - const onChooseOrCreate = useCallback( async () => { - const nextAttributes = { slug, theme }; - if ( postId !== undefined && postId !== null ) { - // Existing template part found. - nextAttributes.postId = postId; - } else { - // Create a new template part. - try { - const cleanSlug = cleanForSlug( slug ); - const templatePart = await saveEntityRecord( - 'postType', - 'wp_template_part', - { - title: cleanSlug, - status: 'publish', - slug: cleanSlug, - meta: { theme }, - } - ); - nextAttributes.postId = templatePart.id; - } catch ( err ) { - setHelp( __( 'Error adding template.' ) ); - } - } - setAttributes( nextAttributes ); - }, [ postId, slug, theme ] ); - return ( - -
- - -
- { preview } - -
- ); -} diff --git a/packages/block-library/src/template-part/edit/placeholder/index.js b/packages/block-library/src/template-part/edit/placeholder/index.js new file mode 100644 index 00000000000000..1da611ae523378 --- /dev/null +++ b/packages/block-library/src/template-part/edit/placeholder/index.js @@ -0,0 +1,171 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useState, useCallback } from '@wordpress/element'; +import { useDispatch } from '@wordpress/data'; +import { cleanForSlug } from '@wordpress/url'; +import { + Placeholder, + TextControl, + Button, + TabPanel, +} from '@wordpress/components'; +import { layout } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import useTemplatePartPost from '../use-template-part-post'; +import TemplatePartPreviews from './template-part-previews'; + +const HELP_PHRASES = { + initial: __( 'Please add a name and theme for your new Template Part.' ), + unavailable: __( + 'Name and theme combination already in use, please try another.' + ), + available: __( 'This name is available!' ), + error: __( 'Error adding template.' ), +}; + +export default function TemplatePartPlaceholder( { setAttributes } ) { + const [ slug, _setSlug ] = useState( '' ); + const [ theme, _setTheme ] = useState( '' ); + const [ help, setHelp ] = useState( '' ); + + // Try to find an existing template part. + const postId = useTemplatePartPost( null, slug, theme ); + + const setSlug = useCallback( + ( nextSlug ) => { + _setSlug( nextSlug ); + if ( help ) { + setHelp( '' ); + } + }, + [ help ] + ); + + const setTheme = useCallback( + ( nextTheme ) => { + _setTheme( nextTheme ); + if ( help ) { + setHelp( '' ); + } + }, + [ help ] + ); + + const getHelpPhrase = () => { + if ( ! slug || ! theme ) { + return HELP_PHRASES.initial; + } else if ( postId ) { + return HELP_PHRASES.unavailable; + } + + return HELP_PHRASES.available; + }; + + const { saveEntityRecord } = useDispatch( 'core' ); + const onCreate = useCallback( async () => { + const nextAttributes = { slug, theme }; + // Create a new template part. + try { + const cleanSlug = cleanForSlug( slug ); + const templatePart = await saveEntityRecord( + 'postType', + 'wp_template_part', + { + title: cleanSlug, + status: 'publish', + slug: cleanSlug, + meta: { theme }, + } + ); + nextAttributes.postId = templatePart.id; + } catch ( err ) { + setHelp( HELP_PHRASES.error ); + } + setAttributes( nextAttributes ); + }, [ postId, slug, theme ] ); + + const [ filterValue, setFilterValue ] = useState( '' ); + + const createTab = ( + <> +
+ + +
+
+ { help || getHelpPhrase() } +
+ + + ); + + const selectTab = ( + <> + + +
+ +
+ + ); + + return ( + + + { ( tab ) => { + if ( tab.name === 'create' ) { + return createTab; + } + return selectTab; + } } + + + ); +} diff --git a/packages/block-library/src/template-part/edit/placeholder/template-part-previews.js b/packages/block-library/src/template-part/edit/placeholder/template-part-previews.js new file mode 100644 index 00000000000000..2d4d87801c0cd0 --- /dev/null +++ b/packages/block-library/src/template-part/edit/placeholder/template-part-previews.js @@ -0,0 +1,219 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { parse } from '@wordpress/blocks'; +import { useMemo, useCallback } from '@wordpress/element'; +import { ENTER, SPACE } from '@wordpress/keycodes'; +import { __, sprintf } from '@wordpress/i18n'; +import { BlockPreview } from '@wordpress/block-editor'; +import { Icon } from '@wordpress/components'; +import { useAsyncList } from '@wordpress/compose'; + +/** + * External dependencies + */ +import { groupBy, uniq, deburr } from 'lodash'; + +function PreviewPlaceholder() { + return ( +
+ ); +} + +function TemplatePartItem( { templatePart, setAttributes } ) { + const { id, slug, theme } = templatePart; + // The 'raw' property is not defined for a brief period in the save cycle. + // The fallback prevents an error in the parse function while saving. + const content = templatePart.content.raw || ''; + const blocks = useMemo( () => parse( content ), [ content ] ); + const { createSuccessNotice } = useDispatch( 'core/notices' ); + + const onClick = useCallback( () => { + setAttributes( { postId: id, slug, theme } ); + createSuccessNotice( + sprintf( + /* translators: %s: template part title. */ + __( 'Template Part "%s" inserted.' ), + slug + ), + { + type: 'snackbar', + } + ); + }, [ id, slug, theme ] ); + + return ( +
{ + if ( ENTER === event.keyCode || SPACE === event.keyCode ) { + onClick(); + } + } } + tabIndex={ 0 } + aria-label={ templatePart.slug } + > + +
+ { templatePart.slug } +
+
+ ); +} + +function PanelGroup( { title, icon, children } ) { + return ( + <> +
+ + { title } + + +
+
+ { children } +
+ + ); +} + +function TemplatePartsByTheme( { templateParts, setAttributes } ) { + const templatePartsByTheme = useMemo( () => { + return Object.values( groupBy( templateParts, 'meta.theme' ) ); + }, [ templateParts ] ); + const currentShownTPs = useAsyncList( templateParts ); + + return templatePartsByTheme.map( ( templatePartList ) => ( + + { templatePartList.map( ( templatePart ) => { + return currentShownTPs.includes( templatePart ) ? ( + + ) : ( + + ); + } ) } + + ) ); +} + +function TemplatePartSearchResults( { + templateParts, + setAttributes, + filterValue, +} ) { + const filteredTPs = useMemo( () => { + // Filter based on value. + // Remove diacritics and convert to lowercase to normalize. + const normalizedFilterValue = deburr( filterValue ).toLowerCase(); + const searchResults = templateParts.filter( + ( { slug, meta: { theme } } ) => + slug.toLowerCase().includes( normalizedFilterValue ) || + // Since diacritics can be used in theme names, remove them for the comparison. + deburr( theme ).toLowerCase().includes( normalizedFilterValue ) + ); + // Order based on value location. + searchResults.sort( ( a, b ) => { + // First prioritize index found in slug. + const indexInSlugA = a.slug + .toLowerCase() + .indexOf( normalizedFilterValue ); + const indexInSlugB = b.slug + .toLowerCase() + .indexOf( normalizedFilterValue ); + if ( indexInSlugA !== -1 && indexInSlugB !== -1 ) { + return indexInSlugA - indexInSlugB; + } else if ( indexInSlugA !== -1 ) { + return -1; + } else if ( indexInSlugB !== -1 ) { + return 1; + } + // Second prioritize index found in theme. + // Since diacritics can be used in theme names, remove them for the comparison. + return ( + deburr( a.meta.theme ) + .toLowerCase() + .indexOf( normalizedFilterValue ) - + deburr( b.meta.theme ) + .toLowerCase() + .indexOf( normalizedFilterValue ) + ); + } ); + return searchResults; + }, [ filterValue, templateParts ] ); + + const currentShownTPs = useAsyncList( filteredTPs ); + + return filteredTPs.map( ( templatePart ) => ( + + { currentShownTPs.includes( templatePart ) ? ( + + ) : ( + + ) } + + ) ); +} + +export default function TemplateParts( { setAttributes, filterValue } ) { + const templateParts = useSelect( ( select ) => { + const publishedTemplateParts = select( 'core' ).getEntityRecords( + 'postType', + 'wp_template_part', + { + status: [ 'publish' ], + per_page: -1, + } + ); + const resolvedTemplateParts = select( 'core' ).getEntityRecords( + 'postType', + 'wp_template_part', + { + resolved: true, + per_page: -1, + } + ); + const combinedTemplateParts = []; + if ( publishedTemplateParts ) { + combinedTemplateParts.push( ...publishedTemplateParts ); + } + if ( resolvedTemplateParts ) { + combinedTemplateParts.push( ...resolvedTemplateParts ); + } + return uniq( combinedTemplateParts ); + }, [] ); + + if ( ! templateParts || ! templateParts.length ) { + return null; + } + + if ( filterValue ) { + return ( + + ); + } + + return ( + + ); +} diff --git a/packages/block-library/src/template-part/editor.scss b/packages/block-library/src/template-part/editor.scss index 2b38d51f999fba..0b2712129a270d 100644 --- a/packages/block-library/src/template-part/editor.scss +++ b/packages/block-library/src/template-part/editor.scss @@ -1,24 +1,105 @@ -.wp-block-template-part__placeholder-input-container { + +.wp-block-template-part__placeholder-tabs { display: flex; - flex-wrap: wrap; - width: 100%; -} + flex-grow: 1; + flex-direction: column; -.wp-block-template-part__placeholder-input { - margin: 5px; -} + .components-tab-panel__tabs { + border-bottom: $border-width solid $light-gray-500; + + .components-tab-panel__tabs-item { + flex-grow: 1; + margin-bottom: -$border-width; + } + } + + .components-tab-panel__tab-content { + margin-top: $grid-unit-20; + display: flex; + flex-grow: 1; + flex-direction: column; + position: relative; + } -.wp-block-template-part__placeholder-preview { - margin-bottom: 15px; - width: 100%; + .wp-block-template-part__placeholder-input-container { + display: flex; + flex-wrap: wrap; + width: 100%; - .block-editor-block-preview__container { - padding: 1px; + .wp-block-template-part__placeholder-input { + margin: 5px; + } + } + + .wp-block-template-part__placeholder-help-phrase { + padding: 0 $grid-unit-15 $grid-unit-15; + } + + .wp-block-template-part__placeholder-create-button { + align-self: flex-start; + } + + .wp-block-template-part__placeholder-preview-filter-input { + width: inherit; } } -.wp-block-template-part__placeholder-preview-title { - font-size: 15px; - font-weight: 600; - margin-bottom: 4px; + +.wp-block-template-part__placeholder-preview-container { + width: inherit; + max-height: 600px; + overflow-y: scroll; + background: $white; + border-radius: $radius-block-ui; + border: $border-width solid $light-gray-500; + top: $grid-unit-20; + left: calc(100% + #{$grid-unit-20}); + + .wp-block-template-part__placeholder-preview-item { + border-radius: $radius-block-ui; + cursor: pointer; + margin-top: $grid-unit-20; + transition: all 0.05s ease-in-out; + position: relative; + border: $border-width solid transparent; + + &:hover { + border: $border-width solid $theme-color; + } + + &:focus { + box-shadow: inset 0 0 0 1px $white, 0 0 0 $border-width-focus $theme-color; + + // Windows High Contrast mode will show this outline, but not the box-shadow. + outline: 2px solid transparent; + } + + &.is-placeholder { + min-height: 100px; + } + } + + .wp-block-template-part__placeholder-preview-item-title { + padding: $grid-unit-05; + font-size: 12px; + text-align: center; + } + + .wp-block-template-part__placeholder-panel-group-header { + display: inline-flex; + align-items: center; + padding: $grid-unit-20 $grid-unit-20 0; + } + + .wp-block-template-part__placeholder-panel-group-content { + padding: 0 $grid-unit-20; + } + + .wp-block-template-part__placeholder-panel-group-title { + color: $theme-color; + text-transform: uppercase; + font-size: 11px; + font-weight: 500; + margin-right: $grid-unit-10; + } } diff --git a/packages/compose/README.md b/packages/compose/README.md index d8cc4678b1d5e1..bbee55eb75da4d 100644 --- a/packages/compose/README.md +++ b/packages/compose/README.md @@ -119,6 +119,19 @@ _Returns_ - `WPComponent`: Component class with generated display name assigned. +# **useAsyncList** + +React hook returns an array which items get asynchronously appended from a source array. +This behavior is useful if we want to render a list of items asynchronously for performance reasons. + +_Parameters_ + +- _list_ `Array`: Source array. + +_Returns_ + +- `Array`: Async array. + # **useCopyOnClick** Copies the text to the clipboard when the element is clicked. diff --git a/packages/compose/package.json b/packages/compose/package.json index 1c2dc23f0acfda..e3606ef74435f7 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -26,6 +26,7 @@ "@babel/runtime": "^7.9.2", "@wordpress/element": "file:../element", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", + "@wordpress/priority-queue": "file:../priority-queue", "clipboard": "^2.0.1", "lodash": "^4.17.15", "mousetrap": "^1.6.5", diff --git a/packages/block-editor/src/components/inserter/use-async-list.js b/packages/compose/src/hooks/use-async-list/index.js similarity index 100% rename from packages/block-editor/src/components/inserter/use-async-list.js rename to packages/compose/src/hooks/use-async-list/index.js diff --git a/packages/compose/src/index.js b/packages/compose/src/index.js index 7acd1620ee8805..9c790f06227e8a 100644 --- a/packages/compose/src/index.js +++ b/packages/compose/src/index.js @@ -22,3 +22,4 @@ export { default as usePrevious } from './hooks/use-previous'; export { default as useReducedMotion } from './hooks/use-reduced-motion'; export { default as useViewportMatch } from './hooks/use-viewport-match'; export { default as useResizeObserver } from './hooks/use-resize-observer'; +export { default as useAsyncList } from './hooks/use-async-list'; diff --git a/packages/compose/src/index.native.js b/packages/compose/src/index.native.js index d1129d4836e3dd..eb3181954b0252 100644 --- a/packages/compose/src/index.native.js +++ b/packages/compose/src/index.native.js @@ -19,6 +19,7 @@ export { default as useKeyboardShortcut } from './hooks/use-keyboard-shortcut'; export { default as useMediaQuery } from './hooks/use-media-query'; export { default as useReducedMotion } from './hooks/use-reduced-motion'; export { default as useViewportMatch } from './hooks/use-viewport-match'; +export { default as useAsyncList } from './hooks/use-async-list'; // Higher-order components export { default as withPreferredColorScheme } from './higher-order/with-preferred-color-scheme'; diff --git a/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js b/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js index d4dcfbd3eca8a9..11de0b508a7611 100644 --- a/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js +++ b/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js @@ -54,6 +54,11 @@ const createTemplatePart = async ( ) => { // Create new template part. await insertBlock( 'Template Part' ); + const [ createNewButton ] = await page.$x( + '//button[contains(text(), "Create new")]' + ); + await createNewButton.click(); + await page.keyboard.press( 'Tab' ); await page.keyboard.type( templatePartName ); await page.keyboard.press( 'Tab' ); await page.keyboard.type( themeName ); diff --git a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js index ffe457d7bad88d..5dd1c1f14d83c8 100644 --- a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js +++ b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js @@ -24,6 +24,7 @@ describe( 'Multi-entity save flow', () => { const activatedTemplatePartSelector = `${ templatePartSelector } .block-editor-inner-blocks`; const savePanelSelector = '.entities-saved-states__panel'; const closePanelButtonSelector = 'button[aria-label="Close panel"]'; + const createNewButtonSelector = '//button[contains(text(), "Create new")]'; // Reusable assertions across Post/Site editors. const assertAllBoxesChecked = async () => { @@ -108,6 +109,11 @@ describe( 'Multi-entity save flow', () => { it( 'Should trigger multi-entity save button once template part edited', async () => { // Create new template part. await insertBlock( 'Template Part' ); + const [ createNewButton ] = await page.$x( + createNewButtonSelector + ); + await createNewButton.click(); + await page.keyboard.press( 'Tab' ); await page.keyboard.type( 'test-template-part' ); await page.keyboard.press( 'Tab' ); await page.keyboard.type( 'test-theme' ); diff --git a/packages/e2e-tests/specs/experiments/template-part.test.js b/packages/e2e-tests/specs/experiments/template-part.test.js index f7b1b480d5605b..827585089e356f 100644 --- a/packages/e2e-tests/specs/experiments/template-part.test.js +++ b/packages/e2e-tests/specs/experiments/template-part.test.js @@ -85,72 +85,80 @@ describe( 'Template Part', () => { const testContent = 'some words...'; // Selectors - const chooseButtonSelector = - '//div[contains(@class,"is-selected")]//button[text()="Choose"]'; const entitiesSaveSelector = '.editor-entities-saved-states__save-button'; const savePostSelector = '.editor-post-publish-button__button'; const templatePartSelector = '*[data-type="core/template-part"]'; const activatedTemplatePartSelector = `${ templatePartSelector } .block-editor-inner-blocks`; - const templatePartButtonSelector = `${ templatePartSelector } button`; const testContentSelector = `//p[contains(., "${ testContent }")]`; + const createNewButtonSelector = + '//button[contains(text(), "Create new")]'; + const disabledButtonSelector = + '.wp-block-template-part__placeholder-create-button[disabled]'; - it( 'Should prompt to create when no match found', async () => { + it( 'Should insert new template part on creation', async () => { await createNewPost(); await disablePrePublishChecks(); // Create new template part. await insertBlock( 'Template Part' ); + const [ createNewButton ] = await page.$x( + createNewButtonSelector + ); + await createNewButton.click(); + await page.keyboard.press( 'Tab' ); await page.keyboard.type( testSlug ); await page.keyboard.press( 'Tab' ); await page.keyboard.type( testTheme ); - // Should say 'Create' - const placeholderButton = await page.$( - templatePartButtonSelector - ); - const text = await page.evaluate( - ( element ) => element.textContent, - placeholderButton + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Enter' ); + + const newTemplatePart = await page.waitForSelector( + activatedTemplatePartSelector ); - expect( text ).toBe( 'Create' ); + expect( newTemplatePart ).toBeTruthy(); // Finish creating template part, insert some text, and save. - await page.keyboard.press( 'Tab' ); - await page.keyboard.press( 'Enter' ); - await page.waitForSelector( activatedTemplatePartSelector ); await page.click( templatePartSelector ); await page.keyboard.type( testContent ); await page.click( savePostSelector ); await page.click( entitiesSaveSelector ); } ); - it( 'Should prompt to Choose when match found', async () => { + it( 'Should preview newly added template part', async () => { await createNewPost(); - await disablePrePublishChecks(); // Try to insert the template part we created. await insertBlock( 'Template Part' ); - await page.keyboard.type( testSlug ); - await page.keyboard.press( 'Tab' ); - await page.keyboard.type( testTheme ); - // Should say 'Choose' - const placeholderButton = await page.waitForXPath( - chooseButtonSelector - ); - expect( placeholderButton ).not.toBeNull(); - } ); - - it( 'Should dispaly a preview when match is found', async () => { - const [ preview ] = await page.$x( testContentSelector ); + const preview = await page.waitForXPath( testContentSelector ); expect( preview ).toBeTruthy(); } ); - it( 'Should insert the desired template part', async () => { - const [ placeholderButton ] = await page.$x( chooseButtonSelector ); - await placeholderButton.click(); + it( 'Should insert template part when preview is selected', async () => { + const [ preview ] = await page.$x( testContentSelector ); + await preview.click(); await page.waitForSelector( activatedTemplatePartSelector ); const templatePartContent = await page.waitForXPath( testContentSelector ); expect( templatePartContent ).toBeTruthy(); } ); + + it( 'Should disable create button for slug/theme combo', async () => { + await createNewPost(); + // Create new template part. + await insertBlock( 'Template Part' ); + const [ createNewButton ] = await page.$x( + createNewButtonSelector + ); + await createNewButton.click(); + await page.keyboard.press( 'Tab' ); + await page.keyboard.type( testSlug ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.type( testTheme ); + + const disabledButton = await page.waitForSelector( + disabledButtonSelector + ); + expect( disabledButton ).toBeTruthy(); + } ); } ); } );