From 90007e3b7e07a5348fb2c5c60503dffe8ec1eeaa Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Fri, 13 Oct 2023 09:37:10 +1100 Subject: [PATCH 01/32] Add selector as id to layout style overrides. (#55291) --- packages/block-editor/src/hooks/layout.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index c26690291c98db..c194e0a2c5f789 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -408,11 +408,11 @@ export const withLayoutStyles = createHigherOrderComponent( useEffect( () => { if ( ! css ) return; - setStyleOverride( id, { css } ); + setStyleOverride( selector, { css } ); return () => { - deleteStyleOverride( id ); + deleteStyleOverride( selector ); }; - }, [ id, css, setStyleOverride, deleteStyleOverride ] ); + }, [ selector, css, setStyleOverride, deleteStyleOverride ] ); return ( { if ( ! css ) return; - setStyleOverride( id, { css } ); + setStyleOverride( selector, { css } ); return () => { - deleteStyleOverride( id ); + deleteStyleOverride( selector ); }; - }, [ id, css, setStyleOverride, deleteStyleOverride ] ); + }, [ selector, css, setStyleOverride, deleteStyleOverride ] ); return ; }, From f4d830bce69ab28a9fecfdea6d2f75c32b179879 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 12 Oct 2023 23:39:19 +0100 Subject: [PATCH 02/32] DataViews: Adds a control to the views actions to switch layouts (#55311) --- .../src/components/dataviews/dataviews.js | 199 ++---------------- .../src/components/dataviews/index.js | 1 - .../src/components/dataviews/pagination.js | 91 +++++--- .../src/components/dataviews/text-filter.js | 24 +-- .../src/components/dataviews/view-actions.js | 72 ++++--- .../src/components/dataviews/view-grid.js | 3 + .../dataviews/{list-view.js => view-list.js} | 182 +++++++++++++++- .../src/components/page-pages/index.js | 3 - 8 files changed, 319 insertions(+), 256 deletions(-) create mode 100644 packages/edit-site/src/components/dataviews/view-grid.js rename packages/edit-site/src/components/dataviews/{list-view.js => view-list.js} (50%) diff --git a/packages/edit-site/src/components/dataviews/dataviews.js b/packages/edit-site/src/components/dataviews/dataviews.js index a0973a82a291ca..755ce8a2ea348d 100644 --- a/packages/edit-site/src/components/dataviews/dataviews.js +++ b/packages/edit-site/src/components/dataviews/dataviews.js @@ -1,211 +1,54 @@ -/** - * External dependencies - */ -import { - getCoreRowModel, - getFilteredRowModel, - getSortedRowModel, - getPaginationRowModel, - useReactTable, -} from '@tanstack/react-table'; - /** * WordPress dependencies */ import { __experimentalVStack as VStack, __experimentalHStack as HStack, - VisuallyHidden, - DropdownMenu, - MenuGroup, - MenuItem, } from '@wordpress/components'; -import { useMemo } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import ListView from './list-view'; -import { Pagination } from './pagination'; +import ViewList from './view-list'; +import Pagination from './pagination'; import ViewActions from './view-actions'; import TextFilter from './text-filter'; -import { moreVertical } from '@wordpress/icons'; - -const EMPTY_OBJECT = {}; +import { ViewGrid } from './view-grid'; export default function DataViews( { - actions, - data, - fields, view, onChangeView, - isLoading, + fields, + actions, + data, + isLoading = false, paginationInfo, - options: { pageCount }, } ) { - const columns = useMemo( () => { - const _columns = [ ...fields ]; - if ( actions?.length ) { - _columns.push( { - header: { __( 'Actions' ) }, - id: 'actions', - cell: ( props ) => { - return ( - - { () => ( - - { actions.map( ( action ) => ( - - action.perform( - props.row.original - ) - } - isDestructive={ - action.isDesctructive - } - > - { action.label } - - ) ) } - - ) } - - ); - }, - enableHiding: false, - } ); - } - - return _columns; - }, [ fields, actions ] ); - - const columnVisibility = useMemo( () => { - if ( ! view.hiddenFields?.length ) { - return; - } - return view.hiddenFields.reduce( - ( accumulator, fieldId ) => ( { - ...accumulator, - [ fieldId ]: false, - } ), - {} - ); - }, [ view.hiddenFields ] ); - - const dataView = useReactTable( { - data, - columns, - manualSorting: true, - manualFiltering: true, - manualPagination: true, - enableRowSelection: true, - state: { - sorting: view.sort - ? [ - { - id: view.sort.field, - desc: view.sort.direction === 'desc', - }, - ] - : [], - globalFilter: view.search, - pagination: { - pageIndex: view.page, - pageSize: view.perPage, - }, - columnVisibility: columnVisibility ?? EMPTY_OBJECT, - }, - onSortingChange: ( sortingUpdater ) => { - onChangeView( ( currentView ) => { - const sort = - typeof sortingUpdater === 'function' - ? sortingUpdater( - currentView.sort - ? [ - { - id: currentView.sort.field, - desc: - currentView.sort - .direction === 'desc', - }, - ] - : [] - ) - : sortingUpdater; - if ( ! sort.length ) { - return { - ...currentView, - sort: {}, - }; - } - const [ { id, desc } ] = sort; - return { - ...currentView, - sort: { field: id, direction: desc ? 'desc' : 'asc' }, - }; - } ); - }, - onColumnVisibilityChange: ( columnVisibilityUpdater ) => { - onChangeView( ( currentView ) => { - const hiddenFields = Object.entries( - columnVisibilityUpdater() - ).reduce( - ( accumulator, [ fieldId, value ] ) => { - if ( value ) { - return accumulator.filter( - ( id ) => id !== fieldId - ); - } - return [ ...accumulator, fieldId ]; - }, - [ ...( currentView.hiddenFields || [] ) ] - ); - return { - ...currentView, - hiddenFields, - }; - } ); - }, - onGlobalFilterChange: ( value ) => { - onChangeView( { ...view, search: value, page: 0 } ); - }, - onPaginationChange: ( paginationUpdater ) => { - onChangeView( ( currentView ) => { - const { pageIndex, pageSize } = paginationUpdater( { - pageIndex: currentView.page, - pageSize: currentView.perPage, - } ); - return { ...view, page: pageIndex, perPage: pageSize }; - } ); - }, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - pageCount, - } ); + const ViewComponent = view.type === 'list' ? ViewList : ViewGrid; return (
- + - { /* This component will be selected based on viewConfigs. Now we only have the list view. */ } - +
diff --git a/packages/edit-site/src/components/dataviews/index.js b/packages/edit-site/src/components/dataviews/index.js index 3fea6fd63714b7..422d128b1461d0 100644 --- a/packages/edit-site/src/components/dataviews/index.js +++ b/packages/edit-site/src/components/dataviews/index.js @@ -1,2 +1 @@ export { default as DataViews } from './dataviews'; -export { PAGE_SIZE_VALUES } from './view-actions'; diff --git a/packages/edit-site/src/components/dataviews/pagination.js b/packages/edit-site/src/components/dataviews/pagination.js index 3adecd2bb80851..bd6cb503653b07 100644 --- a/packages/edit-site/src/components/dataviews/pagination.js +++ b/packages/edit-site/src/components/dataviews/pagination.js @@ -6,26 +6,54 @@ import { __experimentalHStack as HStack, __experimentalText as Text, __experimentalNumberControl as NumberControl, + __experimentalInputControlPrefixWrapper as InputControlPrefixWrapper, + SelectControl, } from '@wordpress/components'; import { createInterpolateElement } from '@wordpress/element'; import { sprintf, __, _x, _n } from '@wordpress/i18n'; -/** - * Internal dependencies - */ -import { PageSizeControl } from './view-actions'; +const PAGE_SIZE_VALUES = [ 5, 20, 50 ]; +function PageSizeControl( { view, onChangeView } ) { + const label = __( 'Rows per page:' ); + return ( + + { label } + + } + value={ view.perPage } + options={ PAGE_SIZE_VALUES.map( ( pageSize ) => ( { + value: pageSize, + label: pageSize, + } ) ) } + onChange={ ( value ) => + onChangeView( { ...view, perPage: value } ) + } + /> + ); +} // For now this is copied from the patterns list Pagination component, because // the datatable pagination starts from index zero(`0`). Eventually all lists will be // using this one. -export function Pagination( { - dataView, - // If passed, use it, as it's for controlled pagination. - totalItems = 0, +function Pagination( { + view, + onChangeView, + paginationInfo: { totalItems = 0, totalPages }, } ) { - const currentPage = dataView.getState().pagination.pageIndex + 1; - const numPages = dataView.getPageCount(); - const _totalItems = totalItems || dataView.getCoreRowModel().rows.length; + const currentPage = view.page + 1; + if ( ! totalItems || ! totalPages ) { + return null; + } return ( - { !! _totalItems && ( + { !! totalItems && ( ) } - + ); } + +export default Pagination; diff --git a/packages/edit-site/src/components/dataviews/text-filter.js b/packages/edit-site/src/components/dataviews/text-filter.js index 76a06c14486170..0b5f7b3cf6790e 100644 --- a/packages/edit-site/src/components/dataviews/text-filter.js +++ b/packages/edit-site/src/components/dataviews/text-filter.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ @@ -15,18 +10,19 @@ import { SearchControl } from '@wordpress/components'; */ import useDebouncedInput from '../../utils/use-debounced-input'; -export default function TextFilter( { - className, - searchLabel = __( 'Filter list' ), - onChange, -} ) { - const [ search, setSearch, debouncedSearch ] = useDebouncedInput(); +export default function TextFilter( { view, onChangeView } ) { + const [ search, setSearch, debouncedSearch ] = useDebouncedInput( + view.search + ); useEffect( () => { - onChange( debouncedSearch ); - }, [ debouncedSearch, onChange ] ); + onChangeView( ( currentView ) => ( { + ...currentView, + search: debouncedSearch, + } ) ); + }, [ debouncedSearch, onChangeView ] ); + const searchLabel = __( 'Filter list' ); return ( view.type === v.id ); return ( - + { activeView.label } + + + } > - { label } - + { __( 'Layout' ) } + } - value={ dataView.getState().pagination.pageSize } - options={ PAGE_SIZE_VALUES.map( ( pageSize ) => ( { - value: pageSize, - label: pageSize, - } ) ) } - onChange={ ( value ) => dataView.setPageSize( +value ) } - /> + > + { availableViews.map( ( availableView ) => { + return ( + + ) + } + onSelect={ ( event ) => { + // We need to handle this on DropDown component probably.. + event.preventDefault(); + onChangeView( { ...view, type: availableView.id } ); + } } + // TODO: check about role and a11y. + role="menuitemcheckbox" + > + { availableView.label } + + ); + } ) } + ); } +const PAGE_SIZE_VALUES = [ 5, 20, 50 ]; function PageSizeMenu( { view, onChangeView } ) { return ( + ); } -function ListView( { dataView, className, isLoading = false }, ref ) { +function ViewList( { + view, + onChangeView, + fields, + actions, + data, + isLoading = false, + paginationInfo, +} ) { + const columns = useMemo( () => { + const _columns = [ ...fields ]; + if ( actions?.length ) { + _columns.push( { + header: { __( 'Actions' ) }, + id: 'actions', + cell: ( props ) => { + return ( + + { () => ( + + { actions.map( ( action ) => ( + + action.perform( + props.row.original + ) + } + isDestructive={ + action.isDesctructive + } + > + { action.label } + + ) ) } + + ) } + + ); + }, + enableHiding: false, + } ); + } + + return _columns; + }, [ fields, actions ] ); + + const columnVisibility = useMemo( () => { + if ( ! view.hiddenFields?.length ) { + return; + } + return view.hiddenFields.reduce( + ( accumulator, fieldId ) => ( { + ...accumulator, + [ fieldId ]: false, + } ), + {} + ); + }, [ view.hiddenFields ] ); + + const dataView = useReactTable( { + data, + columns, + manualSorting: true, + manualFiltering: true, + manualPagination: true, + enableRowSelection: true, + state: { + sorting: view.sort + ? [ + { + id: view.sort.field, + desc: view.sort.direction === 'desc', + }, + ] + : [], + globalFilter: view.search, + pagination: { + pageIndex: view.page, + pageSize: view.perPage, + }, + columnVisibility: columnVisibility ?? EMPTY_OBJECT, + }, + onSortingChange: ( sortingUpdater ) => { + onChangeView( ( currentView ) => { + const sort = + typeof sortingUpdater === 'function' + ? sortingUpdater( + currentView.sort + ? [ + { + id: currentView.sort.field, + desc: + currentView.sort + .direction === 'desc', + }, + ] + : [] + ) + : sortingUpdater; + if ( ! sort.length ) { + return { + ...currentView, + sort: {}, + }; + } + const [ { id, desc } ] = sort; + return { + ...currentView, + sort: { field: id, direction: desc ? 'desc' : 'asc' }, + }; + } ); + }, + onColumnVisibilityChange: ( columnVisibilityUpdater ) => { + onChangeView( ( currentView ) => { + const hiddenFields = Object.entries( + columnVisibilityUpdater() + ).reduce( + ( accumulator, [ fieldId, value ] ) => { + if ( value ) { + return accumulator.filter( + ( id ) => id !== fieldId + ); + } + return [ ...accumulator, fieldId ]; + }, + [ ...( currentView.hiddenFields || [] ) ] + ); + return { + ...currentView, + hiddenFields, + }; + } ); + }, + onGlobalFilterChange: ( value ) => { + onChangeView( { ...view, search: value, page: 0 } ); + }, + onPaginationChange: ( paginationUpdater ) => { + onChangeView( ( currentView ) => { + const { pageIndex, pageSize } = paginationUpdater( { + pageIndex: currentView.page, + pageSize: currentView.perPage, + } ); + return { ...view, page: pageIndex, perPage: pageSize }; + } ); + }, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + pageCount: paginationInfo.totalPages, + } ); + const { rows } = dataView.getRowModel(); const hasRows = !! rows?.length; if ( isLoading ) { @@ -123,10 +290,7 @@ function ListView( { dataView, className, isLoading = false }, ref ) { return (
{ hasRows && ( - +
{ dataView.getHeaderGroups().map( ( headerGroup ) => ( @@ -183,4 +347,4 @@ function ListView( { dataView, className, isLoading = false }, ref ) { ); } -export default forwardRef( ListView ); +export default ViewList; diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index a616ea617feb91..728f7c2c716fe6 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -167,9 +167,6 @@ export default function PagePages() { isLoading={ isLoadingPages } view={ view } onChangeView={ setView } - options={ { - pageCount: totalPages, - } } /> ); From 26dae9e69f7422b7dec632ed0972a50b81aa5650 Mon Sep 17 00:00:00 2001 From: Jason Johnston Date: Thu, 12 Oct 2023 18:57:43 -0400 Subject: [PATCH 03/32] Mobile Release v1.106.0 (#55319) * Release script: Update react-native-editor version to 1.106.0 * Release script: Update with changes from 'npm run core preios' * Add package lock file * update Change log * update Change log --- package-lock.json | 6 +++--- packages/react-native-aztec/package.json | 2 +- packages/react-native-bridge/package.json | 2 +- packages/react-native-editor/CHANGELOG.md | 5 ++++- packages/react-native-editor/ios/Podfile.lock | 8 ++++---- packages/react-native-editor/package.json | 2 +- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index ef1c779c3c4fd5..17481c3d87f6a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57614,7 +57614,7 @@ }, "packages/react-native-aztec": { "name": "@wordpress/react-native-aztec", - "version": "1.105.0", + "version": "1.106.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/element": "file:../element", @@ -57627,7 +57627,7 @@ }, "packages/react-native-bridge": { "name": "@wordpress/react-native-bridge", - "version": "1.105.0", + "version": "1.106.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/react-native-aztec": "file:../react-native-aztec" @@ -57638,7 +57638,7 @@ }, "packages/react-native-editor": { "name": "@wordpress/react-native-editor", - "version": "1.105.0", + "version": "1.106.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json index 87ebdf194696ba..93deeaa9d657cd 100644 --- a/packages/react-native-aztec/package.json +++ b/packages/react-native-aztec/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-aztec", - "version": "1.105.0", + "version": "1.106.0", "description": "Aztec view for react-native.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json index b6de1b5c53fff8..8a99d1ee250608 100644 --- a/packages/react-native-bridge/package.json +++ b/packages/react-native-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-bridge", - "version": "1.105.0", + "version": "1.106.0", "description": "Native bridge library used to integrate the block editor into a native App.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 600f855ba0ec11..9e69c51a6b73ba 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,7 +10,10 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased + +## 1.106.0 - [*] Exit Preformatted and Verse blocks by triple pressing the Return key [#53354] +- [*] Fix quote block border visibility when used with block based themes in dark mode [#54964] ## 1.105.0 - [*] Limit inner blocks nesting depth to avoid call stack size exceeded crash [#54382] @@ -911,4 +914,4 @@ For each user feature we should also add a importance categorization label to i ## 1.6.0 - Fixed issue with link settings where “Open in New Tab” was always OFF on open. -- Added UI to display a warning when a block has invalid content. \ No newline at end of file +- Added UI to display a warning when a block has invalid content. diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index 26a891a2402519..9042c8cbf59745 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -13,7 +13,7 @@ PODS: - ReactCommon/turbomodule/core (= 0.71.11) - fmt (6.2.1) - glog (0.3.5) - - Gutenberg (1.105.0): + - Gutenberg (1.106.0): - React-Core (= 0.71.11) - React-CoreModules (= 0.71.11) - React-RCTImage (= 0.71.11) @@ -429,7 +429,7 @@ PODS: - React-RCTImage - RNSVG (13.9.0): - React-Core - - RNTAztecView (1.105.0): + - RNTAztecView (1.106.0): - React-Core - WordPress-Aztec-iOS (= 1.19.9) - SDWebImage (5.11.1): @@ -617,7 +617,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: f07662560742d82a5b73cee116c70b0b49bcc220 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - Gutenberg: eb59e13cd7ef341f9ef92857009d960b0831f21a + Gutenberg: 599b650ee1ad4ecfc39dc7aa771bf3699bf2cf72 hermes-engine: 34c863b446d0135b85a6536fa5fd89f48196f848 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c @@ -662,7 +662,7 @@ SPEC CHECKSUMS: RNReanimated: d4f363f4987ae0ade3e36ff81c94e68261bf4b8d RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789 RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315 - RNTAztecView: 4b0ffdbaa58dcbf73b63403a888a2334fc4465dd + RNTAztecView: 16f6392f66b5b7db2f950fd8a2481d8bbba4cb67 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d WordPress-Aztec-iOS: fbebd569c61baa252b3f5058c0a2a9a6ada686bb diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index 0c6ff58c7184d5..cb5868da3f7e89 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-editor", - "version": "1.105.0", + "version": "1.106.0", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 617138a05ec848656dc9df5f1943dc03ca8e5616 Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Fri, 13 Oct 2023 00:10:09 +0100 Subject: [PATCH 04/32] List View: Change the aria-description attribute to aria-describedby (#55264) * List View: Change the aria-description attribute to aria-describedby * use a unique id --- .../src/components/list-view/index.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 085864c4c88f45..1d21c28643a73c 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -6,7 +6,10 @@ import { useMergeRefs, __experimentalUseFixedWindowList as useFixedWindowList, } from '@wordpress/compose'; -import { __experimentalTreeGrid as TreeGrid } from '@wordpress/components'; +import { + __experimentalTreeGrid as TreeGrid, + VisuallyHidden, +} from '@wordpress/components'; import { AsyncModeProvider, useSelect } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; import { @@ -257,12 +260,20 @@ function ListViewComponent( return null; } + const describedById = + description && `block-editor-list-view-description-${ instanceId }`; + return ( + { description && ( + + { description } + + ) } Date: Fri, 13 Oct 2023 10:37:04 +1000 Subject: [PATCH 05/32] Patterns: Add duplicate pattern command (#55292) --- .../page-patterns/duplicate-menu-item.js | 69 +++----------- .../src/components/pattern-modal/duplicate.js | 53 +++++++++++ .../src/components/pattern-modal/index.js | 11 ++- .../hooks/commands/use-edit-mode-commands.js | 9 ++ .../src/components/create-pattern-modal.js | 4 +- .../src/components/duplicate-pattern-modal.js | 93 +++++++++++++++++++ packages/patterns/src/private-apis.js | 2 + 7 files changed, 182 insertions(+), 59 deletions(-) create mode 100644 packages/edit-site/src/components/pattern-modal/duplicate.js create mode 100644 packages/patterns/src/components/duplicate-pattern-modal.js diff --git a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js index a0842cf9002a09..118c954a851f3f 100644 --- a/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js +++ b/packages/edit-site/src/components/page-patterns/duplicate-menu-item.js @@ -12,15 +12,11 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies */ -import { - TEMPLATE_PART_POST_TYPE, - PATTERN_SYNC_TYPES, - PATTERN_TYPES, -} from '../../utils/constants'; +import { TEMPLATE_PART_POST_TYPE, PATTERN_TYPES } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; import CreateTemplatePartModal from '../create-template-part-modal'; -const { CreatePatternModal } = unlock( patternsPrivateApis ); +const { DuplicatePatternModal } = unlock( patternsPrivateApis ); const { useHistory } = unlock( routerPrivateApis ); export default function DuplicateMenuItem( { @@ -33,7 +29,10 @@ export default function DuplicateMenuItem( { const [ isModalOpen, setIsModalOpen ] = useState( false ); const history = useHistory(); + const closeModal = () => setIsModalOpen( false ); + const isTemplatePart = item.type === TEMPLATE_PART_POST_TYPE; + const isThemePattern = item.type === PATTERN_TYPES.theme; async function onTemplatePartSuccess( templatePart ) { createSuccessNotice( @@ -59,18 +58,6 @@ export default function DuplicateMenuItem( { } function onPatternSuccess( { pattern } ) { - createSuccessNotice( - sprintf( - // translators: %s: The new pattern's title e.g. 'Call to action (copy)'. - __( '"%s" duplicated.' ), - pattern.title.raw - ), - { - type: 'snackbar', - id: 'edit-site-patterns-success', - } - ); - history.push( { categoryType: PATTERN_TYPES.theme, categoryId, @@ -81,35 +68,6 @@ export default function DuplicateMenuItem( { onClose(); } - const isThemePattern = item.type === PATTERN_TYPES.theme; - const closeModal = () => setIsModalOpen( false ); - const duplicatedProps = isTemplatePart - ? { - blocks: item.blocks, - defaultArea: item.templatePart.area, - defaultTitle: sprintf( - /* translators: %s: Existing template part title */ - __( '%s (Copy)' ), - item.title - ), - } - : { - defaultCategories: isThemePattern - ? item.categories - : item.termLabels, - content: isThemePattern - ? item.content - : item.patternBlock.content, - defaultSyncType: isThemePattern - ? PATTERN_SYNC_TYPES.unsynced - : item.syncStatus, - defaultTitle: sprintf( - /* translators: %s: Existing pattern title */ - __( '%s (Copy)' ), - item.title || item.name - ), - }; - return ( <> { isModalOpen && ! isTemplatePart && ( - ) } { isModalOpen && isTemplatePart && ( ) } diff --git a/packages/edit-site/src/components/pattern-modal/duplicate.js b/packages/edit-site/src/components/pattern-modal/duplicate.js new file mode 100644 index 00000000000000..8de13e9cf39479 --- /dev/null +++ b/packages/edit-site/src/components/pattern-modal/duplicate.js @@ -0,0 +1,53 @@ +/** + * WordPress dependencies + */ +import { useDispatch, useSelect } from '@wordpress/data'; +import { store as interfaceStore } from '@wordpress/interface'; +import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; +import { getQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import { PATTERN_MODALS } from './'; +import { PATTERN_TYPES } from '../../utils/constants'; +import { unlock } from '../../lock-unlock'; +import useEditedEntityRecord from '../use-edited-entity-record'; + +const { DuplicatePatternModal } = unlock( patternsPrivateApis ); +const { useHistory } = unlock( routerPrivateApis ); + +export default function PatternDuplicateModal() { + const { record } = useEditedEntityRecord(); + const { categoryType, categoryId } = getQueryArgs( window.location.href ); + const { closeModal } = useDispatch( interfaceStore ); + const history = useHistory(); + + const isActive = useSelect( ( select ) => + select( interfaceStore ).isModalActive( PATTERN_MODALS.duplicate ) + ); + + if ( ! isActive ) { + return null; + } + + function onSuccess( { pattern: newPattern } ) { + history.push( { + categoryType, + categoryId, + postType: PATTERN_TYPES.user, + postId: newPattern.id, + } ); + + closeModal(); + } + + return ( + + ); +} diff --git a/packages/edit-site/src/components/pattern-modal/index.js b/packages/edit-site/src/components/pattern-modal/index.js index acf96a64877255..95d08a058b3f84 100644 --- a/packages/edit-site/src/components/pattern-modal/index.js +++ b/packages/edit-site/src/components/pattern-modal/index.js @@ -2,13 +2,18 @@ * Internal dependencies */ import PatternRenameModal from './rename'; +import PatternDuplicateModal from './duplicate'; export const PATTERN_MODALS = { rename: 'edit-site/pattern-rename', + duplicate: 'edit-site/pattern-duplicate', }; export default function PatternModal() { - // Further modals are likely - // e.g. duplicating and switching up sync status etc. - return ; + return ( + <> + + + + ); } diff --git a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js index 107efce384eb72..432160a9573729 100644 --- a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js +++ b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js @@ -381,6 +381,15 @@ function usePatternCommands() { close(); }, } ); + commands.push( { + name: 'core/duplicate-pattern', + label: __( 'Duplicate pattern' ), + icon: symbol, + callback: ( { close } ) => { + openModal( PATTERN_MODALS.duplicate ); + close(); + }, + } ); } return { isLoading: false, commands }; diff --git a/packages/patterns/src/components/create-pattern-modal.js b/packages/patterns/src/components/create-pattern-modal.js index 6dd162605506e7..22d20fd0372657 100644 --- a/packages/patterns/src/components/create-pattern-modal.js +++ b/packages/patterns/src/components/create-pattern-modal.js @@ -110,9 +110,9 @@ export default function CreatePatternModal( { } catch ( error ) { createErrorNotice( error.message, { type: 'snackbar', - id: 'convert-to-pattern-error', + id: 'pattern-create', } ); - onError(); + onError?.(); } finally { setIsSaving( false ); setCategoryTerms( [] ); diff --git a/packages/patterns/src/components/duplicate-pattern-modal.js b/packages/patterns/src/components/duplicate-pattern-modal.js new file mode 100644 index 00000000000000..baf977a7b27e2f --- /dev/null +++ b/packages/patterns/src/components/duplicate-pattern-modal.js @@ -0,0 +1,93 @@ +/** + * WordPress dependencies + */ +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { __, sprintf } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import CreatePatternModal from './create-pattern-modal'; +import { PATTERN_SYNC_TYPES } from '../constants'; + +function getTermLabels( pattern, categories ) { + // Theme patterns don't have an id and rely on core pattern categories. + if ( ! pattern.id ) { + return categories.core + ?.filter( ( category ) => + pattern.categories.includes( category.name ) + ) + .map( ( category ) => category.label ); + } + + return categories.user + ?.filter( ( category ) => + pattern.wp_pattern_category.includes( category.id ) + ) + .map( ( category ) => category.label ); +} + +export default function DuplicatePatternModal( { + pattern, + onClose, + onSuccess, +} ) { + const { createSuccessNotice } = useDispatch( noticesStore ); + const categories = useSelect( ( select ) => { + const { getUserPatternCategories, getBlockPatternCategories } = + select( coreStore ); + + return { + core: getBlockPatternCategories(), + user: getUserPatternCategories(), + }; + } ); + + if ( ! pattern ) { + return null; + } + + const duplicatedProps = { + content: pattern.content, + defaultCategories: getTermLabels( pattern, categories ), + defaultSyncType: ! pattern.id // Theme patterns don't have an ID. + ? PATTERN_SYNC_TYPES.unsynced + : pattern.wp_pattern_sync_status || PATTERN_SYNC_TYPES.full, + defaultTitle: sprintf( + /* translators: %s: Existing pattern title */ + __( '%s (Copy)' ), + typeof pattern.title === 'string' + ? pattern.title + : pattern.title.raw + ), + }; + + function handleOnSuccess( { pattern: newPattern } ) { + createSuccessNotice( + sprintf( + // translators: %s: The new pattern's title e.g. 'Call to action (copy)'. + __( '"%s" duplicated.' ), + newPattern.title.raw + ), + { + type: 'snackbar', + id: 'patterns-create', + } + ); + + onSuccess?.( { pattern: newPattern } ); + } + + return ( + + ); +} diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js index 3a68cea8794a54..2246b1a891e1ae 100644 --- a/packages/patterns/src/private-apis.js +++ b/packages/patterns/src/private-apis.js @@ -3,6 +3,7 @@ */ import { lock } from './lock-unlock'; import CreatePatternModal from './components/create-pattern-modal'; +import DuplicatePatternModal from './components/duplicate-pattern-modal'; import RenamePatternModal from './components/rename-pattern-modal'; import PatternsMenuItems from './components'; import { @@ -16,6 +17,7 @@ import { export const privateApis = {}; lock( privateApis, { CreatePatternModal, + DuplicatePatternModal, RenamePatternModal, PatternsMenuItems, PATTERN_TYPES, From 68a33b52bf7c202aed30897c09b16647a0664d53 Mon Sep 17 00:00:00 2001 From: tellthemachines Date: Fri, 13 Oct 2023 14:00:14 +1100 Subject: [PATCH 06/32] Fix filtering for downloadable blocks in inserter. (#55243) * Fix filering for downloadable blocks in inserter. * Don't remove block from list if just installed --- .../downloadable-blocks-panel/index.js | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/block-directory/src/components/downloadable-blocks-panel/index.js b/packages/block-directory/src/components/downloadable-blocks-panel/index.js index 2adf4cff529231..46866003128513 100644 --- a/packages/block-directory/src/components/downloadable-blocks-panel/index.js +++ b/packages/block-directory/src/components/downloadable-blocks-panel/index.js @@ -4,9 +4,9 @@ import { __ } from '@wordpress/i18n'; import { Spinner } from '@wordpress/components'; import { compose } from '@wordpress/compose'; -import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { withSelect } from '@wordpress/data'; +import { getBlockType } from '@wordpress/blocks'; /** * Internal dependencies @@ -72,10 +72,12 @@ function DownloadableBlocksPanel( { } export default compose( [ - withSelect( ( select, { filterValue, rootClientId = null } ) => { - const { getDownloadableBlocks, isRequestingDownloadableBlocks } = - select( blockDirectoryStore ); - const { canInsertBlockType } = select( blockEditorStore ); + withSelect( ( select, { filterValue } ) => { + const { + getDownloadableBlocks, + isRequestingDownloadableBlocks, + getInstalledBlockTypes, + } = select( blockDirectoryStore ); const hasPermission = select( coreStore ).canUser( 'read', @@ -84,9 +86,19 @@ export default compose( [ function getInstallableBlocks( term ) { const downloadableBlocks = getDownloadableBlocks( term ); - const installableBlocks = downloadableBlocks.filter( ( block ) => - canInsertBlockType( block, rootClientId, true ) - ); + const installedBlockTypes = getInstalledBlockTypes(); + // Filter out blocks that are already installed. + const installableBlocks = downloadableBlocks.filter( ( block ) => { + // Check if the block has just been installed, in which case it + // should still show in the list to avoid suddenly disappearing. + // `installedBlockTypes` only returns blocks stored in state + // immediately after installation, not all installed blocks. + const isJustInstalled = !! installedBlockTypes.find( + ( blockType ) => blockType.name === block.name + ); + const isPreviouslyInstalled = getBlockType( block.name ); + return isJustInstalled || ! isPreviouslyInstalled; + } ); if ( downloadableBlocks.length === installableBlocks.length ) { return downloadableBlocks; From ef21f207827e1ac9e23c307ca2a7efaf4366b6c6 Mon Sep 17 00:00:00 2001 From: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> Date: Fri, 13 Oct 2023 16:02:34 +1000 Subject: [PATCH 07/32] Patterns: Add rename/delete options for pattern categories in site editor (#55035) --- packages/core-data/src/resolvers.js | 3 +- .../delete-category-menu-item.js | 104 +++++++++++++++ .../src/components/page-patterns/header.js | 48 ++++++- .../rename-category-menu-item.js | 45 +++++++ .../src/components/page-patterns/style.scss | 8 ++ .../rename-pattern-category-modal.js | 121 ++++++++++++++++++ packages/patterns/src/private-apis.js | 2 + 7 files changed, 324 insertions(+), 7 deletions(-) create mode 100644 packages/edit-site/src/components/page-patterns/delete-category-menu-item.js create mode 100644 packages/edit-site/src/components/page-patterns/rename-category-menu-item.js create mode 100644 packages/patterns/src/components/rename-pattern-category-modal.js diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index ee9cb90d168c35..07e9cd98cb5ec3 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -7,6 +7,7 @@ import { camelCase } from 'change-case'; * WordPress dependencies */ import { addQueryArgs } from '@wordpress/url'; +import { decodeEntities } from '@wordpress/html-entities'; import apiFetch from '@wordpress/api-fetch'; /** @@ -656,7 +657,7 @@ export const getUserPatternCategories = const mappedPatternCategories = patternCategories?.map( ( userCategory ) => ( { ...userCategory, - label: userCategory.name, + label: decodeEntities( userCategory.name ), name: userCategory.slug, } ) ) || []; diff --git a/packages/edit-site/src/components/page-patterns/delete-category-menu-item.js b/packages/edit-site/src/components/page-patterns/delete-category-menu-item.js new file mode 100644 index 00000000000000..0a9a36d93d0099 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/delete-category-menu-item.js @@ -0,0 +1,104 @@ +/** + * WordPress dependencies + */ +import { + MenuItem, + __experimentalConfirmDialog as ConfirmDialog, +} from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { decodeEntities } from '@wordpress/html-entities'; +import { __, sprintf } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; +import { PATTERN_TYPES, PATTERN_DEFAULT_CATEGORY } from '../../utils/constants'; + +const { useHistory } = unlock( routerPrivateApis ); + +export default function DeleteCategoryMenuItem( { category, onClose } ) { + const [ isModalOpen, setIsModalOpen ] = useState( false ); + const history = useHistory(); + + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + const { deleteEntityRecord, invalidateResolution } = + useDispatch( coreStore ); + + const onDelete = async () => { + try { + await deleteEntityRecord( + 'taxonomy', + 'wp_pattern_category', + category.id, + { force: true }, + { throwOnError: true } + ); + + // Prevent the need to refresh the page to get up-to-date categories + // and pattern categorization. + invalidateResolution( 'getUserPatternCategories' ); + invalidateResolution( 'getEntityRecords', [ + 'postType', + PATTERN_TYPES.user, + { per_page: -1 }, + ] ); + + createSuccessNotice( + sprintf( + /* translators: The pattern category's name */ + __( '"%s" deleted.' ), + category.label + ), + { type: 'snackbar', id: 'pattern-category-delete' } + ); + + onClose?.(); + history.push( { + path: `/patterns`, + categoryType: PATTERN_TYPES.theme, + categoryId: PATTERN_DEFAULT_CATEGORY, + } ); + } catch ( error ) { + const errorMessage = + error.message && error.code !== 'unknown_error' + ? error.message + : __( + 'An error occurred while deleting the pattern category.' + ); + + createErrorNotice( errorMessage, { + type: 'snackbar', + id: 'pattern-category-delete', + } ); + } + }; + + return ( + <> + setIsModalOpen( true ) }> + { __( 'Delete' ) } + + setIsModalOpen( false ) } + confirmButtonText={ __( 'Delete' ) } + className="edit-site-patterns__delete-modal" + > + { sprintf( + // translators: %s: The pattern category's name. + __( + 'Are you sure you want to delete the category "%s"? The patterns will not be deleted.' + ), + decodeEntities( category.label ) + ) } + + + ); +} diff --git a/packages/edit-site/src/components/page-patterns/header.js b/packages/edit-site/src/components/page-patterns/header.js index d692f523e84825..a5b19e4e3a6b92 100644 --- a/packages/edit-site/src/components/page-patterns/header.js +++ b/packages/edit-site/src/components/page-patterns/header.js @@ -2,16 +2,23 @@ * WordPress dependencies */ import { - __experimentalVStack as VStack, + DropdownMenu, + MenuGroup, + __experimentalHStack as HStack, __experimentalHeading as Heading, __experimentalText as Text, + __experimentalVStack as VStack, } from '@wordpress/components'; import { store as editorStore } from '@wordpress/editor'; import { useSelect } from '@wordpress/data'; +import { __, sprintf } from '@wordpress/i18n'; +import { moreVertical } from '@wordpress/icons'; /** * Internal dependencies */ +import RenameCategoryMenuItem from './rename-category-menu-item'; +import DeleteCategoryMenuItem from './delete-category-menu-item'; import usePatternCategories from '../sidebar-navigation-screen-patterns/use-pattern-categories'; import { TEMPLATE_PART_POST_TYPE, PATTERN_TYPES } from '../../utils/constants'; @@ -28,7 +35,7 @@ export default function PatternsHeader( { [] ); - let title, description; + let title, description, patternCategory; if ( type === TEMPLATE_PART_POST_TYPE ) { const templatePartArea = templatePartAreas.find( ( area ) => area.area === categoryId @@ -36,7 +43,7 @@ export default function PatternsHeader( { title = templatePartArea?.label; description = templatePartArea?.description; } else if ( type === PATTERN_TYPES.theme ) { - const patternCategory = patternCategories.find( + patternCategory = patternCategories.find( ( category ) => category.name === categoryId ); title = patternCategory?.label; @@ -47,9 +54,38 @@ export default function PatternsHeader( { return ( - - { title } - + + + { title } + + { !! patternCategory?.id && ( + + { ( { onClose } ) => ( + + + + + ) } + + ) } + { description ? ( { description } diff --git a/packages/edit-site/src/components/page-patterns/rename-category-menu-item.js b/packages/edit-site/src/components/page-patterns/rename-category-menu-item.js new file mode 100644 index 00000000000000..97e7de069687a5 --- /dev/null +++ b/packages/edit-site/src/components/page-patterns/rename-category-menu-item.js @@ -0,0 +1,45 @@ +/** + * WordPress dependencies + */ +import { MenuItem } from '@wordpress/components'; +import { useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { privateApis as patternsPrivateApis } from '@wordpress/patterns'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +const { RenamePatternCategoryModal } = unlock( patternsPrivateApis ); + +export default function RenameCategoryMenuItem( { category, onClose } ) { + const [ isModalOpen, setIsModalOpen ] = useState( false ); + + // User created pattern categories have their properties updated when + // retrieved via `getUserPatternCategories`. The rename modal expects an + // object that will match the pattern category entity. + const normalizedCategory = { + id: category.id, + slug: category.slug, + name: category.label, + }; + + return ( + <> + setIsModalOpen( true ) }> + { __( 'Rename' ) } + + { isModalOpen && ( + { + setIsModalOpen( false ); + onClose(); + } } + overlayClassName="edit-site-list__rename-modal" + /> + ) } + + ); +} diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index 4b4d7bb9059d47..d02b82ff2560d0 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -101,6 +101,10 @@ background: $gray-900; padding: $grid-unit-40 $grid-unit-40 $grid-unit-20; z-index: z-index(".edit-site-patterns__header"); + + .edit-site-patterns__button { + color: $gray-600; + } } .edit-site-patterns__section { @@ -218,3 +222,7 @@ .edit-site-patterns__no-results { color: $gray-600; } + +.edit-site-patterns__delete-modal { + width: $modal-width-small; +} diff --git a/packages/patterns/src/components/rename-pattern-category-modal.js b/packages/patterns/src/components/rename-pattern-category-modal.js new file mode 100644 index 00000000000000..e4015f259246b2 --- /dev/null +++ b/packages/patterns/src/components/rename-pattern-category-modal.js @@ -0,0 +1,121 @@ +/** + * WordPress dependencies + */ +import { + Modal, + Button, + TextControl, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { useState } from '@wordpress/element'; +import { decodeEntities } from '@wordpress/html-entities'; +import { __ } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { CATEGORY_SLUG } from './category-selector'; + +export default function RenamePatternCategoryModal( { + category, + onClose, + onError, + onSuccess, + ...props +} ) { + const [ name, setName ] = useState( decodeEntities( category.name ) ); + const [ isSaving, setIsSaving ] = useState( false ); + + const { saveEntityRecord, invalidateResolution } = useDispatch( coreStore ); + + const { createErrorNotice, createSuccessNotice } = + useDispatch( noticesStore ); + + const onRename = async ( event ) => { + event.preventDefault(); + + if ( ! name || name === category.name || isSaving ) { + return; + } + + try { + setIsSaving( true ); + + // User pattern category properties may differ as they can be + // normalized for use alongside template part areas, core pattern + // categories etc. As a result we won't just destructure the passed + // category object. + const savedRecord = await saveEntityRecord( + 'taxonomy', + CATEGORY_SLUG, + { + id: category.id, + slug: category.slug, + name, + } + ); + + invalidateResolution( 'getUserPatternCategories' ); + onSuccess?.( savedRecord ); + onClose(); + + createSuccessNotice( __( 'Pattern category renamed.' ), { + type: 'snackbar', + id: 'pattern-category-update', + } ); + } catch ( error ) { + onError?.(); + createErrorNotice( error.message, { + type: 'snackbar', + id: 'pattern-category-update', + } ); + } finally { + setIsSaving( false ); + setName( '' ); + } + }; + + const onRequestClose = () => { + onClose(); + setName( '' ); + }; + + return ( + +
+ + + + + + + + +
+ ); +} diff --git a/packages/patterns/src/private-apis.js b/packages/patterns/src/private-apis.js index 2246b1a891e1ae..8daee42b21e9d4 100644 --- a/packages/patterns/src/private-apis.js +++ b/packages/patterns/src/private-apis.js @@ -6,6 +6,7 @@ import CreatePatternModal from './components/create-pattern-modal'; import DuplicatePatternModal from './components/duplicate-pattern-modal'; import RenamePatternModal from './components/rename-pattern-modal'; import PatternsMenuItems from './components'; +import RenamePatternCategoryModal from './components/rename-pattern-category-modal'; import { PATTERN_TYPES, PATTERN_DEFAULT_CATEGORY, @@ -20,6 +21,7 @@ lock( privateApis, { DuplicatePatternModal, RenamePatternModal, PatternsMenuItems, + RenamePatternCategoryModal, PATTERN_TYPES, PATTERN_DEFAULT_CATEGORY, PATTERN_USER_CATEGORY, From 24add081d2214b3893ef47dc4e491619266052e7 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Fri, 13 Oct 2023 13:39:13 +0400 Subject: [PATCH 08/32] Playwright Utils: Introduce the 'editor.saveDraft' helper (#55308) * Playwright Utils: Introduce the 'editor.saveDraft' helper * Update e2e tests where possible --- .../src/editor/index.ts | 3 ++ .../src/editor/save-draft.ts | 20 +++++++++++++ test/e2e/specs/editor/blocks/classic.spec.js | 7 +---- .../specs/editor/blocks/post-title.spec.js | 7 +---- test/e2e/specs/editor/plugins/nonce.spec.js | 5 +--- .../plugins/post-type-templates.spec.js | 21 ++------------ .../various/multi-block-selection.spec.js | 8 +----- .../e2e/specs/editor/various/new-post.spec.js | 5 +--- .../various/post-editor-template-mode.spec.js | 5 +--- test/e2e/specs/editor/various/undo.spec.js | 28 +++---------------- 10 files changed, 36 insertions(+), 73 deletions(-) create mode 100644 packages/e2e-test-utils-playwright/src/editor/save-draft.ts diff --git a/packages/e2e-test-utils-playwright/src/editor/index.ts b/packages/e2e-test-utils-playwright/src/editor/index.ts index 673149d4e69e0c..8c10ba370b1a92 100644 --- a/packages/e2e-test-utils-playwright/src/editor/index.ts +++ b/packages/e2e-test-utils-playwright/src/editor/index.ts @@ -19,6 +19,7 @@ import { insertBlock } from './insert-block'; import { openDocumentSettingsSidebar } from './open-document-settings-sidebar'; import { openPreviewPage } from './preview'; import { publishPost } from './publish-post'; +import { saveDraft } from './save-draft'; import { selectBlocks } from './select-blocks'; import { setContent } from './set-content'; import { showBlockToolbar } from './show-block-toolbar'; @@ -66,6 +67,8 @@ export class Editor { openPreviewPage: typeof openPreviewPage = openPreviewPage.bind( this ); /** @borrows publishPost as this.publishPost */ publishPost: typeof publishPost = publishPost.bind( this ); + /** @borrows saveDraft as this.saveDraft */ + saveDraft: typeof saveDraft = saveDraft.bind( this ); /** @borrows saveSiteEditorEntities as this.saveSiteEditorEntities */ saveSiteEditorEntities: typeof saveSiteEditorEntities = saveSiteEditorEntities.bind( this ); diff --git a/packages/e2e-test-utils-playwright/src/editor/save-draft.ts b/packages/e2e-test-utils-playwright/src/editor/save-draft.ts new file mode 100644 index 00000000000000..319cded7a8012e --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/editor/save-draft.ts @@ -0,0 +1,20 @@ +/** + * Internal dependencies + */ +import type { Editor } from './index'; + +/** + * Saves the post as a draft, resolving once the request is complete (once a notice + * is displayed). + */ +export async function saveDraft( this: Editor ) { + await this.page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { name: 'Save draft' } ) + .click(); + + await this.page + .getByRole( 'button', { name: 'Dismiss this notice' } ) + .filter( { hasText: 'Draft saved' } ) + .waitFor(); +} diff --git a/test/e2e/specs/editor/blocks/classic.spec.js b/test/e2e/specs/editor/blocks/classic.spec.js index 2dcc526851743e..95d39906b0d8bb 100644 --- a/test/e2e/specs/editor/blocks/classic.spec.js +++ b/test/e2e/specs/editor/blocks/classic.spec.js @@ -119,12 +119,7 @@ test.describe( 'Classic', () => { // Move focus away. await pageUtils.pressKeys( 'shift+Tab' ); - await page.click( 'role=button[name="Save draft"i]' ); - - await expect( - page.locator( 'role=button[name="Saved"i]' ) - ).toBeDisabled(); - + await editor.saveDraft(); await page.reload(); await page.unroute( '**' ); diff --git a/test/e2e/specs/editor/blocks/post-title.spec.js b/test/e2e/specs/editor/blocks/post-title.spec.js index 6959a1cd0bfa78..b5c536c0ff2460 100644 --- a/test/e2e/specs/editor/blocks/post-title.spec.js +++ b/test/e2e/specs/editor/blocks/post-title.spec.js @@ -19,12 +19,7 @@ test.describe( 'Post Title block', () => { .fill( 'Just tweaking the post title' ); // Save the post draft and reload. - await page.getByRole( 'button', { name: 'Save draft' } ).click(); - await expect( - page - .getByRole( 'button', { name: 'Dismiss this notice' } ) - .filter( { hasText: 'Draft saved' } ) - ).toBeVisible(); + await editor.saveDraft(); await page.reload(); const titleBlock = editor.canvas.getByRole( 'document', { diff --git a/test/e2e/specs/editor/plugins/nonce.spec.js b/test/e2e/specs/editor/plugins/nonce.spec.js index 0ffe0c5efb8139..1fbb6add87ff7b 100644 --- a/test/e2e/specs/editor/plugins/nonce.spec.js +++ b/test/e2e/specs/editor/plugins/nonce.spec.js @@ -75,11 +75,8 @@ test.describe( 'Nonce', () => { } } ); - await page.click( 'role=button[name=/Save draft/i]' ); // Saving draft should still succeed after retrying. - await expect( - page.locator( 'role=button[name="Dismiss this notice"i]' ) - ).toContainText( /Draft saved/i ); + await editor.saveDraft(); // We expect a 403 status only once. expect( saveDraftResponses ).toEqual( [ 403, 200 ] ); diff --git a/test/e2e/specs/editor/plugins/post-type-templates.spec.js b/test/e2e/specs/editor/plugins/post-type-templates.spec.js index 9b2abddb0dd0e2..a95f826c1f74c7 100644 --- a/test/e2e/specs/editor/plugins/post-type-templates.spec.js +++ b/test/e2e/specs/editor/plugins/post-type-templates.spec.js @@ -40,12 +40,7 @@ test.describe( 'Post type templates', () => { .focus(); await page.keyboard.press( 'ArrowDown' ); await page.keyboard.press( 'Backspace' ); - await page.click( 'role=button[name="Save draft"i]' ); - await expect( - page.locator( - 'role=button[name="Dismiss this notice"i] >> text=Draft saved' - ) - ).toBeVisible(); + await editor.saveDraft(); await page.reload(); const expectedContent = await page.evaluate( ( content ) => { @@ -72,12 +67,7 @@ test.describe( 'Post type templates', () => { await page.keyboard.press( 'ArrowDown' ); await pageUtils.pressKeys( 'primary+A' ); await page.keyboard.press( 'Backspace' ); - await page.click( 'role=button[name="Save draft"i]' ); - await expect( - page.locator( - 'role=button[name="Dismiss this notice"i] >> text=Draft saved' - ) - ).toBeVisible(); + await editor.saveDraft(); await page.reload(); await expect.poll( editor.getEditedPostContent ).toBe( '' ); @@ -133,12 +123,7 @@ test.describe( 'Post type templates', () => { .locator( 'role=document[name="Block: Image"i]' ) .focus(); await page.keyboard.press( 'Backspace' ); - await page.click( 'role=button[name="Save draft"i]' ); - await expect( - page.locator( - 'role=button[name="Dismiss this notice"i] >> text=Draft saved' - ) - ).toBeVisible(); + await editor.saveDraft(); await page.reload(); await expect.poll( editor.getEditedPostContent ).toBe( '' ); diff --git a/test/e2e/specs/editor/various/multi-block-selection.spec.js b/test/e2e/specs/editor/various/multi-block-selection.spec.js index 32fe45e6951bcc..4fb39783954fad 100644 --- a/test/e2e/specs/editor/various/multi-block-selection.spec.js +++ b/test/e2e/specs/editor/various/multi-block-selection.spec.js @@ -301,13 +301,7 @@ test.describe( 'Multi-block selection', () => { attributes: { content: 'test' }, } ); - await page.getByRole( 'button', { name: 'Save draft' } ).click(); - await expect( - page - .getByRole( 'button', { name: 'Dismiss this notice' } ) - .filter( { hasText: 'Draft saved' } ) - ).toBeVisible(); - + await editor.saveDraft(); await page.reload(); // To do: run with iframe. await editor.switchToLegacyCanvas(); diff --git a/test/e2e/specs/editor/various/new-post.spec.js b/test/e2e/specs/editor/various/new-post.spec.js index 9d6f4ef45d9db1..cc0243eb8e6312 100644 --- a/test/e2e/specs/editor/various/new-post.spec.js +++ b/test/e2e/specs/editor/various/new-post.spec.js @@ -78,10 +78,7 @@ test.describe( 'new editor state', () => { .locator( 'role=textbox[name="Add title"i]' ) .type( 'Here is the title' ); // Save the post as a draft. - await page.click( 'role=button[name="Save draft"i]' ); - await page.waitForSelector( - 'role=button[name="Dismiss this notice"] >> text=Draft saved' - ); + await editor.saveDraft(); // Reload the browser so a post is loaded with a title. await page.reload(); diff --git a/test/e2e/specs/editor/various/post-editor-template-mode.spec.js b/test/e2e/specs/editor/various/post-editor-template-mode.spec.js index 629a437a41665b..6f8bb5596bf2a5 100644 --- a/test/e2e/specs/editor/various/post-editor-template-mode.spec.js +++ b/test/e2e/specs/editor/various/post-editor-template-mode.spec.js @@ -169,10 +169,7 @@ class PostEditorTemplateMode { // Save the post // Saving shouldn't be necessary but unfortunately, // there's a template resolution bug forcing us to do so. - await this.page.click( 'role=button[name="Save draft"i]' ); - await this.page.waitForSelector( - 'role=button[name="Dismiss this notice"] >> text=Draft saved' - ); + await this.editor.saveDraft(); } async createNewTemplate( templateName ) { diff --git a/test/e2e/specs/editor/various/undo.spec.js b/test/e2e/specs/editor/various/undo.spec.js index 51683997aaf6f0..9df1c43b594746 100644 --- a/test/e2e/specs/editor/various/undo.spec.js +++ b/test/e2e/specs/editor/various/undo.spec.js @@ -168,12 +168,7 @@ test.describe( 'undo', () => { .locator( 'role=button[name="Add default block"i]' ) .click(); await page.keyboard.type( 'test' ); - await page.click( 'role=button[name="Save draft"i]' ); - await expect( - page.locator( - 'role=button[name="Dismiss this notice"i] >> text=Draft saved' - ) - ).toBeVisible(); + await editor.saveDraft(); await page.reload(); await editor.canvas.locator( '[data-type="core/paragraph"]' ).click(); await pageUtils.pressKeys( 'primary+a' ); @@ -344,12 +339,7 @@ test.describe( 'undo', () => { .locator( 'role=button[name="Add default block"i]' ) .click(); await page.keyboard.type( 'original' ); - await page.click( 'role=button[name="Save draft"i]' ); - await expect( - page.locator( - 'role=button[name="Dismiss this notice"i] >> text=Draft saved' - ) - ).toBeVisible(); + await editor.saveDraft(); await page.reload(); // Issue is demonstrated by forcing state merges (multiple inputs) on @@ -384,12 +374,7 @@ test.describe( 'undo', () => { .locator( 'role=button[name="Add default block"i]' ) .click(); await page.keyboard.type( '1' ); - await page.click( 'role=button[name="Save draft"i]' ); - await expect( - page.locator( - 'role=button[name="Dismiss this notice"i] >> text=Draft saved' - ) - ).toBeVisible(); + await editor.saveDraft(); await pageUtils.pressKeys( 'primary+z' ); await expect.poll( editor.getEditedPostContent ).toBe( '' ); @@ -420,12 +405,7 @@ test.describe( 'undo', () => { .click(); await page.keyboard.type( '1' ); - await page.click( 'role=button[name="Save draft"i]' ); - await expect( - page.locator( - 'role=button[name="Dismiss this notice"i] >> text=Draft saved' - ) - ).toBeVisible(); + await editor.saveDraft(); await page.reload(); // Expect undo button to be disabled. From 8c04020275590b790eb029ec4a065799115c4285 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Fri, 13 Oct 2023 20:06:44 +0900 Subject: [PATCH 09/32] Fix flickering when focusing on global style variations (#55267) --- .../src/components/global-styles/style.scss | 20 +++++++++++++++---- .../sidebar-navigation-screen/style.scss | 9 ++++----- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/style.scss b/packages/edit-site/src/components/global-styles/style.scss index 07f3c7a95c800d..3560ef139fa3fe 100644 --- a/packages/edit-site/src/components/global-styles/style.scss +++ b/packages/edit-site/src/components/global-styles/style.scss @@ -85,23 +85,35 @@ .edit-site-global-styles-variations_item { box-sizing: border-box; + // To round the outline in Windows 10 high contrast mode. + border-radius: $radius-block-ui; .edit-site-global-styles-variations_item-preview { padding: $border-width * 2; border-radius: $radius-block-ui; - border: $gray-200 $border-width solid; + box-shadow: 0 0 0 $border-width $gray-200; + // Shown in Windows 10 high contrast mode. + outline: 1px solid transparent; } &.is-active .edit-site-global-styles-variations_item-preview { - border: $gray-900 $border-width solid; + box-shadow: 0 0 0 $border-width $gray-900; + // Shown in Windows 10 high contrast mode. + outline-width: 3px; } &:hover .edit-site-global-styles-variations_item-preview { - border: var(--wp-admin-theme-color) $border-width solid; + box-shadow: 0 0 0 $border-width var(--wp-admin-theme-color); } &:focus .edit-site-global-styles-variations_item-preview { - border: var(--wp-admin-theme-color) var(--wp-admin-border-width-focus) solid; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + } + + &:focus-visible { + // Shown in Windows 10 high contrast mode. + outline: 3px solid transparent; + outline-offset: 0; } } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen/style.scss b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss index 4efdbad33a5430..f658083cc19f4e 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen/style.scss +++ b/packages/edit-site/src/components/sidebar-navigation-screen/style.scss @@ -90,17 +90,16 @@ } .edit-site-global-styles-variations_item-preview { - border: $gray-900 $border-width solid; + box-shadow: 0 0 0 $border-width $gray-900; } .edit-site-global-styles-variations_item.is-active .edit-site-global-styles-variations_item-preview { - border: $gray-100 $border-width solid; + box-shadow: 0 0 0 $border-width $gray-100; } .edit-site-global-styles-variations_item:hover .edit-site-global-styles-variations_item-preview { - border: var(--wp-admin-theme-color) $border-width solid; + box-shadow: 0 0 0 $border-width var(--wp-admin-theme-color); } - .edit-site-global-styles-variations_item:focus .edit-site-global-styles-variations_item-preview { - border: var(--wp-admin-theme-color) var(--wp-admin-border-width-focus) solid; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); } } From e21581b38aaaf2f22f44f41a598929a9e5b82ac6 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Fri, 13 Oct 2023 13:10:43 +0200 Subject: [PATCH 10/32] useBlockControlsFill: avoid unneeded store subscriptions (#55340) --- .../block-editor/src/components/block-controls/hook.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/block-controls/hook.js b/packages/block-editor/src/components/block-controls/hook.js index 8f4940a8597dae..18a38e245e58ab 100644 --- a/packages/block-editor/src/components/block-controls/hook.js +++ b/packages/block-editor/src/components/block-controls/hook.js @@ -17,17 +17,20 @@ export default function useBlockControlsFill( group, shareWithChildBlocks ) { const { clientId } = useBlockEditContext(); const isParentDisplayed = useSelect( ( select ) => { + if ( ! shareWithChildBlocks ) { + return false; + } + const { getBlockName, hasSelectedInnerBlock } = select( blockEditorStore ); const { hasBlockSupport } = select( blocksStore ); + return ( - shareWithChildBlocks && hasBlockSupport( getBlockName( clientId ), '__experimentalExposeControlsToChildren', false - ) && - hasSelectedInnerBlock( clientId ) + ) && hasSelectedInnerBlock( clientId ) ); }, [ shareWithChildBlocks, clientId ] From a54da2d7a71bcd5e3587a0aae85ab9d64e7aa195 Mon Sep 17 00:00:00 2001 From: JR Tashjian Date: Fri, 13 Oct 2023 09:44:14 -0400 Subject: [PATCH 11/32] Add block-specific commands as contextual suggestions [#53539] (#53974) * Add new command palette context for block selection * Take advantage of useCommand in edit-post like edit-site * Apply new context to blockActions commands * Remove duplicate useCommands call. * reorder block commands * Split contextual commands into a "QuickActions" hook. --- .../components/use-block-commands/index.js | 137 ++++++++++++------ .../edit-post/src/components/layout/index.js | 19 +++ packages/edit-post/src/editor.js | 5 - .../edit-site/src/components/layout/index.js | 16 +- 4 files changed, 122 insertions(+), 55 deletions(-) diff --git a/packages/block-editor/src/components/use-block-commands/index.js b/packages/block-editor/src/components/use-block-commands/index.js index 6a1fc28a8cf4b4..9dfc4a6df9ff9f 100644 --- a/packages/block-editor/src/components/use-block-commands/index.js +++ b/packages/block-editor/src/components/use-block-commands/index.js @@ -112,6 +112,60 @@ export const useTransformCommands = () => { }; const useActionsCommands = () => { + const { clientIds } = useSelect( ( select ) => { + const { getSelectedBlockClientIds } = select( blockEditorStore ); + const selectedBlockClientIds = getSelectedBlockClientIds(); + + return { + clientIds: selectedBlockClientIds, + }; + }, [] ); + + const { getBlockRootClientId, canMoveBlocks, getBlockCount } = + useSelect( blockEditorStore ); + + const { setBlockMovingClientId, setNavigationMode, selectBlock } = + useDispatch( blockEditorStore ); + + if ( ! clientIds || clientIds.length < 1 ) { + return { isLoading: false, commands: [] }; + } + + const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); + + const canMove = + canMoveBlocks( clientIds, rootClientId ) && + getBlockCount( rootClientId ) !== 1; + + const commands = []; + + if ( canMove ) { + commands.push( { + name: 'move-to', + label: __( 'Move to' ), + callback: () => { + setNavigationMode( true ); + selectBlock( clientIds[ 0 ] ); + setBlockMovingClientId( clientIds[ 0 ] ); + }, + icon: move, + } ); + } + + return { + isLoading: false, + commands: commands.map( ( command ) => ( { + ...command, + name: 'core/block-editor/action-' + command.name, + callback: ( { close } ) => { + command.callback(); + close(); + }, + } ) ), + }; +}; + +const useQuickActionsCommands = () => { const { clientIds, isUngroupable, isGroupable } = useSelect( ( select ) => { const { getSelectedBlockClientIds, @@ -130,9 +184,7 @@ const useActionsCommands = () => { canInsertBlockType, getBlockRootClientId, getBlocksByClientId, - canMoveBlocks, canRemoveBlocks, - getBlockCount, } = useSelect( blockEditorStore ); const { getDefaultBlockName, getGroupingBlockName } = useSelect( blocksStore ); @@ -145,9 +197,6 @@ const useActionsCommands = () => { duplicateBlocks, insertAfterBlock, insertBeforeBlock, - setBlockMovingClientId, - setNavigationMode, - selectBlock, } = useDispatch( blockEditorStore ); const onGroup = () => { @@ -196,65 +245,54 @@ const useActionsCommands = () => { ); } ); const canRemove = canRemoveBlocks( clientIds, rootClientId ); - const canMove = - canMoveBlocks( clientIds, rootClientId ) && - getBlockCount( rootClientId ) !== 1; const commands = []; + + if ( canDuplicate ) { + commands.push( { + name: 'duplicate', + label: __( 'Duplicate' ), + callback: () => duplicateBlocks( clientIds, true ), + icon: copy, + } ); + } + if ( canInsertDefaultBlock ) { commands.push( { - name: 'add-after', - label: __( 'Add after' ), + name: 'add-before', + label: __( 'Add before' ), callback: () => { const clientId = Array.isArray( clientIds ) - ? clientIds[ clientIds.length - 1 ] + ? clientIds[ 0 ] : clientId; - insertAfterBlock( clientId ); + insertBeforeBlock( clientId ); }, icon: add, }, { - name: 'add-before', - label: __( 'Add before' ), + name: 'add-after', + label: __( 'Add after' ), callback: () => { const clientId = Array.isArray( clientIds ) - ? clientIds[ 0 ] + ? clientIds[ clientIds.length - 1 ] : clientId; - insertBeforeBlock( clientId ); + insertAfterBlock( clientId ); }, icon: add, } ); } - if ( canRemove ) { - commands.push( { - name: 'remove', - label: __( 'Delete' ), - callback: () => removeBlocks( clientIds, true ), - icon: remove, - } ); - } - if ( canDuplicate ) { - commands.push( { - name: 'duplicate', - label: __( 'Duplicate' ), - callback: () => duplicateBlocks( clientIds, true ), - icon: copy, - } ); - } - if ( canMove ) { + + if ( isGroupable ) { commands.push( { - name: 'move-to', - label: __( 'Move to' ), - callback: () => { - setNavigationMode( true ); - selectBlock( clientIds[ 0 ] ); - setBlockMovingClientId( clientIds[ 0 ] ); - }, - icon: move, + name: 'Group', + label: __( 'Group' ), + callback: onGroup, + icon: group, } ); } + if ( isUngroupable ) { commands.push( { name: 'ungroup', @@ -263,14 +301,16 @@ const useActionsCommands = () => { icon: ungroup, } ); } - if ( isGroupable ) { + + if ( canRemove ) { commands.push( { - name: 'Group', - label: __( 'Group' ), - callback: onGroup, - icon: group, + name: 'remove', + label: __( 'Delete' ), + callback: () => removeBlocks( clientIds, true ), + icon: remove, } ); } + return { isLoading: false, commands: commands.map( ( command ) => ( { @@ -293,4 +333,9 @@ export const useBlockCommands = () => { name: 'core/block-editor/blockActions', hook: useActionsCommands, } ); + useCommandLoader( { + name: 'core/block-editor/blockQuickActions', + hook: useQuickActionsCommands, + context: 'block-selection-edit', + } ); }; diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 5265cac569239f..9c854dc33636db 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -22,6 +22,7 @@ import { useBlockCommands, BlockBreadcrumb, privateApis as blockEditorPrivateApis, + store as blockEditorStore, } from '@wordpress/block-editor'; import { Button, ScrollLock } from '@wordpress/components'; import { useViewportMatch } from '@wordpress/compose'; @@ -37,6 +38,12 @@ import { useState, useEffect, useCallback, useMemo } from '@wordpress/element'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as noticesStore } from '@wordpress/notices'; +import { privateApis as commandsPrivateApis } from '@wordpress/commands'; +import { privateApis as coreCommandsPrivateApis } from '@wordpress/core-commands'; + +const { useCommands } = unlock( coreCommandsPrivateApis ); +const { useCommandContext } = unlock( commandsPrivateApis ); + /** * Internal dependencies */ @@ -56,6 +63,7 @@ import ActionsPanel from './actions-panel'; import StartPageOptions from '../start-page-options'; import { store as editPostStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import useCommonCommands from '../../hooks/commands/use-common-commands'; const { getLayoutStyles } = unlock( blockEditorPrivateApis ); @@ -124,7 +132,10 @@ function useEditorStyles() { } function Layout() { + useCommands(); + useCommonCommands(); useBlockCommands(); + const isMobileViewport = useViewportMatch( 'medium', '<' ); const isHugeViewport = useViewportMatch( 'huge', '>=' ); const isLargeViewport = useViewportMatch( 'large' ); @@ -184,9 +195,17 @@ function Layout() { ), // translators: Default label for the Document in the Block Breadcrumb. documentLabel: postTypeLabel || _x( 'Document', 'noun' ), + hasBlockSelected: + select( blockEditorStore ).getBlockSelectionStart(), }; }, [] ); + // Set the right context for the command palette + const commandContext = hasBlockSelected + ? 'block-selection-edit' + : 'post-editor-edit'; + useCommandContext( commandContext ); + const styles = useEditorStyles(); const openSidebarPanel = () => diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index 6668001b76773d..d1ef111e7dc4c3 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -14,7 +14,6 @@ import { SlotFillProvider } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; import { CommandMenu } from '@wordpress/commands'; -import { privateApis as coreCommandsPrivateApis } from '@wordpress/core-commands'; /** * Internal dependencies @@ -23,14 +22,10 @@ import Layout from './components/layout'; import EditorInitialization from './components/editor-initialization'; import { store as editPostStore } from './store'; import { unlock } from './lock-unlock'; -import useCommonCommands from './hooks/commands/use-common-commands'; const { ExperimentalEditorProvider } = unlock( editorPrivateApis ); -const { useCommands } = unlock( coreCommandsPrivateApis ); function Editor( { postId, postType, settings, initialEdits, ...props } ) { - useCommands(); - useCommonCommands(); const { hasFixedToolbar, focusMode, diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index c03ad2cd05ecd8..5fb682353da722 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -29,6 +29,7 @@ import { store as preferencesStore } from '@wordpress/preferences'; import { privateApis as blockEditorPrivateApis, useBlockCommands, + store as blockEditorStore, } from '@wordpress/block-editor'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { privateApis as coreCommandsPrivateApis } from '@wordpress/core-commands'; @@ -80,6 +81,7 @@ export default function Layout() { const { isDistractionFree, hasFixedToolbar, + hasBlockSelected, canvasMode, previousShortcut, nextShortcut, @@ -104,6 +106,8 @@ export default function Layout() { 'core/edit-site', 'distractionFree' ), + hasBlockSelected: + select( blockEditorStore ).getBlockSelectionStart(), }; }, [] ); const isEditing = canvasMode === 'edit'; @@ -152,10 +156,14 @@ export default function Layout() { } // Sets the right context for the command palette - const commandContext = - canvasMode === 'edit' && isEditorPage - ? 'site-editor-edit' - : 'site-editor'; + let commandContext = 'site-editor'; + + if ( canvasMode === 'edit' && isEditorPage ) { + commandContext = 'site-editor-edit'; + } + if ( hasBlockSelected ) { + commandContext = 'block-selection-edit'; + } useCommandContext( commandContext ); const [ backgroundColor ] = useGlobalStyle( 'color.background' ); From c47905f777ca9f305d2f0517bca0a2f4ebb65dac Mon Sep 17 00:00:00 2001 From: Miguel Fonseca <150562+mcsf@users.noreply.github.com> Date: Fri, 13 Oct 2023 15:27:46 +0100 Subject: [PATCH 12/32] PagePages: Fix unintended object mutation inside component (#55314) - Don't reuse references when updating postStatuses - Don't accidentally mutate EMPTY_OBJECT --- .../edit-site/src/components/page-pages/index.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 728f7c2c716fe6..4bdaca8cde2f6d 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -39,13 +39,15 @@ export default function PagePages() { } ); // Request post statuses to get the proper labels. const { records: statuses } = useEntityRecords( 'root', 'status' ); - const postStatuses = - statuses === null - ? EMPTY_OBJECT - : statuses.reduce( ( acc, status ) => { - acc[ status.slug ] = status.name; - return acc; - }, EMPTY_OBJECT ); + const postStatuses = useMemo( + () => + statuses === null + ? EMPTY_OBJECT + : Object.fromEntries( + statuses.map( ( { slug, name } ) => [ slug, name ] ) + ), + [ statuses ] + ); const queryArgs = useMemo( () => ( { From bbbb0a1e457a3230847f7b8006a27ef01d23616d Mon Sep 17 00:00:00 2001 From: Matias Benedetto Date: Fri, 13 Oct 2023 11:31:48 -0300 Subject: [PATCH 13/32] WP_Theme_JSON_Gutenberg Unit tests: fix phpunit warnings about set_spacing_sizes (#55313) * fix warning on unit tests * php format --- lib/class-wp-theme-json-gutenberg.php | 2 +- phpunit/class-wp-theme-json-test.php | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 8c2857fa89d0ca..b2b3fa997b8cdf 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -3389,7 +3389,7 @@ public function set_spacing_sizes() { || ! is_numeric( $spacing_scale['mediumStep'] ) || ( '+' !== $spacing_scale['operator'] && '*' !== $spacing_scale['operator'] ) ) { if ( ! empty( $spacing_scale ) ) { - trigger_error( __( 'Some of the theme.json settings.spacing.spacingScale values are invalid', 'gutenberg' ), E_USER_NOTICE ); + _doing_it_wrong( __METHOD__, __( 'Some of the theme.json settings.spacing.spacingScale values are invalid', 'gutenberg' ), '6.1.0' ); } return null; } diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 0e212983d9080f..35747a290ca74d 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -1282,8 +1282,7 @@ public function data_generate_spacing_scale_fixtures() { * @dataProvider data_set_spacing_sizes_when_invalid */ public function test_set_spacing_sizes_when_invalid( $spacing_scale, $expected_output ) { - $this->expectNotice(); - $this->expectNoticeMessage( 'Some of the theme.json settings.spacing.spacingScale values are invalid' ); + $this->setExpectedIncorrectUsage( 'WP_Theme_JSON_Gutenberg::set_spacing_sizes' ); $theme_json = new WP_Theme_JSON_Gutenberg( array( From b2ac3a5b445006bf87f0836fee4a5c018c9b96f3 Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:42:33 -0400 Subject: [PATCH 14/32] Components: Expose `Tabs` as private API (#55327) Co-authored-by: Marin Atanasov <8436925+tyxla@users.noreply.github.com> --- packages/components/CHANGELOG.md | 1 + packages/components/src/private-apis.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 4f17a60a696b0d..00a7558d389bab 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -9,6 +9,7 @@ - Allow using CSS level 4 viewport-relative units ([54415](https://github.com/WordPress/gutenberg/pull/54415)) - `ToolsPanel`: do not apply the `className` to prop to `ToolsPanelItem` components when rendered as placeholders ([#55207](https://github.com/WordPress/gutenberg/pull/55207)). - `GradientPicker`: remove overflow styles and padding from `ColorPicker` popovers ([#55265](https://github.com/WordPress/gutenberg/pull/55265)). +- `Tabs`: Expose via private APIs ([#55327](https://github.com/WordPress/gutenberg/pull/55327)). - `ColorPalette`/`ToggleGroupControl/ToggleGroupControlOptionBase`: add `type="button"` attribute to native `