diff --git a/backport-changelog/6.8/7903.md b/backport-changelog/6.8/7903.md
new file mode 100644
index 00000000000000..cb20d8d2dd2b1b
--- /dev/null
+++ b/backport-changelog/6.8/7903.md
@@ -0,0 +1,3 @@
+https://github.com/WordPress/wordpress-develop/pull/7903
+
+* https://github.com/WordPress/gutenberg/pull/67199
diff --git a/backport-changelog/6.8/7909.md b/backport-changelog/6.8/7909.md
new file mode 100644
index 00000000000000..32a441ef296a2d
--- /dev/null
+++ b/backport-changelog/6.8/7909.md
@@ -0,0 +1,3 @@
+https://github.com/WordPress/wordpress-develop/pull/7909
+
+* https://github.com/WordPress/gutenberg/pull/67330
diff --git a/changelog.txt b/changelog.txt
index bf13c6b273e9b6..9e07615adf43f6 100644
--- a/changelog.txt
+++ b/changelog.txt
@@ -1,5 +1,296 @@
== Changelog ==
+= 19.7.0 =
+
+## Changelog
+
+### Enhancements
+
+- Add "show template" to preview dropdown. ([66514](https://github.com/WordPress/gutenberg/pull/66514))
+- Iframe: Always enable for block themes, in core too. ([66800](https://github.com/WordPress/gutenberg/pull/66800))
+- Media Utils: Add experimental `sideloadMedia`. ([66378](https://github.com/WordPress/gutenberg/pull/66378))
+- Post fields: Clean up. ([66941](https://github.com/WordPress/gutenberg/pull/66941))
+- Post fields: Extract `title` from `edit-site` to `fields` package. ([66940](https://github.com/WordPress/gutenberg/pull/66940))
+- Post fields: Move `comment_status` from edit-site to fields package. ([66934](https://github.com/WordPress/gutenberg/pull/66934))
+- Post fields: Move `date` fields from `edit-site` to `fields` package. ([66938](https://github.com/WordPress/gutenberg/pull/66938))
+- Post fields: Move `status` from `edit-site` to `fields`. ([66937](https://github.com/WordPress/gutenberg/pull/66937))
+- Relocate “View” external link to end of editor header controls. ([66785](https://github.com/WordPress/gutenberg/pull/66785))
+
+#### Block Library
+- Added toggle control to set any image as feature image if no feature image is set for post. ([65896](https://github.com/WordPress/gutenberg/pull/65896))
+- Improve cover z-index solution. ([66249](https://github.com/WordPress/gutenberg/pull/66249))
+- Post Content: Add border and spacing support. ([66366](https://github.com/WordPress/gutenberg/pull/66366))
+- Query Loop: Use templateSlug and postType for more context. ([65820](https://github.com/WordPress/gutenberg/pull/65820))
+- Update text case of "Starter Content". ([66954](https://github.com/WordPress/gutenberg/pull/66954))
+- [Details Block]: Adds anchor support in details block. ([66734](https://github.com/WordPress/gutenberg/pull/66734))
+
+#### Components
+- Guide: Use small size button for page controls. ([66607](https://github.com/WordPress/gutenberg/pull/66607))
+- MenuItem: Add 40px size prop on Button. ([66596](https://github.com/WordPress/gutenberg/pull/66596))
+- Notice: Add appropriate size props to Buttons. ([66593](https://github.com/WordPress/gutenberg/pull/66593))
+- PaletteEdit: Add appropriate size props to Buttons. ([66590](https://github.com/WordPress/gutenberg/pull/66590))
+- Popover: Add small size prop to close button. ([66587](https://github.com/WordPress/gutenberg/pull/66587))
+
+#### Global Styles
+- Global styles revisions: Move focus and active state to list item. ([66780](https://github.com/WordPress/gutenberg/pull/66780))
+- Site editor: Integrate global styles controls and style book preview into the styles panel. ([65619](https://github.com/WordPress/gutenberg/pull/65619))
+
+#### DataViews
+- DataViews Fields API: Default getValueFromId supports nested objects. ([66890](https://github.com/WordPress/gutenberg/pull/66890))
+
+#### Block Editor
+- Inserter: Add 'Starter Content' category to the inserter. ([66819](https://github.com/WordPress/gutenberg/pull/66819))
+
+#### Zoom Out
+- Enable zoom out mode for non-iframe editor. ([66789](https://github.com/WordPress/gutenberg/pull/66789))
+
+#### Themes
+- Theme JSON Resolver: Remove theme json merge in resolve_theme_file_uris. ([66662](https://github.com/WordPress/gutenberg/pull/66662))
+
+#### Edit Mode
+- Image block: Add support for "more" dropdown for additional tools in Write mode. ([66605](https://github.com/WordPress/gutenberg/pull/66605))
+
+#### Style Book
+- Add a landing section to stylebook tabs. ([66545](https://github.com/WordPress/gutenberg/pull/66545))
+
+#### Media
+- Media Library: Expose filters dropdown for individual images, such as with the Image block. ([65965](https://github.com/WordPress/gutenberg/pull/65965))
+
+
+### Bug Fixes
+
+- Block toolbar: Restrict visible child calculation to known blocks. ([66702](https://github.com/WordPress/gutenberg/pull/66702))
+- ComplementaryArea: Fix button position. ([66677](https://github.com/WordPress/gutenberg/pull/66677))
+- Fix Paragraph appender layout shift (building on 66061). ([66779](https://github.com/WordPress/gutenberg/pull/66779))
+- Fix: Set the `fit-content` width for images that are not `.svg`. ([66643](https://github.com/WordPress/gutenberg/pull/66643))
+- Preference modal: Avoid fetching all reusable blocks when the site editor loads. ([66621](https://github.com/WordPress/gutenberg/pull/66621))
+- Revert "Set image width to `fit-content` to solve aspect ratio problems in Firefox. (#66217)". ([66804](https://github.com/WordPress/gutenberg/pull/66804))
+- Safari: Fix site editor template error. ([66647](https://github.com/WordPress/gutenberg/pull/66647))
+- Safari: Prevent focus capturing caused by flex display. ([66402](https://github.com/WordPress/gutenberg/pull/66402))
+- Select Mode: Hide tool selector in the post editor and force design mode. ([66784](https://github.com/WordPress/gutenberg/pull/66784))
+- Shadow panel: Make the delete modal text translatable. ([66712](https://github.com/WordPress/gutenberg/pull/66712))
+- Site Editor: Fix template for page-on-front option. ([66739](https://github.com/WordPress/gutenberg/pull/66739))
+- WP Scripts: Make watch mode more resilient for developer errors. ([66752](https://github.com/WordPress/gutenberg/pull/66752))
+- getDefaultTemplateId: Ensure entity configuration is loaded. ([66650](https://github.com/WordPress/gutenberg/pull/66650))
+- Comments controller: fix issue where comments are allowed when closed (https://github.com/WordPress/gutenberg/pull/66976)
+
+#### Block Library
+- Cover: Fix media library image selection. ([66782](https://github.com/WordPress/gutenberg/pull/66782))
+- Cover: Show DropZone only when dragging withing the block. ([66912](https://github.com/WordPress/gutenberg/pull/66912))
+- Media & Text: Set `.wp-block-media-text__media a` display to block. ([66915](https://github.com/WordPress/gutenberg/pull/66915))
+- Prevent duplicate post format taxonomy queries. ([66627](https://github.com/WordPress/gutenberg/pull/66627))
+- Query Loop: Check for postTypeFromContext before using it. ([66655](https://github.com/WordPress/gutenberg/pull/66655))
+- Query Loop: Remove postTypeFromContext. ([66681](https://github.com/WordPress/gutenberg/pull/66681))
+
+#### Block Editor
+- Appender: Fix initial position. ([66711](https://github.com/WordPress/gutenberg/pull/66711))
+- Appender: Fix outside canvas styles. ([66630](https://github.com/WordPress/gutenberg/pull/66630))
+- Block Inspector: Restore bottom margin for RadioControl. ([66688](https://github.com/WordPress/gutenberg/pull/66688))
+- Iframed editor: Fix relative wp-content URLs. ([66751](https://github.com/WordPress/gutenberg/pull/66751))
+
+#### Global Styles
+- Section Styles: Fix insecure properties removal for inner block types and elements. ([66896](https://github.com/WordPress/gutenberg/pull/66896))
+- Style book: Reduce margin selector specificity so that it doesn't override global block styles. ([66895](https://github.com/WordPress/gutenberg/pull/66895))
+- Theme JSON: Replace top-level background style objects on merge. ([66656](https://github.com/WordPress/gutenberg/pull/66656))
+
+#### Components
+- FormTokenField: Fix token styles. ([66640](https://github.com/WordPress/gutenberg/pull/66640))
+- Storybook: Fix DataViews action modals. ([66727](https://github.com/WordPress/gutenberg/pull/66727))
+- ToggleGroupControl: Fix active background for `zero` value. ([66855](https://github.com/WordPress/gutenberg/pull/66855))
+
+#### Post Editor
+- Disable device preview button in pattern/template part/navitation editor. ([65970](https://github.com/WordPress/gutenberg/pull/65970))
+- PostTaxonomiesFlatTermSelector: Abstract wrapper component. ([66625](https://github.com/WordPress/gutenberg/pull/66625))
+- VisualEditor: Always output has-global-padding classname when in post only mode. ([66626](https://github.com/WordPress/gutenberg/pull/66626))
+
+#### DataViews
+- Fix TypeError when duplicating uncategorized theme patterns. ([66889](https://github.com/WordPress/gutenberg/pull/66889))
+- Tweak primary field in patterns grid layout. ([66733](https://github.com/WordPress/gutenberg/pull/66733))
+
+#### Meta Boxes
+- Fix: Show Meta Boxes at the bottom of the screen regardless of the current rendering mode. ([66508](https://github.com/WordPress/gutenberg/pull/66508))
+- Hide metaboxes in Zoom Out. ([66886](https://github.com/WordPress/gutenberg/pull/66886))
+
+#### Site Editor
+- DataViews: Fix 'aria-label' for pattern preview element. ([66601](https://github.com/WordPress/gutenberg/pull/66601))
+- Site Hub: Fixed navigation redirect on mobile devices for classic themes. ([66867](https://github.com/WordPress/gutenberg/pull/66867))
+
+#### Media
+- Add `x-wav` mime type for wav files in Firefox. ([66850](https://github.com/WordPress/gutenberg/pull/66850))
+- Ensure HEIC files selectable from “Upload” button. ([66292](https://github.com/WordPress/gutenberg/pull/66292))
+
+#### Patterns
+- Fix uncategorized pattern browsing when pattern has no categories. ([66945](https://github.com/WordPress/gutenberg/pull/66945))
+
+#### Interactivity API
+- Fix property modification from inherited context two or more levels above. ([66872](https://github.com/WordPress/gutenberg/pull/66872))
+
+#### Block API
+- Process Block Type: Copy deprecation to a new object instead of mutating when stabilizing supports. ([66849](https://github.com/WordPress/gutenberg/pull/66849))
+
+#### Design Tools
+- Block Gap: Fix block spacing control for axial gap supported blocks. ([66783](https://github.com/WordPress/gutenberg/pull/66783))
+
+#### Document Settings
+- Editor: Restore the 'PluginPostStatusInfo' slot position. ([66665](https://github.com/WordPress/gutenberg/pull/66665))
+
+#### Templates API
+- Fix flash when clicking template name in the editor when a plugin registered template matches a default WP theme template. ([66359](https://github.com/WordPress/gutenberg/pull/66359))
+
+#### Block bindings
+- Fix unset array key warning in block-bindings.php. ([66337](https://github.com/WordPress/gutenberg/pull/66337))
+
+
+### Accessibility
+
+- Fix : Snackbar Notice Inconsistency. ([66405](https://github.com/WordPress/gutenberg/pull/66405))
+- Image: Add `aria-haspopup` prop write mode `more` tools menu items. ([66815](https://github.com/WordPress/gutenberg/pull/66815))
+- Site Icon Focus fix. ([66952](https://github.com/WordPress/gutenberg/pull/66952))
+
+#### Components
+- Popover: Fix missing label of the headerTitle Close button. ([66813](https://github.com/WordPress/gutenberg/pull/66813))
+
+#### Post Editor
+- Fix inconsistent sidebars close buttons sizes. ([66756](https://github.com/WordPress/gutenberg/pull/66756))
+
+#### Block Library
+- Remove unnecessary tooltip from Video block Text tracks button. ([66716](https://github.com/WordPress/gutenberg/pull/66716))
+
+#### Block Editor
+- Speak 'Block moved up/down' after using keyboard actions to move up/down. ([64966](https://github.com/WordPress/gutenberg/pull/64966))
+
+#### Patterns
+- Block Patterns List: Fix visual title and tooltip inconsistencies. ([64815](https://github.com/WordPress/gutenberg/pull/64815))
+
+
+### Performance
+
+- Inline Commenting: Avoid querying comments on editor load. ([66670](https://github.com/WordPress/gutenberg/pull/66670))
+- Patterns: Receive intermediate responses while unbound request is resolving. ([66713](https://github.com/WordPress/gutenberg/pull/66713))
+- Perf metrics: Update select and other metrics to use non-empty paragraphs. ([66762](https://github.com/WordPress/gutenberg/pull/66762))
+- Site Editor: Preload settings requests. ([66488](https://github.com/WordPress/gutenberg/pull/66488))
+- Site Editor: Speed up load by preloading home and front-page templates. ([66579](https://github.com/WordPress/gutenberg/pull/66579))
+- Site editor: Preload post if needed. ([66631](https://github.com/WordPress/gutenberg/pull/66631))
+
+#### Global Styles
+- Preload user global styles based on user caps. ([66541](https://github.com/WordPress/gutenberg/pull/66541))
+
+
+### Experiments
+
+- Add `isVisible` option to fields within DataForm. ([65826](https://github.com/WordPress/gutenberg/pull/65826))
+- DataViews: Implement `isItemClickable` and `onClickItem` props. ([66365](https://github.com/WordPress/gutenberg/pull/66365))
+
+#### DataViews
+- Quick Edit - Slug Field: Improve slug preview. ([66559](https://github.com/WordPress/gutenberg/pull/66559))
+- QuickEdit: Add password field data to the pages quick edit. ([66567](https://github.com/WordPress/gutenberg/pull/66567))
+
+
+### Documentation
+
+- Add 6.6.2 to Version in WordPress. ([66870](https://github.com/WordPress/gutenberg/pull/66870))
+- Add missing properties for DataViews/DataForm components. ([66749](https://github.com/WordPress/gutenberg/pull/66749))
+- Add section about the Fields API. ([66761](https://github.com/WordPress/gutenberg/pull/66761))
+- Block Bindings: Documentation API reference. ([66251](https://github.com/WordPress/gutenberg/pull/66251))
+- Docs: Include a note about supported licenses in WordPress packages. ([66562](https://github.com/WordPress/gutenberg/pull/66562))
+- Document `filterSortAndPaginate` & `isItemValid` utilities. ([66738](https://github.com/WordPress/gutenberg/pull/66738))
+- Feat: Storybook: Improve component organisation - Navigation Category - Issue #66275. ([66658](https://github.com/WordPress/gutenberg/pull/66658))
+- Feat: Storybook: Improve component organisation - Overlays Category - Issue #66275. ([66657](https://github.com/WordPress/gutenberg/pull/66657))
+- Feat: Storybook: Improve component organisation - Selection & Input Category - Issue #66275. ([66660](https://github.com/WordPress/gutenberg/pull/66660))
+- Feat: Storybook: Improve component organisation - Typography - Issue #66275. ([66633](https://github.com/WordPress/gutenberg/pull/66633))
+- Improve readability of DataViews documentation. ([66766](https://github.com/WordPress/gutenberg/pull/66766))
+- Move documentation for filter operators to proper place. ([66743](https://github.com/WordPress/gutenberg/pull/66743))
+- Reorganize to bootstrap DataForm API section. ([66729](https://github.com/WordPress/gutenberg/pull/66729))
+- Storybook: Improve component organisation - Actions. ([66680](https://github.com/WordPress/gutenberg/pull/66680))
+- Storybook: Log `warning()` when in dev mode. ([66568](https://github.com/WordPress/gutenberg/pull/66568))
+- Update Commands documentation with the existing contexts. ([66860](https://github.com/WordPress/gutenberg/pull/66860))
+
+
+### Code Quality
+
+- BlockPatternsList: Use the Async component. ([66744](https://github.com/WordPress/gutenberg/pull/66744))
+- Core Commands: Fix add new post URL assignment. ([66830](https://github.com/WordPress/gutenberg/pull/66830))
+- Inline Commenting: Optimize store selector and misc changes. ([66592](https://github.com/WordPress/gutenberg/pull/66592))
+- Remove unnecessary boolean assignments. ([66857](https://github.com/WordPress/gutenberg/pull/66857))
+- TypeScript: Fix and improve types for private-apis. ([66667](https://github.com/WordPress/gutenberg/pull/66667))
+
+#### Block Editor
+- Fix 'useSelect' dependencies for the 'RichText' component. ([66964](https://github.com/WordPress/gutenberg/pull/66964))
+- Fix ESLint warning for 'useBlockTypesState' hook. ([66757](https://github.com/WordPress/gutenberg/pull/66757))
+- Fix React Compiler error for 'BlockProps' util component. ([66809](https://github.com/WordPress/gutenberg/pull/66809))
+- Optimize `getVisibleElementBounds` in scrollable cases. ([66546](https://github.com/WordPress/gutenberg/pull/66546))
+- Revert: Fix unable to remove empty blocks on merge (#65262) + alternative. ([66564](https://github.com/WordPress/gutenberg/pull/66564))
+- URLInput: Fix incorrect classname for suggestions. ([66714](https://github.com/WordPress/gutenberg/pull/66714))
+
+#### Site Editor
+- Avoid using edited entity state in site editor loading hook. ([66924](https://github.com/WordPress/gutenberg/pull/66924))
+- Avoid using edited post selectors in welcome guide. ([66926](https://github.com/WordPress/gutenberg/pull/66926))
+- Edit Site: Refactor to remove usage of edited entity state. ([66922](https://github.com/WordPress/gutenberg/pull/66922))
+- Edit Site: Remove leftover 'priority-queue' dependency. ([66773](https://github.com/WordPress/gutenberg/pull/66773))
+- Remove useEditedEntityRecord hook. ([66955](https://github.com/WordPress/gutenberg/pull/66955))
+
+#### Components
+- Fix React Compiler error for 'useScrollRectIntoView'. ([66498](https://github.com/WordPress/gutenberg/pull/66498))
+- Panel: Add 40px size prop to Button. ([66589](https://github.com/WordPress/gutenberg/pull/66589))
+- Radio: Deprecate 36px default size. ([66572](https://github.com/WordPress/gutenberg/pull/66572))
+- Snackbar: Use `link` variant for action Button. ([66560](https://github.com/WordPress/gutenberg/pull/66560))
+
+#### Data Layer
+- Convert the emitter module in data package to TS. ([66669](https://github.com/WordPress/gutenberg/pull/66669))
+- Data: Rename useSelect internals to fix React Compiler violations. ([66807](https://github.com/WordPress/gutenberg/pull/66807))
+- Data: Upgrade Redux to v5.0.1. ([66966](https://github.com/WordPress/gutenberg/pull/66966))
+
+#### Post Editor
+- ESLint: Fix React Compiler violations in various commands. ([66787](https://github.com/WordPress/gutenberg/pull/66787))
+- Fix TS types for editor package. ([66754](https://github.com/WordPress/gutenberg/pull/66754))
+
+#### Zoom Out
+- Zoom-out: Move default background to the iframe component. ([66284](https://github.com/WordPress/gutenberg/pull/66284))
+
+#### Design Tools
+- Typography: Stabilize typography block supports within block processing. ([63401](https://github.com/WordPress/gutenberg/pull/63401))
+
+
+### Tools
+
+#### Testing
+- Media: Check for `wav` mime type using isset. ([66947](https://github.com/WordPress/gutenberg/pull/66947))
+
+#### Build Tooling
+- Enforce the same order of fields in `package.json` files. ([66239](https://github.com/WordPress/gutenberg/pull/66239))
+- Introduce React Scanner for component usage stats. ([65463](https://github.com/WordPress/gutenberg/pull/65463))
+
+
+### Various
+
+- Style engine: Wrap array_merge in conditionals to prevent unnecessary merging. ([66661](https://github.com/WordPress/gutenberg/pull/66661))
+
+#### Block Library
+- Update placeholder text for blocks that support drag and drop. ([66842](https://github.com/WordPress/gutenberg/pull/66842))
+- update: Add Media to Add media in cover block. ([66835](https://github.com/WordPress/gutenberg/pull/66835))
+
+
+## First-time contributors
+
+The following PRs were merged by first-time contributors:
+
+- @benharri: Fix unset array key warning in block-bindings.php. ([66337](https://github.com/WordPress/gutenberg/pull/66337))
+- @benniledl: Add 6.6.2 to Version in WordPress. ([66870](https://github.com/WordPress/gutenberg/pull/66870))
+- @Infinite-Null: Media & Text: Set `.wp-block-media-text__media a` display to block. ([66915](https://github.com/WordPress/gutenberg/pull/66915))
+- @karthick-murugan: Site Icon Focus fix. ([66952](https://github.com/WordPress/gutenberg/pull/66952))
+- @rinkalpagdar: Post Content: Add border and spacing support. ([66366](https://github.com/WordPress/gutenberg/pull/66366))
+- @yogeshbhutkar: Site Hub: Fixed navigation redirect on mobile devices for classic themes. ([66867](https://github.com/WordPress/gutenberg/pull/66867))
+
+
+## Contributors
+
+The following contributors merged PRs in this release:
+
+@aaronrobertshaw @adamsilverstein @afercia @Aljullu @amitraj2203 @andrewserong @benharri @benniledl @carolinan @cbravobernal @DAreRodz @dcalhoun @ellatrix @fabiankaegy @gigitux @gziolo @hbhalodia @Infinite-Null @jasmussen @jorgefilipecosta @jsnajdr @juanfra @karthick-murugan @kevin940726 @louwie17 @Mamaduka @manzoorwanijk @matiasbenedetto @mikachan @mirka @n2erjo00 @ntsekouras @oandregal @ramonjd @renatho @rinkalpagdar @Soean @stokesman @swissspidy @t-hamano @tellthemachines @tyxla @up1512001 @Vrishabhsk @yogeshbhutkar @youknowriad
+
+
+
+
= 19.8.0-rc.1 =
diff --git a/docs/reference-guides/data/data-core.md b/docs/reference-guides/data/data-core.md
index 474207aa20460f..199c29cd67dd2e 100644
--- a/docs/reference-guides/data/data-core.md
+++ b/docs/reference-guides/data/data-core.md
@@ -359,7 +359,7 @@ _Parameters_
- _state_ `State`: State tree
- _kind_ `string`: Entity kind.
- _name_ `string`: Entity name.
-- _key_ `EntityRecordKey`: Record's key
+- _key_ `EntityRecordKey`: Optional record's key. If requesting a global record (e.g. site settings), the key can be omitted. If requesting a specific item, the key must always be included.
- _query_ `GetRecordsHttpQuery`: Optional query. If requesting specific fields, fields must always include the ID. For valid query parameters see the [Reference](https://developer.wordpress.org/rest-api/reference/) in the REST API Handbook and select the entity kind. Then see the arguments available "Retrieve a [Entity kind]".
_Returns_
diff --git a/lib/compat/wordpress-6.8/class-gutenberg-rest-user-controller.php b/lib/compat/wordpress-6.8/class-gutenberg-rest-user-controller.php
new file mode 100644
index 00000000000000..c1ecb8c86660cd
--- /dev/null
+++ b/lib/compat/wordpress-6.8/class-gutenberg-rest-user-controller.php
@@ -0,0 +1,62 @@
+ array(),
+ 'description' => __( 'Array of column names to be searched.' ),
+ 'type' => 'array',
+ 'items' => array(
+ 'enum' => array( 'email', 'name', 'id', 'username', 'slug' ),
+ 'type' => 'string',
+ ),
+ );
+
+ return $query_params;
+}
+
+add_filter( 'rest_user_collection_params', 'gutenberg_add_search_columns_param', 10, 1 );
+
+/**
+ * Modify user query based on search_columns parameter
+ *
+ * @param array $prepared_args Array of arguments for WP_User_Query.
+ * @param WP_REST_Request $request The REST API request.
+ * @return array Modified arguments
+ */
+function gutenberg_modify_user_query_args( $prepared_args, $request ) {
+ if ( $request->get_param( 'search' ) && $request->get_param( 'search_columns' ) ) {
+ $search_columns = $request->get_param( 'search_columns' );
+
+ // Validate search columns
+ $valid_columns = isset( $prepared_args['search_columns'] )
+ ? $prepared_args['search_columns']
+ : array( 'ID', 'user_login', 'user_nicename', 'user_email', 'user_url', 'display_name' );
+ $search_columns_mapping = array(
+ 'id' => 'ID',
+ 'username' => 'user_login',
+ 'slug' => 'user_nicename',
+ 'email' => 'user_email',
+ 'name' => 'display_name',
+ );
+ $search_columns = array_map(
+ static function ( $column ) use ( $search_columns_mapping ) {
+ return $search_columns_mapping[ $column ];
+ },
+ $search_columns
+ );
+ $search_columns = array_intersect( $search_columns, $valid_columns );
+
+ if ( ! empty( $search_columns ) ) {
+ $prepared_args['search_columns'] = $search_columns;
+ }
+ }
+
+ return $prepared_args;
+}
+add_filter( 'rest_user_query', 'gutenberg_modify_user_query_args', 10, 2 );
diff --git a/lib/compat/wordpress-6.8/site-editor.php b/lib/compat/wordpress-6.8/site-editor.php
new file mode 100644
index 00000000000000..cde108830b1d2c
--- /dev/null
+++ b/lib/compat/wordpress-6.8/site-editor.php
@@ -0,0 +1,124 @@
+ '/wp_navigation/' . $_REQUEST['postId'] ), remove_query_arg( array( 'postType', 'postId' ) ) );
+ }
+
+ if ( isset( $_REQUEST['postType'] ) && 'wp_navigation' === $_REQUEST['postType'] && empty( $_REQUEST['postId'] ) ) {
+ return add_query_arg( array( 'p' => '/navigation' ), remove_query_arg( 'postType' ) );
+ }
+
+ if ( isset( $_REQUEST['path'] ) && '/wp_global_styles' === $_REQUEST['path'] ) {
+ return add_query_arg( array( 'p' => '/styles' ), remove_query_arg( 'path' ) );
+ }
+
+ if ( isset( $_REQUEST['postType'] ) && 'page' === $_REQUEST['postType'] && ( empty( $_REQUEST['canvas'] ) || empty( $_REQUEST['postId'] ) ) ) {
+ return add_query_arg( array( 'p' => '/page' ), remove_query_arg( 'postType' ) );
+ }
+
+ if ( isset( $_REQUEST['postType'] ) && 'page' === $_REQUEST['postType'] && ! empty( $_REQUEST['postId'] ) ) {
+ return add_query_arg( array( 'p' => '/page/' . $_REQUEST['postId'] ), remove_query_arg( array( 'postType', 'postId' ) ) );
+ }
+
+ if ( isset( $_REQUEST['postType'] ) && 'wp_template' === $_REQUEST['postType'] && ( empty( $_REQUEST['canvas'] ) || empty( $_REQUEST['postId'] ) ) ) {
+ return add_query_arg( array( 'p' => '/template' ), remove_query_arg( 'postType' ) );
+ }
+
+ if ( isset( $_REQUEST['postType'] ) && 'wp_template' === $_REQUEST['postType'] && ! empty( $_REQUEST['postId'] ) ) {
+ return add_query_arg( array( 'p' => '/wp_template/' . $_REQUEST['postId'] ), remove_query_arg( array( 'postType', 'postId' ) ) );
+ }
+
+ if ( isset( $_REQUEST['postType'] ) && 'wp_block' === $_REQUEST['postType'] && ( empty( $_REQUEST['canvas'] ) || empty( $_REQUEST['postId'] ) ) ) {
+ return add_query_arg( array( 'p' => '/pattern' ), remove_query_arg( 'postType' ) );
+ }
+
+ if ( isset( $_REQUEST['postType'] ) && 'wp_block' === $_REQUEST['postType'] && ! empty( $_REQUEST['postId'] ) ) {
+ return add_query_arg( array( 'p' => '/wp_block/' . $_REQUEST['postId'] ), remove_query_arg( array( 'postType', 'postId' ) ) );
+ }
+
+ if ( isset( $_REQUEST['postType'] ) && 'wp_template_part' === $_REQUEST['postType'] && ( empty( $_REQUEST['canvas'] ) || empty( $_REQUEST['postId'] ) ) ) {
+ return add_query_arg( array( 'p' => '/pattern' ) );
+ }
+
+ if ( isset( $_REQUEST['postType'] ) && 'wp_template_part' === $_REQUEST['postType'] && ! empty( $_REQUEST['postId'] ) ) {
+ return add_query_arg( array( 'p' => '/wp_template_part/' . $_REQUEST['postId'] ), remove_query_arg( array( 'postType', 'postId' ) ) );
+ }
+
+ // The following redirects are for backward compatibility with the old site editor URLs.
+ if ( isset( $_REQUEST['path'] ) && '/wp_template_part/all' === $_REQUEST['path'] ) {
+ return add_query_arg(
+ array(
+ 'p' => '/pattern',
+ 'postType' => 'wp_template_part',
+ ),
+ remove_query_arg( 'path' )
+ );
+ }
+
+ if ( isset( $_REQUEST['path'] ) && '/page' === $_REQUEST['path'] ) {
+ return add_query_arg( array( 'p' => '/page' ), remove_query_arg( 'path' ) );
+ }
+
+ if ( isset( $_REQUEST['path'] ) && '/wp_template' === $_REQUEST['path'] ) {
+ return add_query_arg( array( 'p' => '/template' ), remove_query_arg( 'path' ) );
+ }
+
+ if ( isset( $_REQUEST['path'] ) && '/patterns' === $_REQUEST['path'] ) {
+ return add_query_arg( array( 'p' => '/pattern' ), remove_query_arg( 'path' ) );
+ }
+
+ if ( isset( $_REQUEST['path'] ) && '/navigation' === $_REQUEST['path'] ) {
+ return add_query_arg( array( 'p' => '/navigation' ), remove_query_arg( 'path' ) );
+ }
+
+ return add_query_arg( array( 'p' => '/' ) );
+}
+
+function gutenberg_redirect_site_editor_deprecated_urls() {
+ $redirection = gutenberg_get_site_editor_redirection();
+ if ( false !== $redirection ) {
+ wp_redirect( $redirection, 301 );
+ exit;
+ }
+}
+add_action( 'admin_init', 'gutenberg_redirect_site_editor_deprecated_urls' );
+
+/**
+ * Filter the `wp_die_handler` to allow access to the Site Editor's new pages page
+ * for Classic themes.
+ *
+ * site-editor.php's access is forbidden for hybrid/classic themes and only allowed with some very special query args (some very special pages like template parts...).
+ * The only way to disable this protection since we're changing the urls in Gutenberg is to override the wp_die_handler.
+ *
+ * @param callable $default_handler The default handler.
+ * @return callable The default handler or a custom handler.
+ */
+function gutenberg_styles_wp_die_handler( $default_handler ) {
+ if ( ! wp_is_block_theme() && str_contains( $_SERVER['REQUEST_URI'], 'site-editor.php' ) && isset( $_GET['p'] ) ) {
+ return '__return_false';
+ }
+ return $default_handler;
+}
+add_filter( 'wp_die_handler', 'gutenberg_styles_wp_die_handler' );
diff --git a/lib/experimental/posts/load.php b/lib/experimental/posts/load.php
index 7321392b11a25d..699534f1886f52 100644
--- a/lib/experimental/posts/load.php
+++ b/lib/experimental/posts/load.php
@@ -69,18 +69,6 @@ function gutenberg_posts_dashboard() {
echo '
';
}
-/**
- * Redirects to the new posts dashboard page and adds the postType query arg.
- */
-function gutenberg_add_post_type_arg() {
- global $pagenow;
- if ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'gutenberg-posts-dashboard' === $_GET['page'] && empty( $_GET['postType'] ) ) {
- wp_redirect( admin_url( '/admin.php?page=gutenberg-posts-dashboard&postType=post' ) );
- exit;
- }
-}
-add_action( 'admin_init', 'gutenberg_add_post_type_arg' );
-
/**
* Replaces the default posts menu item with the new posts dashboard.
*/
diff --git a/lib/load.php b/lib/load.php
index 85d1c7e3292b50..26af78f3173c53 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -98,6 +98,8 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/compat/wordpress-6.8/blocks.php';
require __DIR__ . '/compat/wordpress-6.8/functions.php';
require __DIR__ . '/compat/wordpress-6.8/post.php';
+require __DIR__ . '/compat/wordpress-6.8/site-editor.php';
+require __DIR__ . '/compat/wordpress-6.8/class-gutenberg-rest-user-controller.php';
// Experimental features.
require __DIR__ . '/experimental/block-editor-settings-mobile.php';
diff --git a/package-lock.json b/package-lock.json
index ccf779f2d67eab..80a64c6f7a04ba 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44492,6 +44492,12 @@
"node": ">=10.0.0"
}
},
+ "node_modules/route-recognizer": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/route-recognizer/-/route-recognizer-0.3.4.tgz",
+ "integrity": "sha512-2+MhsfPhvauN1O8KaXpXAOfR/fwe8dnUXVM+xw7yt40lJRfPVQxV6yryZm0cgRvAj5fMF/mdRZbL2ptwbs5i2g==",
+ "license": "MIT"
+ },
"node_modules/rrweb-cssom": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
@@ -53957,7 +53963,6 @@
"@wordpress/core-data": "*",
"@wordpress/data": "*",
"@wordpress/dom": "*",
- "@wordpress/editor": "*",
"@wordpress/element": "*",
"@wordpress/hooks": "*",
"@wordpress/i18n": "*",
@@ -54386,7 +54391,6 @@
"@wordpress/data": "*",
"@wordpress/deprecated": "*",
"@wordpress/dom": "*",
- "@wordpress/editor": "*",
"@wordpress/element": "*",
"@wordpress/hooks": "*",
"@wordpress/i18n": "*",
@@ -55563,10 +55567,12 @@
"license": "GPL-2.0-or-later",
"dependencies": {
"@babel/runtime": "7.25.7",
+ "@wordpress/compose": "*",
"@wordpress/element": "*",
"@wordpress/private-apis": "*",
"@wordpress/url": "*",
- "history": "^5.3.0"
+ "history": "^5.3.0",
+ "route-recognizer": "^0.3.4"
},
"engines": {
"node": ">=18.12.0",
diff --git a/packages/block-editor/src/components/alignment-control/stories/aliginment-toolbar.story.js b/packages/block-editor/src/components/alignment-control/stories/aliginment-toolbar.story.js
new file mode 100644
index 00000000000000..f2191220d6bb4c
--- /dev/null
+++ b/packages/block-editor/src/components/alignment-control/stories/aliginment-toolbar.story.js
@@ -0,0 +1,47 @@
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { AlignmentToolbar } from '..';
+
+/**
+ * The `AlignmentToolbar` component renders a dropdown menu that displays alignment options for the selected block in `Toolbar`.
+ */
+const meta = {
+ title: 'BlockEditor/AlignmentToolbar',
+ component: AlignmentToolbar,
+ argTypes: {
+ value: {
+ control: { type: null },
+ defaultValue: 'undefined',
+ description: 'The current value of the alignment setting.',
+ },
+ onChange: {
+ action: 'onChange',
+ control: { type: null },
+ description:
+ "A callback function invoked when the toolbar's alignment value is changed via an interaction with any of the toolbar's buttons. Called with the new alignment value (ie: `left`, `center`, `right`, `undefined`) as the only argument.",
+ },
+ },
+};
+export default meta;
+
+export const Default = {
+ render: function Template( { onChange, ...args } ) {
+ const [ value, setValue ] = useState();
+ return (
+ {
+ onChange( ...changeArgs );
+ setValue( ...changeArgs );
+ } }
+ value={ value }
+ />
+ );
+ },
+};
diff --git a/packages/block-editor/src/components/alignment-control/stories/index.story.js b/packages/block-editor/src/components/alignment-control/stories/index.story.js
new file mode 100644
index 00000000000000..85c92f7e0665a4
--- /dev/null
+++ b/packages/block-editor/src/components/alignment-control/stories/index.story.js
@@ -0,0 +1,51 @@
+/**
+ * WordPress dependencies
+ */
+import { useState } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import { AlignmentControl } from '../';
+
+/**
+ * The `AlignmentControl` component renders a dropdown menu that displays alignment options for the selected block.
+ *
+ * This component is mostly used for blocks that display text, such as Heading, Paragraph, Post Author, Post Comments, Verse, Quote, Post Title, etc... And the available alignment options are `left`, `center` or `right` alignment.
+ *
+ * If you want to use the alignment control in a toolbar, you should use the `AlignmentToolbar` component instead.
+ */
+const meta = {
+ title: 'BlockEditor/AlignmentControl',
+ component: AlignmentControl,
+ argTypes: {
+ value: {
+ control: { type: null },
+ defaultValue: 'undefined',
+ description: 'The current value of the alignment setting.',
+ },
+ onChange: {
+ action: 'onChange',
+ control: { type: null },
+ description:
+ "A callback function invoked when the toolbar's alignment value is changed via an interaction with any of the toolbar's buttons. Called with the new alignment value (ie: `left`, `center`, `right`, `undefined`) as the only argument.",
+ },
+ },
+};
+export default meta;
+
+export const Default = {
+ render: function Template( { onChange, ...args } ) {
+ const [ value, setValue ] = useState();
+ return (
+ {
+ onChange( ...changeArgs );
+ setValue( ...changeArgs );
+ } }
+ value={ value }
+ />
+ );
+ },
+};
diff --git a/packages/block-editor/src/components/block-patterns-list/index.js b/packages/block-editor/src/components/block-patterns-list/index.js
index 8128e89418f45a..0c7e54c3c62b24 100644
--- a/packages/block-editor/src/components/block-patterns-list/index.js
+++ b/packages/block-editor/src/components/block-patterns-list/index.js
@@ -41,6 +41,7 @@ function BlockPattern( {
onHover,
showTitlesAsTooltip,
category,
+ isSelected,
} ) {
const [ isDragging, setIsDragging ] = useState( false );
const { blocks, viewportWidth } = pattern;
@@ -114,6 +115,7 @@ function BlockPattern( {
pattern.type ===
INSERTER_PATTERN_TYPES.user &&
! pattern.syncStatus,
+ 'is-selected': isSelected,
}
) }
/>
@@ -192,6 +194,7 @@ function BlockPatternsList(
ref
) {
const [ activeCompositeId, setActiveCompositeId ] = useState( undefined );
+ const [ activePattern, setActivePattern ] = useState( null ); // State to track active pattern
useEffect( () => {
// Reset the active composite item whenever the available patterns change,
@@ -201,6 +204,11 @@ function BlockPatternsList(
setActiveCompositeId( firstCompositeItemId );
}, [ blockPatterns ] );
+ const handleClickPattern = ( pattern, blocks ) => {
+ setActivePattern( pattern.name );
+ onClickPattern( pattern, blocks );
+ };
+
return (
) ) }
{ pagingProps && }
diff --git a/packages/block-editor/src/components/block-patterns-list/style.scss b/packages/block-editor/src/components/block-patterns-list/style.scss
index c46bb49b9a9016..8b1b0b54c9b1a0 100644
--- a/packages/block-editor/src/components/block-patterns-list/style.scss
+++ b/packages/block-editor/src/components/block-patterns-list/style.scss
@@ -44,19 +44,29 @@
outline: $border-width solid rgba($black, 0.1);
outline-offset: -$border-width;
border-radius: $radius-medium;
+
+ transition: outline 0.1s linear;
+ @include reduce-motion("transition");
}
}
- &:hover:not(:focus) .block-editor-block-preview__container::after {
+ // Selected
+ &.is-selected .block-editor-block-preview__container::after {
+ outline-color: $gray-900;
+ outline-width: var(--wp-admin-border-width-focus);
+ outline-offset: calc(-1 * var(--wp-admin-border-width-focus));
+ }
+
+ // Hover state
+ &:hover .block-editor-block-preview__container::after {
outline-color: rgba($black, 0.3);
}
- &:focus .block-editor-block-preview__container::after {
+ // Focused state
+ &[data-focus-visible] .block-editor-block-preview__container::after {
outline-color: var(--wp-admin-theme-color);
outline-width: var(--wp-admin-border-width-focus);
- outline-offset: calc((-1 * var(--wp-admin-border-width-focus)));
- transition: outline 0.1s linear;
- @include reduce-motion("transition");
+ outline-offset: calc(-1 * var(--wp-admin-border-width-focus));
}
.block-editor-patterns__pattern-details:not(:empty) {
@@ -68,6 +78,7 @@
.block-editor-patterns__pattern-icon-wrapper {
min-width: 24px;
height: 24px;
+
.block-editor-patterns__pattern-icon {
fill: var(--wp-block-synced-color);
}
diff --git a/packages/block-editor/src/components/block-settings-menu-controls/index.js b/packages/block-editor/src/components/block-settings-menu-controls/index.js
index 4ebce4172e9b37..b0755be4c26297 100644
--- a/packages/block-editor/src/components/block-settings-menu-controls/index.js
+++ b/packages/block-editor/src/components/block-settings-menu-controls/index.js
@@ -55,7 +55,8 @@ const BlockSettingsMenuControlsSlot = ( { fillProps, clientIds = null } ) => {
const convertToGroupButtonProps =
useConvertToGroupButtonProps( selectedClientIds );
const { isGroupable, isUngroupable } = convertToGroupButtonProps;
- const showConvertToGroupButton = isGroupable || isUngroupable;
+ const showConvertToGroupButton =
+ ( isGroupable || isUngroupable ) && ! isContentOnly;
return (
{
const {
@@ -74,6 +76,7 @@ export function BlockSettingsDropdown( {
getBlockAttributes,
getOpenedBlockSettingsMenu,
getBlockEditingMode,
+ isZoomOut: _isZoomOut,
} = unlock( select( blockEditorStore ) );
const { getActiveBlockVariation } = select( blocksStore );
@@ -98,10 +101,12 @@ export function BlockSettingsDropdown( {
openedBlockSettingsMenu: getOpenedBlockSettingsMenu(),
isContentOnly:
getBlockEditingMode( firstBlockClientId ) === 'contentOnly',
+ isZoomOut: _isZoomOut(),
};
},
[ firstBlockClientId ]
);
+
const { getBlockOrder, getSelectedBlockClientIds } =
useSelect( blockEditorStore );
@@ -248,7 +253,7 @@ export function BlockSettingsDropdown( {
clientId={ firstBlockClientId }
/>
) }
- { ! isContentOnly && (
+ { ( ! isContentOnly || isZoomOut ) && (
{
+ const actualImplementation = jest.requireActual( '@wordpress/blocks' );
return {
+ ...actualImplementation,
isReusableBlock( { title } ) {
return title === 'Reusable Block';
},
diff --git a/packages/block-editor/src/components/inserter-draggable-blocks/index.js b/packages/block-editor/src/components/inserter-draggable-blocks/index.js
index 0e1aaadc72e67b..ebef6304937aa7 100644
--- a/packages/block-editor/src/components/inserter-draggable-blocks/index.js
+++ b/packages/block-editor/src/components/inserter-draggable-blocks/index.js
@@ -2,12 +2,9 @@
* WordPress dependencies
*/
import { Draggable } from '@wordpress/components';
-import {
- createBlock,
- serialize,
- store as blocksStore,
-} from '@wordpress/blocks';
+import { createBlock, store as blocksStore } from '@wordpress/blocks';
import { useDispatch, useSelect } from '@wordpress/data';
+import { useMemo } from '@wordpress/element';
/**
* Internal dependencies
@@ -24,20 +21,6 @@ const InserterDraggableBlocks = ( {
children,
pattern,
} ) => {
- const transferData = {
- type: 'inserter',
- blocks,
- };
-
- const blocksContainMedia =
- blocks.filter(
- ( block ) =>
- ( block.name === 'core/image' ||
- block.name === 'core/audio' ||
- block.name === 'core/video' ) &&
- ( block.attributes.url || block.attributes.src )
- ).length > 0;
-
const blockTypeIcon = useSelect(
( select ) => {
const { getBlockType } = select( blocksStore );
@@ -52,6 +35,13 @@ const InserterDraggableBlocks = ( {
useDispatch( blockEditorStore )
);
+ const patternBlock = useMemo( () => {
+ return pattern?.type === INSERTER_PATTERN_TYPES.user &&
+ pattern?.syncStatus !== 'unsynced'
+ ? [ createBlock( 'core/block', { ref: pattern.id } ) ]
+ : undefined;
+ }, [ pattern?.type, pattern?.syncStatus, pattern?.id ] );
+
if ( ! isEnabled ) {
return children( {
draggable: false,
@@ -60,21 +50,21 @@ const InserterDraggableBlocks = ( {
} );
}
+ const draggableBlocks = patternBlock ?? blocks;
return (
{
startDragging();
- const parsedBlocks =
- pattern?.type === INSERTER_PATTERN_TYPES.user &&
- pattern?.syncStatus !== 'unsynced'
- ? [ createBlock( 'core/block', { ref: pattern.id } ) ]
- : blocks;
- event.dataTransfer.setData(
- blocksContainMedia ? 'default' : 'text/html',
- serialize( parsedBlocks )
- );
+ for ( const block of draggableBlocks ) {
+ const type = `wp-block:${ block.name }`;
+ // This will fill in the dataTransfer.types array so that
+ // the drop zone can check if the draggable is eligible.
+ // Unfortuantely, on drag start, we don't have access to the
+ // actual data, only the data keys/types.
+ event.dataTransfer.items.add( '', type );
+ }
} }
onDragEnd={ () => {
stopDragging();
diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js
index e7b6c836468f02..0cbc6c8c26203f 100644
--- a/packages/block-editor/src/components/media-placeholder/index.js
+++ b/packages/block-editor/src/components/media-placeholder/index.js
@@ -19,7 +19,6 @@ import { __ } from '@wordpress/i18n';
import { useState, useEffect } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { keyboardReturn } from '@wordpress/icons';
-import { pasteHandler } from '@wordpress/blocks';
import deprecated from '@wordpress/deprecated';
/**
@@ -29,6 +28,7 @@ import MediaUpload from '../media-upload';
import MediaUploadCheck from '../media-upload/check';
import URLPopover from '../url-popover';
import { store as blockEditorStore } from '../../store';
+import { parseDropEvent } from '../use-on-block-drop';
const noop = () => {};
@@ -229,30 +229,15 @@ export function MediaPlaceholder( {
} );
};
- async function handleBlocksDrop( blocks ) {
- if ( ! blocks || ! Array.isArray( blocks ) ) {
- return;
- }
+ async function handleBlocksDrop( event ) {
+ const { blocks } = parseDropEvent( event );
- function recursivelyFindMediaFromBlocks( _blocks ) {
- return _blocks.flatMap( ( block ) =>
- ( block.name === 'core/image' ||
- block.name === 'core/audio' ||
- block.name === 'core/video' ) &&
- ( block.attributes.url || block.attributes.src )
- ? [ block ]
- : recursivelyFindMediaFromBlocks( block.innerBlocks )
- );
- }
-
- const mediaBlocks = recursivelyFindMediaFromBlocks( blocks );
-
- if ( ! mediaBlocks.length ) {
+ if ( ! blocks?.length ) {
return;
}
const uploadedMediaList = await Promise.all(
- mediaBlocks.map( ( block ) => {
+ blocks.map( ( block ) => {
const blockType = block.name.split( '/' )[ 1 ];
if ( block.attributes.id ) {
block.attributes.type = blockType;
@@ -292,13 +277,6 @@ export function MediaPlaceholder( {
}
}
- async function onDrop( event ) {
- const blocks = pasteHandler( {
- HTML: event.dataTransfer?.getData( 'default' ),
- } );
- return await handleBlocksDrop( blocks );
- }
-
const onUpload = ( event ) => {
onFilesUpload( event.target.files );
};
@@ -385,7 +363,26 @@ export function MediaPlaceholder( {
return null;
}
- return ;
+ return (
+ {
+ const prefix = 'wp-block:core/';
+ const types = [];
+ for ( const type of dataTransfer.types ) {
+ if ( type.startsWith( prefix ) ) {
+ types.push( type.slice( prefix.length ) );
+ }
+ }
+ return (
+ types.every( ( type ) =>
+ allowedTypes.includes( type )
+ ) && ( multiple ? true : types.length === 1 )
+ );
+ } }
+ />
+ );
};
const renderCancelLink = () => {
diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js
index 5a5ce7a801594b..9779ae1300fb57 100644
--- a/packages/block-editor/src/store/private-selectors.js
+++ b/packages/block-editor/src/store/private-selectors.js
@@ -109,16 +109,16 @@ function getEnabledClientIdsTreeUnmemoized( state, rootClientId ) {
*
* @return {Object[]} Tree of block objects with only clientID and innerBlocks set.
*/
-export const getEnabledClientIdsTree = createRegistrySelector( ( select ) =>
- createSelector( getEnabledClientIdsTreeUnmemoized, ( state ) => [
+export const getEnabledClientIdsTree = createSelector(
+ getEnabledClientIdsTreeUnmemoized,
+ ( state ) => [
state.blocks.order,
+ state.derivedBlockEditingModes,
+ state.derivedNavModeBlockEditingModes,
state.blockEditingModes,
state.settings.templateLock,
state.blockListSettings,
- select( STORE_NAME ).__unstableGetEditorMode( state ),
- state.zoomLevel,
- getSectionRootClientId( state ),
- ] )
+ ]
);
/**
diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js
index 2f0fa70d616fd9..edae9c392c37de 100644
--- a/packages/block-editor/src/store/reducer.js
+++ b/packages/block-editor/src/store/reducer.js
@@ -9,12 +9,20 @@ import fastDeepEqual from 'fast-deep-equal/es6';
import { pipe } from '@wordpress/compose';
import { combineReducers, select } from '@wordpress/data';
import deprecated from '@wordpress/deprecated';
-import { store as blocksStore } from '@wordpress/blocks';
+import {
+ store as blocksStore,
+ privateApis as blocksPrivateApis,
+} from '@wordpress/blocks';
+
/**
* Internal dependencies
*/
import { PREFERENCES_DEFAULTS, SETTINGS_DEFAULTS } from './defaults';
import { insertAt, moveTo } from './array';
+import { sectionRootClientIdKey } from './private-keys';
+import { unlock } from '../lock-unlock';
+
+const { isContentBlock } = unlock( blocksPrivateApis );
const identity = ( x ) => x;
@@ -2131,6 +2139,632 @@ const combinedReducers = combineReducers( {
zoomLevel,
} );
+/**
+ * Retrieves a block's tree structure, handling both controlled and uncontrolled inner blocks.
+ *
+ * @param {Object} state The current state object.
+ * @param {string} clientId The client ID of the block to retrieve.
+ *
+ * @return {Object|undefined} The block tree object, or undefined if not found. For controlled blocks,
+ * returns a merged tree with controlled inner blocks.
+ */
+function getBlockTreeBlock( state, clientId ) {
+ if ( clientId === '' ) {
+ const rootBlock = state.blocks.tree.get( clientId );
+
+ if ( ! rootBlock ) {
+ return;
+ }
+
+ // Patch the root block to have a clientId property.
+ // TODO - consider updating the blocks reducer so that the root block has this property.
+ return {
+ clientId: '',
+ ...rootBlock,
+ };
+ }
+
+ if ( ! state.blocks.controlledInnerBlocks[ clientId ] ) {
+ return state.blocks.tree.get( clientId );
+ }
+
+ const controlledTree = state.blocks.tree.get( `controlled||${ clientId }` );
+ const regularTree = state.blocks.tree.get( clientId );
+
+ return {
+ ...regularTree,
+ innerBlocks: controlledTree?.innerBlocks,
+ };
+}
+
+/**
+ * Recursively traverses through a block tree of a given block and executes a callback for each block.
+ *
+ * @param {Object} state The store state.
+ * @param {string} clientId The clientId of the block to start traversing from.
+ * @param {Function} callback Function to execute for each block encountered during traversal.
+ * The callback receives the current block as its argument.
+ */
+function traverseBlockTree( state, clientId, callback ) {
+ const parentTree = getBlockTreeBlock( state, clientId );
+ if ( ! parentTree ) {
+ return;
+ }
+
+ callback( parentTree );
+
+ if ( ! parentTree?.innerBlocks?.length ) {
+ return;
+ }
+
+ for ( const block of parentTree?.innerBlocks ) {
+ traverseBlockTree( state, block.clientId, callback );
+ }
+}
+
+/**
+ * Checks if a block has a parent in a list of client IDs, and if so returns the client ID of the parent.
+ *
+ * @param {Object} state The current state object.
+ * @param {string} clientId The client ID of the block to search the parents of.
+ * @param {Array} clientIds The client IDs of the blocks to check.
+ *
+ * @return {string|undefined} The client ID of the parent block if found, undefined otherwise.
+ */
+function findParentInClientIdsList( state, clientId, clientIds ) {
+ let parent = state.blocks.parents.get( clientId );
+ while ( parent ) {
+ if ( clientIds.includes( parent ) ) {
+ return parent;
+ }
+ parent = state.blocks.parents.get( parent );
+ }
+}
+
+/**
+ * Checks if a block has any bindings in its metadata attributes.
+ *
+ * @param {Object} block The block object to check for bindings.
+ * @return {boolean} True if the block has bindings, false otherwise.
+ */
+function hasBindings( block ) {
+ return (
+ block?.attributes?.metadata?.bindings &&
+ Object.keys( block?.attributes?.metadata?.bindings ).length
+ );
+}
+
+/**
+ * Computes and returns derived block editing modes for a given block tree.
+ *
+ * This function calculates the editing modes for each block in the tree, taking into account
+ * various factors such as zoom level, navigation mode, sections, and synced patterns.
+ *
+ * @param {Object} state The current state object.
+ * @param {boolean} isNavMode Whether the navigation mode is active.
+ * @param {string} treeClientId The client ID of the root block for the tree. Defaults to an empty string.
+ * @return {Map} A Map containing the derived block editing modes, keyed by block client ID.
+ */
+function getDerivedBlockEditingModesForTree(
+ state,
+ isNavMode = false,
+ treeClientId = ''
+) {
+ const isZoomedOut =
+ state?.zoomLevel < 100 || state?.zoomLevel === 'auto-scaled';
+ const derivedBlockEditingModes = new Map();
+
+ // When there are sections, the majority of blocks are disabled,
+ // so the default block editing mode is set to disabled.
+ const sectionRootClientId = state.settings?.[ sectionRootClientIdKey ];
+ const sectionClientIds = state.blocks.order.get( sectionRootClientId );
+ const syncedPatternClientIds = Object.keys(
+ state.blocks.controlledInnerBlocks
+ ).filter(
+ ( clientId ) =>
+ state.blocks.byClientId?.get( clientId )?.name === 'core/block'
+ );
+
+ traverseBlockTree( state, treeClientId, ( block ) => {
+ const { clientId, name: blockName } = block;
+ if ( isZoomedOut || isNavMode ) {
+ // If the root block is the section root set its editing mode to contentOnly.
+ if ( clientId === sectionRootClientId ) {
+ derivedBlockEditingModes.set( clientId, 'contentOnly' );
+ return;
+ }
+
+ // There are no sections, so everything else is disabled.
+ if ( ! sectionClientIds?.length ) {
+ derivedBlockEditingModes.set( clientId, 'disabled' );
+ return;
+ }
+
+ if ( sectionClientIds.includes( clientId ) ) {
+ derivedBlockEditingModes.set( clientId, 'contentOnly' );
+ return;
+ }
+
+ // If zoomed out, all blocks that aren't sections or the section root are
+ // disabled.
+ // If the tree root is not in a section, set its editing mode to disabled.
+ if (
+ isZoomedOut ||
+ ! findParentInClientIdsList( state, clientId, sectionClientIds )
+ ) {
+ derivedBlockEditingModes.set( clientId, 'disabled' );
+ return;
+ }
+
+ // Handle synced pattern content so the inner blocks of a synced pattern are
+ // properly disabled.
+ if ( syncedPatternClientIds.length ) {
+ const parentPatternClientId = findParentInClientIdsList(
+ state,
+ clientId,
+ syncedPatternClientIds
+ );
+
+ if ( parentPatternClientId ) {
+ // This is a pattern nested in another pattern, it should be disabled.
+ if (
+ findParentInClientIdsList(
+ state,
+ parentPatternClientId,
+ syncedPatternClientIds
+ )
+ ) {
+ derivedBlockEditingModes.set( clientId, 'disabled' );
+ return;
+ }
+
+ if ( hasBindings( block ) ) {
+ derivedBlockEditingModes.set( clientId, 'contentOnly' );
+ return;
+ }
+
+ // Synced pattern content without a binding isn't editable
+ // from the instance, the user has to edit the pattern source,
+ // so return 'disabled'.
+ derivedBlockEditingModes.set( clientId, 'disabled' );
+ return;
+ }
+ }
+
+ if ( blockName && isContentBlock( blockName ) ) {
+ derivedBlockEditingModes.set( clientId, 'contentOnly' );
+ return;
+ }
+
+ derivedBlockEditingModes.set( clientId, 'disabled' );
+ return;
+ }
+
+ if ( syncedPatternClientIds.length ) {
+ // Synced pattern blocks (core/block).
+ if ( syncedPatternClientIds.includes( clientId ) ) {
+ // This is a pattern nested in another pattern, it should be disabled.
+ if (
+ findParentInClientIdsList(
+ state,
+ clientId,
+ syncedPatternClientIds
+ )
+ ) {
+ derivedBlockEditingModes.set( clientId, 'disabled' );
+ return;
+ }
+
+ // Else do nothing, use the default block editing mode.
+ return;
+ }
+
+ // Inner blocks of synced patterns.
+ const parentPatternClientId = findParentInClientIdsList(
+ state,
+ clientId,
+ syncedPatternClientIds
+ );
+ if ( parentPatternClientId ) {
+ // This is a pattern nested in another pattern, it should be disabled.
+ if (
+ findParentInClientIdsList(
+ state,
+ parentPatternClientId,
+ syncedPatternClientIds
+ )
+ ) {
+ derivedBlockEditingModes.set( clientId, 'disabled' );
+ return;
+ }
+
+ if ( hasBindings( block ) ) {
+ derivedBlockEditingModes.set( clientId, 'contentOnly' );
+ return;
+ }
+
+ // Synced pattern content without a binding isn't editable
+ // from the instance, the user has to edit the pattern source,
+ // so return 'disabled'.
+ derivedBlockEditingModes.set( clientId, 'disabled' );
+ }
+ }
+ } );
+
+ return derivedBlockEditingModes;
+}
+
+/**
+ * Updates the derived block editing modes based on added and removed blocks.
+ *
+ * This function handles the updating of block editing modes when blocks are added,
+ * removed, or moved within the editor.
+ *
+ * It only returns a value when modifications are made to the block editing modes.
+ *
+ * @param {Object} options The options for updating derived block editing modes.
+ * @param {Object} options.prevState The previous state object.
+ * @param {Object} options.nextState The next state object.
+ * @param {Array} [options.addedBlocks] An array of blocks that were added.
+ * @param {Array} [options.removedClientIds] An array of client IDs of blocks that were removed.
+ * @param {boolean} [options.isNavMode] Whether the navigation mode is active.
+ * @return {Map|undefined} The updated derived block editing modes, or undefined if no changes were made.
+ */
+function getDerivedBlockEditingModesUpdates( {
+ prevState,
+ nextState,
+ addedBlocks,
+ removedClientIds,
+ isNavMode = false,
+} ) {
+ const prevDerivedBlockEditingModes = isNavMode
+ ? prevState.derivedNavModeBlockEditingModes
+ : prevState.derivedBlockEditingModes;
+ let nextDerivedBlockEditingModes;
+
+ // Perform removals before additions to handle cases like the `MOVE_BLOCKS_TO_POSITION` action.
+ // That action removes a set of clientIds, but adds the same blocks back in a different location.
+ // If removals were performed after additions, those moved clientIds would be removed incorrectly.
+ removedClientIds?.forEach( ( clientId ) => {
+ // The actions only receive parent block IDs for removal.
+ // Recurse through the block tree to ensure all blocks are removed.
+ // Specifically use the previous state, before the blocks were removed.
+ traverseBlockTree( prevState, clientId, ( block ) => {
+ if ( prevDerivedBlockEditingModes.has( block.clientId ) ) {
+ if ( ! nextDerivedBlockEditingModes ) {
+ nextDerivedBlockEditingModes = new Map(
+ prevDerivedBlockEditingModes
+ );
+ }
+ nextDerivedBlockEditingModes.delete( block.clientId );
+ }
+ } );
+ } );
+
+ addedBlocks?.forEach( ( addedBlock ) => {
+ traverseBlockTree( nextState, addedBlock.clientId, ( block ) => {
+ const updates = getDerivedBlockEditingModesForTree(
+ nextState,
+ isNavMode,
+ block.clientId
+ );
+
+ if ( updates.size ) {
+ if ( ! nextDerivedBlockEditingModes ) {
+ nextDerivedBlockEditingModes = new Map( [
+ ...( prevDerivedBlockEditingModes?.size
+ ? prevDerivedBlockEditingModes
+ : [] ),
+ ...updates,
+ ] );
+ } else {
+ nextDerivedBlockEditingModes = new Map( [
+ ...( nextDerivedBlockEditingModes?.size
+ ? nextDerivedBlockEditingModes
+ : [] ),
+ ...updates,
+ ] );
+ }
+ }
+ } );
+ } );
+
+ return nextDerivedBlockEditingModes;
+}
+
+/**
+ * Higher-order reducer that adds derived block editing modes to the state.
+ *
+ * This function wraps a reducer and enhances it to handle actions that affect
+ * block editing modes. It updates the derivedBlockEditingModes in the state
+ * based on various actions such as adding, removing, or moving blocks, or changing
+ * the editor mode.
+ *
+ * @param {Function} reducer The original reducer function to be wrapped.
+ * @return {Function} A new reducer function that includes derived block editing modes handling.
+ */
+export function withDerivedBlockEditingModes( reducer ) {
+ return ( state, action ) => {
+ const nextState = reducer( state, action );
+
+ // An exception is needed here to still recompute the block editing modes when
+ // the editor mode changes since the editor mode isn't stored within the
+ // block editor state and changing it won't trigger an altered new state.
+ if ( action.type !== 'SET_EDITOR_MODE' && nextState === state ) {
+ return state;
+ }
+
+ switch ( action.type ) {
+ case 'REMOVE_BLOCKS': {
+ const nextDerivedBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ removedClientIds: action.clientIds,
+ isNavMode: false,
+ } );
+ const nextDerivedNavModeBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ removedClientIds: action.clientIds,
+ isNavMode: true,
+ } );
+
+ if (
+ nextDerivedBlockEditingModes ||
+ nextDerivedNavModeBlockEditingModes
+ ) {
+ return {
+ ...nextState,
+ derivedBlockEditingModes:
+ nextDerivedBlockEditingModes ??
+ state.derivedBlockEditingModes,
+ derivedNavModeBlockEditingModes:
+ nextDerivedNavModeBlockEditingModes ??
+ state.derivedNavModeBlockEditingModes,
+ };
+ }
+ break;
+ }
+ case 'RECEIVE_BLOCKS':
+ case 'INSERT_BLOCKS': {
+ const nextDerivedBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ addedBlocks: action.blocks,
+ isNavMode: false,
+ } );
+ const nextDerivedNavModeBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ addedBlocks: action.blocks,
+ isNavMode: true,
+ } );
+
+ if (
+ nextDerivedBlockEditingModes ||
+ nextDerivedNavModeBlockEditingModes
+ ) {
+ return {
+ ...nextState,
+ derivedBlockEditingModes:
+ nextDerivedBlockEditingModes ??
+ state.derivedBlockEditingModes,
+ derivedNavModeBlockEditingModes:
+ nextDerivedNavModeBlockEditingModes ??
+ state.derivedNavModeBlockEditingModes,
+ };
+ }
+ break;
+ }
+ case 'SET_HAS_CONTROLLED_INNER_BLOCKS': {
+ const updatedBlock = nextState.blocks.tree.get(
+ action.clientId
+ );
+ // The block might have been removed.
+ if ( ! updatedBlock ) {
+ break;
+ }
+
+ const nextDerivedBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ addedBlocks: [ updatedBlock ],
+ isNavMode: false,
+ } );
+ const nextDerivedNavModeBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ addedBlocks: [ updatedBlock ],
+ isNavMode: true,
+ } );
+
+ if (
+ nextDerivedBlockEditingModes ||
+ nextDerivedNavModeBlockEditingModes
+ ) {
+ return {
+ ...nextState,
+ derivedBlockEditingModes:
+ nextDerivedBlockEditingModes ??
+ state.derivedBlockEditingModes,
+ derivedNavModeBlockEditingModes:
+ nextDerivedNavModeBlockEditingModes ??
+ state.derivedNavModeBlockEditingModes,
+ };
+ }
+ break;
+ }
+ case 'REPLACE_BLOCKS': {
+ const nextDerivedBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ addedBlocks: action.blocks,
+ removedClientIds: action.clientIds,
+ isNavMode: false,
+ } );
+ const nextDerivedNavModeBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ addedBlocks: action.blocks,
+ removedClientIds: action.clientIds,
+ isNavMode: true,
+ } );
+
+ if (
+ nextDerivedBlockEditingModes ||
+ nextDerivedNavModeBlockEditingModes
+ ) {
+ return {
+ ...nextState,
+ derivedBlockEditingModes:
+ nextDerivedBlockEditingModes ??
+ state.derivedBlockEditingModes,
+ derivedNavModeBlockEditingModes:
+ nextDerivedNavModeBlockEditingModes ??
+ state.derivedNavModeBlockEditingModes,
+ };
+ }
+ break;
+ }
+ case 'REPLACE_INNER_BLOCKS': {
+ // Get the clientIds of the blocks that are being replaced
+ // from the old state, before they were removed.
+ const removedClientIds = state.blocks.order.get(
+ action.rootClientId
+ );
+ const nextDerivedBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ addedBlocks: action.blocks,
+ removedClientIds,
+ isNavMode: false,
+ } );
+ const nextDerivedNavModeBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ addedBlocks: action.blocks,
+ removedClientIds,
+ isNavMode: true,
+ } );
+
+ if (
+ nextDerivedBlockEditingModes ||
+ nextDerivedNavModeBlockEditingModes
+ ) {
+ return {
+ ...nextState,
+ derivedBlockEditingModes:
+ nextDerivedBlockEditingModes ??
+ state.derivedBlockEditingModes,
+ derivedNavModeBlockEditingModes:
+ nextDerivedNavModeBlockEditingModes ??
+ state.derivedNavModeBlockEditingModes,
+ };
+ }
+ break;
+ }
+ case 'MOVE_BLOCKS_TO_POSITION': {
+ const addedBlocks = action.clientIds.map( ( clientId ) => {
+ return nextState.blocks.byClientId.get( clientId );
+ } );
+ const nextDerivedBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ addedBlocks,
+ removedClientIds: action.clientIds,
+ isNavMode: false,
+ } );
+ const nextDerivedNavModeBlockEditingModes =
+ getDerivedBlockEditingModesUpdates( {
+ prevState: state,
+ nextState,
+ addedBlocks,
+ removedClientIds: action.clientIds,
+ isNavMode: true,
+ } );
+
+ if (
+ nextDerivedBlockEditingModes ||
+ nextDerivedNavModeBlockEditingModes
+ ) {
+ return {
+ ...nextState,
+ derivedBlockEditingModes:
+ nextDerivedBlockEditingModes ??
+ state.derivedBlockEditingModes,
+ derivedNavModeBlockEditingModes:
+ nextDerivedNavModeBlockEditingModes ??
+ state.derivedNavModeBlockEditingModes,
+ };
+ }
+ break;
+ }
+ case 'UPDATE_SETTINGS': {
+ // Recompute the entire tree if the section root changes.
+ if (
+ state?.settings?.[ sectionRootClientIdKey ] !==
+ nextState?.settings?.[ sectionRootClientIdKey ]
+ ) {
+ return {
+ ...nextState,
+ derivedBlockEditingModes:
+ getDerivedBlockEditingModesForTree(
+ nextState,
+ false /* Nav mode off */
+ ),
+ derivedNavModeBlockEditingModes:
+ getDerivedBlockEditingModesForTree(
+ nextState,
+ true /* Nav mode on */
+ ),
+ };
+ }
+ break;
+ }
+ case 'RESET_BLOCKS':
+ case 'SET_EDITOR_MODE':
+ case 'RESET_ZOOM_LEVEL':
+ case 'SET_ZOOM_LEVEL': {
+ // Recompute the entire tree if the editor mode or zoom level changes,
+ // or if all the blocks are reset.
+ return {
+ ...nextState,
+ derivedBlockEditingModes:
+ getDerivedBlockEditingModesForTree(
+ nextState,
+ false /* Nav mode off */
+ ),
+ derivedNavModeBlockEditingModes:
+ getDerivedBlockEditingModesForTree(
+ nextState,
+ true /* Nav mode on */
+ ),
+ };
+ }
+ }
+
+ // If there's no change, the derivedBlockEditingModes from the previous
+ // state need to be preserved.
+ nextState.derivedBlockEditingModes =
+ state?.derivedBlockEditingModes ?? new Map();
+ nextState.derivedNavModeBlockEditingModes =
+ state?.derivedNavModeBlockEditingModes ?? new Map();
+
+ return nextState;
+ };
+}
+
function withAutomaticChangeReset( reducer ) {
return ( state, action ) => {
const nextState = reducer( state, action );
@@ -2184,4 +2818,7 @@ function withAutomaticChangeReset( reducer ) {
};
}
-export default withAutomaticChangeReset( combinedReducers );
+export default pipe(
+ withDerivedBlockEditingModes,
+ withAutomaticChangeReset
+)( combinedReducers );
diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js
index ac1d178f43de7c..75c43770f7e175 100644
--- a/packages/block-editor/src/store/selectors.js
+++ b/packages/block-editor/src/store/selectors.js
@@ -3000,14 +3000,6 @@ export function __unstableIsWithinBlockOverlay( state, clientId ) {
return false;
}
-function isWithinBlock( state, clientId, parentClientId ) {
- let parent = state.blocks.parents.get( clientId );
- while ( !! parent && parent !== parentClientId ) {
- parent = state.blocks.parents.get( parent );
- }
- return parent === parentClientId;
-}
-
/**
* @typedef {import('../components/block-editing-mode').BlockEditingMode} BlockEditingMode
*/
@@ -3049,68 +3041,28 @@ export const getBlockEditingMode = createRegistrySelector(
clientId = '';
}
- // In zoom-out mode, override the behavior set by
- // __unstableSetBlockEditingMode to only allow editing the top-level
- // sections.
- if ( isZoomOut( state ) ) {
- const sectionRootClientId = getSectionRootClientId( state );
-
- if ( clientId === '' /* ROOT_CONTAINER_CLIENT_ID */ ) {
- return sectionRootClientId ? 'disabled' : 'contentOnly';
- }
- if ( clientId === sectionRootClientId ) {
- return 'contentOnly';
- }
- const sectionsClientIds = getBlockOrder(
- state,
- sectionRootClientId
- );
-
- // Sections are always contentOnly.
- if ( sectionsClientIds?.includes( clientId ) ) {
- return 'contentOnly';
- }
-
- return 'disabled';
+ const isNavMode =
+ select( preferencesStore )?.get( 'core', 'editorTool' ) ===
+ 'navigation';
+
+ // If the editor is currently not in navigation mode, check if the clientId
+ // has an editing mode set in the regular derived map.
+ // There may be an editing mode set here for synced patterns or in zoomed out
+ // mode.
+ if (
+ ! isNavMode &&
+ state.derivedBlockEditingModes?.has( clientId )
+ ) {
+ return state.derivedBlockEditingModes.get( clientId );
}
- const editorMode = __unstableGetEditorMode( state );
- if ( editorMode === 'navigation' ) {
- const sectionRootClientId = getSectionRootClientId( state );
-
- // The root section is "default mode"
- if ( clientId === sectionRootClientId ) {
- return 'default';
- }
-
- // Sections should always be contentOnly in navigation mode.
- const sectionsClientIds = getBlockOrder(
- state,
- sectionRootClientId
- );
- if ( sectionsClientIds.includes( clientId ) ) {
- return 'contentOnly';
- }
-
- // Blocks outside sections should be disabled.
- const isWithinSectionRoot = isWithinBlock(
- state,
- clientId,
- sectionRootClientId
- );
- if ( ! isWithinSectionRoot ) {
- return 'disabled';
- }
-
- // The rest of the blocks depend on whether they are content blocks or not.
- // This "flattens" the sections tree.
- const name = getBlockName( state, clientId );
- const { hasContentRoleAttribute } = unlock(
- select( blocksStore )
- );
- const isContent = hasContentRoleAttribute( name );
-
- return isContent ? 'contentOnly' : 'disabled';
+ // If the editor *is* in navigation mode, the block editing mode states
+ // are stored in the derivedNavModeBlockEditingModes map.
+ if (
+ isNavMode &&
+ state.derivedNavModeBlockEditingModes?.has( clientId )
+ ) {
+ return state.derivedNavModeBlockEditingModes.get( clientId );
}
// In normal mode, consider that an explicitely set editing mode takes over.
@@ -3120,7 +3072,7 @@ export const getBlockEditingMode = createRegistrySelector(
}
// In normal mode, top level is default mode.
- if ( ! clientId ) {
+ if ( clientId === '' ) {
return 'default';
}
diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js
index fb1d736e175af0..268d463f227d4d 100644
--- a/packages/block-editor/src/store/test/private-selectors.js
+++ b/packages/block-editor/src/store/test/private-selectors.js
@@ -129,6 +129,7 @@ describe( 'private selectors', () => {
getBlockEditingMode.registry = {
select: jest.fn( () => ( {
hasContentRoleAttribute,
+ get,
} ) ),
};
__unstableGetEditorMode.registry = {
diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js
index c99d639ba8a09e..dd1665d6736ada 100644
--- a/packages/block-editor/src/store/test/reducer.js
+++ b/packages/block-editor/src/store/test/reducer.js
@@ -10,7 +10,10 @@ import {
registerBlockType,
unregisterBlockType,
createBlock,
+ privateApis,
} from '@wordpress/blocks';
+import { combineReducers, select } from '@wordpress/data';
+import { store as preferencesStore } from '@wordpress/preferences';
/**
* Internal dependencies
@@ -38,8 +41,30 @@ import {
blockEditingModes,
openedBlockSettingsMenu,
expandedBlock,
+ zoomLevel,
+ withDerivedBlockEditingModes,
} from '../reducer';
+import { unlock } from '../../lock-unlock';
+import { sectionRootClientIdKey } from '.././private-keys';
+
+const { isContentBlock } = unlock( privateApis );
+
+jest.mock( '@wordpress/data/src/select', () => {
+ const actualSelect = jest.requireActual( '@wordpress/data/src/select' );
+
+ return {
+ select: jest.fn( ( ...args ) => actualSelect.select( ...args ) ),
+ };
+} );
+
+jest.mock( '@wordpress/blocks/src/api/utils', () => {
+ return {
+ ...jest.requireActual( '@wordpress/blocks/src/api/utils' ),
+ isContentBlock: jest.fn(),
+ };
+} );
+
const noop = () => {};
describe( 'state', () => {
@@ -3544,4 +3569,828 @@ describe( 'state', () => {
expect( state ).toBe( null );
} );
} );
+
+ describe( 'withDerivedBlockEditingModes', () => {
+ const testReducer = withDerivedBlockEditingModes(
+ combineReducers( {
+ blocks,
+ settings,
+ zoomLevel,
+ } )
+ );
+
+ function dispatchActions( actions, reducer, initialState = {} ) {
+ return actions.reduce( ( _state, action ) => {
+ return reducer( _state, action );
+ }, initialState );
+ }
+
+ beforeEach( () => {
+ isContentBlock.mockImplementation(
+ ( blockName ) => blockName === 'core/paragraph'
+ );
+ } );
+
+ afterAll( () => {
+ isContentBlock.mockRestore();
+ } );
+
+ describe( 'edit mode', () => {
+ let initialState;
+ beforeAll( () => {
+ select.mockImplementation( ( storeName ) => {
+ if ( storeName === preferencesStore ) {
+ return {
+ get: jest.fn( () => 'edit' ),
+ };
+ }
+ return select( storeName );
+ } );
+
+ initialState = dispatchActions(
+ [
+ {
+ type: 'UPDATE_SETTINGS',
+ settings: {
+ [ sectionRootClientIdKey ]: '',
+ },
+ },
+ {
+ type: 'RESET_BLOCKS',
+ blocks: [
+ {
+ name: 'core/group',
+ clientId: 'group-1',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'paragraph-1',
+ attributes: {},
+ innerBlocks: [],
+ },
+ {
+ name: 'core/group',
+ clientId: 'group-2',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'paragraph-2',
+ attributes: {},
+ innerBlocks: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ testReducer
+ );
+ } );
+
+ afterAll( () => {
+ select.mockRestore();
+ } );
+
+ it( 'returns no block editing modes when zoomed out / navigation mode are not active and there are no synced patterns', () => {
+ expect( initialState.derivedBlockEditingModes ).toEqual(
+ new Map()
+ );
+ } );
+ } );
+
+ describe( 'synced patterns', () => {
+ let initialState;
+ beforeAll( () => {
+ select.mockImplementation( ( storeName ) => {
+ if ( storeName === preferencesStore ) {
+ return {
+ get: jest.fn( () => 'edit' ),
+ };
+ }
+ return select( storeName );
+ } );
+
+ // Simulates how the editor typically inserts controlled blocks,
+ // - first the pattern is inserted with no inner blocks.
+ // - next the pattern is marked as a controlled block.
+ // - finally, once the inner blocks of the pattern are received, they're inserted.
+ // This process is repeated for the two patterns in this test.
+ initialState = dispatchActions(
+ [
+ {
+ type: 'UPDATE_SETTINGS',
+ settings: {
+ [ sectionRootClientIdKey ]: '',
+ },
+ },
+ {
+ type: 'RESET_BLOCKS',
+ blocks: [
+ {
+ name: 'core/group',
+ clientId: 'group-1',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'paragraph-1',
+ attributes: {},
+ innerBlocks: [],
+ },
+ {
+ name: 'core/group',
+ clientId: 'group-2',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'paragraph-2',
+ attributes: {},
+ innerBlocks: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'INSERT_BLOCKS',
+ rootClientId: '',
+ blocks: [
+ {
+ name: 'core/block',
+ clientId: 'root-pattern',
+ attributes: {},
+ innerBlocks: [],
+ },
+ ],
+ },
+ {
+ type: 'SET_HAS_CONTROLLED_INNER_BLOCKS',
+ clientId: 'root-pattern',
+ hasControlledInnerBlocks: true,
+ },
+ {
+ type: 'REPLACE_INNER_BLOCKS',
+ rootClientId: 'root-pattern',
+ blocks: [
+ {
+ name: 'core/block',
+ clientId: 'nested-pattern',
+ attributes: {},
+ innerBlocks: [],
+ },
+ {
+ name: 'core/paragraph',
+ clientId: 'pattern-paragraph',
+ attributes: {},
+ innerBlocks: [],
+ },
+ {
+ name: 'core/group',
+ clientId: 'pattern-group',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId:
+ 'pattern-paragraph-with-overrides',
+ attributes: {
+ metadata: {
+ bindings: {
+ __default:
+ 'core/pattern-overrides',
+ },
+ },
+ },
+ innerBlocks: [],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ type: 'SET_HAS_CONTROLLED_INNER_BLOCKS',
+ clientId: 'nested-pattern',
+ hasControlledInnerBlocks: true,
+ },
+ {
+ type: 'REPLACE_INNER_BLOCKS',
+ rootClientId: 'nested-pattern',
+ blocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'nested-paragraph',
+ attributes: {},
+ innerBlocks: [],
+ },
+ {
+ name: 'core/group',
+ clientId: 'nested-group',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId:
+ 'nested-paragraph-with-overrides',
+ attributes: {
+ metadata: {
+ bindings: {
+ __default:
+ 'core/pattern-overrides',
+ },
+ },
+ },
+ innerBlocks: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ testReducer,
+ initialState
+ );
+ } );
+
+ afterAll( () => {
+ select.mockRestore();
+ } );
+
+ it( 'returns the expected block editing modes for synced patterns', () => {
+ // Only the parent pattern and its own children that have bindings
+ // are in contentOnly mode. All other blocks are disabled.
+ expect( initialState.derivedBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ 'pattern-paragraph': 'disabled',
+ 'pattern-group': 'disabled',
+ 'pattern-paragraph-with-overrides': 'contentOnly',
+ 'nested-pattern': 'disabled',
+ 'nested-paragraph': 'disabled',
+ 'nested-group': 'disabled',
+ 'nested-paragraph-with-overrides': 'disabled',
+ } )
+ )
+ );
+ } );
+
+ it( 'removes block editing modes when synced patterns are removed', () => {
+ const { derivedBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'REMOVE_BLOCKS',
+ clientIds: [ 'root-pattern' ],
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedBlockEditingModes ).toEqual( new Map() );
+ } );
+
+ it( 'returns the expected block editing modes for synced patterns when switching to navigation mode', () => {
+ select.mockImplementation( ( storeName ) => {
+ if ( storeName === preferencesStore ) {
+ return {
+ get: jest.fn( () => 'navigation' ),
+ };
+ }
+ return select( storeName );
+ } );
+
+ const {
+ derivedBlockEditingModes,
+ derivedNavModeBlockEditingModes,
+ } = dispatchActions(
+ [
+ {
+ type: 'SET_EDITOR_MODE',
+ mode: 'navigation',
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ 'pattern-paragraph': 'disabled',
+ 'pattern-group': 'disabled',
+ 'pattern-paragraph-with-overrides': 'contentOnly', // Pattern child with bindings.
+ 'nested-pattern': 'disabled',
+ 'nested-paragraph': 'disabled',
+ 'nested-group': 'disabled',
+ 'nested-paragraph-with-overrides': 'disabled',
+ } )
+ )
+ );
+
+ expect( derivedNavModeBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly', // Section root.
+ 'group-1': 'contentOnly', // Section.
+ 'paragraph-1': 'contentOnly', // Content block in section.
+ 'group-2': 'disabled',
+ 'paragraph-2': 'contentOnly', // Content block in section.
+ 'root-pattern': 'contentOnly', // Section.
+ 'pattern-paragraph': 'disabled',
+ 'pattern-group': 'disabled',
+ 'pattern-paragraph-with-overrides': 'contentOnly', // Pattern child with bindings.
+ 'nested-pattern': 'disabled',
+ 'nested-paragraph': 'disabled',
+ 'nested-group': 'disabled',
+ 'nested-paragraph-with-overrides': 'disabled',
+ } )
+ )
+ );
+
+ select.mockImplementation( ( storeName ) => {
+ if ( storeName === preferencesStore ) {
+ return {
+ get: jest.fn( () => 'edit' ),
+ };
+ }
+ return select( storeName );
+ } );
+ } );
+
+ it( 'returns the expected block editing modes for synced patterns when switching to zoomed out mode', () => {
+ const { derivedBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'SET_ZOOM_LEVEL',
+ zoom: 'auto-scaled',
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly', // Section root.
+ 'group-1': 'contentOnly', // Section.
+ 'paragraph-1': 'disabled',
+ 'group-2': 'disabled',
+ 'paragraph-2': 'disabled',
+ 'root-pattern': 'contentOnly', // Pattern and section.
+ 'pattern-paragraph': 'disabled',
+ 'pattern-group': 'disabled',
+ 'pattern-paragraph-with-overrides': 'disabled',
+ 'nested-pattern': 'disabled',
+ 'nested-paragraph': 'disabled',
+ 'nested-group': 'disabled',
+ 'nested-paragraph-with-overrides': 'disabled',
+ } )
+ )
+ );
+ } );
+ } );
+
+ describe( 'navigation mode', () => {
+ let initialState;
+
+ beforeAll( () => {
+ select.mockImplementation( ( storeName ) => {
+ if ( storeName === preferencesStore ) {
+ return {
+ get: jest.fn( () => 'navigation' ),
+ };
+ }
+ return select( storeName );
+ } );
+
+ initialState = dispatchActions(
+ [
+ {
+ type: 'UPDATE_SETTINGS',
+ settings: {
+ [ sectionRootClientIdKey ]: '',
+ },
+ },
+ {
+ type: 'RESET_BLOCKS',
+ blocks: [
+ {
+ name: 'core/group',
+ clientId: 'group-1',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'paragraph-1',
+ attributes: {},
+ innerBlocks: [],
+ },
+ {
+ name: 'core/group',
+ clientId: 'group-2',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'paragraph-2',
+ attributes: {},
+ innerBlocks: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ testReducer
+ );
+ } );
+
+ afterAll( () => {
+ select.mockRestore();
+ } );
+
+ it( 'returns the expected block editing modes', () => {
+ expect( initialState.derivedNavModeBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly', // Section root.
+ 'group-1': 'contentOnly', // Section block.
+ 'paragraph-1': 'contentOnly', // Content block in section.
+ 'group-2': 'disabled', // Non-content block in section.
+ 'paragraph-2': 'contentOnly', // Content block in section.
+ } )
+ )
+ );
+ } );
+
+ it( 'removes block editing modes when blocks are removed', () => {
+ const { derivedNavModeBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'REMOVE_BLOCKS',
+ clientIds: [ 'group-2' ],
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedNavModeBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly',
+ 'group-1': 'contentOnly',
+ 'paragraph-1': 'contentOnly',
+ } )
+ )
+ );
+ } );
+
+ it( 'updates block editing modes when new blocks are inserted', () => {
+ const { derivedNavModeBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'INSERT_BLOCKS',
+ rootClientId: '',
+ blocks: [
+ {
+ name: 'core/group',
+ clientId: 'group-3',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'paragraph-3',
+ attributes: {},
+ innerBlocks: [],
+ },
+ {
+ name: 'core/group',
+ clientId: 'group-4',
+ attributes: {},
+ innerBlocks: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedNavModeBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly', // Section root.
+ 'group-1': 'contentOnly', // Section block.
+ 'paragraph-1': 'contentOnly', // Content block in section.
+ 'group-2': 'disabled', // Non-content block in section.
+ 'paragraph-2': 'contentOnly', // Content block in section.
+ 'group-3': 'contentOnly', // New section block.
+ 'paragraph-3': 'contentOnly', // New content block in section.
+ 'group-4': 'disabled', // Non-content block in section.
+ } )
+ )
+ );
+ } );
+
+ it( 'updates block editing modes when blocks are moved to a new position', () => {
+ const { derivedNavModeBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'MOVE_BLOCKS_TO_POSITION',
+ clientIds: [ 'group-2' ],
+ fromRootClientId: 'group-1',
+ toRootClientId: '',
+ },
+ ],
+ testReducer,
+ initialState
+ );
+ expect( derivedNavModeBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly', // Section root.
+ 'group-1': 'contentOnly', // Section block.
+ 'paragraph-1': 'contentOnly', // Content block in section.
+ 'group-2': 'contentOnly', // New section block.
+ 'paragraph-2': 'contentOnly', // Still a content block in a section.
+ } )
+ )
+ );
+ } );
+
+ it( 'handles changes to the section root', () => {
+ const { derivedNavModeBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'UPDATE_SETTINGS',
+ settings: {
+ [ sectionRootClientIdKey ]: 'group-1',
+ },
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedNavModeBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'disabled',
+ 'group-1': 'contentOnly',
+ 'paragraph-1': 'contentOnly',
+ 'group-2': 'contentOnly',
+ 'paragraph-2': 'contentOnly',
+ } )
+ )
+ );
+ } );
+ } );
+
+ describe( 'zoom out mode', () => {
+ let initialState;
+
+ beforeAll( () => {
+ initialState = dispatchActions(
+ [
+ {
+ type: 'UPDATE_SETTINGS',
+ settings: {
+ [ sectionRootClientIdKey ]: '',
+ },
+ },
+ {
+ type: 'SET_ZOOM_LEVEL',
+ zoom: 'auto-scaled',
+ },
+ {
+ type: 'RESET_BLOCKS',
+ blocks: [
+ {
+ name: 'core/group',
+ clientId: 'group-1',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'paragraph-1',
+ attributes: {},
+ innerBlocks: [],
+ },
+ {
+ name: 'core/group',
+ clientId: 'group-2',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'paragraph-2',
+ attributes: {},
+ innerBlocks: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ testReducer
+ );
+ } );
+
+ it( 'returns the expected block editing modes', () => {
+ expect( initialState.derivedBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly', // Section root.
+ 'group-1': 'contentOnly', // Section block.
+ 'paragraph-1': 'disabled',
+ 'group-2': 'disabled',
+ 'paragraph-2': 'disabled',
+ } )
+ )
+ );
+ } );
+
+ it( 'overrides navigation mode', () => {
+ select.mockImplementation( ( storeName ) => {
+ if ( storeName === preferencesStore ) {
+ return {
+ get: jest.fn( () => 'navigation' ),
+ };
+ }
+ return select( storeName );
+ } );
+
+ const { derivedBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'SET_EDITOR_MODE',
+ mode: 'navigation',
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly', // Section root.
+ 'group-1': 'contentOnly', // Section block.
+ 'paragraph-1': 'disabled',
+ 'group-2': 'disabled',
+ 'paragraph-2': 'disabled',
+ } )
+ )
+ );
+
+ select.mockImplementation( ( storeName ) => {
+ if ( storeName === preferencesStore ) {
+ return {
+ get: jest.fn( () => 'edit' ),
+ };
+ }
+ return select( storeName );
+ } );
+ } );
+
+ it( 'removes block editing modes when blocks are removed', () => {
+ const { derivedBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'REMOVE_BLOCKS',
+ clientIds: [ 'group-2' ],
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly',
+ 'group-1': 'contentOnly',
+ 'paragraph-1': 'disabled',
+ } )
+ )
+ );
+ } );
+
+ it( 'updates block editing modes when new blocks are inserted', () => {
+ const { derivedBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'INSERT_BLOCKS',
+ rootClientId: '',
+ blocks: [
+ {
+ name: 'core/group',
+ clientId: 'group-3',
+ attributes: {},
+ innerBlocks: [
+ {
+ name: 'core/paragraph',
+ clientId: 'paragraph-3',
+ attributes: {},
+ innerBlocks: [],
+ },
+ {
+ name: 'core/group',
+ clientId: 'group-4',
+ attributes: {},
+ innerBlocks: [],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly', // Section root.
+ 'group-1': 'contentOnly', // Section block.
+ 'paragraph-1': 'disabled',
+ 'group-2': 'disabled',
+ 'paragraph-2': 'disabled',
+ 'group-3': 'contentOnly', // New section block.
+ 'paragraph-3': 'disabled',
+ 'group-4': 'disabled',
+ } )
+ )
+ );
+ } );
+
+ it( 'updates block editing modes when blocks are moved to a new position', () => {
+ const { derivedBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'MOVE_BLOCKS_TO_POSITION',
+ clientIds: [ 'group-2' ],
+ fromRootClientId: 'group-1',
+ toRootClientId: '',
+ },
+ ],
+ testReducer,
+ initialState
+ );
+ expect( derivedBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'contentOnly', // Section root.
+ 'group-1': 'contentOnly', // Section block.
+ 'paragraph-1': 'disabled',
+ 'group-2': 'contentOnly', // New section block.
+ 'paragraph-2': 'disabled',
+ } )
+ )
+ );
+ } );
+
+ it( 'handles changes to the section root', () => {
+ const { derivedBlockEditingModes } = dispatchActions(
+ [
+ {
+ type: 'UPDATE_SETTINGS',
+ settings: {
+ [ sectionRootClientIdKey ]: 'group-1',
+ },
+ },
+ ],
+ testReducer,
+ initialState
+ );
+
+ expect( derivedBlockEditingModes ).toEqual(
+ new Map(
+ Object.entries( {
+ '': 'disabled',
+ 'group-1': 'contentOnly', // New section root.
+ 'paragraph-1': 'contentOnly', // Section block.
+ 'group-2': 'contentOnly', // Section block.
+ 'paragraph-2': 'disabled',
+ } )
+ )
+ );
+ } );
+ } );
+ } );
} );
diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js
index 7c0361449c5fca..7692bd6bf2cbb6 100644
--- a/packages/block-editor/src/store/test/selectors.js
+++ b/packages/block-editor/src/store/test/selectors.js
@@ -9,14 +9,12 @@ import {
import { RawHTML } from '@wordpress/element';
import { symbol } from '@wordpress/icons';
import { select, dispatch } from '@wordpress/data';
-import { store as preferencesStore } from '@wordpress/preferences';
/**
* Internal dependencies
*/
import * as selectors from '../selectors';
import { store } from '../';
-import { sectionRootClientIdKey } from '../private-keys';
import { lock } from '../../lock-unlock';
const {
@@ -4469,29 +4467,19 @@ describe( 'getBlockEditingMode', () => {
blockEditingModes: new Map( [] ),
};
- const navigationModeStateWithRootSection = {
- ...baseState,
- settings: {
- [ sectionRootClientIdKey ]: 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337', // The group is the "main" container
- },
- };
-
const hasContentRoleAttribute = jest.fn( () => false );
+ const get = jest.fn( () => 'edit' );
- const fauxPrivateAPIs = {};
+ const mockedSelectors = { get };
- lock( fauxPrivateAPIs, {
+ lock( mockedSelectors, {
hasContentRoleAttribute,
} );
getBlockEditingMode.registry = {
- select: jest.fn( () => fauxPrivateAPIs ),
+ select: jest.fn( () => mockedSelectors ),
};
- afterEach( () => {
- dispatch( preferencesStore ).set( 'core', 'editorTool', undefined );
- } );
-
it( 'should return default by default', () => {
expect(
getBlockEditingMode(
@@ -4614,98 +4602,4 @@ describe( 'getBlockEditingMode', () => {
getBlockEditingMode( state, 'b3247f75-fd94-4fef-97f9-5bfd162cc416' )
).toBe( 'contentOnly' );
} );
-
- describe( 'navigation mode', () => {
- const writeModeExperiment = window.__experimentalEditorWriteMode;
- beforeAll( () => {
- window.__experimentalEditorWriteMode = true;
- } );
- afterAll( () => {
- window.__experimentalEditorWriteMode = writeModeExperiment;
- } );
- it( 'in navigation mode, the root section container is default', () => {
- dispatch( preferencesStore ).set(
- 'core',
- 'editorTool',
- 'navigation'
- );
- expect(
- getBlockEditingMode(
- navigationModeStateWithRootSection,
- 'ef45d5fd-5234-4fd5-ac4f-c3736c7f9337'
- )
- ).toBe( 'default' );
- } );
-
- it( 'in navigation mode, anything outside the section container is disabled', () => {
- dispatch( preferencesStore ).set(
- 'core',
- 'editorTool',
- 'navigation'
- );
- expect(
- getBlockEditingMode(
- navigationModeStateWithRootSection,
- '6cf70164-9097-4460-bcbf-200560546988'
- )
- ).toBe( 'disabled' );
- } );
-
- it( 'in navigation mode, sections are contentOnly', () => {
- dispatch( preferencesStore ).set(
- 'core',
- 'editorTool',
- 'navigation'
- );
- expect(
- getBlockEditingMode(
- navigationModeStateWithRootSection,
- 'b26fc763-417d-4f01-b81c-2ec61e14a972'
- )
- ).toBe( 'contentOnly' );
- expect(
- getBlockEditingMode(
- navigationModeStateWithRootSection,
- '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f'
- )
- ).toBe( 'contentOnly' );
- } );
-
- it( 'in navigation mode, blocks with content attributes within sections are contentOnly', () => {
- dispatch( preferencesStore ).set(
- 'core',
- 'editorTool',
- 'navigation'
- );
- hasContentRoleAttribute.mockReturnValueOnce( true );
- expect(
- getBlockEditingMode(
- navigationModeStateWithRootSection,
- 'b3247f75-fd94-4fef-97f9-5bfd162cc416'
- )
- ).toBe( 'contentOnly' );
-
- hasContentRoleAttribute.mockReturnValueOnce( true );
- expect(
- getBlockEditingMode(
- navigationModeStateWithRootSection,
- 'e178812d-ce5e-48c7-a945-8ae4ffcbbb7c'
- )
- ).toBe( 'contentOnly' );
- } );
-
- it( 'in navigation mode, blocks without content attributes within sections are disabled', () => {
- dispatch( preferencesStore ).set(
- 'core',
- 'editorTool',
- 'navigation'
- );
- expect(
- getBlockEditingMode(
- navigationModeStateWithRootSection,
- '9b9c5c3f-2e46-4f02-9e14-9fed515b958s'
- )
- ).toBe( 'disabled' );
- } );
- } );
} );
diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js
index 104b07157cba74..3d4d07e52b386a 100644
--- a/packages/block-library/src/block/edit.js
+++ b/packages/block-library/src/block/edit.js
@@ -7,7 +7,7 @@ import clsx from 'clsx';
* WordPress dependencies
*/
import { useSelect, useDispatch } from '@wordpress/data';
-import { useRef, useMemo, useEffect } from '@wordpress/element';
+import { useRef, useMemo } from '@wordpress/element';
import {
useEntityRecord,
store as coreStore,
@@ -37,12 +37,10 @@ import { getBlockBindingsSource } from '@wordpress/blocks';
/**
* Internal dependencies
*/
-import { name as patternBlockName } from './index';
import { unlock } from '../lock-unlock';
const { useLayoutClasses } = unlock( blockEditorPrivateApis );
-const { isOverridableBlock, hasOverridableBlocks } =
- unlock( patternsPrivateApis );
+const { hasOverridableBlocks } = unlock( patternsPrivateApis );
const fullAlignments = [ 'full', 'wide', 'left', 'right' ];
@@ -75,22 +73,6 @@ const useInferredLayout = ( blocks, parentLayout ) => {
}, [ blocks, parentLayout ] );
};
-function setBlockEditMode( setEditMode, blocks, mode ) {
- blocks.forEach( ( block ) => {
- const editMode =
- mode ||
- ( isOverridableBlock( block ) ? 'contentOnly' : 'disabled' );
- setEditMode( block.clientId, editMode );
-
- setBlockEditMode(
- setEditMode,
- block.innerBlocks,
- // Disable editing for nested patterns.
- block.name === patternBlockName ? 'disabled' : mode
- );
- } );
-}
-
function RecursionWarning() {
const blockProps = useBlockProps();
return (
@@ -171,7 +153,6 @@ function ReusableBlockEdit( {
name,
attributes: { ref, content },
__unstableParentLayout: parentLayout,
- clientId: patternClientId,
setAttributes,
} ) {
const { record, hasResolved } = useEntityRecord(
@@ -184,49 +165,24 @@ function ReusableBlockEdit( {
} );
const isMissing = hasResolved && ! record;
- const { setBlockEditingMode, __unstableMarkLastChangeAsPersistent } =
+ const { __unstableMarkLastChangeAsPersistent } =
useDispatch( blockEditorStore );
- const {
- innerBlocks,
- onNavigateToEntityRecord,
- editingMode,
- hasPatternOverridesSource,
- } = useSelect(
+ const { onNavigateToEntityRecord, hasPatternOverridesSource } = useSelect(
( select ) => {
- const { getBlocks, getSettings, getBlockEditingMode } =
- select( blockEditorStore );
+ const { getSettings } = select( blockEditorStore );
// For editing link to the site editor if the theme and user permissions support it.
return {
- innerBlocks: getBlocks( patternClientId ),
onNavigateToEntityRecord:
getSettings().onNavigateToEntityRecord,
- editingMode: getBlockEditingMode( patternClientId ),
hasPatternOverridesSource: !! getBlockBindingsSource(
'core/pattern-overrides'
),
};
},
- [ patternClientId ]
+ []
);
- // Sync the editing mode of the pattern block with the inner blocks.
- useEffect( () => {
- setBlockEditMode(
- setBlockEditingMode,
- innerBlocks,
- // Disable editing if the pattern itself is disabled.
- editingMode === 'disabled' || ! hasPatternOverridesSource
- ? 'disabled'
- : undefined
- );
- }, [
- editingMode,
- innerBlocks,
- setBlockEditingMode,
- hasPatternOverridesSource,
- ] );
-
const canOverrideBlocks = useMemo(
() => hasPatternOverridesSource && hasOverridableBlocks( blocks ),
[ hasPatternOverridesSource, blocks ]
@@ -244,7 +200,6 @@ function ReusableBlockEdit( {
} );
const innerBlocksProps = useInnerBlocksProps( blockProps, {
- templateLock: 'all',
layout,
value: blocks,
onInput: NOOP,
diff --git a/packages/block-library/src/comments-pagination/editor.scss b/packages/block-library/src/comments-pagination/editor.scss
index a875c9e0ee21ce..3cd99c632ee833 100644
--- a/packages/block-library/src/comments-pagination/editor.scss
+++ b/packages/block-library/src/comments-pagination/editor.scss
@@ -26,6 +26,7 @@ $pagination-margin: 0.5em;
margin-right: $pagination-margin;
margin-bottom: $pagination-margin;
+ font-size: inherit;
&:last-child {
/*rtl:ignore*/
margin-right: 0;
diff --git a/packages/block-library/src/comments-pagination/style.scss b/packages/block-library/src/comments-pagination/style.scss
index c6b5d9a0a29e91..2fb6e3dd2d48f4 100644
--- a/packages/block-library/src/comments-pagination/style.scss
+++ b/packages/block-library/src/comments-pagination/style.scss
@@ -8,6 +8,7 @@ $pagination-margin: 0.5em;
margin-right: $pagination-margin;
margin-bottom: $pagination-margin;
+ font-size: inherit;
&:last-child {
/*rtl:ignore*/
margin-right: 0;
diff --git a/packages/block-library/src/missing/test/edit.native.js b/packages/block-library/src/missing/test/edit.native.js
index 47d0da572b7c88..eba1169ae643b7 100644
--- a/packages/block-library/src/missing/test/edit.native.js
+++ b/packages/block-library/src/missing/test/edit.native.js
@@ -10,7 +10,6 @@ import { Text } from 'react-native';
import { BottomSheet, Icon } from '@wordpress/components';
import { help, plugins } from '@wordpress/icons';
import { storeConfig } from '@wordpress/block-editor';
-jest.mock( '@wordpress/blocks' );
jest.mock( '@wordpress/block-editor/src/store/selectors' );
/**
diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php
index ae3b9620a33584..68b23aceeced65 100644
--- a/packages/block-library/src/navigation/index.php
+++ b/packages/block-library/src/navigation/index.php
@@ -1436,20 +1436,6 @@ function block_core_navigation_get_most_recently_published_navigation() {
return null;
}
-/**
- * Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the inner blocks.
- *
- * @since 6.5.0
- *
- * @param string $serialized_block The serialized markup of a block and its inner blocks.
- * @return string
- */
-function block_core_navigation_remove_serialized_parent_block( $serialized_block ) {
- $start = strpos( $serialized_block, '-->' ) + strlen( '-->' );
- $end = strrpos( $serialized_block, '
+ Pattern Overrides
+
+
+ Post Meta Binding
+
+
+ No Overrides or Binding
+
+ `;
+
+ const { id } = await requestUtils.createBlock( {
+ title: 'Pattern',
+ content,
+ status: 'publish',
+ } );
+
+ await admin.visitSiteEditor( {
+ postId: 'emptytheme//index',
+ postType: 'wp_template',
+ canvas: 'edit',
+ } );
+
+ await editor.setContent( '' );
+
+ await editor.insertBlock( {
+ name: 'core/block',
+ attributes: { ref: id },
+ } );
+
+ const patternBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Pattern',
+ } );
+ const paragraphs = editor.canvas.getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } );
+ const blockWithOverrides = paragraphs.filter( {
+ hasText: 'Pattern Overrides',
+ } );
+ const blockWithBindings = paragraphs.filter( {
+ hasText: 'Post Meta Binding',
+ } );
+ const blockWithoutOverridesOrBindings = paragraphs.filter( {
+ hasText: 'No Overrides or Binding',
+ } );
+
+ await test.step( 'Zoomed in / Design mode', async () => {
+ await editor.switchEditorTool( 'Design' );
+ // In zoomed in and design mode the pattern block and child blocks
+ // with bindings are editable.
+ await expect( patternBlock ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithOverrides ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ await test.step( 'Zoomed in / Write mode - pattern as a section', async () => {
+ await editor.switchEditorTool( 'Write' );
+ // The pattern block is still editable as a section.
+ await expect( patternBlock ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ // Child blocks of the pattern with bindings are editable.
+ await expect( blockWithOverrides ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ await test.step( 'Zoomed out / Write mode - pattern as a section', async () => {
+ await page.getByLabel( 'Zoom Out' ).click();
+ // In zoomed out only the pattern block is editable, as in this scenario it's a section.
+ await expect( patternBlock ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithOverrides ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ await test.step( 'Zoomed out / Design mode - pattern as a section', async () => {
+ await editor.switchEditorTool( 'Design' );
+ // In zoomed out only the pattern block is editable, as in this scenario it's a section.
+ await expect( patternBlock ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithOverrides ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ // Zoom out and group the pattern.
+ await page.getByLabel( 'Zoom Out' ).click();
+ await editor.selectBlocks( patternBlock );
+ await editor.clickBlockOptionsMenuItem( 'Group' );
+
+ await test.step( 'Zoomed in / Write mode - pattern nested in a section', async () => {
+ await editor.switchEditorTool( 'Write' );
+ // The pattern block is not inert as it has editable content, but it shouldn't be selectable.
+ // TODO: find a way to test that the block is not selectable.
+ await expect( patternBlock ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ // Child blocks of the pattern are editable as normal.
+ await expect( blockWithOverrides ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).not.toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ await test.step( 'Zoomed out / Write mode - pattern nested in a section', async () => {
+ await page.getByLabel( 'Zoom Out' ).click();
+ // None of the pattern is editable in zoomed out when nested in a section.
+ await expect( patternBlock ).toHaveAttribute( 'inert', 'true' );
+ await expect( blockWithOverrides ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+
+ await test.step( 'Zoomed out / Design mode - pattern nested in a section', async () => {
+ await editor.switchEditorTool( 'Design' );
+ // None of the pattern is editable in zoomed out when nested in a section.
+ await expect( patternBlock ).toHaveAttribute( 'inert', 'true' );
+ await expect( blockWithOverrides ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ await expect( blockWithoutOverridesOrBindings ).toHaveAttribute(
+ 'inert',
+ 'true'
+ );
+ } );
+ } );
+
+ test( 'disables editing of nested patterns', async ( {
+ page,
+ admin,
+ requestUtils,
+ editor,
+ } ) => {
+ const paragraphName = 'Editable paragraph';
+ const headingName = 'Editable heading';
+ const innerPattern = await requestUtils.createBlock( {
+ title: 'Inner Pattern',
+ content: `
+ Inner paragraph
+ `,
+ status: 'publish',
+ } );
+ const outerPattern = await requestUtils.createBlock( {
+ title: 'Outer Pattern',
+ content: `
+ Outer heading
+
+ `,
+ status: 'publish',
+ } );
+
+ await admin.createNewPost();
+
+ await editor.insertBlock( {
+ name: 'core/block',
+ attributes: { ref: outerPattern.id },
+ } );
+
+ // Make an edit to the outer pattern heading.
+ await editor.canvas
+ .getByRole( 'document', { name: 'Block: Heading' } )
+ .fill( 'Outer heading (edited)' );
+
+ const postId = await editor.publishPost();
+
+ // Check the pattern has the correct attributes.
+ await expect.poll( editor.getBlocks ).toMatchObject( [
+ {
+ name: 'core/block',
+ attributes: {
+ ref: outerPattern.id,
+ content: {
+ [ headingName ]: {
+ content: 'Outer heading (edited)',
+ },
+ },
+ },
+ innerBlocks: [],
+ },
+ ] );
+ // Check it renders correctly.
+ const headingBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Heading',
+ } );
+ const paragraphBlock = editor.canvas.getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } );
+ await expect( headingBlock ).toHaveText( 'Outer heading (edited)' );
+ await expect( headingBlock ).not.toHaveAttribute( 'inert', 'true' );
+ await expect( paragraphBlock ).toHaveText(
+ 'Inner paragraph (edited)'
+ );
+ await expect( paragraphBlock ).toHaveAttribute( 'inert', 'true' );
+
+ // Edit the outer pattern.
+ await editor.selectBlocks(
+ editor.canvas
+ .getByRole( 'document', { name: 'Block: Pattern' } )
+ .first()
+ );
+ await editor.showBlockToolbar();
+ await page
+ .getByRole( 'toolbar', { name: 'Block tools' } )
+ .getByRole( 'button', { name: 'Edit original' } )
+ .click();
+
+ // The inner paragraph should be editable in the pattern focus mode.
+ await editor.selectBlocks(
+ editor.canvas
+ .getByRole( 'document', { name: 'Block: Pattern' } )
+ .first()
+ );
+ await expect(
+ editor.canvas.getByRole( 'document', {
+ name: 'Block: Paragraph',
+ } ),
+ 'The inner paragraph should be editable'
+ ).not.toHaveAttribute( 'inert', 'true' );
+
+ // Visit the post on the frontend.
+ await page.goto( `/?p=${ postId }` );
+
+ await expect(
+ page.getByRole( 'heading', { level: 2 } )
+ ).toHaveText( 'Outer heading (edited)' );
+ await expect(
+ page.getByText( 'Inner paragraph (edited)' )
+ ).toBeVisible();
+ } );
+ } );
+
test( 'retains override values when converting a pattern block to regular blocks', async ( {
page,
admin,
@@ -425,107 +744,6 @@ test.describe( 'Pattern Overrides', () => {
await expect( buttonLink ).toHaveAttribute( 'rel', /^\s*nofollow\s*$/ );
} );
- test( 'disables editing of nested patterns', async ( {
- page,
- admin,
- requestUtils,
- editor,
- } ) => {
- const paragraphName = 'Editable paragraph';
- const headingName = 'Editable heading';
- const innerPattern = await requestUtils.createBlock( {
- title: 'Inner Pattern',
- content: `
-Inner paragraph
-`,
- status: 'publish',
- } );
- const outerPattern = await requestUtils.createBlock( {
- title: 'Outer Pattern',
- content: `
-Outer heading
-
-`,
- status: 'publish',
- } );
-
- await admin.createNewPost();
-
- await editor.insertBlock( {
- name: 'core/block',
- attributes: { ref: outerPattern.id },
- } );
-
- // Make an edit to the outer pattern heading.
- await editor.canvas
- .getByRole( 'document', { name: 'Block: Heading' } )
- .fill( 'Outer heading (edited)' );
-
- const postId = await editor.publishPost();
-
- // Check the pattern has the correct attributes.
- await expect.poll( editor.getBlocks ).toMatchObject( [
- {
- name: 'core/block',
- attributes: {
- ref: outerPattern.id,
- content: {
- [ headingName ]: {
- content: 'Outer heading (edited)',
- },
- },
- },
- innerBlocks: [],
- },
- ] );
- // Check it renders correctly.
- const headingBlock = editor.canvas.getByRole( 'document', {
- name: 'Block: Heading',
- } );
- const paragraphBlock = editor.canvas.getByRole( 'document', {
- name: 'Block: Paragraph',
- } );
- await expect( headingBlock ).toHaveText( 'Outer heading (edited)' );
- await expect( headingBlock ).not.toHaveAttribute( 'inert', 'true' );
- await expect( paragraphBlock ).toHaveText( 'Inner paragraph (edited)' );
- await expect( paragraphBlock ).toHaveAttribute( 'inert', 'true' );
-
- // Edit the outer pattern.
- await editor.selectBlocks(
- editor.canvas
- .getByRole( 'document', { name: 'Block: Pattern' } )
- .first()
- );
- await editor.showBlockToolbar();
- await page
- .getByRole( 'toolbar', { name: 'Block tools' } )
- .getByRole( 'button', { name: 'Edit original' } )
- .click();
-
- // The inner paragraph should be editable in the pattern focus mode.
- await editor.selectBlocks(
- editor.canvas
- .getByRole( 'document', { name: 'Block: Pattern' } )
- .first()
- );
- await expect(
- editor.canvas.getByRole( 'document', {
- name: 'Block: Paragraph',
- } ),
- 'The inner paragraph should be editable'
- ).not.toHaveAttribute( 'inert', 'true' );
-
- // Visit the post on the frontend.
- await page.goto( `/?p=${ postId }` );
-
- await expect( page.getByRole( 'heading', { level: 2 } ) ).toHaveText(
- 'Outer heading (edited)'
- );
- await expect(
- page.getByText( 'Inner paragraph (edited)' )
- ).toBeVisible();
- } );
-
test( 'resets overrides after clicking the reset button', async ( {
page,
admin,
@@ -993,7 +1211,11 @@ test.describe( 'Pattern Overrides', () => {
page.getByRole( 'button', { name: 'Dismiss this notice' } )
).toBeVisible();
- patternId = new URL( page.url() ).searchParams.get( 'postId' );
+ patternId = await page.evaluate( () => {
+ return window.wp.data
+ .select( 'core/editor' )
+ .getCurrentPostId();
+ } );
} );
await test.step( 'create a post and insert the pattern with synced values', async () => {
diff --git a/test/e2e/specs/site-editor/browser-history.spec.js b/test/e2e/specs/site-editor/browser-history.spec.js
index eaafb3aad1b3fd..a2326d10e3cc51 100644
--- a/test/e2e/specs/site-editor/browser-history.spec.js
+++ b/test/e2e/specs/site-editor/browser-history.spec.js
@@ -21,13 +21,13 @@ test.describe( 'Site editor browser history', () => {
await page.click( 'role=button[name="Templates"]' );
await page.getByRole( 'link', { name: 'Index' } ).click();
await expect( page ).toHaveURL(
- '/wp-admin/site-editor.php?postId=emptytheme%2F%2Findex&postType=wp_template&canvas=edit'
+ '/wp-admin/site-editor.php?p=%2Fwp_template%2Femptytheme%2F%2Findex&canvas=edit'
);
// Navigate back to the template list
await page.goBack();
await expect( page ).toHaveURL(
- '/wp-admin/site-editor.php?postType=wp_template'
+ '/wp-admin/site-editor.php?p=%2Ftemplate'
);
// Navigate back to the dashboard
diff --git a/test/e2e/specs/site-editor/command-center.spec.js b/test/e2e/specs/site-editor/command-center.spec.js
index 19318081aa171b..197a01c43c8b46 100644
--- a/test/e2e/specs/site-editor/command-center.spec.js
+++ b/test/e2e/specs/site-editor/command-center.spec.js
@@ -28,7 +28,7 @@ test.describe( 'Site editor command palette', () => {
await page.keyboard.type( 'new page' );
await page.getByRole( 'option', { name: 'Add new page' } ).click();
await expect( page ).toHaveURL(
- /\/wp-admin\/site-editor.php\?postId=(\d+)&postType=page&canvas=edit/
+ /\/wp-admin\/site-editor.php\?p=%2Fpage%2F(\d+)&canvas=edit/
);
await expect(
editor.canvas
diff --git a/test/e2e/specs/site-editor/homepage-settings.spec.js b/test/e2e/specs/site-editor/homepage-settings.spec.js
new file mode 100644
index 00000000000000..d53130af23ac8b
--- /dev/null
+++ b/test/e2e/specs/site-editor/homepage-settings.spec.js
@@ -0,0 +1,72 @@
+/**
+ * WordPress dependencies
+ */
+const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' );
+
+test.describe( 'Homepage Settings via Editor', () => {
+ test.beforeAll( async ( { requestUtils } ) => {
+ await Promise.all( [ requestUtils.activateTheme( 'emptytheme' ) ] );
+ await requestUtils.createPage( {
+ title: 'Homepage',
+ status: 'publish',
+ } );
+ } );
+
+ test.beforeEach( async ( { admin, page } ) => {
+ await admin.visitSiteEditor();
+ await page.getByRole( 'button', { name: 'Pages' } ).click();
+ } );
+
+ test.afterAll( async ( { requestUtils } ) => {
+ await Promise.all( [
+ requestUtils.deleteAllPages(),
+ requestUtils.updateSiteSettings( {
+ show_on_front: 'posts',
+ page_on_front: 0,
+ page_for_posts: 0,
+ } ),
+ ] );
+ } );
+
+ test( 'should show "Set as homepage" action on pages with `publish` status', async ( {
+ page,
+ } ) => {
+ const samplePage = page
+ .getByRole( 'gridcell' )
+ .getByLabel( 'Homepage' );
+ const samplePageRow = page
+ .getByRole( 'row' )
+ .filter( { has: samplePage } );
+ await samplePageRow.hover();
+ await samplePageRow
+ .getByRole( 'button', {
+ name: 'Actions',
+ } )
+ .click();
+ await expect(
+ page.getByRole( 'menuitem', { name: 'Set as homepage' } )
+ ).toBeVisible();
+ } );
+
+ test( 'should not show "Set as homepage" action on current homepage', async ( {
+ page,
+ } ) => {
+ const samplePage = page
+ .getByRole( 'gridcell' )
+ .getByLabel( 'Homepage' );
+ const samplePageRow = page
+ .getByRole( 'row' )
+ .filter( { has: samplePage } );
+ await samplePageRow.click();
+ await samplePageRow
+ .getByRole( 'button', {
+ name: 'Actions',
+ } )
+ .click();
+ await page.getByRole( 'menuitem', { name: 'Set as homepage' } ).click();
+ await page.getByRole( 'button', { name: 'Set homepage' } ).click();
+ await expect(
+ page.getByRole( 'menuitem', { name: 'Set as homepage' } )
+ ).toBeHidden();
+ } );
+} );
diff --git a/test/e2e/specs/site-editor/hybrid-theme.spec.js b/test/e2e/specs/site-editor/hybrid-theme.spec.js
index b568aaf4445b5c..042cb1042cac22 100644
--- a/test/e2e/specs/site-editor/hybrid-theme.spec.js
+++ b/test/e2e/specs/site-editor/hybrid-theme.spec.js
@@ -33,7 +33,7 @@ test.describe( 'Hybrid theme', () => {
);
await expect( page ).toHaveURL(
- '/wp-admin/site-editor.php?postType=wp_template_part'
+ '/wp-admin/site-editor.php?p=%2Fpattern&postType=wp_template_part'
);
await expect(
diff --git a/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js b/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js
index f26fb8e13b8c3c..a0cc0af5463aed 100644
--- a/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js
+++ b/test/e2e/specs/site-editor/site-editor-url-navigation.spec.js
@@ -44,7 +44,7 @@ test.describe( 'Site editor url navigation', () => {
.click();
await page.getByRole( 'option', { name: 'Demo' } ).click();
await expect( page ).toHaveURL(
- '/wp-admin/site-editor.php?postId=emptytheme%2F%2Fsingle-post-demo&postType=wp_template&canvas=edit'
+ '/wp-admin/site-editor.php?p=%2Fwp_template%2Femptytheme%2F%2Fsingle-post-demo&canvas=edit'
);
} );
@@ -63,7 +63,7 @@ test.describe( 'Site editor url navigation', () => {
await page.type( 'role=dialog >> role=textbox[name="Name"i]', 'Demo' );
await page.keyboard.press( 'Enter' );
await expect( page ).toHaveURL(
- '/wp-admin/site-editor.php?postId=emptytheme%2F%2Fdemo&postType=wp_template_part&canvas=edit'
+ '/wp-admin/site-editor.php?p=%2Fwp_template_part%2Femptytheme%2F%2Fdemo&canvas=edit'
);
} );