diff --git a/docs/README.md b/docs/README.md index ba6b35a761f6e0..94f640ae46bfcf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Block Editor Handbook -👋 Welcome to the Block Editor Handbook. +Welcome to the Block Editor Handbook. The [**Block Editor**](https://wordpress.org/gutenberg/) is a modern and up-to-date paradigm for WordPress site building and publishing. It uses a modular system of **Blocks** to compose and format content and is designed to create rich and flexible layouts for websites and digital products. diff --git a/docs/contributors/code/back-merging-to-wp-core.md b/docs/contributors/code/back-merging-to-wp-core.md new file mode 100644 index 00000000000000..2b1ec77df1e550 --- /dev/null +++ b/docs/contributors/code/back-merging-to-wp-core.md @@ -0,0 +1,31 @@ +# Back-merging code to WordPress Core + +For major releases of the WordPress software, Gutenberg features need to be merged into WordPress Core. Typically this involves taking changes made in `.php` files within the Gutenberg repository and making the equivalent updates in the WP Core codebase. + +## Files/Directories + +Changes to files within the following files/directories will typically require back-merging to WP Core: + +- `lib/` +- `phpunit/` + +## Ignored directories/files + +The following directories/files do _not_ require back-merging to WP Core: + +- `lib/load.php` - Plugin specific code. +- `lib/experiments-page.php` - experiments are Plugin specific. +- `packages/block-library` - this is handled automatically during the packages sync process. +- `packages/e2e-tests/plugins` - PHP files related to e2e tests only. Mostly fixture data generators. +- `phpunit/blocks` - the code is maintained in Gutenberg so the test should be as well. + +Please note this list is not exhaustive. + +## Pull Request Criteria + +In general, all PHP code committed to the Gutenberg repository since the date of the final Gutenberg release that was included in [the _last_ stable WP Core release](https://developer.wordpress.org/block-editor/contributors/versions-in-wordpress/) should be considered for back merging to WP Core. + +There are however certain exceptions to that rule. PRs with the following criteria do _not_ require back-merging to WP Core: + +- Does not contain changes to PHP code. +- Has label `Backport from WordPress Core` - this code is already in WP Core. diff --git a/package-lock.json b/package-lock.json index d8b4d1038d8a48..56374e1eeb89a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55598,6 +55598,7 @@ "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", + "@wordpress/deprecated": "file:../deprecated", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", @@ -70290,6 +70291,7 @@ "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", + "@wordpress/deprecated": "file:../deprecated", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index af96f0e3483d1c..7690386764f6cc 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useCallback, RawHTML, useContext } from '@wordpress/element'; +import { memo, useCallback, RawHTML, useContext } from '@wordpress/element'; import { getBlockType, getSaveContent, @@ -21,7 +21,7 @@ import { } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; import { withDispatch, useDispatch, useSelect } from '@wordpress/data'; -import { compose, pure } from '@wordpress/compose'; +import { compose } from '@wordpress/compose'; import { safeHTML } from '@wordpress/dom'; /** @@ -739,4 +739,4 @@ function BlockListBlockProvider( props ) { ); } -export default pure( BlockListBlockProvider ); +export default memo( BlockListBlockProvider ); diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js index c4f86cfe25a428..1c0de681bd4c29 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -7,7 +7,13 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useCallback, useMemo, useState, useRef } from '@wordpress/element'; +import { + useCallback, + useMemo, + useState, + useRef, + memo, +} from '@wordpress/element'; import { GlobalStylesContext, getMergedGlobalStyles, @@ -29,7 +35,7 @@ import { withDispatch, withSelect, } from '@wordpress/data'; -import { compose, ifCondition, pure } from '@wordpress/compose'; +import { compose, ifCondition } from '@wordpress/compose'; /** * Internal dependencies @@ -682,7 +688,7 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { } ); export default compose( - pure, + memo, applyWithSelect, applyWithDispatch, // Block is sometimes not mounted at the right time, causing it be undefined diff --git a/packages/block-editor/src/components/block-preview/auto.js b/packages/block-editor/src/components/block-preview/auto.js index 8972370cac6897..b4fa5e27b072ef 100644 --- a/packages/block-editor/src/components/block-preview/auto.js +++ b/packages/block-editor/src/components/block-preview/auto.js @@ -1,9 +1,9 @@ /** * WordPress dependencies */ -import { useResizeObserver, pure, useRefEffect } from '@wordpress/compose'; +import { useResizeObserver, useRefEffect } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; -import { useMemo } from '@wordpress/element'; +import { memo, useMemo } from '@wordpress/element'; import { Disabled } from '@wordpress/components'; /** @@ -55,7 +55,7 @@ function ScaledBlockPreview( { }, [ styles, additionalStyles ] ); // Initialize on render instead of module top level, to avoid circular dependency issues. - MemoizedBlockList = MemoizedBlockList || pure( BlockList ); + MemoizedBlockList = MemoizedBlockList || memo( BlockList ); const scale = containerWidth / viewportWidth; const aspectRatio = contentHeight diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index c3a87dfb5ff004..b140a2af8c095e 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -46,7 +46,9 @@ export const ExperimentalBlockEditorProvider = withRegistryProvider( return ( - + { ! settings.__unstableIsPreviewMode && ( + + ) } { children } ); diff --git a/packages/block-editor/src/hooks/typography.native.js b/packages/block-editor/src/hooks/typography.native.js index d8cbf71d84e13f..f0e9c9c10913d7 100644 --- a/packages/block-editor/src/hooks/typography.native.js +++ b/packages/block-editor/src/hooks/typography.native.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { useSelect } from '@wordpress/data'; -import { pure } from '@wordpress/compose'; +import { memo } from '@wordpress/element'; import { PanelBody } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; @@ -57,4 +57,4 @@ function TypographyPanelPure( { clientId, setAttributes, settings } ) { // We don't want block controls to re-render when typing inside a block. `pure` // will prevent re-renders unless props change, so only pass the needed props // and not the whole attributes object. -export const TypographyPanel = pure( TypographyPanelPure ); +export const TypographyPanel = memo( TypographyPanelPure ); diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index cd660c85826c28..e63029e4e34e81 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -2,9 +2,9 @@ * WordPress dependencies */ import { getBlockSupport } from '@wordpress/blocks'; -import { useMemo, useEffect, useId, useState } from '@wordpress/element'; +import { memo, useMemo, useEffect, useId, useState } from '@wordpress/element'; import { useDispatch } from '@wordpress/data'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; +import { createHigherOrderComponent } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; /** @@ -402,10 +402,10 @@ export function useBlockSettings( name, parentLayout ) { export function createBlockEditFilter( features ) { // We don't want block controls to re-render when typing inside a block. - // `pure` will prevent re-renders unless props change, so only pass the + // `memo` will prevent re-renders unless props change, so only pass the // needed props and not the whole attributes object. features = features.map( ( settings ) => { - return { ...settings, Edit: pure( settings.edit ) }; + return { ...settings, Edit: memo( settings.edit ) }; } ); const withBlockEditHooks = createHigherOrderComponent( ( OriginalBlockEdit ) => ( props ) => { @@ -488,7 +488,7 @@ function BlockProps( { index, useBlockProps, setAllWrapperProps, ...props } ) { return null; } -const BlockPropsPure = pure( BlockProps ); +const BlockPropsPure = memo( BlockProps ); export function createBlockListBlockFilter( features ) { const withBlockListBlockHooks = createHigherOrderComponent( diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index 2945fb0fbe888b..0b166ebd1e9c72 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -127,16 +127,17 @@ const EmbedEdit = ( props ) => { }; useEffect( () => { - if ( ! preview?.html || ! cannotEmbed || fetching ) { + if ( preview?.html || ! cannotEmbed || fetching ) { return; } + // At this stage, we're not fetching the preview and know it can't be embedded, // so try removing any trailing slash, and resubmit. const newURL = attributesUrl.replace( /\/$/, '' ); setURL( newURL ); setIsEditingURL( false ); setAttributes( { url: newURL } ); - }, [ preview?.html, attributesUrl, cannotEmbed, fetching ] ); + }, [ preview?.html, attributesUrl, cannotEmbed, fetching, setAttributes ] ); // Try a different provider in case the embed url is not supported. useEffect( () => { diff --git a/packages/block-library/src/footnotes/edit.js b/packages/block-library/src/footnotes/edit.js index 111e0ba5d3a0ee..88254657ced3c4 100644 --- a/packages/block-library/src/footnotes/edit.js +++ b/packages/block-library/src/footnotes/edit.js @@ -14,10 +14,11 @@ export default function FootnotesEdit( { context: { postType, postId } } ) { 'meta', postId ); + const footnotesSupported = 'string' === typeof meta?.footnotes; const footnotes = meta?.footnotes ? JSON.parse( meta.footnotes ) : []; const blockProps = useBlockProps(); - if ( postType !== 'post' && postType !== 'page' ) { + if ( ! footnotesSupported ) { return (
0; }, [] ); + const [ meta ] = useEntityProp( 'postType', postType, 'meta', postId ); + const footnotesSupported = 'string' === typeof meta?.footnotes; + const { selectionChange, insertBlock } = useDispatch( blockEditorStore ); @@ -81,7 +85,7 @@ export const format = { return null; } - if ( postType !== 'post' && postType !== 'page' ) { + if ( ! footnotesSupported ) { return null; } diff --git a/packages/block-library/src/footnotes/index.php b/packages/block-library/src/footnotes/index.php index bc6291dd21c38b..0cd2ad73ef3d42 100644 --- a/packages/block-library/src/footnotes/index.php +++ b/packages/block-library/src/footnotes/index.php @@ -68,17 +68,26 @@ function render_block_core_footnotes( $attributes, $content, $block ) { * @since 6.3.0 */ function register_block_core_footnotes() { - foreach ( array( 'post', 'page' ) as $post_type ) { - register_post_meta( - $post_type, - 'footnotes', - array( - 'show_in_rest' => true, - 'single' => true, - 'type' => 'string', - 'revisions_enabled' => true, - ) - ); + $post_types = get_post_types( + array( + 'show_in_rest' => true, + 'public' => true, + ) + ); + foreach ( $post_types as $post_type ) { + // Only register the meta field if the post type supports the editor, custom fields, and revisions. + if ( post_type_supports( $post_type, 'editor' ) && post_type_supports( $post_type, 'custom-fields' ) && post_type_supports( $post_type, 'revisions' ) ) { + register_post_meta( + $post_type, + 'footnotes', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'revisions_enabled' => true, + ) + ); + } } register_block_type_from_metadata( __DIR__ . '/footnotes', diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js index b974e61e109605..e37db3feb63d3b 100644 --- a/packages/block-library/src/video/edit.native.js +++ b/packages/block-library/src/video/edit.native.js @@ -212,7 +212,7 @@ class VideoEdit extends Component { render() { const { setAttributes, attributes, isSelected, wasBlockJustInserted } = this.props; - const { id, src } = attributes; + const { id, src, guid } = attributes; const { videoContainerHeight } = this.state; const toolbarEditButton = ( @@ -236,7 +236,10 @@ class VideoEdit extends Component { > ); - if ( ! src ) { + // NOTE: `guid` is not part of the block's attribute definition. This case + // handled here is a temporary fix until a we find a better approach. + const isSourcePresent = src || ( guid && id ); + if ( ! isSourcePresent ) { return ( { expect( screen.getByText( 'Invalid URL.' ) ).toBeVisible(); } ); + + it( 'should render empty state when source is not present', async () => { + await initializeEditor( { + initialHtml: ` + +
+ + `, + } ); + const addVideoButton = screen.queryByText( 'Add video' ); + expect( addVideoButton ).toBeVisible(); + } ); + + it( 'should not render empty state when video source is present', async () => { + await initializeEditor( { + initialHtml: ` + +
+ + `, + } ); + const addVideoButton = screen.queryByText( 'Add video' ); + expect( addVideoButton ).toBeNull(); + } ); + + it( `should not render empty state when 'guid' and 'id' attributes are present`, async () => { + await initializeEditor( { + initialHtml: ` + +
+https://videopress.com/ +
+ + `, + } ); + const addVideoButton = screen.queryByText( 'Add video' ); + expect( addVideoButton ).toBeNull(); + } ); } ); diff --git a/packages/compose/CHANGELOG.md b/packages/compose/CHANGELOG.md index 25ea99778733f6..54ff6a16252e37 100644 --- a/packages/compose/CHANGELOG.md +++ b/packages/compose/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Deprecations + +- The `pure` HoC has been deprecated. Use `memo` or `PureComponent` instead ([#57173](https://github.com/WordPress/gutenberg/pull/57173)). + ## 6.26.0 (2024-01-10) ## 6.25.0 (2023-12-13) diff --git a/packages/compose/README.md b/packages/compose/README.md index ce393f2b5fd18c..7eb70a7300f07f 100644 --- a/packages/compose/README.md +++ b/packages/compose/README.md @@ -141,6 +141,8 @@ _Related_ ### pure +> **Deprecated** Use `memo` or `PureComponent` instead. + Given a component returns the enhanced component augmented with a component only re-rendering when its props/state change ### throttle diff --git a/packages/compose/src/higher-order/pure/index.tsx b/packages/compose/src/higher-order/pure/index.tsx index 65684738b708ac..38a85a5eebe9ab 100644 --- a/packages/compose/src/higher-order/pure/index.tsx +++ b/packages/compose/src/higher-order/pure/index.tsx @@ -17,6 +17,8 @@ import { createHigherOrderComponent } from '../../utils/create-higher-order-comp /** * Given a component returns the enhanced component augmented with a component * only re-rendering when its props/state change + * + * @deprecated Use `memo` or `PureComponent` instead. */ const pure = createHigherOrderComponent( function < Props extends {} >( WrappedComponent: ComponentType< Props > diff --git a/packages/data/src/factory.js b/packages/data/src/factory.js index 389c4a7e2b33ab..be4ef8cf673c5e 100644 --- a/packages/data/src/factory.js +++ b/packages/data/src/factory.js @@ -39,15 +39,19 @@ * @return {Function} Registry selector that can be registered with a store. */ export function createRegistrySelector( registrySelector ) { - let selector; - let lastRegistry; + const selectorsByRegistry = new WeakMap(); // Create a selector function that is bound to the registry referenced by `selector.registry` // and that has the same API as a regular selector. Binding it in such a way makes it // possible to call the selector directly from another selector. const wrappedSelector = ( ...args ) => { - if ( ! selector || lastRegistry !== wrappedSelector.registry ) { + let selector = selectorsByRegistry.get( wrappedSelector.registry ); + // We want to make sure the cache persists even when new registry + // instances are created. For example patterns create their own editors + // with their own core/block-editor stores, so we should keep track of + // the cache for each registry instance. + if ( ! selector ) { selector = registrySelector( wrappedSelector.registry.select ); - lastRegistry = wrappedSelector.registry; + selectorsByRegistry.set( wrappedSelector.registry, selector ); } return selector( ...args ); }; diff --git a/packages/data/src/redux-store/index.js b/packages/data/src/redux-store/index.js index e8394087215428..7fdc9331a2474b 100644 --- a/packages/data/src/redux-store/index.js +++ b/packages/data/src/redux-store/index.js @@ -237,6 +237,11 @@ export default function createReduxStore( key, options ) { const boundSelector = ( ...args ) => { args = normalize( selector, args ); const state = store.__unstableOriginalGetState(); + // Before calling the selector, switch to the correct + // registry. + if ( selector.isRegistrySelector ) { + selector.registry = registry; + } return selector( state.root, ...args ); }; diff --git a/packages/data/src/test/registry-selectors.js b/packages/data/src/test/registry-selectors.js index bc567474541e24..edcadef8356c6b 100644 --- a/packages/data/src/test/registry-selectors.js +++ b/packages/data/src/test/registry-selectors.js @@ -78,8 +78,7 @@ describe( 'createRegistrySelector', () => { expect( registry.select( uiStore ).getElementCount() ).toBe( 1 ); } ); - // Even without createSelector, this fails in trunk. - it.skip( 'can bind one selector to multiple registries', () => { + it( 'can bind one selector to multiple registries (createRegistrySelector)', () => { const registry1 = createRegistry(); const registry2 = createRegistry(); @@ -102,6 +101,26 @@ describe( 'createRegistrySelector', () => { expect( registry2.select( uiStore ).getElementCount() ).toBe( 1 ); } ); + it( 'can bind one selector to multiple registries (createRegistrySelector + createSelector)', () => { + const registry1 = createRegistry(); + registry1.register( elementsStore ); + registry1.register( uiStore ); + registry1.dispatch( elementsStore ).add( 'Carbon' ); + + const registry2 = createRegistry(); + registry2.register( elementsStore ); + registry2.register( uiStore ); + registry2.dispatch( elementsStore ).add( 'Helium' ); + + // Expects the `getFilteredElements` to be bound separately and independently to the two registries + expect( registry1.select( uiStore ).getFilteredElements() ).toEqual( [ + 'Carbon', + ] ); + expect( registry2.select( uiStore ).getFilteredElements() ).toEqual( [ + 'Helium', + ] ); + } ); + it( 'can bind a memoized selector to a registry', () => { const registry = createRegistry(); registry.register( elementsStore ); diff --git a/packages/e2e-tests/plugins/interactive-blocks/with-scope/block.json b/packages/e2e-tests/plugins/interactive-blocks/with-scope/block.json new file mode 100644 index 00000000000000..b9d6a4fc3cf7c6 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/with-scope/block.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 2, + "name": "test/with-scope", + "title": "E2E Interactivity tests - with scope", + "category": "text", + "icon": "heart", + "description": "", + "supports": { + "interactivity": true + }, + "textdomain": "e2e-interactivity", + "viewScript": "with-scope-view", + "render": "file:./render.php" +} diff --git a/packages/e2e-tests/plugins/interactive-blocks/with-scope/render.php b/packages/e2e-tests/plugins/interactive-blocks/with-scope/render.php new file mode 100644 index 00000000000000..e1a844e33246a9 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/with-scope/render.php @@ -0,0 +1,14 @@ + + +
+

0

+

0

+
diff --git a/packages/e2e-tests/plugins/interactive-blocks/with-scope/view.js b/packages/e2e-tests/plugins/interactive-blocks/with-scope/view.js new file mode 100644 index 00000000000000..9df421a6f50b74 --- /dev/null +++ b/packages/e2e-tests/plugins/interactive-blocks/with-scope/view.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { store, getContext, withScope } from '@wordpress/interactivity'; + +store( 'with-scope', { + callbacks: { + asyncInit: () => { + setTimeout( withScope(function*() { + yield new Promise(resolve => setTimeout(resolve, 1)); + const context = getContext() + context.asyncCounter += 1; + }, 1 )); + }, + syncInit: () => { + const context = getContext() + context.syncCounter += 1; + } + }, +} ); diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index e8c45ccdb02a6d..163d4fe6e938ff 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -31,17 +31,14 @@ import { useBlockCommands, store as blockEditorStore, } from '@wordpress/block-editor'; -import { privateApis as routerPrivateApis } from '@wordpress/router'; import { privateApis as coreCommandsPrivateApis } from '@wordpress/core-commands'; /** * Internal dependencies */ import Sidebar from '../sidebar'; -import Editor from '../editor'; import ErrorBoundary from '../error-boundary'; import { store as editSiteStore } from '../../store'; -import getIsListPage from '../../utils/get-is-list-page'; import Header from '../header-edit-mode'; import useInitEditedEntityFromURL from '../sync-state-with-url/use-init-edited-entity-from-url'; import SiteHub from '../site-hub'; @@ -53,12 +50,11 @@ import KeyboardShortcutsRegister from '../keyboard-shortcuts/register'; import KeyboardShortcutsGlobal from '../keyboard-shortcuts/global'; import { useCommonCommands } from '../../hooks/commands/use-common-commands'; import { useEditModeCommands } from '../../hooks/commands/use-edit-mode-commands'; -import PageMain from '../page-main'; import { useIsSiteEditorLoading } from './hooks'; +import useLayoutAreas from './router'; const { useCommands } = unlock( coreCommandsPrivateApis ); const { useCommandContext } = unlock( commandsPrivateApis ); -const { useLocation } = unlock( routerPrivateApis ); const { useGlobalStyle } = unlock( blockEditorPrivateApis ); const ANIMATION_DURATION = 0.5; @@ -72,10 +68,7 @@ export default function Layout() { useCommonCommands(); useBlockCommands(); - const { params } = useLocation(); const isMobileViewport = useViewportMatch( 'medium', '<' ); - const isListPage = getIsListPage( params, isMobileViewport ); - const isEditorPage = ! isListPage; const { isDistractionFree, @@ -109,26 +102,17 @@ export default function Layout() { select( blockEditorStore ).getBlockSelectionStart(), }; }, [] ); - const isEditing = canvasMode === 'edit'; const navigateRegionsProps = useNavigateRegions( { previous: previousShortcut, next: nextShortcut, } ); const disableMotion = useReducedMotion(); - const showSidebar = - ( isMobileViewport && canvasMode === 'view' && ! isListPage ) || - ( ! isMobileViewport && ( canvasMode === 'view' || ! isEditorPage ) ); - const showCanvas = - ( isMobileViewport && isEditorPage && isEditing ) || - ! isMobileViewport || - ! isEditorPage; - const isFullCanvas = - ( isMobileViewport && isListPage ) || ( isEditorPage && isEditing ); const [ canvasResizer, canvasSize ] = useResizeObserver(); const [ fullResizer ] = useResizeObserver(); const isEditorLoading = useIsSiteEditorLoading(); const [ isResizableFrameOversized, setIsResizableFrameOversized ] = useState( false ); + const { areas, widths } = useLayoutAreas(); // This determines which animation variant should apply to the header. // There is also a `isDistractionFreeHovering` state that gets priority @@ -154,7 +138,7 @@ export default function Layout() { // Sets the right context for the command palette let commandContext = 'site-editor'; - if ( canvasMode === 'edit' && isEditorPage ) { + if ( canvasMode === 'edit' ) { commandContext = 'site-editor-edit'; } if ( hasBlockSelected ) { @@ -185,9 +169,9 @@ export default function Layout() { 'edit-site-layout', navigateRegionsProps.className, { - 'is-distraction-free': isDistractionFree && isEditing, - 'is-full-canvas': isFullCanvas, - 'is-edit-mode': isEditing, + 'is-distraction-free': + isDistractionFree && canvasMode === 'edit', + 'is-full-canvas': canvasMode === 'edit', 'has-fixed-toolbar': hasFixedToolbar, 'is-block-toolbar-visible': hasBlockSelected, } @@ -228,7 +212,7 @@ export default function Layout() { /> - { isEditorPage && isEditing && ( + { canvasMode === 'edit' && ( - { showSidebar && ( + { canvasMode === 'view' && ( - { showCanvas && ( - <> - { isListPage && } - { isEditorPage && ( -
- { canvasResizer } - { !! canvasSize.width && ( - + { areas.content } +
+ ) } + + { areas.preview && ( +
+ { canvasResizer } + { !! canvasSize.width && ( + + + - - - - - - - ) } -
+ { areas.preview } + + +
) } - +
) } diff --git a/packages/edit-site/src/components/layout/router.js b/packages/edit-site/src/components/layout/router.js new file mode 100644 index 00000000000000..3f9a8be7f82de4 --- /dev/null +++ b/packages/edit-site/src/components/layout/router.js @@ -0,0 +1,103 @@ +/** + * WordPress dependencies + */ +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { useIsSiteEditorLoading } from './hooks'; +import Editor from '../editor'; +import DataviewsPatterns from '../page-patterns/dataviews-patterns'; +import PagePages from '../page-pages'; +import PagePatterns from '../page-patterns'; +import PageTemplateParts from '../page-template-parts'; +import PageTemplates from '../page-templates'; + +const { useLocation } = unlock( routerPrivateApis ); + +export default function useLayoutAreas() { + const isSiteEditorLoading = useIsSiteEditorLoading(); + const { params } = useLocation(); + const { postType, postId, path, layout, isCustom } = params ?? {}; + + // Regular page + if ( path === '/page' ) { + const isListLayout = + isCustom !== 'true' && ( ! layout || layout === 'list' ); + return { + areas: { + content: window.__experimentalAdminViews ? ( + + ) : undefined, + preview: ( isListLayout || + ! window.__experimentalAdminViews ) && ( + + ), + }, + widths: { + content: + window.__experimentalAdminViews && isListLayout + ? 380 + : undefined, + }, + }; + } + + // Regular other post types + if ( postType && postId ) { + return { + areas: { + preview: , + }, + }; + } + + // Templates + if ( + path === '/wp_template/all' || + ( path === '/wp_template' && window?.__experimentalAdminViews ) + ) { + const isListLayout = + isCustom !== 'true' && ( ! layout || layout === 'list' ); + return { + areas: { + content: , + preview: isListLayout && ( + + ), + }, + widths: { + content: isListLayout ? 380 : undefined, + }, + }; + } + + // Template parts + if ( path === '/wp_template_part/all' ) { + return { + areas: { + content: , + }, + }; + } + + // Patterns + if ( path === '/patterns' ) { + return { + areas: { + content: window?.__experimentalAdminViews ? ( + + ) : ( + + ), + }, + }; + } + + // Fallback shows the home page preview + return { + areas: { preview: }, + }; +} diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 53d3ed82a88bc2..c14d4c2f68274b 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -14,11 +14,6 @@ height: $header-height; z-index: z-index(".edit-site-layout__hub"); - .edit-site-layout.is-full-canvas.is-edit-mode & { - width: $header-height; - padding-right: 0; - } - @include break-medium { width: calc(#{$nav-sidebar-width} - #{$grid-unit-30}); } @@ -30,7 +25,7 @@ box-shadow: none; @include break-medium { - width: auto; + width: $header-height; padding-right: 0; } } @@ -156,20 +151,13 @@ } } -.edit-site-template-pages-list-view, -.edit-site-page-pages-list-view { - max-width: $nav-sidebar-width; -} - // This shouldn't be necessary (we should have a way to say that a skeletton is relative .edit-site-layout__canvas .interface-interface-skeleton, -.edit-site-page-pages-preview .interface-interface-skeleton, .edit-site-template-pages-preview .interface-interface-skeleton { position: relative !important; min-height: 100% !important; } -.edit-site-page-pages-preview, .edit-site-template-pages-preview { height: 100%; } @@ -187,7 +175,7 @@ justify-content: center; border-bottom: 1px solid transparent; - .edit-site-layout.is-full-canvas.is-edit-mode & { + .edit-site-layout.is-full-canvas & { border-bottom-color: $gray-200; transition: border-bottom-color 0.15s 0.4s ease-out; } @@ -261,8 +249,7 @@ } } -.is-edit-mode.is-distraction-free { - +.edit-site-layout.is-distraction-free { .edit-site-layout__header-container { height: $header-height; position: absolute; @@ -300,3 +287,16 @@ } } + +.edit-site-layout__area { + color: $gray-800; + background: $white; + flex-grow: 1; + overflow: auto; + margin: 0; + margin-top: $header-height; + @include break-medium() { + border-radius: 8px; + margin: $canvas-padding $canvas-padding $canvas-padding 0; + } +} diff --git a/packages/edit-site/src/components/page-main/index.js b/packages/edit-site/src/components/page-main/index.js deleted file mode 100644 index fead1b9dbc11e0..00000000000000 --- a/packages/edit-site/src/components/page-main/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/** - * WordPress dependencies - */ -import { privateApis as routerPrivateApis } from '@wordpress/router'; - -/** - * Internal dependencies - */ -import PagePatterns from '../page-patterns'; -import DataviewsPatterns from '../page-patterns/dataviews-patterns'; -import PageTemplateParts from '../page-template-parts'; -import PageTemplates from '../page-templates'; -import PagePages from '../page-pages'; -import { unlock } from '../../lock-unlock'; - -const { useLocation } = unlock( routerPrivateApis ); - -export default function PageMain() { - const { - params: { path }, - } = useLocation(); - - if ( - path === '/wp_template/all' || - ( path === '/wp_template' && window?.__experimentalAdminViews ) - ) { - return ; - } else if ( path === '/wp_template_part/all' ) { - return ; - } else if ( path === '/patterns' ) { - return window?.__experimentalAdminViews ? ( - - ) : ( - - ); - } else if ( window?.__experimentalAdminViews && path === '/page' ) { - return ; - } - - return null; -} diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 35382a25039fa8..048821b048f6aa 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -10,7 +10,6 @@ import { dateI18n, getDate, getSettings } from '@wordpress/date'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { useSelect, useDispatch } from '@wordpress/data'; import { DataViews } from '@wordpress/dataviews'; -import { ENTER, SPACE } from '@wordpress/keycodes'; /** * Internal dependencies @@ -38,7 +37,6 @@ import { viewPostAction, useEditPostAction, } from '../actions'; -import PostPreview from '../post-preview'; import AddNewPageModal from '../add-new-page'; import Media from '../media'; import { unlock } from '../../lock-unlock'; @@ -46,13 +44,24 @@ const { useLocation, useHistory } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; -function useView( type ) { - const { - params: { activeView = 'all', isCustom = 'false' }, - } = useLocation(); - const selectedDefaultView = - isCustom === 'false' && - DEFAULT_VIEWS[ type ].find( ( { slug } ) => slug === activeView )?.view; +function useView( postType ) { + const { params } = useLocation(); + const { activeView = 'all', isCustom = 'false', layout } = params; + const history = useHistory(); + const selectedDefaultView = useMemo( () => { + const defaultView = + isCustom === 'false' && + DEFAULT_VIEWS[ postType ].find( + ( { slug } ) => slug === activeView + )?.view; + if ( isCustom === 'false' && layout ) { + return { + ...defaultView, + type: layout, + }; + } + return defaultView; + }, [ isCustom, activeView, layout, postType ] ); const [ view, setView ] = useState( selectedDefaultView ); useEffect( () => { @@ -107,13 +116,26 @@ function useView( type ) { [ editEntityRecord, editedViewRecord?.id ] ); + const setDefaultViewAndUpdateUrl = useCallback( + ( viewToSet ) => { + if ( viewToSet.type !== view?.type ) { + history.push( { + ...params, + layout: viewToSet.type, + } ); + } + setView( viewToSet ); + }, + [ params, view?.type, history ] + ); + if ( isCustom === 'false' ) { - return [ view, setView ]; + return [ view, setDefaultViewAndUpdateUrl ]; } else if ( isCustom === 'true' && customView ) { return [ customView, setCustomView ]; } // Loading state where no the view was not found on custom views or default views. - return [ DEFAULT_VIEWS[ type ][ 0 ].view, setView ]; + return [ DEFAULT_VIEWS[ postType ][ 0 ].view, setDefaultViewAndUpdateUrl ]; } // See https://github.com/WordPress/gutenberg/issues/55886 @@ -131,12 +153,20 @@ const DEFAULT_STATUSES = 'draft,future,pending,private,publish'; // All but 'tra export default function PagePages() { const postType = 'page'; const [ view, setView ] = useView( postType ); - const [ pageId, setPageId ] = useState( null ); const history = useHistory(); + const { params } = useLocation(); + const { isCustom = 'false' } = params; const onSelectionChange = useCallback( - ( items ) => setPageId( items?.length === 1 ? items[ 0 ].id : null ), - [ setPageId ] + ( items ) => { + if ( isCustom === 'false' && view?.type === LAYOUT_LIST ) { + history.push( { + ...params, + postId: items.length === 1 ? items[ 0 ].id : undefined, + } ); + } + }, + [ history, params, view?.type, isCustom ] ); const onDetailsChange = useCallback( @@ -366,85 +396,33 @@ export default function PagePages() { // TODO: we need to handle properly `data={ data || EMPTY_ARRAY }` for when `isLoading`. return ( - <> - - - { showAddPageModal && ( - - ) } - - } - > - - - { view.type === LAYOUT_LIST && ( - -
{ - const { keyCode } = event; - if ( keyCode === ENTER || keyCode === SPACE ) { - history.push( { - postId: pageId, - postType, - canvas: 'edit', - } ); - } - } } - onClick={ () => - history.push( { - postId: pageId, - postType, - canvas: 'edit', - } ) - } - > - { pageId !== null ? ( - - ) : ( -
-

{ __( 'Select a page to preview' ) }

-
- ) } -
-
- ) } - + + + { showAddPageModal && ( + + ) } + + } + > + + ); } diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index 6be2f904a65ec5..b5d092f438e8e7 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -1,10 +1,11 @@ .edit-site-patterns { + background: #1e1e1e; border-left: 1px solid $gray-800; - background: none; margin: $header-height 0 0; border-radius: 0; padding: 0; overflow-x: auto; + min-height: 100%; .components-base-control { width: 100%; diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index 4ef099aed6d8d3..7deec674f9b419 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -16,7 +16,6 @@ import { __ } from '@wordpress/i18n'; import { useState, useMemo, useCallback } from '@wordpress/element'; import { useEntityRecords } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; -import { ENTER, SPACE } from '@wordpress/keycodes'; import { parse } from '@wordpress/blocks'; import { BlockPreview, @@ -53,12 +52,11 @@ import { import { postRevisionsAction } from '../actions'; import usePatternSettings from '../page-patterns/use-pattern-settings'; import { unlock } from '../../lock-unlock'; -import PostPreview from '../post-preview'; const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( blockEditorPrivateApis ); -const { useHistory } = unlock( routerPrivateApis ); +const { useHistory, useLocation } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; @@ -156,18 +154,30 @@ function TemplatePreview( { content, viewType } ) { } export default function DataviewsTemplates() { - const [ templateId, setTemplateId ] = useState( null ); - const [ view, setView ] = useState( DEFAULT_VIEW ); + const { params } = useLocation(); + const { layout } = params; + const defaultView = useMemo( () => { + return { + ...DEFAULT_VIEW, + type: layout ?? DEFAULT_VIEW.type, + }; + }, [ layout ] ); + const [ view, setView ] = useState( defaultView ); const { records: allTemplates, isResolving: isLoadingData } = useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { per_page: -1, } ); const history = useHistory(); - const onSelectionChange = useCallback( - ( items ) => - setTemplateId( items?.length === 1 ? items[ 0 ].id : null ), - [ setTemplateId ] + ( items ) => { + if ( view?.type === LAYOUT_LIST ) { + history.push( { + ...params, + postId: items.length === 1 ? items[ 0 ].id : undefined, + } ); + } + }, + [ history, params, view?.type ] ); const onDetailsChange = useCallback( @@ -348,90 +358,41 @@ export default function DataviewsTemplates() { ...defaultConfigPerViewType[ newView.type ], }, }; + + history.push( { + ...params, + layout: newView.type, + } ); } setView( newView ); }, - [ view.type, setView ] + [ view.type, setView, history, params ] ); return ( - <> - - } - > - - - { view.type === LAYOUT_LIST && ( - -
{ - const { keyCode } = event; - if ( keyCode === ENTER || keyCode === SPACE ) { - history.push( { - postId: templateId, - postType: TEMPLATE_POST_TYPE, - canvas: 'edit', - } ); - } - } } - onClick={ () => - history.push( { - postId: templateId, - postType: TEMPLATE_POST_TYPE, - canvas: 'edit', - } ) - } - > - { templateId !== null ? ( - - ) : ( -
-

{ __( 'Select a template to preview' ) }

-
- ) } -
-
- ) } - + } + > + + ); } diff --git a/packages/edit-site/src/components/post-preview/index.js b/packages/edit-site/src/components/post-preview/index.js deleted file mode 100644 index 8f325c26275948..00000000000000 --- a/packages/edit-site/src/components/post-preview/index.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Internal dependencies - */ -import Editor from '../editor'; -import { useInitEditedEntity } from '../sync-state-with-url/use-init-edited-entity-from-url'; -import { useIsSiteEditorLoading } from '../layout/hooks'; - -export default function PostPreview( { postType, postId } ) { - useInitEditedEntity( { - postId, - postType, - } ); - const isEditorLoading = useIsSiteEditorLoading(); - - return ; -} diff --git a/packages/edit-site/src/components/site-icon/style.scss b/packages/edit-site/src/components/site-icon/style.scss index ac16279c9fe184..fc680166bf2691 100644 --- a/packages/edit-site/src/components/site-icon/style.scss +++ b/packages/edit-site/src/components/site-icon/style.scss @@ -9,7 +9,7 @@ object-fit: cover; background: #333; - .edit-site-layout.is-full-canvas.is-edit-mode & { + .edit-site-layout.is-full-canvas & { border-radius: 0; } } diff --git a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js index 36056cf4b43bda..11c065cf862dba 100644 --- a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js +++ b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js @@ -27,7 +27,7 @@ const postTypesWithoutParentTemplate = [ PATTERN_TYPES.user, ]; -function useResolveEditedEntityAndContext( { postId, postType } ) { +function useResolveEditedEntityAndContext( { path, postId, postType } ) { const { hasLoadedAllDependencies, homepageId, url, frontPageTemplateId } = useSelect( ( select ) => { const { getSite, getUnstableBase, getEntityRecords } = @@ -159,6 +159,11 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { return resolveTemplateForPostTypeAndId( postType, postId ); } + // Some URLs in list views are different + if ( path === '/page' && postId ) { + return resolveTemplateForPostTypeAndId( 'page', postId ); + } + // If we're rendering the home page, and we have a static home page, resolve its template. if ( homepageId ) { return resolveTemplateForPostTypeAndId( 'page', homepageId ); @@ -176,6 +181,7 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { url, postId, postType, + path, frontPageTemplateId, ] ); @@ -189,12 +195,25 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { return { postType, postId }; } + // Some URLs in list views are different + if ( path === '/page' && postId ) { + return { postType: 'page', postId }; + } + if ( homepageId ) { return { postType: 'page', postId: homepageId }; } return {}; - }, [ homepageId, postType, postId ] ); + }, [ homepageId, postType, postId, path ] ); + + if ( + ( path === '/wp_template/all' || + ( path === '/wp_template' && window?.__experimentalAdminViews ) ) && + postId + ) { + return { isReady: true, postType: 'wp_template', postId, context }; + } if ( postTypesWithoutParentTemplate.includes( postType ) ) { return { isReady: true, postType, postId, context }; @@ -212,7 +231,8 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { return { isReady: false }; } -export function useInitEditedEntity( params ) { +export default function useInitEditedEntityFromURL() { + const { params = {} } = useLocation(); const { postType, postId, context, isReady } = useResolveEditedEntityAndContext( params ); @@ -224,8 +244,3 @@ export function useInitEditedEntity( params ) { } }, [ isReady, postType, postId, context, setEditedEntity ] ); } - -export default function useInitEditedEntityFromURL() { - const { params = {} } = useLocation(); - return useInitEditedEntity( params ); -} diff --git a/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js b/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js index 5f176dff8198d3..3eb6d6e6f4fadf 100644 --- a/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js +++ b/packages/edit-site/src/components/sync-state-with-url/use-sync-path-with-url.js @@ -82,6 +82,7 @@ export default function useSyncPathWithURL() { postType: navigatorParams?.postType, postId: navigatorParams?.postId, path: undefined, + layout: undefined, } ); } else if ( navigatorLocation.path.startsWith( '/page/' ) && @@ -91,6 +92,7 @@ export default function useSyncPathWithURL() { postType: 'page', postId: navigatorParams?.postId, path: undefined, + layout: undefined, } ); } else if ( navigatorLocation.path === '/patterns' ) { updateUrlParams( { @@ -99,12 +101,29 @@ export default function useSyncPathWithURL() { canvas: undefined, path: navigatorLocation.path, } ); + } else if ( + // These sidebar paths are special in the sense that the url in these pages may or may not have a postId and we need to retain it if it has. + // The "type" property should be kept as well. + ( navigatorLocation.path === '/page' && + window?.__experimentalAdminViews ) || + ( navigatorLocation.path === '/wp_template' && + window?.__experimentalAdminViews ) || + ( navigatorLocation.path === '/wp_template/all' && + ! window?.__experimentalAdminViews ) + ) { + updateUrlParams( { + postType: undefined, + categoryType: undefined, + categoryId: undefined, + path: navigatorLocation.path, + } ); } else { updateUrlParams( { postType: undefined, postId: undefined, categoryType: undefined, categoryId: undefined, + layout: undefined, path: navigatorLocation.path === '/' ? undefined diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index f3f37c6f70b82c..252b200f9d4d01 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -5,12 +5,13 @@ ### Enhancements - Prevent the usage of Preact components in `wp-text`. ([#57879](https://github.com/WordPress/gutenberg/pull/57879)) -- Update `preact`, `@preact/signals` and `deepsignal` dependencies. ([57891](https://github.com/WordPress/gutenberg/pull/57891)) +- Update `preact`, `@preact/signals` and `deepsignal` dependencies. ([#57891](https://github.com/WordPress/gutenberg/pull/57891)) +- Export `withScope()` and allow to use it with asynchronous operations. ([#58013](https://github.com/WordPress/gutenberg/pull/58013)) ### New Features -- Add the `data-wp-run` directive along with the `useInit` and `useWatch` hooks. ([57805](https://github.com/WordPress/gutenberg/pull/57805)) -- Add `wp-data-on-window` and `wp-data-on-document` directives. ([57931](https://github.com/WordPress/gutenberg/pull/57931)) +- Add the `data-wp-run` directive along with the `useInit` and `useWatch` hooks. ([#57805](https://github.com/WordPress/gutenberg/pull/57805)) +- Add `wp-data-on-window` and `wp-data-on-document` directives. ([#57931](https://github.com/WordPress/gutenberg/pull/57931)) - Add the `data-wp-each` directive to render lists of items using a template. ([57859](https://github.com/WordPress/gutenberg/pull/57859)) ### Breaking Changes diff --git a/packages/interactivity/src/directives.js b/packages/interactivity/src/directives.js index dd482045e3022c..c1879af1fe19a6 100644 --- a/packages/interactivity/src/directives.js +++ b/packages/interactivity/src/directives.js @@ -132,6 +132,7 @@ export default () => { // data-wp-init--[name] directive( 'init', ( { directives: { init }, evaluate } ) => { init.forEach( ( entry ) => { + // TODO: Replace with useEffect to prevent unneeded scopes. useInit( () => evaluate( entry ) ); } ); } ); diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index ae543506c01feb..c133eb9981880b 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -156,6 +156,8 @@ export const resetScope = () => { scopeStack.pop(); }; +export const getNamespace = () => namespaceStack.slice( -1 )[ 0 ]; + export const setNamespace = ( namespace: string ) => { namespaceStack.push( namespace ); }; diff --git a/packages/interactivity/src/index.js b/packages/interactivity/src/index.js index cf0b4c88cac4bf..0864291455310d 100644 --- a/packages/interactivity/src/index.js +++ b/packages/interactivity/src/index.js @@ -5,9 +5,10 @@ import registerDirectives from './directives'; import { init } from './router'; export { store } from './store'; -export { directive, getContext, getElement } from './hooks'; +export { directive, getContext, getElement, getNamespace } from './hooks'; export { navigate, prefetch } from './router'; export { + withScope, useWatch, useInit, useEffect, diff --git a/packages/interactivity/src/utils.js b/packages/interactivity/src/utils.js index 021df983cb4f0a..84e04803cea4f5 100644 --- a/packages/interactivity/src/utils.js +++ b/packages/interactivity/src/utils.js @@ -12,7 +12,14 @@ import { effect } from '@preact/signals'; /** * Internal dependencies */ -import { getScope, setScope, resetScope } from './hooks'; +import { + getScope, + setScope, + resetScope, + getNamespace, + setNamespace, + resetNamespace, +} from './hooks'; const afterNextFrame = ( callback ) => { return new Promise( ( resolve ) => { @@ -71,13 +78,40 @@ export function useSignalEffect( callback ) { * @param {Function} func The passed function. * @return {Function} The wrapped function. */ -const withScope = ( func ) => { +export const withScope = ( func ) => { const scope = getScope(); + const ns = getNamespace(); + if ( func?.constructor?.name === 'GeneratorFunction' ) { + return async ( ...args ) => { + const gen = func( ...args ); + let value; + let it; + while ( true ) { + setNamespace( ns ); + setScope( scope ); + try { + it = gen.next( value ); + } finally { + resetNamespace(); + resetScope(); + } + try { + value = await it.value; + } catch ( e ) { + gen.throw( e ); + } + if ( it.done ) break; + } + return value; + }; + } return ( ...args ) => { + setNamespace( ns ); setScope( scope ); try { return func( ...args ); } finally { + resetNamespace(); resetScope(); } }; diff --git a/packages/preferences/package.json b/packages/preferences/package.json index ac1f1c9ade95cc..7cab99bfab0329 100644 --- a/packages/preferences/package.json +++ b/packages/preferences/package.json @@ -33,6 +33,7 @@ "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", + "@wordpress/deprecated": "file:../deprecated", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", diff --git a/packages/preferences/src/store/selectors.js b/packages/preferences/src/store/selectors.js index 58b4d18bc362aa..e249acf7bbdd33 100644 --- a/packages/preferences/src/store/selectors.js +++ b/packages/preferences/src/store/selectors.js @@ -1,3 +1,43 @@ +/** + * WordPress dependencies + */ +import deprecated from '@wordpress/deprecated'; + +const withDeprecatedKeys = ( originalGet ) => ( state, scope, name ) => { + const settingsToMoveToCore = [ + 'allowRightClickOverrides', + 'distractionFree', + 'editorMode', + 'fixedToolbar', + 'focusMode', + 'hiddenBlockTypes', + 'inactivePanels', + 'keepCaretInsideBlock', + 'mostUsedBlocks', + 'openPanels', + 'showBlockBreadcrumbs', + 'showIconLabels', + 'showListViewByDefault', + ]; + + if ( + settingsToMoveToCore.includes( name ) && + [ 'core/edit-post', 'core/edit-site' ].includes( scope ) + ) { + deprecated( + `wp.data.select( 'core/preferences' ).get( '${ scope }', '${ name }' )`, + { + since: '6.5', + alternative: `wp.data.select( 'core/preferences' ).get( 'core', '${ name }' )`, + } + ); + + return originalGet( state, 'core', name ); + } + + return originalGet( state, scope, name ); +}; + /** * Returns a boolean indicating whether a prefer is active for a particular * scope. @@ -8,7 +48,7 @@ * * @return {*} Is the feature enabled? */ -export function get( state, scope, name ) { +export const get = withDeprecatedKeys( ( state, scope, name ) => { const value = state.preferences[ scope ]?.[ name ]; return value !== undefined ? value : state.defaults[ scope ]?.[ name ]; -} +} ); diff --git a/packages/react-native-aztec/android/build.gradle b/packages/react-native-aztec/android/build.gradle index 18093ff1c2c136..6c1f4e5b02cce3 100644 --- a/packages/react-native-aztec/android/build.gradle +++ b/packages/react-native-aztec/android/build.gradle @@ -36,7 +36,7 @@ plugins { // import the `readReactNativeVersion()` function apply from: 'https://gist.githubusercontent.com/hypest/742448b9588b3a0aa580a5e80ae95bdf/raw/8eb62d40ee7a5104d2fcaeff21ce6f29bd93b054/readReactNativeVersion.gradle' -group = 'org.wordpress-mobile.gutenberg-mobile' +group = 'org.wordpress.gutenberg-mobile' // The sample build uses multiple directories to // keep boilerplate and common code separate from @@ -119,7 +119,7 @@ project.afterEvaluate { ReactNativeAztecPublication(MavenPublication) { artifact bundleReleaseAar - groupId 'org.wordpress-mobile.gutenberg-mobile' + groupId 'org.wordpress.gutenberg-mobile' artifactId 'react-native-aztec' // version is set by 'publish-to-s3' plugin diff --git a/packages/react-native-bridge/android/react-native-bridge/build.gradle b/packages/react-native-bridge/android/react-native-bridge/build.gradle index 7800be076c842a..c54e73b9a4b6a1 100644 --- a/packages/react-native-bridge/android/react-native-bridge/build.gradle +++ b/packages/react-native-bridge/android/react-native-bridge/build.gradle @@ -25,7 +25,7 @@ plugins { apply from: 'https://gist.githubusercontent.com/hypest/742448b9588b3a0aa580a5e80ae95bdf/raw/8eb62d40ee7a5104d2fcaeff21ce6f29bd93b054/readReactNativeVersion.gradle' apply from: '../extractPackageVersion.gradle' -group='org.wordpress-mobile.gutenberg-mobile' +group='org.wordpress.gutenberg-mobile' def buildAssetsFolder = 'build/assets' @@ -93,8 +93,8 @@ dependencies { // Published by `wordpress-mobile/react-native-libraries-publisher` // See the documentation for this value in `build.gradle.kts` of `wordpress-mobile/react-native-libraries-publisher` - def reactNativeLibrariesPublisherVersion = "v3" - def reactNativeLibrariesGroupId = "org.wordpress-mobile.react-native-libraries.$reactNativeLibrariesPublisherVersion" + def reactNativeLibrariesPublisherVersion = "v4" + def reactNativeLibrariesGroupId = "org.wordpress.react-native-libraries.$reactNativeLibrariesPublisherVersion" implementation "$reactNativeLibrariesGroupId:react-native-get-random-values:${extractPackageVersion(packageJson, 'react-native-get-random-values', 'dependencies')}" implementation "$reactNativeLibrariesGroupId:react-native-safe-area-context:${extractPackageVersion(packageJson, 'react-native-safe-area-context', 'dependencies')}" implementation "$reactNativeLibrariesGroupId:react-native-screens:${extractPackageVersion(packageJson, 'react-native-screens', 'dependencies')}" @@ -110,7 +110,7 @@ dependencies { runtimeOnly "com.facebook.react:hermes-android:$rnVersion" if (willPublishReactNativeBridgeBinary) { - implementation "org.wordpress-mobile.gutenberg-mobile:react-native-aztec:$reactNativeAztecVersion" + implementation "org.wordpress.gutenberg-mobile:react-native-aztec:$reactNativeAztecVersion" } else { api project(':@wordpress_react-native-aztec') } @@ -122,7 +122,7 @@ project.afterEvaluate { ReactNativeBridgePublication(MavenPublication) { artifact bundleReleaseAar - groupId 'org.wordpress-mobile.gutenberg-mobile' + groupId 'org.wordpress.gutenberg-mobile' artifactId 'react-native-gutenberg-bridge' // version is set by 'publish-to-s3' plugin diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index c5973ddace958d..fba0781e594fb5 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,7 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased +- [**] Video block: Fix logic for displaying empty state based on source presence [#58015] ## 1.111.0 - [**] Image block media uploads display a custom error message when there is no internet connection [#56937] diff --git a/packages/react-native-editor/__device-tests__/helpers/utils.js b/packages/react-native-editor/__device-tests__/helpers/utils.js index 8af4a0b1a60d71..5af85b9e6847d0 100644 --- a/packages/react-native-editor/__device-tests__/helpers/utils.js +++ b/packages/react-native-editor/__device-tests__/helpers/utils.js @@ -306,19 +306,11 @@ const clickBeginningOfElement = async ( driver, element ) => { .perform(); }; -// Long press to activate context menu. -const longPressMiddleOfElement = async ( +async function longPressElement( driver, element, - waitTime = 1000, - customElementSize -) => { - const location = await element.getLocation(); - const size = customElementSize || ( await element.getSize() ); - - const x = location.x + size.width / 2; - const y = location.y + size.height / 2; - + { waitTime = 1000, offset = { x: 0, y: 0 } } = {} +) { // Focus on the element first, otherwise on iOS it fails to open the context menu. // We can't do it all in one action because it detects it as a force press and it // is not supported by the simulator. @@ -331,16 +323,43 @@ const longPressMiddleOfElement = async ( .up() .perform(); + const location = await element.getLocation(); + const size = await element.getSize(); + + let offsetX = offset.x; + if ( typeof offset.x === 'function' ) { + offsetX = offset.x( size.width ); + } + let offsetY = offset.y; + if ( typeof offset.y === 'function' ) { + offsetY = offset.y( size.height ); + } + // Long-press await driver .action( 'pointer', { parameters: { pointerType: 'touch' }, } ) - .move( { x, y } ) + .move( { x: location.x + offsetX, y: location.y + offsetY } ) .down() .pause( waitTime ) .up() .perform(); +} + +// Long press to activate context menu. +const longPressMiddleOfElement = async ( + driver, + element, + { waitTime = 1000 } = {} +) => { + await longPressElement( driver, element, { + waitTime, + offset: { + x: ( width ) => width / 2, + y: ( height ) => height / 2, + }, + } ); }; const tapStatusBariOS = async ( driver ) => { @@ -717,6 +736,7 @@ module.exports = { isElementVisible, isLocalEnvironment, launchApp, + longPressElement, longPressMiddleOfElement, selectTextFromElement, setupDriver, diff --git a/packages/react-native-editor/__device-tests__/pages/editor-page.js b/packages/react-native-editor/__device-tests__/pages/editor-page.js index b00be20458e802..fe3f410601a8dc 100644 --- a/packages/react-native-editor/__device-tests__/pages/editor-page.js +++ b/packages/react-native-editor/__device-tests__/pages/editor-page.js @@ -15,6 +15,8 @@ const { clickIfClickable, launchApp, tapStatusBariOS, + longPressElement, + longPressMiddleOfElement, } = require( '../helpers/utils' ); const ADD_BLOCK_ID = isAndroid() ? 'Add block' : 'add-block-button'; @@ -105,6 +107,47 @@ class EditorPage { await typeString( this.driver, block, text, clear ); } + async pasteClipboardToTextBlock( element, { timeout = 1000 } = {} ) { + if ( this.driver.isAndroid ) { + await longPressMiddleOfElement( this.driver, element ); + } else { + await longPressElement( this.driver, element ); + } + + if ( this.driver.isAndroid ) { + // Long pressing seemingly results in drag-and-drop blurring the input, so + // we tap again to re-focus the input. + await this.driver + .action( 'pointer', { + parameters: { pointerType: 'touch' }, + } ) + .move( { origin: element } ) + .down() + .up() + .perform(); + + const location = await element.getLocation(); + const approximatePasteMenuLocation = { + x: location.x + 30, + y: location.y - 120, + }; + await this.driver + .action( 'pointer', { + parameters: { pointerType: 'touch' }, + } ) + .move( approximatePasteMenuLocation ) + .down() + .up() + .perform(); + } else { + const pasteMenuItem = await this.driver.$( + '//XCUIElementTypeMenuItem[@name="Paste"]' + ); + await pasteMenuItem.waitForDisplayed( { timeout } ); + await pasteMenuItem.click(); + } + } + // Finds the wd element for new block that was added and sets the element attribute // and accessibilityId attributes on this object and selects the block // position uses one based numbering. diff --git a/packages/react-native-editor/android/app/build.gradle b/packages/react-native-editor/android/app/build.gradle index ecb63589bcf1c4..514c645354232d 100644 --- a/packages/react-native-editor/android/app/build.gradle +++ b/packages/react-native-editor/android/app/build.gradle @@ -132,7 +132,7 @@ android { dependencies { def packageJson = '../../package.json' - implementation "org.wordpress-mobile.gutenberg-mobile:react-native-bridge" + implementation "org.wordpress.gutenberg-mobile:react-native-bridge" implementation 'androidx.appcompat:appcompat:1.2.0' implementation "com.google.android.material:material:1.9.0" // The version of react-native is set by the React Native Gradle Plugin @@ -145,8 +145,8 @@ dependencies { // Published by `wordpress-mobile/react-native-libraries-publisher` // See the documentation for this value in `build.gradle.kts` of `wordpress-mobile/react-native-libraries-publisher` - def reactNativeLibrariesPublisherVersion = "v3" - def reactNativeLibrariesGroupId = "org.wordpress-mobile.react-native-libraries.$reactNativeLibrariesPublisherVersion" + def reactNativeLibrariesPublisherVersion = "v4" + def reactNativeLibrariesGroupId = "org.wordpress.react-native-libraries.$reactNativeLibrariesPublisherVersion" implementation "$reactNativeLibrariesGroupId:react-native-get-random-values:${extractPackageVersion(packageJson, 'react-native-get-random-values', 'dependencies')}" implementation "$reactNativeLibrariesGroupId:react-native-safe-area-context:${extractPackageVersion(packageJson, 'react-native-safe-area-context', 'dependencies')}" implementation "$reactNativeLibrariesGroupId:react-native-screens:${extractPackageVersion(packageJson, 'react-native-screens', 'dependencies')}" diff --git a/packages/react-native-editor/android/build.gradle b/packages/react-native-editor/android/build.gradle index 7baee554d330c7..f08019a79c9007 100644 --- a/packages/react-native-editor/android/build.gradle +++ b/packages/react-native-editor/android/build.gradle @@ -7,7 +7,7 @@ buildscript { compileSdkVersion = 34 targetSdkVersion = 33 supportLibVersion = '28.0.0' - + // We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP. ndkVersion = "23.1.7779620" } @@ -30,8 +30,7 @@ allprojects { content { includeGroup "org.wordpress" includeGroup "org.wordpress.aztec" - includeGroup "org.wordpress-mobile" - includeGroupByRegex "org.wordpress-mobile.react-native-libraries.*" + includeGroupByRegex "org.wordpress.react-native-libraries.*" } } maven { url 'https://www.jitpack.io' } diff --git a/schemas/json/font-collection.json b/schemas/json/font-collection.json index a6ca2b1412e6d2..4973c71312b605 100644 --- a/schemas/json/font-collection.json +++ b/schemas/json/font-collection.json @@ -7,11 +7,6 @@ "description": "JSON schema URI for font-collection.json.", "type": "string" }, - "version": { - "description": "Version of font-collection.json schema to use.", - "type": "integer", - "enum": [ 1 ] - }, "font_families": { "type": "array", "description": "Array of font families ready to be installed", @@ -55,5 +50,5 @@ } }, "additionalProperties": false, - "required": [ "$schema", "version", "font_families" ] + "required": [ "$schema", "font_families" ] } diff --git a/test/e2e/specs/editor/various/embedding.spec.js b/test/e2e/specs/editor/various/embedding.spec.js index 2fa963f66685d2..3e04960e7fbf87 100644 --- a/test/e2e/specs/editor/various/embedding.spec.js +++ b/test/e2e/specs/editor/various/embedding.spec.js @@ -122,7 +122,7 @@ test.describe( 'Embedding content', () => { await expect( currenEmbedBlock.getByRole( 'textbox', { name: 'Embed URL' } ), 'WordPress invalid content. Should render failed, edit state.' - ).toHaveValue( 'https://wordpress.org/gutenberg/handbook/' ); + ).toHaveValue( 'https://wordpress.org/gutenberg/handbook' ); await embedUtils.insertEmbed( 'https://twitter.com/thatbunty' ); await expect( @@ -189,7 +189,7 @@ test.describe( 'Embedding content', () => { } ); // Reason: A possible regression of https://github.com/WordPress/gutenberg/pull/14705. - test.skip( 'should retry embeds that could not be embedded with trailing slashes, without the trailing slashes', async ( { + test( 'should retry embeds that could not be embedded with trailing slashes, without the trailing slashes', async ( { editor, embedUtils, } ) => { diff --git a/test/e2e/specs/interactivity/with-scope.spec.ts b/test/e2e/specs/interactivity/with-scope.spec.ts new file mode 100644 index 00000000000000..1cb73cc915aca7 --- /dev/null +++ b/test/e2e/specs/interactivity/with-scope.spec.ts @@ -0,0 +1,27 @@ +/** + * Internal dependencies + */ +import { test, expect } from './fixtures'; + +test.describe( 'withScope', () => { + test.beforeAll( async ( { interactivityUtils: utils } ) => { + await utils.activatePlugins(); + await utils.addPostWithBlock( 'test/with-scope' ); + } ); + test.beforeEach( async ( { interactivityUtils: utils, page } ) => { + await page.goto( utils.getLink( 'test/with-scope' ) ); + } ); + test.afterAll( async ( { interactivityUtils: utils } ) => { + await utils.deactivatePlugins(); + await utils.deleteAllPosts(); + } ); + + test( 'directives using withScope should work with async and sync functions', async ( { + page, + } ) => { + const asyncCounter = page.getByTestId( 'asyncCounter' ); + await expect( asyncCounter ).toHaveText( '1' ); + const syncCounter = page.getByTestId( 'syncCounter' ); + await expect( syncCounter ).toHaveText( '1' ); + } ); +} );