From e0b54b3596b35e7df15bd674f7c9ead918f05b07 Mon Sep 17 00:00:00 2001 From: Xinyu Liu Date: Sun, 11 Aug 2024 18:35:46 +0800 Subject: [PATCH 01/48] Fix example of useBlockProps hook (#64363) * Fix example of useBlockProps hook * chore: tweaks * chore: updates Co-authored-by: meteorlxy Co-authored-by: t-hamano --- packages/block-editor/README.md | 23 ++++++++----------- .../block-list/use-block-props/index.js | 4 ++-- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 776b217ba54f6e..c798015804b3e5 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -920,20 +920,15 @@ _Usage_ import { useBlockProps } from '@wordpress/block-editor'; export default function Edit() { - - const blockProps = useBlockProps( - className: 'my-custom-class', - style: { - color: '#222222', - backgroundColor: '#eeeeee' - } - ) - - return ( -
- -
- ) + const blockProps = useBlockProps( { + className: 'my-custom-class', + style: { + color: '#222222', + backgroundColor: '#eeeeee', + }, + } ); + + return
; } ``` diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 6c44aa5c5d9705..15fb83139237cc 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -49,13 +49,13 @@ import { canBindBlock } from '../../../hooks/use-bindings-attributes'; * * export default function Edit() { * - * const blockProps = useBlockProps( + * const blockProps = useBlockProps( { * className: 'my-custom-class', * style: { * color: '#222222', * backgroundColor: '#eeeeee' * } - * ) + * } ) * * return ( *
From 24f9ca62b0b83d3832233d276fea564b1714a172 Mon Sep 17 00:00:00 2001 From: Nick Diego Date: Sun, 11 Aug 2024 23:41:56 -0500 Subject: [PATCH 02/48] Docs: Fix typos in the Block Filters documentation (#64426) Co-authored-by: ndiego Co-authored-by: fabiankaegy --- docs/reference-guides/filters/block-filters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index e7a31c1e3bbc83..c70bb356a445a8 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -144,7 +144,7 @@ Filters the font-end content of any block. This filter has no impact on the beha The callback function for this filter receives three parameters: - `$block_content` (`string`): The block content. -- `block` (`array`): The full block, including name and attributes. +- `$block` (`array`): The full block, including name and attributes. - `$instance` (`WP_Block`): The block instance. In the following example, the class `example-class` is added to all Paragraph blocks on the front end. Here the [HTML API](https://make.wordpress.org/core/2023/03/07/introducing-the-html-api-in-wordpress-6-2/) is used to easily add the class instead of relying on regex. @@ -177,7 +177,7 @@ Filters the font-end content of the defined block. This is just a simpler form o The callback function for this filter receives three parameters: - `$block_content` (`string`): The block content. -- `block` (`array`): The full block, including name and attributes. +- `$block` (`array`): The full block, including name and attributes. - `$instance` (`WP_Block`): The block instance. In the following example, the class `example-class` is added to all Paragraph blocks on the front end. Notice that compared to the `render_block` example above, you no longer need to check the block type before modifying the content. Again, the [HTML API](https://make.wordpress.org/core/2023/03/07/introducing-the-html-api-in-wordpress-6-2/) is used instead of regex. From 92ea19f8cb946d051855aaa6791b0018a5d15fcb Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Mon, 12 Aug 2024 10:18:00 +0200 Subject: [PATCH 03/48] Remove typed function from API reference (#64429) Co-authored-by: luisherranz Co-authored-by: fabiankaegy Co-authored-by: atachibana --- docs/reference-guides/interactivity-api/api-reference.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference-guides/interactivity-api/api-reference.md b/docs/reference-guides/interactivity-api/api-reference.md index 29ec67cc17507f..46bd20bece0bda 100644 --- a/docs/reference-guides/interactivity-api/api-reference.md +++ b/docs/reference-guides/interactivity-api/api-reference.md @@ -776,7 +776,7 @@ Actions are just regular JavaScript functions. Usually triggered by the `data-wp ```ts const { state, actions } = store("myPlugin", { actions: { - selectItem: (id?: number) => { + selectItem: ( id ) => { const context = getContext(); // `id` is optional here, so this action can be used in a directive. state.selected = id || context.id; From 21cfde3f3ccba896a7026312ae646c74626d9621 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 12 Aug 2024 12:37:18 +0400 Subject: [PATCH 04/48] Site Editor: Fix Template Parts post type preload path (#64401) * Site Editor: Fix Template Parts post type preload path * Add backport changelog file Co-authored-by: Mamaduka Co-authored-by: tyxla --- backport-changelog/6.7/7179.md | 3 +++ lib/compat/wordpress-6.7/rest-api.php | 31 +++++++++++++++++++++++++++ lib/load.php | 3 +++ 3 files changed, 37 insertions(+) create mode 100644 backport-changelog/6.7/7179.md create mode 100644 lib/compat/wordpress-6.7/rest-api.php diff --git a/backport-changelog/6.7/7179.md b/backport-changelog/6.7/7179.md new file mode 100644 index 00000000000000..f359b6610a94e6 --- /dev/null +++ b/backport-changelog/6.7/7179.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7179 + +* https://github.com/WordPress/gutenberg/pull/64401 diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php new file mode 100644 index 00000000000000..713d31c4632c74 --- /dev/null +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -0,0 +1,31 @@ +name ) { + // Fixes post type name. It should be `type/wp_template_part`. + $parts_key = array_search( '/wp/v2/types/wp_template-part?context=edit', $paths, true ); + if ( false !== $parts_key ) { + $paths[ $parts_key ] = '/wp/v2/types/wp_template_part?context=edit'; + } + } + + return $paths; +} +add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_6_7', 10, 2 ); diff --git a/lib/load.php b/lib/load.php index 5a299f3b696968..4bb0ab88024a4a 100644 --- a/lib/load.php +++ b/lib/load.php @@ -40,6 +40,9 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.6/class-gutenberg-rest-templates-controller-6-6.php'; require __DIR__ . '/compat/wordpress-6.6/rest-api.php'; + // WordPress 6.7 compat. + require __DIR__ . '/compat/wordpress-6.7/rest-api.php'; + // Plugin specific code. require_once __DIR__ . '/class-wp-rest-global-styles-controller-gutenberg.php'; require_once __DIR__ . '/class-wp-rest-edit-site-export-controller-gutenberg.php'; From acc9a38a331cd75b559e11e268ddde2eddfc2b89 Mon Sep 17 00:00:00 2001 From: James Koster Date: Mon, 12 Aug 2024 09:52:36 +0100 Subject: [PATCH 05/48] Add elevation scale (#64108) Co-authored-by: jameskoster Co-authored-by: DaniGuardiola Co-authored-by: ciampo Co-authored-by: tyxla Co-authored-by: jasmussen Co-authored-by: richtabor Co-authored-by: hanneslsm --- packages/base-styles/_variables.scss | 26 +++++++++++++------ .../components/src/utils/config-values.js | 4 +++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/packages/base-styles/_variables.scss b/packages/base-styles/_variables.scss index 97eb513cf38aeb..0d3e139a7dd555 100644 --- a/packages/base-styles/_variables.scss +++ b/packages/base-styles/_variables.scss @@ -50,6 +50,22 @@ $radius-large: 8px; // Applied to containers with larger padding. $radius-full: 9999px; // For lozenges. $radius-round: 50%; // For circles and ovals. +/** + * Elevation scale. + */ + +// For sections and containers that group related content and controls, which may overlap other content. Example: Preview Frame. +$elevation-x-small: 0 0.7px 1px rgba($black, 0.1), 0 1.2px 1.7px -0.2px rgba($black, 0.1), 0 2.3px 3.3px -0.5px rgba($black, 0.1); + +// For components that provide contextual feedback without being intrusive. Generally non-interruptive. Example: Tooltips, Snackbar. +$elevation-small: 0 0.7px 1px 0 rgba(0, 0, 0, 0.12), 0 2.2px 3.7px -0.2px rgba(0, 0, 0, 0.12), 0 5.3px 7.3px -0.5px rgba(0, 0, 0, 0.12); + +// For components that offer additional actions. Example: Menus, Command Palette +$elevation-medium: 0 0.7px 1px 0 rgba(0, 0, 0, 0.14), 0 4.2px 5.7px -0.2px rgba(0, 0, 0, 0.14), 0 7.3px 9.3px -0.5px rgba(0, 0, 0, 0.14); + +// For components that confirm decisions or handle necessary interruptions. Example: Modals. +$elevation-large: 0 0.7px 1px rgba($black, 0.15), 0 2.7px 3.8px -0.2px rgba($black, 0.15), 0 5.5px 7.8px -0.3px rgba($black, 0.15), 0.1px 11.5px 16.4px -0.5px rgba($black, 0.15); + /** * Dimensions. */ @@ -74,14 +90,6 @@ $modal-width-large: 840px; $spinner-size: 16px; $canvas-padding: $grid-unit-20; - -/** - * Shadows. - */ - -$shadow-popover: 0 0.7px 1px rgba($black, 0.1), 0 1.2px 1.7px -0.2px rgba($black, 0.1), 0 2.3px 3.3px -0.5px rgba($black, 0.1); -$shadow-modal: 0 0.7px 1px rgba($black, 0.15), 0 2.7px 3.8px -0.2px rgba($black, 0.15), 0 5.5px 7.8px -0.3px rgba($black, 0.15), 0.1px 11.5px 16.4px -0.5px rgba($black, 0.15); - /** * Editor widths. */ @@ -107,6 +115,8 @@ $radio-input-size-sm: 24px; // Width & height for small viewports. // Deprecated, please avoid using these. $block-padding: 14px; // Used to define space between block footprint and surrouding borders. $radius-block-ui: $radius-small; +$shadow-popover: $elevation-x-small; +$shadow-modal: $elevation-large; /** diff --git a/packages/components/src/utils/config-values.js b/packages/components/src/utils/config-values.js index ba92813bdbfb0f..65632170c9e3d7 100644 --- a/packages/components/src/utils/config-values.js +++ b/packages/components/src/utils/config-values.js @@ -74,6 +74,10 @@ export default Object.assign( {}, CONTROL_PROPS, TOGGLE_GROUP_CONTROL_PROPS, { cardPaddingMedium: `${ space( 4 ) } ${ space( 6 ) }`, cardPaddingLarge: `${ space( 6 ) } ${ space( 8 ) }`, popoverShadow: `0 0.7px 1px rgba(0, 0, 0, 0.1), 0 1.2px 1.7px -0.2px rgba(0, 0, 0, 0.1), 0 2.3px 3.3px -0.5px rgba(0, 0, 0, 0.1)`, + elevationXSmall: `0 0.7px 1px rgba(0, 0, 0, 0.1), 0 1.2px 1.7px -0.2px rgba(0, 0, 0, 0.1), 0 2.3px 3.3px -0.5px rgba(0, 0, 0, 0.1)`, + elevationSmall: `0 0.7px 1px 0 rgba(0, 0, 0, 0.12), 0 2.2px 3.7px -0.2px rgba(0, 0, 0, 0.12), 0 5.3px 7.3px -0.5px rgba(0, 0, 0, 0.12)`, + elevationMedium: `0 0.7px 1px 0 rgba(0, 0, 0, 0.14), 0 4.2px 5.7px -0.2px rgba(0, 0, 0, 0.14), 0 7.3px 9.3px -0.5px rgba(0, 0, 0, 0.14)`, + elevationLarge: `0 0.7px 1px rgba(0, 0, 0, 0.15), 0 2.7px 3.8px -0.2px rgba(0, 0, 0, 0.15), 0 5.5px 7.8px -0.3px rgba(0, 0, 0, 0.15), 0.1px 11.5px 16.4px -0.5px rgba(0, 0, 0, 0.15)`, surfaceBackgroundColor: COLORS.white, surfaceBackgroundSubtleColor: '#F3F3F3', surfaceBackgroundTintColor: '#F5F5F5', From 4a8b166d623c442087a36eb41c35cc6dcd4e14ca Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 12 Aug 2024 11:47:55 +0200 Subject: [PATCH 06/48] DataViews: Refactor the edit function to be based on discrete controls (#64404) Co-authored-by: youknowriad Co-authored-by: oandregal Co-authored-by: ntsekouras --- .../src/dataform-controls/datetime.tsx | 37 ++++++++++ .../dataform-controls/index.tsx | 25 +++++-- .../src/dataform-controls/integer.tsx | 38 ++++++++++ .../dataform-controls/radio.tsx | 9 ++- .../src/dataform-controls/select.tsx | 52 ++++++++++++++ .../dataviews/src/dataform-controls/text.tsx | 40 +++++++++++ .../dataviews/src/field-types/datetime.tsx | 67 +---------------- .../dataviews/src/field-types/integer.tsx | 72 +------------------ packages/dataviews/src/field-types/text.tsx | 71 +----------------- packages/dataviews/src/normalize-fields.ts | 2 +- packages/dataviews/src/types.ts | 6 +- 11 files changed, 201 insertions(+), 218 deletions(-) create mode 100644 packages/dataviews/src/dataform-controls/datetime.tsx rename packages/dataviews/src/{components => }/dataform-controls/index.tsx (60%) create mode 100644 packages/dataviews/src/dataform-controls/integer.tsx rename packages/dataviews/src/{components => }/dataform-controls/radio.tsx (80%) create mode 100644 packages/dataviews/src/dataform-controls/select.tsx create mode 100644 packages/dataviews/src/dataform-controls/text.tsx diff --git a/packages/dataviews/src/dataform-controls/datetime.tsx b/packages/dataviews/src/dataform-controls/datetime.tsx new file mode 100644 index 00000000000000..3ba22dc0c1b089 --- /dev/null +++ b/packages/dataviews/src/dataform-controls/datetime.tsx @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { BaseControl, TimePicker } from '@wordpress/components'; +import { useCallback } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { DataFormControlProps } from '../types'; + +export default function DateTime< Item >( { + data, + field, + onChange, +}: DataFormControlProps< Item > ) { + const { id, label } = field; + const value = field.getValue( { item: data } ); + + const onChangeControl = useCallback( + ( newValue: string | null ) => onChange( { [ id ]: newValue } ), + [ id, onChange ] + ); + + return ( +
+ + { label } + + +
+ ); +} diff --git a/packages/dataviews/src/components/dataform-controls/index.tsx b/packages/dataviews/src/dataform-controls/index.tsx similarity index 60% rename from packages/dataviews/src/components/dataform-controls/index.tsx rename to packages/dataviews/src/dataform-controls/index.tsx index dd913269cd09ea..297e73c28f837c 100644 --- a/packages/dataviews/src/components/dataform-controls/index.tsx +++ b/packages/dataviews/src/dataform-controls/index.tsx @@ -10,15 +10,23 @@ import type { DataFormControlProps, Field, FieldTypeDefinition, -} from '../../types'; +} from '../types'; +import datetime from './datetime'; +import integer from './integer'; import radio from './radio'; +import select from './select'; +import text from './text'; interface FormControls { [ key: string ]: ComponentType< DataFormControlProps< any > >; } const FORM_CONTROLS: FormControls = { + datetime, + integer, radio, + select, + text, }; export function getControl< Item >( @@ -29,12 +37,19 @@ export function getControl< Item >( return field.Edit; } - let control; if ( typeof field.Edit === 'string' ) { - control = getControlByType( field.Edit ); + return getControlByType( field.Edit ); } - return control || fieldTypeDefinition.Edit; + if ( field.elements ) { + return getControlByType( 'select' ); + } + + if ( typeof fieldTypeDefinition.Edit === 'string' ) { + return getControlByType( fieldTypeDefinition.Edit ); + } + + return fieldTypeDefinition.Edit; } export function getControlByType( type: string ) { @@ -42,5 +57,5 @@ export function getControlByType( type: string ) { return FORM_CONTROLS[ type ]; } - return null; + throw 'Control ' + type + ' not found'; } diff --git a/packages/dataviews/src/dataform-controls/integer.tsx b/packages/dataviews/src/dataform-controls/integer.tsx new file mode 100644 index 00000000000000..f70a90ffe15239 --- /dev/null +++ b/packages/dataviews/src/dataform-controls/integer.tsx @@ -0,0 +1,38 @@ +/** + * WordPress dependencies + */ +import { __experimentalNumberControl as NumberControl } from '@wordpress/components'; +import { useCallback } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { DataFormControlProps } from '../types'; + +export default function Integer< Item >( { + data, + field, + onChange, + hideLabelFromVision, +}: DataFormControlProps< Item > ) { + const { id, label, description } = field; + const value = field.getValue( { item: data } ) ?? ''; + const onChangeControl = useCallback( + ( newValue: string | undefined ) => + onChange( { + [ id ]: Number( newValue ), + } ), + [ id, onChange ] + ); + + return ( + + ); +} diff --git a/packages/dataviews/src/components/dataform-controls/radio.tsx b/packages/dataviews/src/dataform-controls/radio.tsx similarity index 80% rename from packages/dataviews/src/components/dataform-controls/radio.tsx rename to packages/dataviews/src/dataform-controls/radio.tsx index d264aa6c24b7fb..3d616404e0c053 100644 --- a/packages/dataviews/src/components/dataform-controls/radio.tsx +++ b/packages/dataviews/src/dataform-controls/radio.tsx @@ -7,9 +7,9 @@ import { useCallback } from '@wordpress/element'; /** * Internal dependencies */ -import type { DataFormControlProps } from '../../types'; +import type { DataFormControlProps } from '../types'; -export default function Edit< Item >( { +export default function Radio< Item >( { data, field, onChange, @@ -20,10 +20,9 @@ export default function Edit< Item >( { const onChangeControl = useCallback( ( newValue: string ) => - onChange( ( prevItem: Item ) => ( { - ...prevItem, + onChange( { [ id ]: newValue, - } ) ), + } ), [ id, onChange ] ); diff --git a/packages/dataviews/src/dataform-controls/select.tsx b/packages/dataviews/src/dataform-controls/select.tsx new file mode 100644 index 00000000000000..2b3bd9373fc155 --- /dev/null +++ b/packages/dataviews/src/dataform-controls/select.tsx @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +import { SelectControl } from '@wordpress/components'; +import { useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import type { DataFormControlProps } from '../types'; + +export default function Select< Item >( { + data, + field, + onChange, + hideLabelFromVision, +}: DataFormControlProps< Item > ) { + const { id, label } = field; + const value = field.getValue( { item: data } ) ?? ''; + const onChangeControl = useCallback( + ( newValue: any ) => + onChange( { + [ id ]: newValue, + } ), + [ id, onChange ] + ); + + const elements = [ + /* + * Value can be undefined when: + * + * - the field is not required + * - in bulk editing + * + */ + { label: __( 'Select item' ), value: '' }, + ...( field?.elements ?? [] ), + ]; + + return ( + + ); +} diff --git a/packages/dataviews/src/dataform-controls/text.tsx b/packages/dataviews/src/dataform-controls/text.tsx new file mode 100644 index 00000000000000..7ac095f4abede7 --- /dev/null +++ b/packages/dataviews/src/dataform-controls/text.tsx @@ -0,0 +1,40 @@ +/** + * WordPress dependencies + */ +import { TextControl } from '@wordpress/components'; +import { useCallback } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import type { DataFormControlProps } from '../types'; + +export default function Text< Item >( { + data, + field, + onChange, + hideLabelFromVision, +}: DataFormControlProps< Item > ) { + const { id, label, placeholder } = field; + const value = field.getValue( { item: data } ); + + const onChangeControl = useCallback( + ( newValue: string ) => + onChange( { + [ id ]: newValue, + } ), + [ id, onChange ] + ); + + return ( + + ); +} diff --git a/packages/dataviews/src/field-types/datetime.tsx b/packages/dataviews/src/field-types/datetime.tsx index c6b69048efe13c..aa97fc86c318c2 100644 --- a/packages/dataviews/src/field-types/datetime.tsx +++ b/packages/dataviews/src/field-types/datetime.tsx @@ -1,18 +1,7 @@ -/** - * WordPress dependencies - */ -import { BaseControl, TimePicker, SelectControl } from '@wordpress/components'; -import { useCallback } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ -import type { - SortDirection, - ValidationContext, - DataFormControlProps, -} from '../types'; +import type { SortDirection, ValidationContext } from '../types'; function sort( a: any, b: any, direction: SortDirection ) { const timeA = new Date( a ).getTime(); @@ -32,60 +21,8 @@ function isValid( value: any, context?: ValidationContext ) { return true; } -function Edit< Item >( { - data, - field, - onChange, -}: DataFormControlProps< Item > ) { - const { id, label } = field; - const value = field.getValue( { item: data } ); - - const onChangeControl = useCallback( - ( newValue: string | null ) => onChange( { [ id ]: newValue } ), - [ id, onChange ] - ); - - if ( field.elements ) { - const elements = [ - /* - * Value can be undefined when: - * - * - the field is not required - * - in bulk editing - * - */ - { label: __( 'Select item' ), value: '' }, - ...field.elements, - ]; - - return ( - - ); - } - - return ( -
- - { label } - - -
- ); -} - export default { sort, isValid, - Edit, + Edit: 'datetime', }; diff --git a/packages/dataviews/src/field-types/integer.tsx b/packages/dataviews/src/field-types/integer.tsx index 38570ea6fec1a5..f57c8e382db816 100644 --- a/packages/dataviews/src/field-types/integer.tsx +++ b/packages/dataviews/src/field-types/integer.tsx @@ -1,21 +1,7 @@ -/** - * WordPress dependencies - */ -import { - __experimentalNumberControl as NumberControl, - SelectControl, -} from '@wordpress/components'; -import { useCallback } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ -import type { - SortDirection, - ValidationContext, - DataFormControlProps, -} from '../types'; +import type { SortDirection, ValidationContext } from '../types'; function sort( a: any, b: any, direction: SortDirection ) { return direction === 'asc' ? a - b : b - a; @@ -41,62 +27,8 @@ function isValid( value: any, context?: ValidationContext ) { return true; } -function Edit< Item >( { - data, - field, - onChange, - hideLabelFromVision, -}: DataFormControlProps< Item > ) { - const { id, label, description } = field; - const value = field.getValue( { item: data } ) ?? ''; - const onChangeControl = useCallback( - ( newValue: string | undefined ) => - onChange( { - [ id ]: Number( newValue ), - } ), - [ id, onChange ] - ); - - if ( field.elements ) { - const elements = [ - /* - * Value can be undefined when: - * - * - the field is not required - * - in bulk editing - * - */ - { label: __( 'Select item' ), value: '' }, - ...field.elements, - ]; - - return ( - - ); - } - - return ( - - ); -} - export default { sort, isValid, - Edit, + Edit: 'integer', }; diff --git a/packages/dataviews/src/field-types/text.tsx b/packages/dataviews/src/field-types/text.tsx index 5364017c629b00..76ff699d0848c4 100644 --- a/packages/dataviews/src/field-types/text.tsx +++ b/packages/dataviews/src/field-types/text.tsx @@ -1,18 +1,7 @@ -/** - * WordPress dependencies - */ -import { SelectControl, TextControl } from '@wordpress/components'; -import { useCallback } from '@wordpress/element'; -import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ -import type { - SortDirection, - ValidationContext, - DataFormControlProps, -} from '../types'; +import type { SortDirection, ValidationContext } from '../types'; function sort( valueA: any, valueB: any, direction: SortDirection ) { return direction === 'asc' @@ -31,64 +20,8 @@ function isValid( value: any, context?: ValidationContext ) { return true; } -function Edit< Item >( { - data, - field, - onChange, - hideLabelFromVision, -}: DataFormControlProps< Item > ) { - const { id, label, placeholder } = field; - const value = field.getValue( { item: data } ); - - const onChangeControl = useCallback( - ( newValue: string ) => - onChange( { - [ id ]: newValue, - } ), - [ id, onChange ] - ); - - if ( field.elements ) { - const elements = [ - /* - * Value can be undefined when: - * - * - the field is not required - * - in bulk editing - * - */ - { label: __( 'Select item' ), value: '' }, - ...field.elements, - ]; - - return ( - - ); - } - - return ( - - ); -} - export default { sort, isValid, - Edit, + Edit: 'text', }; diff --git a/packages/dataviews/src/normalize-fields.ts b/packages/dataviews/src/normalize-fields.ts index 2cdde5b3343798..f9f95b5b8140dc 100644 --- a/packages/dataviews/src/normalize-fields.ts +++ b/packages/dataviews/src/normalize-fields.ts @@ -3,7 +3,7 @@ */ import getFieldTypeDefinition from './field-types'; import type { Field, NormalizedField } from './types'; -import { getControl } from './components/dataform-controls'; +import { getControl } from './dataform-controls'; /** * Apply default values and normalize the fields config. diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 34e74eabd7c7d8..7bbbc8cb863c09 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -63,9 +63,9 @@ export type FieldTypeDefinition< Item > = { isValid: ( item: Item, context?: ValidationContext ) => boolean; /** - * Callback used to render an edit control for the field. + * Callback used to render an edit control for the field or control name. */ - Edit: ComponentType< DataFormControlProps< Item > >; + Edit: ComponentType< DataFormControlProps< Item > > | string; }; /** @@ -105,7 +105,7 @@ export type Field< Item > = { /** * Callback used to render an edit control for the field. */ - Edit?: ComponentType< DataFormControlProps< Item > > | 'radio'; + Edit?: ComponentType< DataFormControlProps< Item > > | string; /** * Callback used to sort the field. From 1e13913453eb520805e3aaadd6c2670be2c642e2 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Mon, 12 Aug 2024 14:03:34 +0400 Subject: [PATCH 07/48] Edit Post: Avoid unnecessary post-template ID lookup (#64431) Co-authored-by: Mamaduka Co-authored-by: ntsekouras --- packages/edit-post/src/store/private-selectors.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/edit-post/src/store/private-selectors.js b/packages/edit-post/src/store/private-selectors.js index c151f935d68d50..0c0ac3c518024d 100644 --- a/packages/edit-post/src/store/private-selectors.js +++ b/packages/edit-post/src/store/private-selectors.js @@ -50,8 +50,11 @@ export const getEditedPostTemplateId = createRegistrySelector( } else { slugToCheck = postType === 'page' ? 'page' : `single-${ postType }`; } - return select( coreStore ).getDefaultTemplateId( { - slug: slugToCheck, - } ); + + if ( postType ) { + return select( coreStore ).getDefaultTemplateId( { + slug: slugToCheck, + } ); + } } ); From da50610ac45e2cd18b59424d260bb3d693535061 Mon Sep 17 00:00:00 2001 From: JuanMa Date: Mon, 12 Aug 2024 11:28:40 +0100 Subject: [PATCH 08/48] Docs/Dataviews component - Update README with missing properties and recent changes (#64435) Co-authored-by: juanmaguitar Co-authored-by: oandregal --- packages/dataviews/README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index c82a748df98858..df537d2a8cecff 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -72,12 +72,12 @@ const STATUSES = [ const fields = [ { id: 'title', - header: 'Title', + label: 'Title', enableHiding: false, }, { id: 'date', - header: 'Date', + label: 'Date', render: ( { item } ) => { return ( @@ -86,7 +86,7 @@ const fields = [ }, { id: 'author', - header: __( 'Author' ), + label: __( 'Author' ), render: ( { item } ) => { return ( { item.author } @@ -102,7 +102,7 @@ const fields = [ enableSorting: false }, { - header: __( 'Status' ), + label: __( 'Status' ), id: 'status', getValue: ( { item } ) => STATUSES.find( ( { value } ) => value === item.status ) @@ -119,13 +119,14 @@ const fields = [ Each field is an object with the following properties: - `id`: identifier for the field. Unique. -- `header`: the field's name to be shown in the UI. +- `label`: the field's name to be shown in the UI. - `getValue`: function that returns the value of the field, defaults to `field[id]`. - `render`: function that renders the field. Optional, `getValue` will be used if `render` is not defined. - `elements`: the set of valid values for the field's value. - `type`: the type of the field. See "Field types". - `enableSorting`: whether the data can be sorted by the given field. True by default. - `enableHiding`: whether the field can be hidden. True by default. +- `enableGlobalSearch`: whether the field is searchable. False by default. - `filterBy`: configuration for the filters. - `operators`: the list of operators supported by the field. - `isPrimary`: whether it is a primary filter. A primary filter is always visible and is not listed in the "Add filter" component, except for the list layout where it behaves like a secondary filter. From d86a4999338183fc1f17067bed11d6b53ed46f87 Mon Sep 17 00:00:00 2001 From: Sunil Prajapati <61308756+akasunil@users.noreply.github.com> Date: Mon, 12 Aug 2024 16:27:41 +0530 Subject: [PATCH 09/48] Tag Cloud: Add border block support (#63579) * Add border support to tag cloud block * Prevent duplicate border styles in editor * Add comments on style and edit component changes in tag cloud block Co-authored-by: akasunil Co-authored-by: aaronrobertshaw --- packages/block-library/src/tag-cloud/block.json | 12 ++++++++++++ packages/block-library/src/tag-cloud/edit.js | 11 ++++++++++- packages/block-library/src/tag-cloud/editor.scss | 4 +++- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/tag-cloud/block.json b/packages/block-library/src/tag-cloud/block.json index 0c2095bff2a152..044bc0c5333768 100644 --- a/packages/block-library/src/tag-cloud/block.json +++ b/packages/block-library/src/tag-cloud/block.json @@ -51,6 +51,18 @@ }, "interactivity": { "clientNavigation": true + }, + "__experimentalBorder": { + "radius": true, + "color": true, + "width": true, + "style": true, + "__experimentalDefaultControls": { + "radius": true, + "color": true, + "width": true, + "style": true + } } }, "editorStyle": "wp-block-tag-cloud-editor" diff --git a/packages/block-library/src/tag-cloud/edit.js b/packages/block-library/src/tag-cloud/edit.js index 9a2b531b30f8ab..8ced99271e4807 100644 --- a/packages/block-library/src/tag-cloud/edit.js +++ b/packages/block-library/src/tag-cloud/edit.js @@ -107,6 +107,15 @@ function TagCloudEdit( { attributes, setAttributes } ) { setAttributes( updateObj ); }; + // Remove border styles from the server-side attributes to prevent duplicate border. + const serverSideAttributes = { + ...attributes, + style: { + ...attributes?.style, + border: undefined, + }, + }; + const inspectorControls = ( @@ -188,7 +197,7 @@ function TagCloudEdit( { attributes, setAttributes } ) {
diff --git a/packages/block-library/src/tag-cloud/editor.scss b/packages/block-library/src/tag-cloud/editor.scss index de2a95a386fa85..d00a450174f2fd 100644 --- a/packages/block-library/src/tag-cloud/editor.scss +++ b/packages/block-library/src/tag-cloud/editor.scss @@ -1,4 +1,4 @@ -// The following styles are to prevent duplicate spacing for the tag cloud +// The following styles are to prevent duplicate spacing and border for the tag cloud // block in the editor given it uses server side rendering. The specificity // must be higher than `0-1-0` to override global styles. Targeting the // inner use of the .wp-block-tag-cloud class should minimize impact on @@ -6,4 +6,6 @@ .wp-block-tag-cloud .wp-block-tag-cloud { margin: 0; padding: 0; + border: none; + border-radius: inherit; } From ba8c2c0d76957cb5534619ea496a79344fcacc6f Mon Sep 17 00:00:00 2001 From: Anton Vlasenko <43744263+anton-vlasenko@users.noreply.github.com> Date: Mon, 12 Aug 2024 13:06:56 +0200 Subject: [PATCH 10/48] Fix gutenberg/gutenberg-coding-standards licensing issues (#61913) 1. change the license to `GPL-2.0-or-later`; 2. remove third-party code from the `SinceTagSniff::find_docblock()` method; 3. remove third-party code from the `SinceTagSniff::is_function_call()` method; 4. remove the `SinceTagSniff::find_hook_docblock()` method and add the new `find_previous_line_token()` method (refactoring); 5. fix typo in the `SinceTagSniff::process()` method; 6. remove unused code from `bootstrap.php` and make variables use snake case; 7. add the `License` section to the `README.md` file. Co-authored-by: anton-vlasenko Co-authored-by: rodrigoprimo Co-authored-by: azaozz --- .../.phpcs.xml.dist | 9 +- .../ForbiddenFunctionsAndClassesSniff.php | 3 +- .../GuardedFunctionAndClassNamesSniff.php | 3 +- .../Sniffs/Commenting/SinceTagSniff.php | 122 +++++++----------- .../ValidBlockLibraryFunctionNameSniff.php | 3 +- .../Gutenberg/Tests/AbstractSniffUnitTest.php | 10 +- .../ForbiddenFunctionsAndClassesUnitTest.php | 3 +- .../GuardedFunctionAndClassNamesUnitTest.php | 3 +- .../Tests/Commenting/SinceTagUnitTest.php | 3 +- .../ValidBlockLibraryFunctionNameUnitTest.php | 3 +- test/php/gutenberg-coding-standards/README.md | 8 +- .../Tests/bootstrap.php | 87 ++++--------- .../gutenberg-coding-standards/composer.json | 2 +- 13 files changed, 92 insertions(+), 167 deletions(-) diff --git a/test/php/gutenberg-coding-standards/.phpcs.xml.dist b/test/php/gutenberg-coding-standards/.phpcs.xml.dist index 7f717f65374d7b..7ebe60e3d28567 100644 --- a/test/php/gutenberg-coding-standards/.phpcs.xml.dist +++ b/test/php/gutenberg-coding-standards/.phpcs.xml.dist @@ -1,15 +1,8 @@ - + The Coding standard for the Gutenberg Coding Standards itself. - - . diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/CodeAnalysis/ForbiddenFunctionsAndClassesSniff.php b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/CodeAnalysis/ForbiddenFunctionsAndClassesSniff.php index 87d966ecb56030..12d388cc378180 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/CodeAnalysis/ForbiddenFunctionsAndClassesSniff.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/CodeAnalysis/ForbiddenFunctionsAndClassesSniff.php @@ -3,8 +3,7 @@ * Gutenberg Coding Standards. * * @package gutenberg/gutenberg-coding-standards - * @link https://github.com/WordPress/gutenberg - * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/WordPress/gutenberg/tree/trunk/test/php/gutenberg-coding-standards */ namespace GutenbergCS\Gutenberg\Sniffs\CodeAnalysis; diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/CodeAnalysis/GuardedFunctionAndClassNamesSniff.php b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/CodeAnalysis/GuardedFunctionAndClassNamesSniff.php index c6e51119f1513a..d947d6d9382b35 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/CodeAnalysis/GuardedFunctionAndClassNamesSniff.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/CodeAnalysis/GuardedFunctionAndClassNamesSniff.php @@ -3,8 +3,7 @@ * Gutenberg Coding Standards. * * @package gutenberg/gutenberg-coding-standards - * @link https://github.com/WordPress/gutenberg - * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/WordPress/gutenberg/tree/trunk/test/php/gutenberg-coding-standards */ namespace GutenbergCS\Gutenberg\Sniffs\CodeAnalysis; diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/SinceTagSniff.php b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/SinceTagSniff.php index f216f4f681f0e2..20f698954cb267 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/SinceTagSniff.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/Commenting/SinceTagSniff.php @@ -3,8 +3,7 @@ * Gutenberg Coding Standards. * * @package gutenberg/gutenberg-coding-standards - * @link https://github.com/WordPress/gutenberg - * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/WordPress/gutenberg/tree/trunk/test/php/gutenberg-coding-standards */ namespace GutenbergCS\Gutenberg\Sniffs\Commenting; @@ -22,7 +21,7 @@ /** * This sniff verifies the presence of valid `@since` tags in the docblocks of various PHP structures * and WordPress hooks. Supported structures include classes, interfaces, traits, enums, functions, methods and properties. - * Files located within the __experimental block of the block-library are excluded from checks. + * Files located within the __experimental blocks of the block-library folder are excluded from checks. */ class SinceTagSniff implements Sniff { @@ -147,7 +146,7 @@ protected function process_hook( File $phpcs_file, $stack_pointer ) { $violation_codes = static::get_violation_codes( 'Hook' ); - $docblock = static::find_hook_docblock( $phpcs_file, $stack_pointer ); + $docblock = static::find_docblock( $phpcs_file, $stack_pointer ); $version_tags = static::parse_since_tags( $phpcs_file, $docblock ); if ( empty( $version_tags ) ) { @@ -436,114 +435,81 @@ protected function check_below_minimum_visibility( $visibility ) { } /** - * Finds the docblock associated with a hook, starting from a specified position in the token stack. - * Since a line containing a hook can include any type of tokens, this method backtracks through the tokens - * to locate the first token on the current line. This token is then used as the starting point for searching the docblock. + * Finds the first token on the previous line relative to the stack pointer passed to the method. * * @param File $phpcs_file The file being scanned. - * @param int $stack_pointer The position to start looking for the docblock. - * @return array|false An associative array containing the start and end tokens of the docblock, or false if not found. + * @param int $stack_pointer The position to find the previous line token from. + * @return int|false The last token on the previous line, or false if not found. */ - protected static function find_hook_docblock( File $phpcs_file, $stack_pointer ) { + protected static function find_previous_line_token( File $phpcs_file, $stack_pointer ) { $tokens = $phpcs_file->getTokens(); $current_line = $tokens[ $stack_pointer ]['line']; - for ( $i = $stack_pointer; $i >= 0; $i-- ) { - if ( $tokens[ $i ]['line'] < $current_line ) { - // The previous token is on the previous line, so the current token is the first on the line. - return static::find_docblock( $phpcs_file, $i + 1 ); + for ( $token = $stack_pointer; $token >= 0; $token-- ) { + if ( $tokens[ $token ]['line'] < $current_line ) { + return $token; } } - return static::find_docblock( $phpcs_file, 0 ); + return false; } /** - * Determines if a T_STRING token represents a function call. - * The implementation was copied from PHPCompatibility\Sniffs\Extensions\RemovedExtensionsSniff::process(). + * Finds the docblock preceding a specified position (stack pointer) in a given PHP file. * * @param File $phpcs_file The file being scanned. - * @param int $stack_pointer The position of the T_STRING token in question. - * @return bool True if the token represents a function call, false otherwise. + * @param int $stack_pointer The position (stack pointer) in the token stack from which to start searching backwards. + * @return array|false An associative array containing the start and end tokens of the docblock, or false if not found. */ - protected static function is_function_call( File $phpcs_file, $stack_pointer ) { - $tokens = $phpcs_file->getTokens(); - - // Find the next non-empty token. - $open_bracket = $phpcs_file->findNext( Tokens::$emptyTokens, ( $stack_pointer + 1 ), null, true ); - - if ( T_OPEN_PARENTHESIS !== $tokens[ $open_bracket ]['code'] ) { - // Not a function call. + protected static function find_docblock( File $phpcs_file, $stack_pointer ) { + // It can be assumed that the DocBlock should end on the previous line, not the current one. + $previous_line_end_token = static::find_previous_line_token( $phpcs_file, $stack_pointer ); + if ( false === $previous_line_end_token ) { return false; } - if ( false === isset( $tokens[ $open_bracket ]['parenthesis_closer'] ) ) { - // Not a function call. + $docblock_end_token = $phpcs_file->findPrevious( array( T_WHITESPACE ), $previous_line_end_token, null, true ); + + $tokens = $phpcs_file->getTokens(); + if ( false === $docblock_end_token || T_DOC_COMMENT_CLOSE_TAG !== $tokens[ $docblock_end_token ]['code'] ) { + // Only "/**" style comments are supported. return false; } - // Find the previous non-empty token. - $search = Tokens::$emptyTokens; - $search[] = T_BITWISE_AND; - $previous = $phpcs_file->findPrevious( $search, ( $stack_pointer - 1 ), null, true ); - - $previous_tokens_to_ignore = array( - T_FUNCTION, // Function declaration. - T_NEW, // Creating an object. - T_OBJECT_OPERATOR, // Calling an object. + return array( + 'start_token' => $tokens[ $docblock_end_token ]['comment_opener'], + 'end_token' => $docblock_end_token, ); - - return ! in_array( $tokens[ $previous ]['code'], $previous_tokens_to_ignore, true ); } /** - * Finds the docblock preceding a specified position (stack pointer) in a given PHP file. - * The implementation was copied from PHP_CodeSniffer\Standards\PEAR\Sniffs\Commenting\FunctionCommentSniff::process(). + * Determines if a T_STRING token represents a function call. * * @param File $phpcs_file The file being scanned. - * @param int $stack_pointer The position (stack pointer) in the token stack from which to start searching backwards. - * @return array|false An associative array containing the start and end tokens of the docblock, or false if not found. + * @param int $stack_pointer The position of the T_STRING token in question. + * @return bool True if the token represents a function call, false otherwise. */ - protected static function find_docblock( File $phpcs_file, $stack_pointer ) { - $tokens = $phpcs_file->getTokens(); - $ignore = Tokens::$methodPrefixes; - $ignore[ T_WHITESPACE ] = T_WHITESPACE; - - for ( $comment_end = ( $stack_pointer - 1 ); $comment_end >= 0; $comment_end-- ) { - if ( isset( $ignore[ $tokens[ $comment_end ]['code'] ] ) ) { - continue; - } - - if ( T_ATTRIBUTE_END === $tokens[ $comment_end ]['code'] - && isset( $tokens[ $comment_end ]['attribute_opener'] ) - ) { - $comment_end = $tokens[ $comment_end ]['attribute_opener']; - continue; - } + protected static function is_function_call( File $phpcs_file, $stack_pointer ) { + $tokens = $phpcs_file->getTokens(); - break; - } + // Find the previous non-empty token. + $previous = $phpcs_file->findPrevious( Tokens::$emptyTokens, ( $stack_pointer - 1 ), null, true ); - if ( $tokens[ $comment_end ]['code'] === T_COMMENT ) { - // Inline comments might just be closing comments for - // control structures or functions instead of function comments - // using the wrong comment type. If there is other code on the line, - // assume they relate to that code. - $previous = $phpcs_file->findPrevious( $ignore, ( $comment_end - 1 ), null, true ); - if ( false !== $previous && $tokens[ $previous ]['line'] === $tokens[ $comment_end ]['line'] ) { - $comment_end = $previous; - } - } + $previous_tokens_to_ignore = array( + T_NEW, // Creating an object. + T_OBJECT_OPERATOR, // Calling an object. + T_FUNCTION, // Function declaration. + ); - if ( T_DOC_COMMENT_CLOSE_TAG !== $tokens[ $comment_end ]['code'] ) { - // Only "/**" style comments are supported. + if ( in_array( $tokens[ $previous ]['code'], $previous_tokens_to_ignore, true ) ) { + // This is an object or function declaration. return false; } - return array( - 'start_token' => $tokens[ $comment_end ]['comment_opener'], - 'end_token' => $comment_end, - ); + // Find the next non-empty token. + $open_bracket = $phpcs_file->findNext( Tokens::$emptyTokens, ( $stack_pointer + 1 ), null, true ); + + return ( false !== $open_bracket ) && ( T_OPEN_PARENTHESIS === $tokens[ $open_bracket ]['code'] ); } /** diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/NamingConventions/ValidBlockLibraryFunctionNameSniff.php b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/NamingConventions/ValidBlockLibraryFunctionNameSniff.php index 74608921c32d89..bf7dbc2fd85d3c 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/NamingConventions/ValidBlockLibraryFunctionNameSniff.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Sniffs/NamingConventions/ValidBlockLibraryFunctionNameSniff.php @@ -3,8 +3,7 @@ * Gutenberg Coding Standards. * * @package gutenberg/gutenberg-coding-standards - * @link https://github.com/WordPress/gutenberg - * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/WordPress/gutenberg/tree/trunk/test/php/gutenberg-coding-standards */ namespace GutenbergCS\Gutenberg\Sniffs\NamingConventions; diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/AbstractSniffUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/AbstractSniffUnitTest.php index 08838ce412fc3a..f1be5ab00b0750 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/AbstractSniffUnitTest.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/AbstractSniffUnitTest.php @@ -3,8 +3,7 @@ * An abstract class that all sniff unit tests must extend. * * @package gutenberg-coding-standards/gbc - * @link https://github.com/WordPress/gutenberg - * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/WordPress/gutenberg/tree/trunk/test/php/gutenberg-coding-standards */ namespace GutenbergCS\Gutenberg\Tests; @@ -14,6 +13,9 @@ use PHP_CodeSniffer\Ruleset; use PHP_CodeSniffer\Sniffs\Sniff; +/** + * An abstract test class that contains common methods for all sniff unit tests. + */ abstract class AbstractSniffUnitTest extends BaseAbstractSniffUnitTest { /** @@ -65,7 +67,7 @@ public function setCliValues( $filename, $config ) { if ( ! isset( $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] ) || ( ! $GLOBALS['PHP_CODESNIFFER_RULESETS']['Gutenberg'] instanceof Ruleset ) ) { - throw new \RuntimeException( $error_message ); + throw new \RuntimeException( $error_message ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- this is non-production code. } // Backup the original Ruleset instance. @@ -76,7 +78,7 @@ public function setCliValues( $filename, $config ) { $sniff_fqcn = $this->get_sniff_fqcn(); if ( ! isset( $current_ruleset->sniffs[ $sniff_fqcn ] ) ) { - throw new \RuntimeException( $error_message ); + throw new \RuntimeException( $error_message ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- this is non-production code. } $sniff = $current_ruleset->sniffs[ $sniff_fqcn ]; diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/ForbiddenFunctionsAndClassesUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/ForbiddenFunctionsAndClassesUnitTest.php index 8026e88f1d9453..79719fba6039db 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/ForbiddenFunctionsAndClassesUnitTest.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/ForbiddenFunctionsAndClassesUnitTest.php @@ -3,8 +3,7 @@ * Unit test class for Gutenberg Coding Standard. * * @package gutenberg-coding-standards/gbc - * @link https://github.com/WordPress/gutenberg - * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/WordPress/gutenberg/tree/trunk/test/php/gutenberg-coding-standards */ namespace GutenbergCS\Gutenberg\Tests\CodeAnalysis; diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/GuardedFunctionAndClassNamesUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/GuardedFunctionAndClassNamesUnitTest.php index 652f6b735378ce..b4e26fd2e69c4f 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/GuardedFunctionAndClassNamesUnitTest.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/CodeAnalysis/GuardedFunctionAndClassNamesUnitTest.php @@ -3,8 +3,7 @@ * Unit test class for Gutenberg Coding Standard. * * @package gutenberg-coding-standards/gbc - * @link https://github.com/WordPress/gutenberg - * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/WordPress/gutenberg/tree/trunk/test/php/gutenberg-coding-standards */ namespace GutenbergCS\Gutenberg\Tests\CodeAnalysis; diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.php index bc7ca28c263ffe..1318687768c9dd 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/Commenting/SinceTagUnitTest.php @@ -3,8 +3,7 @@ * Unit test class for Gutenberg Coding Standard. * * @package gutenberg-coding-standards/gbc - * @link https://github.com/WordPress/gutenberg - * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/WordPress/gutenberg/tree/trunk/test/php/gutenberg-coding-standards */ namespace GutenbergCS\Gutenberg\Tests\Commenting; diff --git a/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php b/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php index 51174dd769d0a3..14a2cb1a97dd71 100644 --- a/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php +++ b/test/php/gutenberg-coding-standards/Gutenberg/Tests/NamingConventions/ValidBlockLibraryFunctionNameUnitTest.php @@ -3,8 +3,7 @@ * Unit test class for Gutenberg Coding Standard. * * @package gutenberg-coding-standards/gbc - * @link https://github.com/WordPress/gutenberg - * @license https://opensource.org/licenses/MIT MIT + * @link https://github.com/WordPress/gutenberg/tree/trunk/test/php/gutenberg-coding-standards */ namespace GutenbergCS\Gutenberg\Tests\NamingConventions; diff --git a/test/php/gutenberg-coding-standards/README.md b/test/php/gutenberg-coding-standards/README.md index 51f6574fa20534..500abe2e5b4812 100644 --- a/test/php/gutenberg-coding-standards/README.md +++ b/test/php/gutenberg-coding-standards/README.md @@ -1,3 +1,9 @@ # Gutenberg Coding Standards for Gutenberg -This project is a collection of [PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer) rules (sniffs) to validate code developed for Gutenberg. It ensures code quality and adherence to coding conventions. \ No newline at end of file +This project is a collection of [PHP_CodeSniffer](https://github.com/PHPCSStandards/PHP_CodeSniffer) rules (sniffs) to validate code developed for Gutenberg. It ensures code quality and adherence to coding conventions. + +## License + +This project is licensed under the same terms as the Gutenberg project. + +Please refer to this [LICENSE.md](../../../LICENSE.md) file for detailed license information. diff --git a/test/php/gutenberg-coding-standards/Tests/bootstrap.php b/test/php/gutenberg-coding-standards/Tests/bootstrap.php index f28528bc1b9726..547a1ce303835f 100644 --- a/test/php/gutenberg-coding-standards/Tests/bootstrap.php +++ b/test/php/gutenberg-coding-standards/Tests/bootstrap.php @@ -1,86 +1,51 @@ true, -); - -$allStandards = PHP_CodeSniffer\Util\Standards::getInstalledStandards(); -$allStandards[] = 'Generic'; +$available_standards = PHP_CodeSniffer\Util\Standards::getInstalledStandards(); +$ignored_standards = array( 'Generic' ); -$standardsToIgnore = array(); -foreach ( $allStandards as $standard ) { - if ( isset( $gbcsStandards[ $standard ] ) === true ) { +foreach ( $available_standards as $available_standard ) { + if ( 'Gutenberg' === $available_standard ) { continue; } - $standardsToIgnore[] = $standard; + $ignored_standards[] = $available_standard; } -$standardsToIgnoreString = implode( ',', $standardsToIgnore ); +$ignore_standards_string = implode( ',', $ignored_standards ); -// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv -- This is not production, but test code. -putenv( "PHPCS_IGNORE_TESTS={$standardsToIgnoreString}" ); +// phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.runtime_configuration_putenv -- This is non-production code. +putenv( "PHPCS_IGNORE_TESTS={$ignore_standards_string}" ); -// Clean up. -unset( $ds, $phpcsDir, $composerPHPCSPath, $allStandards, $standardsToIgnore, $standard, $standardsToIgnoreString ); +// Cleanup. +unset( $dir_separator, $phpcs_path, $available_standards, $ignored_standards, $available_standard, $ignore_standards_string ); diff --git a/test/php/gutenberg-coding-standards/composer.json b/test/php/gutenberg-coding-standards/composer.json index c1c27f81818aa5..2d79dc906a8d9e 100644 --- a/test/php/gutenberg-coding-standards/composer.json +++ b/test/php/gutenberg-coding-standards/composer.json @@ -8,7 +8,7 @@ "static analysis", "Gutenberg" ], - "license": "MIT", + "license": "GPL-2.0-or-later", "authors": [ { "name": "Contributors", From 7969ba3b21006a33455c40df59f32e95058d26a0 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 12 Aug 2024 13:43:07 +0200 Subject: [PATCH 11/48] DataForm: Update the style of the datetime fields to match the other types (#64438) Co-authored-by: youknowriad Co-authored-by: oandregal --- .../dataviews/src/dataform-controls/datetime.tsx | 16 +++++++++++----- .../dataviews/src/dataform-controls/style.scss | 4 ++++ packages/dataviews/src/style.scss | 1 + 3 files changed, 16 insertions(+), 5 deletions(-) create mode 100644 packages/dataviews/src/dataform-controls/style.scss diff --git a/packages/dataviews/src/dataform-controls/datetime.tsx b/packages/dataviews/src/dataform-controls/datetime.tsx index 3ba22dc0c1b089..b31c5c60bb9c6e 100644 --- a/packages/dataviews/src/dataform-controls/datetime.tsx +++ b/packages/dataviews/src/dataform-controls/datetime.tsx @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { BaseControl, TimePicker } from '@wordpress/components'; +import { BaseControl, TimePicker, VisuallyHidden } from '@wordpress/components'; import { useCallback } from '@wordpress/element'; /** @@ -13,6 +13,7 @@ export default function DateTime< Item >( { data, field, onChange, + hideLabelFromVision, }: DataFormControlProps< Item > ) { const { id, label } = field; const value = field.getValue( { item: data } ); @@ -23,10 +24,15 @@ export default function DateTime< Item >( { ); return ( -
- - { label } - +
+ { ! hideLabelFromVision && ( + + { label } + + ) } + { hideLabelFromVision && ( + { label } + ) } Date: Mon, 12 Aug 2024 14:48:18 +0200 Subject: [PATCH 12/48] Script Modules: Move data passing to 6.7 compat file (#64006) Move Script Modules data passing to compatibility directory. Add conditions to ensure the functionality is duplicated when present in Core. This prevents duplicate script tags with duplicate IDs from being printed. This feature is planned to ship in WordPress 6.7: https://core.trac.wordpress.org/ticket/61510 --- lib/compat/wordpress-6.7/script-modules.php | 104 ++++++++++++++++++++ lib/experimental/script-modules.php | 93 ----------------- lib/load.php | 1 + 3 files changed, 105 insertions(+), 93 deletions(-) create mode 100644 lib/compat/wordpress-6.7/script-modules.php diff --git a/lib/compat/wordpress-6.7/script-modules.php b/lib/compat/wordpress-6.7/script-modules.php new file mode 100644 index 00000000000000..0a440ec81688d2 --- /dev/null +++ b/lib/compat/wordpress-6.7/script-modules.php @@ -0,0 +1,104 @@ +setAccessible( true ); + $get_import_map = new ReflectionMethod( 'WP_Script_Modules', 'get_import_map' ); + $get_import_map->setAccessible( true ); + + $modules = array(); + foreach ( array_keys( $get_marked_for_enqueue->invoke( wp_script_modules() ) ) as $id ) { + $modules[ $id ] = true; + } + foreach ( array_keys( $get_import_map->invoke( wp_script_modules() )['imports'] ) as $id ) { + $modules[ $id ] = true; + } + + foreach ( array_keys( $modules ) as $module_id ) { + /** + * Filters data associated with a given Script Module. + * + * Script Modules may require data that is required for initialization or is essential to + * have immediately available on page load. These are suitable use cases for this data. + * + * This is best suited to a minimal set of data and is not intended to replace the REST API. + * + * If the filter returns no data (an empty array), nothing will be embedded in the page. + * + * The data for a given Script Module, if provided, will be JSON serialized in a script tag + * with an ID like `wp-script-module-data-{$module_id}`. + * + * The dynamic portion of the hook name, `$module_id`, refers to the Script Module ID that + * the data is associated with. + * + * @param array $data The data that should be associated with the array. + */ + $data = apply_filters( "script_module_data_{$module_id}", array() ); + + if ( is_array( $data ) && ! empty( $data ) ) { + /* + * This data will be printed as JSON inside a script tag like this: + * + * + * A script tag must be closed by a sequence beginning with `` will be printed as `\u003C/script\u00E3`. + * + * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E. + * - JSON_UNESCAPED_SLASHES: Don't escape /. + * + * If the page will use UTF-8 encoding, it's safe to print unescaped unicode: + * + * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`). + * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when + * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was + * before PHP 7.1 without this constant. Available as of PHP 7.1.0. + * + * The JSON specification requires encoding in UTF-8, so if the generated HTML page + * is not encoded in UTF-8 then it's not safe to include those literals. They must + * be escaped to avoid encoding issues. + * + * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements. + * @see https://www.php.net/manual/en/json.constants.php for details on these constants. + * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing. + */ + $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS; + if ( 'UTF-8' !== get_option( 'blog_charset' ) ) { + $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES; + } + + wp_print_inline_script_tag( + wp_json_encode( $data, $json_encode_flags ), + array( + 'type' => 'application/json', + 'id' => "wp-script-module-data-{$module_id}", + ) + ); + } + } +} + +add_action( + 'after_setup_theme', + function () { + if ( ! has_action( 'wp_footer', array( wp_script_modules(), 'print_script_module_data' ) ) ) { + add_action( 'wp_footer', 'gutenberg_print_script_module_data' ); + } + + if ( ! has_action( 'admin_print_footer_scripts', array( wp_script_modules(), 'print_script_module_data' ) ) ) { + add_action( 'admin_print_footer_scripts', 'gutenberg_print_script_module_data' ); + } + }, + 20 +); diff --git a/lib/experimental/script-modules.php b/lib/experimental/script-modules.php index 0093c2e974568f..5a14e1418ed6de 100644 --- a/lib/experimental/script-modules.php +++ b/lib/experimental/script-modules.php @@ -200,96 +200,3 @@ function gutenberg_dequeue_module( $module_identifier ) { _deprecated_function( __FUNCTION__, 'Gutenberg 17.6.0', 'wp_dequeue_script_module' ); wp_script_modules()->dequeue( $module_identifier ); } - - -/** - * Print data associated with Script Modules in Script tags. - * - * This embeds data in the page HTML so that it is available on page load. - * - * Data can be associated with a given Script Module by using the - * `script_module_data_{$module_id}` filter. - * - * The data for a given Script Module will be JSON serialized in a script tag with an ID - * like `wp-script-module-data-{$module_id}`. - */ -function gutenberg_print_script_module_data(): void { - $get_marked_for_enqueue = new ReflectionMethod( 'WP_Script_Modules', 'get_marked_for_enqueue' ); - $get_marked_for_enqueue->setAccessible( true ); - $get_import_map = new ReflectionMethod( 'WP_Script_Modules', 'get_import_map' ); - $get_import_map->setAccessible( true ); - - $modules = array(); - foreach ( array_keys( $get_marked_for_enqueue->invoke( wp_script_modules() ) ) as $id ) { - $modules[ $id ] = true; - } - foreach ( array_keys( $get_import_map->invoke( wp_script_modules() )['imports'] ) as $id ) { - $modules[ $id ] = true; - } - - foreach ( array_keys( $modules ) as $module_id ) { - /** - * Filters data associated with a given Script Module. - * - * Script Modules may require data that is required for initialization or is essential to - * have immediately available on page load. These are suitable use cases for this data. - * - * This is best suited to a minimal set of data and is not intended to replace the REST API. - * - * If the filter returns no data (an empty array), nothing will be embedded in the page. - * - * The data for a given Script Module, if provided, will be JSON serialized in a script tag - * with an ID like `wp-script-module-data-{$module_id}`. - * - * The dynamic portion of the hook name, `$module_id`, refers to the Script Module ID that - * the data is associated with. - * - * @param array $data The data that should be associated with the array. - */ - $data = apply_filters( "script_module_data_{$module_id}", array() ); - - if ( is_array( $data ) && ! empty( $data ) ) { - /* - * This data will be printed as JSON inside a script tag like this: - * - * - * A script tag must be closed by a sequence beginning with `` will be printed as `\u003C/script\u00E3`. - * - * - JSON_HEX_TAG: All < and > are converted to \u003C and \u003E. - * - JSON_UNESCAPED_SLASHES: Don't escape /. - * - * If the page will use UTF-8 encoding, it's safe to print unescaped unicode: - * - * - JSON_UNESCAPED_UNICODE: Encode multibyte Unicode characters literally (instead of as `\uXXXX`). - * - JSON_UNESCAPED_LINE_TERMINATORS: The line terminators are kept unescaped when - * JSON_UNESCAPED_UNICODE is supplied. It uses the same behaviour as it was - * before PHP 7.1 without this constant. Available as of PHP 7.1.0. - * - * The JSON specification requires encoding in UTF-8, so if the generated HTML page - * is not encoded in UTF-8 then it's not safe to include those literals. They must - * be escaped to avoid encoding issues. - * - * @see https://www.rfc-editor.org/rfc/rfc8259.html for details on encoding requirements. - * @see https://www.php.net/manual/en/json.constants.php for details on these constants. - * @see https://html.spec.whatwg.org/#script-data-state for details on script tag parsing. - */ - $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_LINE_TERMINATORS; - if ( 'UTF-8' !== get_option( 'blog_charset' ) ) { - $json_encode_flags = JSON_HEX_TAG | JSON_UNESCAPED_SLASHES; - } - - wp_print_inline_script_tag( - wp_json_encode( $data, $json_encode_flags ), - array( - 'type' => 'application/json', - 'id' => "wp-script-module-data-{$module_id}", - ) - ); - } - } -} - -add_action( 'wp_footer', 'gutenberg_print_script_module_data' ); -add_action( 'admin_print_footer_scripts', 'gutenberg_print_script_module_data' ); diff --git a/lib/load.php b/lib/load.php index 4bb0ab88024a4a..c5f12af1654df2 100644 --- a/lib/load.php +++ b/lib/load.php @@ -103,6 +103,7 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.7 compat. require __DIR__ . '/compat/wordpress-6.7/blocks.php'; require __DIR__ . '/compat/wordpress-6.7/block-bindings.php'; +require __DIR__ . '/compat/wordpress-6.7/script-modules.php'; // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; From 9f5ffd2e8dd5ffc0abedf2b258dd5ace6b3bdb1e Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 12 Aug 2024 14:58:37 +0200 Subject: [PATCH 13/48] DataViews Extensibility: Allow unregistering the duplicate template part action (#64388) Co-authored-by: youknowriad Co-authored-by: ntsekouras --- .../src/components/post-actions/actions.js | 72 +------------------ .../actions/duplicate-template-part.tsx | 70 ++++++++++++++++++ .../src/dataviews/store/private-actions.ts | 8 +++ packages/editor/src/dataviews/types.ts | 4 +- 4 files changed, 83 insertions(+), 71 deletions(-) create mode 100644 packages/editor/src/dataviews/actions/duplicate-template-part.tsx diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index ff5cd7ddbb5452..0df73d1996deb5 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -9,7 +9,6 @@ import { store as coreStore } from '@wordpress/core-data'; import { __, sprintf, _x } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { useMemo, useState, useEffect } from '@wordpress/element'; -import { parse } from '@wordpress/blocks'; import { DataForm } from '@wordpress/dataviews'; import { Button, @@ -27,7 +26,6 @@ import { } from '../../store/constants'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; -import { CreateTemplatePartModalContents } from '../create-template-part-modal'; import { getItemTitle } from '../../dataviews/actions/utils'; // TODO: this should be shared with other components (see post-fields in edit-site). @@ -268,75 +266,14 @@ const useDuplicatePostAction = ( postType ) => { ); }; -export const duplicateTemplatePartAction = { - id: 'duplicate-template-part', - label: _x( 'Duplicate', 'action label' ), - isEligible: ( item ) => item.type === TEMPLATE_PART_POST_TYPE, - modalHeader: _x( 'Duplicate template part', 'action label' ), - RenderModal: ( { items, closeModal } ) => { - const [ item ] = items; - const blocks = useMemo( () => { - return ( - item.blocks ?? - parse( - typeof item.content === 'string' - ? item.content - : item.content.raw, - { - __unstableSkipMigrationLogs: true, - } - ) - ); - }, [ item.content, item.blocks ] ); - const { createSuccessNotice } = useDispatch( noticesStore ); - function onTemplatePartSuccess() { - createSuccessNotice( - sprintf( - // translators: %s: The new template part's title e.g. 'Call to action (copy)'. - __( '"%s" duplicated.' ), - getItemTitle( item ) - ), - { type: 'snackbar', id: 'edit-site-patterns-success' } - ); - closeModal(); - } - return ( - - ); - }, -}; - export function usePostActions( { postType, onActionPerformed, context } ) { - const { - defaultActions, - postTypeObject, - userCanCreatePostType, - isBlockBasedTheme, - } = useSelect( + const { defaultActions, postTypeObject } = useSelect( ( select ) => { - const { getPostType, canUser, getCurrentTheme } = - select( coreStore ); + const { getPostType } = select( coreStore ); const { getEntityActions } = unlock( select( editorStore ) ); return { postTypeObject: getPostType( postType ), defaultActions: getEntityActions( 'postType', postType ), - userCanCreatePostType: canUser( 'create', { - kind: 'postType', - name: postType, - } ), - isBlockBasedTheme: getCurrentTheme()?.is_block_theme, }; }, [ postType ] @@ -368,10 +305,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { ! isPattern && duplicatePostAction : false, - isTemplateOrTemplatePart && - userCanCreatePostType && - isBlockBasedTheme && - duplicateTemplatePartAction, ...defaultActions, ].filter( Boolean ); // Filter actions based on provided context. If not provided @@ -437,7 +370,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { return actions; }, [ defaultActions, - userCanCreatePostType, isTemplateOrTemplatePart, isPattern, postTypeObject?.viewable, diff --git a/packages/editor/src/dataviews/actions/duplicate-template-part.tsx b/packages/editor/src/dataviews/actions/duplicate-template-part.tsx new file mode 100644 index 00000000000000..fa3cf39ba76268 --- /dev/null +++ b/packages/editor/src/dataviews/actions/duplicate-template-part.tsx @@ -0,0 +1,70 @@ +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { __, sprintf, _x } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { useMemo } from '@wordpress/element'; +// @ts-ignore +import { parse } from '@wordpress/blocks'; +import type { Action } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import { TEMPLATE_PART_POST_TYPE } from '../../store/constants'; +import { CreateTemplatePartModalContents } from '../../components/create-template-part-modal'; +import { getItemTitle } from './utils'; +import type { TemplatePart } from '../types'; + +const duplicateTemplatePart: Action< TemplatePart > = { + id: 'duplicate-template-part', + label: _x( 'Duplicate', 'action label' ), + isEligible: ( item ) => item.type === TEMPLATE_PART_POST_TYPE, + modalHeader: _x( 'Duplicate template part', 'action label' ), + RenderModal: ( { items, closeModal } ) => { + const [ item ] = items; + const blocks = useMemo( () => { + return ( + item.blocks ?? + parse( + typeof item.content === 'string' + ? item.content + : item.content.raw, + { + __unstableSkipMigrationLogs: true, + } + ) + ); + }, [ item.content, item.blocks ] ); + const { createSuccessNotice } = useDispatch( noticesStore ); + function onTemplatePartSuccess() { + createSuccessNotice( + sprintf( + // translators: %s: The new template part's title e.g. 'Call to action (copy)'. + __( '"%s" duplicated.' ), + getItemTitle( item ) + ), + { type: 'snackbar', id: 'edit-site-patterns-success' } + ); + closeModal?.(); + } + return ( + + ); + }, +}; + +export default duplicateTemplatePart; diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index 6854c29bb0c4e4..80449d1b7a0798 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -10,6 +10,7 @@ import { doAction } from '@wordpress/hooks'; */ import deletePost from '../actions/delete-post'; import duplicatePattern from '../actions/duplicate-pattern'; +import duplicateTemplatePart from '../actions/duplicate-template-part'; import exportPattern from '../actions/export-pattern'; import resetPost from '../actions/reset-post'; import trashPost from '../actions/trash-post'; @@ -81,8 +82,15 @@ export const registerPostTypeActions = kind: 'postType', name: postType, } ); + const currentTheme = await registry + .resolveSelect( coreStore ) + .getCurrentTheme(); const actions = [ + postTypeConfig.slug === 'wp_template_part' && + canCreate && + currentTheme?.is_block_theme && + duplicateTemplatePart, canCreate && postTypeConfig.slug === 'wp_block' ? duplicatePattern : undefined, diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index 80b6f3c5ceb852..5750ab96eeae81 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -10,8 +10,10 @@ type PostStatus = export interface BasePost { status?: PostStatus; title: string | { rendered: string } | { raw: string }; + content: string | { raw: string; rendered: string }; type: string; id: string | number; + blocks?: Object[]; } export interface Template extends BasePost { @@ -27,12 +29,12 @@ export interface TemplatePart extends BasePost { source: string; has_theme_file: boolean; id: string; + area: string; } export interface Pattern extends BasePost { slug: string; title: { raw: string }; - content: { raw: string } | string; wp_pattern_sync_status: string; } From c73aa361c4bd82d8b3ee004e5195a22158cad1bf Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Mon, 12 Aug 2024 14:05:40 +0100 Subject: [PATCH 14/48] Block Insertion: Clear the insertion point when selecting a different block or clearing block selection (#64048) * Clear the insertion point when selecting a different block or clearing block selection * Use the block insertion point in the inserter, not the the one from the editor package * use block insertion point index * Don't change the insertion point when clearing block selection * Also set the insertion point in the quick inserter Co-authored-by: scruffian Co-authored-by: MaggieCabrera --- .../src/components/inserter/quick-inserter.js | 5 ++++- packages/block-editor/src/store/reducer.js | 2 ++ .../src/components/inserter-sidebar/index.js | 14 ++++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/inserter/quick-inserter.js b/packages/block-editor/src/components/inserter/quick-inserter.js index aa3d54e87a7fd1..f4c52c04d20c4b 100644 --- a/packages/block-editor/src/components/inserter/quick-inserter.js +++ b/packages/block-editor/src/components/inserter/quick-inserter.js @@ -9,7 +9,7 @@ import clsx from 'clsx'; import { useState, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { Button, SearchControl } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -82,6 +82,8 @@ export default function QuickInserter( { } }, [ setInserterIsOpened ] ); + const { showInsertionPoint } = useDispatch( blockEditorStore ); + // When clicking Browse All select the appropriate block so as // the insertion point can work as expected. const onBrowseAll = () => { @@ -91,6 +93,7 @@ export default function QuickInserter( { filterValue, onSelect, } ); + showInsertionPoint( rootClientId, insertionIndex ); }; let maxBlockPatterns = 0; diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index cd4569c45e5801..91d853a50a6a42 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1624,6 +1624,8 @@ export function insertionPoint( state = null, action ) { } case 'HIDE_INSERTION_POINT': + case 'CLEAR_SELECTED_BLOCK': + case 'SELECT_BLOCK': return null; } diff --git a/packages/editor/src/components/inserter-sidebar/index.js b/packages/editor/src/components/inserter-sidebar/index.js index 675ae5e11544bc..bf613b5c8c001a 100644 --- a/packages/editor/src/components/inserter-sidebar/index.js +++ b/packages/editor/src/components/inserter-sidebar/index.js @@ -22,6 +22,7 @@ const { PrivateInserterLibrary } = unlock( blockEditorPrivateApis ); export default function InserterSidebar() { const { + blockInsertionPoint, blockSectionRootClientId, inserterSidebarToggleRef, insertionPoint, @@ -33,8 +34,12 @@ export default function InserterSidebar() { getInsertionPoint, isPublishSidebarOpened, } = unlock( select( editorStore ) ); - const { getBlockRootClientId, __unstableGetEditorMode, getSettings } = - select( blockEditorStore ); + const { + getBlockInsertionPoint, + getBlockRootClientId, + __unstableGetEditorMode, + getSettings, + } = select( blockEditorStore ); const { get } = select( preferencesStore ); const { getActiveComplementaryArea } = select( interfaceStore ); const getBlockSectionRootClientId = () => { @@ -47,6 +52,7 @@ export default function InserterSidebar() { return getBlockRootClientId(); }; return { + blockInsertionPoint: getBlockInsertionPoint(), inserterSidebarToggleRef: getInserterSidebarToggleRef(), insertionPoint: getInsertionPoint(), showMostUsedBlocks: get( 'core', 'mostUsedBlocks' ), @@ -85,9 +91,9 @@ export default function InserterSidebar() { showInserterHelpPanel shouldFocusBlock={ isMobileViewport } rootClientId={ - blockSectionRootClientId ?? insertionPoint.rootClientId + blockSectionRootClientId ?? blockInsertionPoint.rootClientId } - __experimentalInsertionIndex={ insertionPoint.insertionIndex } + __experimentalInsertionIndex={ blockInsertionPoint.index } onSelect={ insertionPoint.onSelect } __experimentalInitialTab={ insertionPoint.tab } __experimentalInitialCategory={ insertionPoint.category } From d6a27a056c5fe5c995a96f05a10105b80c7fa838 Mon Sep 17 00:00:00 2001 From: JuanMa Date: Mon, 12 Aug 2024 15:47:54 +0100 Subject: [PATCH 15/48] DataViews: document missing action properties and link to storybook example (#64442) Co-authored-by: juanmaguitar Co-authored-by: oandregal --- packages/dataviews/README.md | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index df537d2a8cecff..56cecedaef66ca 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -30,6 +30,9 @@ const Example = () => { }; ``` +> [!TIP] +> At https://wordpress.github.io/gutenberg/?path=/docs/dataviews-dataviews--docs there's an example implementation of the Dataviews component." + ## Properties ### `data`: `Object[]` @@ -79,41 +82,37 @@ const fields = [ id: 'date', label: 'Date', render: ( { item } ) => { - return ( - - ); - } + return ; + }, }, { id: 'author', label: __( 'Author' ), render: ( { item } ) => { - return ( - { item.author } - ); + return { item.author }; }, elements: [ { value: 1, label: 'Admin' }, - { value: 2, label: 'User' } + { value: 2, label: 'User' }, ], filterBy: { - operators: [ 'is', 'isNot' ] + operators: [ 'is', 'isNot' ], }, - enableSorting: false + enableSorting: false, }, { label: __( 'Status' ), id: 'status', getValue: ( { item } ) => - STATUSES.find( ( { value } ) => value === item.status ) - ?.label ?? item.status, + STATUSES.find( ( { value } ) => value === item.status )?.label ?? + item.status, elements: STATUSES, filterBy: { operators: [ 'isAny' ], }, enableSorting: false, }, -] +]; ``` Each field is an object with the following properties: @@ -256,6 +255,8 @@ Each action is an object with the following properties: - `callback`: function, required unless `RenderModal` is provided. Callback function that takes the record as input and performs the required action. - `RenderModal`: ReactElement, optional. If an action requires that some UI be rendered in a modal, it can provide a component which takes as props the record as `item` and a `closeModal` function. When this prop is provided, the `callback` property is ignored. - `hideModalHeader`: boolean, optional. This property is used in combination with `RenderModal` and controls the visibility of the modal's header. If the action renders a modal and doesn't hide the header, the action's label is going to be used in the modal's header. +- `supportsBulk`: Whether the action can be used as a bulk action. False by default. +- `disabled`: Whether the action is disabled. False by default. ### `paginationInfo`: `Object` @@ -289,8 +290,8 @@ const defaultLayouts = { table: { layout: { primaryKey: 'my-key', - } - } + }, + }, }; ``` From 52171c992fbb9a43e69a25054e44de595ac7390d Mon Sep 17 00:00:00 2001 From: James Koster Date: Mon, 12 Aug 2024 16:03:13 +0100 Subject: [PATCH 16/48] Update DropdownMenuV2 elevation, remove unused config value (#64432) Co-authored-by: jameskoster Co-authored-by: DaniGuardiola Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + packages/components/src/dropdown-menu-v2/styles.ts | 2 +- packages/components/src/utils/config-values.js | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 96bf76815e71dd..57ba69197cfa98 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,7 @@ - `Composite`: improve Storybook examples and add interactive controls ([#64397](https://github.com/WordPress/gutenberg/pull/64397)). - `TimePicker`: add `hideLabelFromVision` prop ([#64267](https://github.com/WordPress/gutenberg/pull/64267)). +- `DropdownMenuV2`: adopt elevation scale ([#64432](https://github.com/WordPress/gutenberg/pull/64432)). ## 28.5.0 (2024-08-07) diff --git a/packages/components/src/dropdown-menu-v2/styles.ts b/packages/components/src/dropdown-menu-v2/styles.ts index ab7c763b5f4569..950a549f8566a4 100644 --- a/packages/components/src/dropdown-menu-v2/styles.ts +++ b/packages/components/src/dropdown-menu-v2/styles.ts @@ -31,7 +31,7 @@ const ITEM_PADDING_INLINE = space( 3 ); const DEFAULT_BORDER_COLOR = COLORS.gray[ 300 ]; const DIVIDER_COLOR = COLORS.gray[ 200 ]; const TOOLBAR_VARIANT_BORDER_COLOR = COLORS.gray[ '900' ]; -const DEFAULT_BOX_SHADOW = `0 0 0 ${ CONFIG.borderWidth } ${ DEFAULT_BORDER_COLOR }, ${ CONFIG.popoverShadow }`; +const DEFAULT_BOX_SHADOW = `0 0 0 ${ CONFIG.borderWidth } ${ DEFAULT_BORDER_COLOR }, ${ CONFIG.elevationXSmall }`; const TOOLBAR_VARIANT_BOX_SHADOW = `0 0 0 ${ CONFIG.borderWidth } ${ TOOLBAR_VARIANT_BORDER_COLOR }`; const GRID_TEMPLATE_COLS = 'minmax( 0, max-content ) 1fr'; diff --git a/packages/components/src/utils/config-values.js b/packages/components/src/utils/config-values.js index 65632170c9e3d7..02c6f69544c2bf 100644 --- a/packages/components/src/utils/config-values.js +++ b/packages/components/src/utils/config-values.js @@ -73,7 +73,6 @@ export default Object.assign( {}, CONTROL_PROPS, TOGGLE_GROUP_CONTROL_PROPS, { cardPaddingSmall: `${ space( 4 ) }`, cardPaddingMedium: `${ space( 4 ) } ${ space( 6 ) }`, cardPaddingLarge: `${ space( 6 ) } ${ space( 8 ) }`, - popoverShadow: `0 0.7px 1px rgba(0, 0, 0, 0.1), 0 1.2px 1.7px -0.2px rgba(0, 0, 0, 0.1), 0 2.3px 3.3px -0.5px rgba(0, 0, 0, 0.1)`, elevationXSmall: `0 0.7px 1px rgba(0, 0, 0, 0.1), 0 1.2px 1.7px -0.2px rgba(0, 0, 0, 0.1), 0 2.3px 3.3px -0.5px rgba(0, 0, 0, 0.1)`, elevationSmall: `0 0.7px 1px 0 rgba(0, 0, 0, 0.12), 0 2.2px 3.7px -0.2px rgba(0, 0, 0, 0.12), 0 5.3px 7.3px -0.5px rgba(0, 0, 0, 0.12)`, elevationMedium: `0 0.7px 1px 0 rgba(0, 0, 0, 0.14), 0 4.2px 5.7px -0.2px rgba(0, 0, 0, 0.14), 0 7.3px 9.3px -0.5px rgba(0, 0, 0, 0.14)`, From 9c96970fdfa010b2b0b710c79c13684398b792f1 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Tue, 13 Aug 2024 01:22:31 +0900 Subject: [PATCH 17/48] Add margin-bottom lint rules for BaseControl (#64355) * Fix in Gallery block * Fix in Latest Posts block * Fix in Search block * Fix in Tag Cloud block * Fix in Video block * Fix in Global Styles Font Size * Fix in Global Styles PushChangesToGlobalStylesControl * Fix in new template part modal * Fix in pattern overrides block inspector * Add lint rule * Update docs Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- .eslintrc.js | 1 + packages/block-library/src/gallery/edit.js | 5 +- packages/block-library/src/search/edit.js | 18 ++-- packages/block-library/src/search/editor.scss | 7 +- packages/block-library/src/tag-cloud/edit.js | 69 +++++++------- .../block-library/src/tag-cloud/editor.scss | 8 ++ .../src/video/edit-common-settings.js | 1 + packages/block-library/src/video/edit.js | 4 +- .../components/src/base-control/README.md | 7 +- .../components/src/base-control/index.tsx | 89 ++++++++++--------- .../global-styles/size-control/index.js | 8 +- .../push-changes-to-global-styles/index.js | 1 + .../create-template-part-modal/index.js | 1 + .../components/pattern-overrides-controls.js | 1 + 14 files changed, 128 insertions(+), 92 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index cb669fb4177206..6143d62c28bc07 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -290,6 +290,7 @@ module.exports = { ...restrictedSyntax, ...restrictedSyntaxComponents, ...[ + 'BaseControl', 'CheckboxControl', 'ComboboxControl', 'DimensionControl', diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index 1a7ce5fae04f8c..c73fe1977ce4cc 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -602,7 +602,10 @@ export default function GalleryEdit( props ) { /> ) } { Platform.isWeb && ! imageSizeOptions && hasImageIds && ( - + { __( 'Resolution' ) } diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index cfe7b29caf5de0..e2f3bb3999e42c 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -28,7 +28,7 @@ import { ToolbarButton, ResizableBox, PanelBody, - BaseControl, + __experimentalVStack as VStack, __experimentalUseCustomUnits as useCustomUnits, __experimentalUnitControl as UnitControl, } from '@wordpress/components'; @@ -408,12 +408,14 @@ export default function SearchEdit( { - 100 ? 100 : newWidth; - setAttributes( { width: parseInt( filteredWidth, 10 ), } ); @@ -445,9 +446,8 @@ export default function SearchEdit( { value={ `${ width }${ widthUnit }` } units={ units } /> - { [ 25, 50, 75, 100 ].map( ( widthValue ) => { @@ -473,7 +473,7 @@ export default function SearchEdit( { ); } ) } - + diff --git a/packages/block-library/src/search/editor.scss b/packages/block-library/src/search/editor.scss index 35ccfc5e633fc2..ecc244d3341e1e 100644 --- a/packages/block-library/src/search/editor.scss +++ b/packages/block-library/src/search/editor.scss @@ -15,8 +15,11 @@ justify-content: center; text-align: center; } +} - &__components-button-group { - margin-top: 10px; +.wp-block-search__inspector-controls { + .components-base-control { + // Counteract the margin added by the block inspector. + margin-bottom: 0; } } diff --git a/packages/block-library/src/tag-cloud/edit.js b/packages/block-library/src/tag-cloud/edit.js index 8ced99271e4807..eeb568e7a89ef1 100644 --- a/packages/block-library/src/tag-cloud/edit.js +++ b/packages/block-library/src/tag-cloud/edit.js @@ -11,8 +11,8 @@ import { __experimentalUnitControl as UnitControl, __experimentalUseCustomUnits as useCustomUnits, __experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue, + __experimentalVStack as VStack, Disabled, - BaseControl, } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; @@ -119,17 +119,20 @@ function TagCloudEdit( { attributes, setAttributes } ) { const inspectorControls = ( - - setAttributes( { taxonomy: selectedTaxonomy } ) - } - /> - + + + setAttributes( { taxonomy: selectedTaxonomy } ) + } + /> - - - setAttributes( { numberOfTags: value } ) - } - min={ MIN_TAGS } - max={ MAX_TAGS } - required - /> - - setAttributes( { showTagCounts: ! showTagCounts } ) - } - /> + + setAttributes( { numberOfTags: value } ) + } + min={ MIN_TAGS } + max={ MAX_TAGS } + required + /> + + setAttributes( { showTagCounts: ! showTagCounts } ) + } + /> + ); diff --git a/packages/block-library/src/tag-cloud/editor.scss b/packages/block-library/src/tag-cloud/editor.scss index d00a450174f2fd..e85129e22f1aca 100644 --- a/packages/block-library/src/tag-cloud/editor.scss +++ b/packages/block-library/src/tag-cloud/editor.scss @@ -9,3 +9,11 @@ border: none; border-radius: inherit; } + +.wp-block-tag-cloud__inspector-settings { + .components-base-control, + .components-base-control:last-child { + // Cancel out extra margins added by block inspector + margin-bottom: 0; + } +} diff --git a/packages/block-library/src/video/edit-common-settings.js b/packages/block-library/src/video/edit-common-settings.js index 5ebf431ade3fc0..9394bfaf5c6145 100644 --- a/packages/block-library/src/video/edit-common-settings.js +++ b/packages/block-library/src/video/edit-common-settings.js @@ -83,6 +83,7 @@ const VideoSettings = ( { setAttributes, attributes } ) => { ) } /> - +
{ __( 'Poster image' ) } @@ -265,7 +265,7 @@ function VideoEdit( { { __( 'Remove' ) } ) } - +
diff --git a/packages/components/src/base-control/README.md b/packages/components/src/base-control/README.md index dc3d8c0e29c8e0..d51629de6f7253 100644 --- a/packages/components/src/base-control/README.md +++ b/packages/components/src/base-control/README.md @@ -15,7 +15,7 @@ const MyCustomTextareaControl = ({ children, ...baseProps }) => ( const { baseControlProps, controlProps } = useBaseControlProps( baseProps ); return ( - + @@ -92,7 +92,10 @@ It should only be used in cases where the children being rendered inside BaseCon import { BaseControl } from '@wordpress/components'; const MyBaseControl = () => ( - + Author diff --git a/packages/components/src/base-control/index.tsx b/packages/components/src/base-control/index.tsx index 14ecce1bdd729d..77899b6480daed 100644 --- a/packages/components/src/base-control/index.tsx +++ b/packages/components/src/base-control/index.tsx @@ -26,29 +26,6 @@ import { contextConnectWithoutRef, useContextSystem } from '../context'; export { useBaseControlProps } from './hooks'; -/** - * `BaseControl` is a component used to generate labels and help text for components handling user inputs. - * - * ```jsx - * import { BaseControl, useBaseControlProps } from '@wordpress/components'; - * - * // Render a `BaseControl` for a textarea input - * const MyCustomTextareaControl = ({ children, ...baseProps }) => ( - * // `useBaseControlProps` is a convenience hook to get the props for the `BaseControl` - * // and the inner control itself. Namely, it takes care of generating a unique `id`, - * // properly associating it with the `label` and `help` elements. - * const { baseControlProps, controlProps } = useBaseControlProps( baseProps ); - * - * return ( - * - * - * - * ); - * ); - * ``` - */ const UnconnectedBaseControl = ( props: WordPressComponentProps< BaseControlProps, null > ) => { @@ -105,23 +82,6 @@ const UnconnectedBaseControl = ( ); }; -/** - * `BaseControl.VisualLabel` is used to render a purely visual label inside a `BaseControl` component. - * - * It should only be used in cases where the children being rendered inside `BaseControl` are already accessibly labeled, - * e.g., a button, but we want an additional visual label for that section equivalent to the labels `BaseControl` would - * otherwise use if the `label` prop was passed. - * - * @example - * import { BaseControl } from '@wordpress/components'; - * - * const MyBaseControl = () => ( - * - * Author - * - * - * ); - */ const UnforwardedVisualLabel = ( props: WordPressComponentProps< BaseControlVisualLabelProps, 'span' >, ref: ForwardedRef< any > @@ -141,9 +101,56 @@ const UnforwardedVisualLabel = ( export const VisualLabel = forwardRef( UnforwardedVisualLabel ); +/** + * `BaseControl` is a component used to generate labels and help text for components handling user inputs. + * + * ```jsx + * import { BaseControl, useBaseControlProps } from '@wordpress/components'; + * + * // Render a `BaseControl` for a textarea input + * const MyCustomTextareaControl = ({ children, ...baseProps }) => ( + * // `useBaseControlProps` is a convenience hook to get the props for the `BaseControl` + * // and the inner control itself. Namely, it takes care of generating a unique `id`, + * // properly associating it with the `label` and `help` elements. + * const { baseControlProps, controlProps } = useBaseControlProps( baseProps ); + * + * return ( + * + * + * + * ); + * ); + * ``` + */ export const BaseControl = Object.assign( contextConnectWithoutRef( UnconnectedBaseControl, 'BaseControl' ), - { VisualLabel } + + { + /** + * `BaseControl.VisualLabel` is used to render a purely visual label inside a `BaseControl` component. + * + * It should only be used in cases where the children being rendered inside `BaseControl` are already accessibly labeled, + * e.g., a button, but we want an additional visual label for that section equivalent to the labels `BaseControl` would + * otherwise use if the `label` prop was passed. + * + * ```jsx + * import { BaseControl } from '@wordpress/components'; + * + * const MyBaseControl = () => ( + * + * Author + * + * + * ); + * ``` + */ + VisualLabel, + } ); export default BaseControl; diff --git a/packages/edit-site/src/components/global-styles/size-control/index.js b/packages/edit-site/src/components/global-styles/size-control/index.js index a7e7bd6127a5fb..28fa64c643cbbd 100644 --- a/packages/edit-site/src/components/global-styles/size-control/index.js +++ b/packages/edit-site/src/components/global-styles/size-control/index.js @@ -20,7 +20,11 @@ import { const DEFAULT_UNITS = [ 'px', 'em', 'rem', 'vw', 'vh' ]; -function SizeControl( props ) { +function SizeControl( { + // Do not allow manipulation of margin bottom + __nextHasNoMarginBottom, + ...props +} ) { const { baseControlProps } = useBaseControlProps( props ); const { value, onChange, fallbackValue, disabled } = props; @@ -45,7 +49,7 @@ function SizeControl( props ) { }; return ( - + Date: Mon, 12 Aug 2024 19:34:49 +0200 Subject: [PATCH 18/48] fix typo in block-filters.md (#64452) https://github.com/WordPress/gutenberg/pull/64447#pullrequestreview-2233534636 --- docs/reference-guides/filters/block-filters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index c70bb356a445a8..637cecadf1402b 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -139,7 +139,7 @@ The following PHP filters are available to change the output of a block on the f ### `render_block` -Filters the font-end content of any block. This filter has no impact on the behavior of blocks in the Editor. +Filters the front-end content of any block. This filter has no impact on the behavior of blocks in the Editor. The callback function for this filter receives three parameters: @@ -172,7 +172,7 @@ add_filter( 'render_block', 'example_add_custom_class_to_paragraph_block', 10, 2 ### `render_block_{namespace/block}` -Filters the font-end content of the defined block. This is just a simpler form of `render_block` when you only need to modify a specific block type. +Filters the front-end content of the defined block. This is just a simpler form of `render_block` when you only need to modify a specific block type. The callback function for this filter receives three parameters: From 47138724bf9e6abc67f448e3403428d431f2a1b9 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:33:25 +0900 Subject: [PATCH 19/48] In-between Inserter: Show inserter when it doesn't conflict with block toolbar (#64229) Unlinked contributors: digitalex11. Co-authored-by: t-hamano Co-authored-by: Mamaduka Co-authored-by: ellatrix Co-authored-by: annezazu Co-authored-by: talldan Co-authored-by: hanneslsm --- .../block-list/use-in-between-inserter.js | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/block-list/use-in-between-inserter.js b/packages/block-editor/src/components/block-list/use-in-between-inserter.js index 74151fb3b070ba..bb307816fd1501 100644 --- a/packages/block-editor/src/components/block-list/use-in-between-inserter.js +++ b/packages/block-editor/src/components/block-list/use-in-between-inserter.js @@ -25,6 +25,7 @@ export function useInBetweenInserter() { getBlockIndex, isMultiSelecting, getSelectedBlockClientIds, + getSettings, getTemplateLock, __unstableIsWithinBlockOverlay, getBlockEditingMode, @@ -88,9 +89,11 @@ export function useInBetweenInserter() { return; } + const blockListSettings = getBlockListSettings( rootClientId ); const orientation = - getBlockListSettings( rootClientId )?.orientation || - 'vertical'; + blockListSettings?.orientation || 'vertical'; + const captureToolbars = + !! blockListSettings?.__experimentalCaptureToolbars; const offsetTop = event.clientY; const offsetLeft = event.clientX; @@ -135,9 +138,18 @@ export function useInBetweenInserter() { return; } - // Don't show the inserter when hovering above (conflicts with - // block toolbar) or inside selected block(s). - if ( getSelectedBlockClientIds().includes( clientId ) ) { + // Don't show the inserter if the following conditions are met, + // as it conflicts with the block toolbar: + // 1. when hovering above or inside selected block(s) + // 2. when the orientation is vertical + // 3. when the __experimentalCaptureToolbars is not enabled + // 4. when the Top Toolbar is not disabled + if ( + getSelectedBlockClientIds().includes( clientId ) && + orientation === 'vertical' && + ! captureToolbars && + ! getSettings().hasFixedToolbar + ) { return; } const elementRect = element.getBoundingClientRect(); From 344d61e5df74411283014bcbb5dbbed0b54f000a Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 13 Aug 2024 08:48:19 +0400 Subject: [PATCH 20/48] Block Editor: Use hooks instead of HoC in 'BlockModeToggle' (#64460) Co-authored-by: Mamaduka Co-authored-by: t-hamano --- .../block-settings-menu/block-mode-toggle.js | 59 +++++++++---------- .../test/block-mode-toggle.js | 49 +++++++-------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/packages/block-editor/src/components/block-settings-menu/block-mode-toggle.js b/packages/block-editor/src/components/block-settings-menu/block-mode-toggle.js index 6810a21581f12e..7ca294a2894158 100644 --- a/packages/block-editor/src/components/block-settings-menu/block-mode-toggle.js +++ b/packages/block-editor/src/components/block-settings-menu/block-mode-toggle.js @@ -4,8 +4,7 @@ import { __ } from '@wordpress/i18n'; import { MenuItem } from '@wordpress/components'; import { getBlockType, hasBlockSupport } from '@wordpress/blocks'; -import { withSelect, withDispatch } from '@wordpress/data'; -import { compose } from '@wordpress/compose'; +import { useDispatch, useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -14,13 +13,23 @@ import { store as blockEditorStore } from '../../store'; const noop = () => {}; -export function BlockModeToggle( { - blockType, - mode, - onToggleMode, - small = false, - isCodeEditingEnabled = true, -} ) { +export default function BlockModeToggle( { clientId, onToggle = noop } ) { + const { blockType, mode, isCodeEditingEnabled } = useSelect( + ( select ) => { + const { getBlock, getBlockMode, getSettings } = + select( blockEditorStore ); + const block = getBlock( clientId ); + + return { + mode: getBlockMode( clientId ), + blockType: block ? getBlockType( block.name ) : null, + isCodeEditingEnabled: getSettings().codeEditingEnabled, + }; + }, + [ clientId ] + ); + const { toggleBlockMode } = useDispatch( blockEditorStore ); + if ( ! blockType || ! hasBlockSupport( blockType, 'html', true ) || @@ -32,26 +41,14 @@ export function BlockModeToggle( { const label = mode === 'visual' ? __( 'Edit as HTML' ) : __( 'Edit visually' ); - return { ! small && label }; + return ( + { + toggleBlockMode( clientId ); + onToggle(); + } } + > + { label } + + ); } - -export default compose( [ - withSelect( ( select, { clientId } ) => { - const { getBlock, getBlockMode, getSettings } = - select( blockEditorStore ); - const block = getBlock( clientId ); - const isCodeEditingEnabled = getSettings().codeEditingEnabled; - - return { - mode: getBlockMode( clientId ), - blockType: block ? getBlockType( block.name ) : null, - isCodeEditingEnabled, - }; - } ), - withDispatch( ( dispatch, { onToggle = noop, clientId } ) => ( { - onToggleMode() { - dispatch( blockEditorStore ).toggleBlockMode( clientId ); - onToggle(); - }, - } ) ), -] )( BlockModeToggle ); diff --git a/packages/block-editor/src/components/block-settings-menu/test/block-mode-toggle.js b/packages/block-editor/src/components/block-settings-menu/test/block-mode-toggle.js index c297bad15f29ea..67d88125e3429c 100644 --- a/packages/block-editor/src/components/block-settings-menu/test/block-mode-toggle.js +++ b/packages/block-editor/src/components/block-settings-menu/test/block-mode-toggle.js @@ -3,16 +3,32 @@ */ import { render, screen } from '@testing-library/react'; +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + /** * Internal dependencies */ -import { BlockModeToggle } from '../block-mode-toggle'; +import BlockModeToggle from '../block-mode-toggle'; + +jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() ); + +function setupUseSelectMock( mode, blockType, codeEditingEnabled = true ) { + useSelect.mockImplementation( () => { + return { + mode, + blockType, + isCodeEditingEnabled: codeEditingEnabled, + }; + } ); +} describe( 'BlockModeToggle', () => { it( "should not render the HTML mode button if the block doesn't support it", () => { - render( - - ); + setupUseSelectMock( undefined, { supports: { html: false } } ); + render( ); expect( screen.queryByRole( 'menuitem', { name: 'Edit as HTML' } ) @@ -20,12 +36,8 @@ describe( 'BlockModeToggle', () => { } ); it( 'should render the HTML mode button', () => { - render( - - ); + setupUseSelectMock( 'visual', { supports: { html: true } } ); + render( ); expect( screen.getByRole( 'menuitem', { name: 'Edit as HTML' } ) @@ -33,12 +45,8 @@ describe( 'BlockModeToggle', () => { } ); it( 'should render the Visual mode button', () => { - render( - - ); + setupUseSelectMock( 'html', { supports: { html: true } } ); + render( ); expect( screen.getByRole( 'menuitem', { name: 'Edit visually' } ) @@ -46,13 +54,8 @@ describe( 'BlockModeToggle', () => { } ); it( 'should not render the Visual mode button if code editing is disabled', () => { - render( - - ); + setupUseSelectMock( 'html', { supports: { html: true } }, false ); + render( ); expect( screen.queryByRole( 'menuitem', { name: 'Edit visually' } ) From 6445ff13273b7bdbf5315c1394b5aeda8011e47c Mon Sep 17 00:00:00 2001 From: Robert Lee Date: Mon, 12 Aug 2024 22:46:36 -0700 Subject: [PATCH 21/48] WPCompleter: Restrict block list to allowed blocks only (#64413) Unlinked contributors: ssang. Co-authored-by: talldan --- packages/block-editor/src/autocompleters/block.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/autocompleters/block.js b/packages/block-editor/src/autocompleters/block.js index bc06c9de5aaaff..859ae11036c82a 100644 --- a/packages/block-editor/src/autocompleters/block.js +++ b/packages/block-editor/src/autocompleters/block.js @@ -60,7 +60,8 @@ function createBlockCompleter() { }, [] ); const [ items, categories, collections ] = useBlockTypesState( rootClientId, - noop + noop, + true ); const filteredItems = useMemo( () => { From 476c78b36e0ac1af0f1cbec75507a6b288c4df29 Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 13 Aug 2024 14:15:23 +0800 Subject: [PATCH 22/48] Fix bumped specificity for layout styles in non-iframed editor (#64076) * Fix too specific layout styles in non-iframed editor * Ensure first/last child rules take precedence * Adjust selectors so that `> :first-child`/`> :last-child` still has 0,2,0 specificity to override theme.json spacing * Update tests * Update client side layout selectors to match theme json * Add backport changelog ---- Co-authored-by: talldan Co-authored-by: andrewserong Co-authored-by: ramonjd Co-authored-by: aaronrobertshaw --- backport-changelog/6.6/7145.md | 3 +++ lib/class-wp-theme-json-gutenberg.php | 2 +- .../global-styles/test/use-global-styles-output.js | 6 +++--- .../global-styles/use-global-styles-output.js | 4 ++-- phpunit/class-wp-theme-json-test.php | 11 +++++------ 5 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 backport-changelog/6.6/7145.md diff --git a/backport-changelog/6.6/7145.md b/backport-changelog/6.6/7145.md new file mode 100644 index 00000000000000..386f765cb22fa8 --- /dev/null +++ b/backport-changelog/6.6/7145.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7145 + +* https://github.com/WordPress/gutenberg/pull/64076 diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 7558438dcbe745..20ea31090407b4 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -1744,7 +1744,7 @@ protected function get_layout_styles( $block_metadata, $types = array() ) { $spacing_rule['selector'] ); } else { - $format = static::ROOT_BLOCK_SELECTOR === $selector ? '.%2$s %3$s' : '%1$s-%2$s %3$s'; + $format = static::ROOT_BLOCK_SELECTOR === $selector ? ':root :where(.%2$s)%3$s' : ':root :where(%1$s-%2$s)%3$s'; $layout_selector = sprintf( $format, $selector, diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index e2530fdb85f812..1b061f6921f2c2 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -763,7 +763,7 @@ describe( 'global styles renderer', () => { } ); expect( layoutStyles ).toEqual( - '.is-layout-flow > * { margin-block-start: 0; margin-block-end: 0; }.is-layout-flow > * + * { margin-block-start: 0.5em; margin-block-end: 0; }.is-layout-flex { gap: 0.5em; }:root { --wp--style--block-gap: 0.5em; }.is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-flex { display:flex; }.is-layout-flex { flex-wrap: wrap; align-items: center; }.is-layout-flex > * { margin: 0; }' + ':root :where(.is-layout-flow) > * { margin-block-start: 0; margin-block-end: 0; }:root :where(.is-layout-flow) > * + * { margin-block-start: 0.5em; margin-block-end: 0; }:root :where(.is-layout-flex) { gap: 0.5em; }:root { --wp--style--block-gap: 0.5em; }.is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-flex { display:flex; }.is-layout-flex { flex-wrap: wrap; align-items: center; }.is-layout-flex > * { margin: 0; }' ); } ); @@ -780,7 +780,7 @@ describe( 'global styles renderer', () => { } ); expect( layoutStyles ).toEqual( - '.is-layout-flow > * { margin-block-start: 0; margin-block-end: 0; }.is-layout-flow > * + * { margin-block-start: 12px; margin-block-end: 0; }.is-layout-flex { gap: 12px; }:root { --wp--style--block-gap: 12px; }.is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-flex { display:flex; }.is-layout-flex { flex-wrap: wrap; align-items: center; }.is-layout-flex > * { margin: 0; }' + ':root :where(.is-layout-flow) > * { margin-block-start: 0; margin-block-end: 0; }:root :where(.is-layout-flow) > * + * { margin-block-start: 12px; margin-block-end: 0; }:root :where(.is-layout-flex) { gap: 12px; }:root { --wp--style--block-gap: 12px; }.is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }.is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }.is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-flex { display:flex; }.is-layout-flex { flex-wrap: wrap; align-items: center; }.is-layout-flex > * { margin: 0; }' ); } ); @@ -797,7 +797,7 @@ describe( 'global styles renderer', () => { } ); expect( layoutStyles ).toEqual( - '.wp-block-group-is-layout-flow > * { margin-block-start: 0; margin-block-end: 0; }.wp-block-group-is-layout-flow > * + * { margin-block-start: 12px; margin-block-end: 0; }.wp-block-group-is-layout-flex { gap: 12px; }' + ':root :where(.wp-block-group-is-layout-flow) > * { margin-block-start: 0; margin-block-end: 0; }:root :where(.wp-block-group-is-layout-flow) > * + * { margin-block-start: 12px; margin-block-end: 0; }:root :where(.wp-block-group-is-layout-flex) { gap: 12px; }' ); } ); diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 9190733d5b6607..c1449d6d8b298f 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -538,10 +538,10 @@ export function getLayoutStyles( { } else { combinedSelector = selector === ROOT_BLOCK_SELECTOR - ? `.${ className }${ + ? `:root :where(.${ className })${ spacingStyle?.selector || '' }` - : `${ selector }-${ className }${ + : `:root :where(${ selector }-${ className })${ spacingStyle?.selector || '' }`; } diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index d366e0df44c4a2..3f11dd97a6688f 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -714,7 +714,7 @@ public function test_get_stylesheet_renders_enabled_protected_properties() { ) ); - $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: 1em;margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: 1em;margin-block-end: 0;}.is-layout-flex {gap: 1em;}.is-layout-grid {gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; + $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1em;}:root :where(.is-layout-grid){gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; $this->assertSameCSS( $expected, $theme_json->get_stylesheet() ); $this->assertSameCSS( $expected, $theme_json->get_stylesheet( array( 'styles' ) ) ); } @@ -1131,7 +1131,7 @@ public function test_get_stylesheet_generates_layout_styles() { // Results also include root site blocks styles. $this->assertSameCSS( - ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: 1em;margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: 1em;margin-block-end: 0;}.is-layout-flex {gap: 1em;}.is-layout-grid {gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}', + ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1em; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1em; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1em;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1em;}:root :where(.is-layout-grid){gap: 1em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}', $theme_json->get_stylesheet( array( 'styles' ) ) ); } @@ -1160,7 +1160,7 @@ public function test_get_stylesheet_generates_layout_styles_with_spacing_presets // Results also include root site blocks styles. $this->assertSameCSS( - ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: var(--wp--preset--spacing--60); margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: var(--wp--preset--spacing--60); }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: var(--wp--preset--spacing--60);margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: var(--wp--preset--spacing--60);margin-block-end: 0;}.is-layout-flex {gap: var(--wp--preset--spacing--60);}.is-layout-grid {gap: var(--wp--preset--spacing--60);}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}', + ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: var(--wp--preset--spacing--60); margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: var(--wp--preset--spacing--60); }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: var(--wp--preset--spacing--60);margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: var(--wp--preset--spacing--60);margin-block-end: 0;}:root :where(.is-layout-flex){gap: var(--wp--preset--spacing--60);}:root :where(.is-layout-grid){gap: var(--wp--preset--spacing--60);}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}', $theme_json->get_stylesheet( array( 'styles' ) ) ); } @@ -1284,8 +1284,7 @@ public function test_get_stylesheet_generates_valid_block_gap_values_and_skips_n ); $this->assertSameCSS( - ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1rem; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1rem; }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: 1rem;margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: 1rem;margin-block-end: 0;}.is-layout-flex {gap: 1rem;}.is-layout-grid {gap: 1rem;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}' . - ':root :where(.wp-block-post-content){color: gray;}.wp-block-social-links-is-layout-flow > :first-child{margin-block-start: 0;}.wp-block-social-links-is-layout-flow > :last-child{margin-block-end: 0;}.wp-block-social-links-is-layout-flow > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-social-links-is-layout-constrained > :first-child{margin-block-start: 0;}.wp-block-social-links-is-layout-constrained > :last-child{margin-block-end: 0;}.wp-block-social-links-is-layout-constrained > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-social-links-is-layout-flex {gap: 0;}.wp-block-social-links-is-layout-grid {gap: 0;}.wp-block-buttons-is-layout-flow > :first-child{margin-block-start: 0;}.wp-block-buttons-is-layout-flow > :last-child{margin-block-end: 0;}.wp-block-buttons-is-layout-flow > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-buttons-is-layout-constrained > :first-child{margin-block-start: 0;}.wp-block-buttons-is-layout-constrained > :last-child{margin-block-end: 0;}.wp-block-buttons-is-layout-constrained > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-buttons-is-layout-flex {gap: 0;}.wp-block-buttons-is-layout-grid {gap: 0;}', + ':root { --wp--style--global--content-size: 640px;--wp--style--global--wide-size: 1200px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1rem; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1rem; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1rem;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1rem;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1rem;}:root :where(.is-layout-grid){gap: 1rem;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}:root :where(.wp-block-post-content){color: gray;}:root :where(.wp-block-social-links-is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.wp-block-social-links-is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-flow) > *{margin-block-start: 0;margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.wp-block-social-links-is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-constrained) > *{margin-block-start: 0;margin-block-end: 0;}:root :where(.wp-block-social-links-is-layout-flex){gap: 0;}:root :where(.wp-block-social-links-is-layout-grid){gap: 0;}:root :where(.wp-block-buttons-is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.wp-block-buttons-is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-flow) > *{margin-block-start: 0;margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-constrained) > *{margin-block-start: 0;margin-block-end: 0;}:root :where(.wp-block-buttons-is-layout-flex){gap: 0;}:root :where(.wp-block-buttons-is-layout-grid){gap: 0;}', $theme_json->get_stylesheet() ); } @@ -3690,7 +3689,7 @@ public function test_get_styles_with_appearance_tools() { 'selector' => 'body', ); - $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: ; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: ; }.is-layout-flow > :first-child{margin-block-start: 0;}.is-layout-flow > :last-child{margin-block-end: 0;}.is-layout-flow > *{margin-block-start: 1;margin-block-end: 0;}.is-layout-constrained > :first-child{margin-block-start: 0;}.is-layout-constrained > :last-child{margin-block-end: 0;}.is-layout-constrained > *{margin-block-start: 1;margin-block-end: 0;}.is-layout-flex {gap: 1;}.is-layout-grid {gap: 1;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; + $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: ; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: ; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1;}:root :where(.is-layout-grid){gap: 1;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; $this->assertSameCSS( $expected, $theme_json->get_root_layout_rules( WP_Theme_JSON_Gutenberg::ROOT_BLOCK_SELECTOR, $metadata ) ); } From 33093652ea76c4463e78252bb53f2f331a81610c Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Tue, 13 Aug 2024 08:29:48 +0100 Subject: [PATCH 23/48] Focus pattern inserter search when activating zoom out inserter (#64396) * Add new search focus option for inserter panel * Try focusing when inserter clicked in Zoom Out * Store search input ref in block editor settings * Remove legacy selector * Remove more legacy code * Move all ref selectors to block editor * Update packages/editor/src/components/inserter-sidebar/index.js --------- Co-authored-by: Ben Dwyer Co-authored-by: getdave Co-authored-by: scruffian --- .../block-tools/zoom-out-mode-inserters.js | 8 ++++- .../src/components/inserter/library.js | 2 ++ .../src/components/inserter/menu.js | 34 ++++++++++++------- .../src/store/private-selectors.js | 4 +++ packages/block-editor/src/store/reducer.js | 5 +++ 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js b/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js index bb044f9479c024..6f986ce90dc3bd 100644 --- a/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js +++ b/packages/block-editor/src/components/block-tools/zoom-out-mode-inserters.js @@ -23,6 +23,7 @@ function ZoomOutModeInserters() { sectionRootClientId, selectedBlockClientId, hoveredBlockClientId, + inserterSearchInputRef, } = useSelect( ( select ) => { const { getSettings, @@ -32,8 +33,11 @@ function ZoomOutModeInserters() { getSelectedBlockClientId, getHoveredBlockClientId, isBlockInsertionPointVisible, - } = select( blockEditorStore ); + getInserterSearchInputRef, + } = unlock( select( blockEditorStore ) ); + const { sectionRootClientId: root } = unlock( getSettings() ); + return { hasSelection: !! getSelectionStart().clientId, blockInsertionPoint: getBlockInsertionPoint(), @@ -44,6 +48,7 @@ function ZoomOutModeInserters() { getSettings().__experimentalSetIsInserterOpened, selectedBlockClientId: getSelectedBlockClientId(), hoveredBlockClientId: getHoveredBlockClientId(), + inserterSearchInputRef: getInserterSearchInputRef(), }; }, [] ); @@ -110,6 +115,7 @@ function ZoomOutModeInserters() { showInsertionPoint( sectionRootClientId, index, { operation: 'insert', } ); + inserterSearchInputRef?.current?.focus(); } } /> ) } diff --git a/packages/block-editor/src/components/inserter/library.js b/packages/block-editor/src/components/inserter/library.js index 4e10a051996a9f..fe14d48bb4016b 100644 --- a/packages/block-editor/src/components/inserter/library.js +++ b/packages/block-editor/src/components/inserter/library.js @@ -27,6 +27,7 @@ function InserterLibrary( onSelect = noop, shouldFocusBlock = false, onClose, + __experimentalSearchInputRef, }, ref ) { @@ -58,6 +59,7 @@ function InserterLibrary( shouldFocusBlock={ shouldFocusBlock } ref={ ref } onClose={ onClose } + __experimentalSearchInputRef={ __experimentalSearchInputRef } /> ); } diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index 52e4e3062e9024..c5f41f9e3bf0a0 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -33,6 +33,7 @@ import useInsertionPoint from './hooks/use-insertion-point'; import { store as blockEditorStore } from '../../store'; import TabbedSidebar from '../tabbed-sidebar'; import { useZoomOut } from '../../hooks/use-zoom-out'; +import { unlock } from '../../lock-unlock'; const NOOP = () => {}; function InserterMenu( @@ -53,11 +54,16 @@ function InserterMenu( }, ref ) { - const isZoomOutMode = useSelect( - ( select ) => - select( blockEditorStore ).__unstableGetEditorMode() === 'zoom-out', - [] - ); + const { isZoomOutMode, inserterSearchInputRef } = useSelect( ( select ) => { + const { __unstableGetEditorMode, getInserterSearchInputRef } = unlock( + select( blockEditorStore ) + ); + return { + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + inserterSearchInputRef: getInserterSearchInputRef(), + }; + }, [] ); + const [ filterValue, setFilterValue, delayedFilterValue ] = useDebouncedInput( __experimentalFilterValue ); const [ hoveredItem, setHoveredItem ] = useState( null ); @@ -104,7 +110,7 @@ function InserterMenu( } } ); }, - [ onInsertBlocks, onSelect, shouldFocusBlock ] + [ onInsertBlocks, onSelect, ref, shouldFocusBlock ] ); const onInsertPattern = useCallback( @@ -113,7 +119,7 @@ function InserterMenu( onInsertBlocks( blocks, { patternName } ); onSelect(); }, - [ onInsertBlocks, onSelect ] + [ onInsertBlocks, onSelect, onToggleInsertionPoint ] ); const onHover = useCallback( @@ -164,7 +170,9 @@ function InserterMenu( value={ filterValue } label={ __( 'Search for blocks and patterns' ) } placeholder={ __( 'Search' ) } + ref={ inserterSearchInputRef } /> + { !! delayedFilterValue && ( { diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index c534c65b8defe7..dcd315a0ae2804 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -511,3 +511,7 @@ export function getTemporarilyEditingAsBlocks( state ) { export function getTemporarilyEditingFocusModeToRevert( state ) { return state.temporarilyEditingFocusModeRevert; } + +export function getInserterSearchInputRef( state ) { + return state.inserterSearchInputRef; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index 91d853a50a6a42..d9352670776371 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -2087,6 +2087,10 @@ export function hoveredBlockClientId( state = false, action ) { return state; } +export function inserterSearchInputRef( state = { current: null } ) { + return state; +} + const combinedReducers = combineReducers( { blocks, isDragging, @@ -2120,6 +2124,7 @@ const combinedReducers = combineReducers( { openedBlockSettingsMenu, registeredInserterMediaCategories, hoveredBlockClientId, + inserterSearchInputRef, } ); function withAutomaticChangeReset( reducer ) { From 27e441209db11f384e484f5a71453143c64c9b4a Mon Sep 17 00:00:00 2001 From: Daniel Richards Date: Tue, 13 Aug 2024 16:00:20 +0800 Subject: [PATCH 24/48] Update postcss-prefixwrap dependency to 1.51.0 to fix prefixing in `:where` selectors (#64458) * Update postcss-prefixwrap dependency to 1.51.0 to fix prefixing in :where selectors * Add extra test for :where with a pseudo selector ---- Co-authored-by: talldan Co-authored-by: aaronrobertshaw Co-authored-by: andrewserong Co-authored-by: andreiglingeanu --- package-lock.json | 31 ++++++------ packages/block-editor/package.json | 2 +- .../src/utils/test/transform-styles.js | 49 +++++++++++++++++++ 3 files changed, 66 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 66c32076d3b364..471d7b845d24f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41754,14 +41754,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, - "node_modules/postcss-prefixwrap": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.41.0.tgz", - "integrity": "sha512-gmwwAEE+ci3/ZKjUZppTETINlh1QwihY8gCstInuS7ohk353KYItU4d64hvnUvO2GUy29hBGPHz4Ce+qJRi90A==", - "peerDependencies": { - "postcss": "*" - } - }, "node_modules/postcss-reduce-initial": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", @@ -52237,7 +52229,7 @@ "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.41.0", + "postcss-prefixwrap": "^1.51.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", @@ -52252,6 +52244,15 @@ "react-dom": "^18.0.0" } }, + "packages/block-editor/node_modules/postcss-prefixwrap": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.51.0.tgz", + "integrity": "sha512-PuP4md5zFSY921dUcLShwSLv2YyyDec0dK9/puXl/lu7ZNvJ1U59+ZEFRMS67xwfNg5nIIlPXnAycPJlhA/Isw==", + "license": "MIT", + "peerDependencies": { + "postcss": "*" + } + }, "packages/block-editor/node_modules/postcss-urlrebase": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/postcss-urlrebase/-/postcss-urlrebase-1.4.0.tgz", @@ -67255,13 +67256,18 @@ "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.41.0", + "postcss-prefixwrap": "^1.51.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", "remove-accents": "^0.5.0" }, "dependencies": { + "postcss-prefixwrap": { + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.51.0.tgz", + "integrity": "sha512-PuP4md5zFSY921dUcLShwSLv2YyyDec0dK9/puXl/lu7ZNvJ1U59+ZEFRMS67xwfNg5nIIlPXnAycPJlhA/Isw==" + }, "postcss-urlrebase": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/postcss-urlrebase/-/postcss-urlrebase-1.4.0.tgz", @@ -87820,11 +87826,6 @@ } } }, - "postcss-prefixwrap": { - "version": "1.41.0", - "resolved": "https://registry.npmjs.org/postcss-prefixwrap/-/postcss-prefixwrap-1.41.0.tgz", - "integrity": "sha512-gmwwAEE+ci3/ZKjUZppTETINlh1QwihY8gCstInuS7ohk353KYItU4d64hvnUvO2GUy29hBGPHz4Ce+qJRi90A==" - }, "postcss-reduce-initial": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.0.0.tgz", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 8ccaee6f0a955c..02765376e314b6 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -73,7 +73,7 @@ "fast-deep-equal": "^3.1.3", "memize": "^2.1.0", "postcss": "^8.4.21", - "postcss-prefixwrap": "^1.41.0", + "postcss-prefixwrap": "^1.51.0", "postcss-urlrebase": "^1.4.0", "react-autosize-textarea": "^7.1.0", "react-easy-crop": "^5.0.6", diff --git a/packages/block-editor/src/utils/test/transform-styles.js b/packages/block-editor/src/utils/test/transform-styles.js index 8245ce62831078..b0c6ca48deb355 100644 --- a/packages/block-editor/src/utils/test/transform-styles.js +++ b/packages/block-editor/src/utils/test/transform-styles.js @@ -125,6 +125,21 @@ describe( 'transformStyles', () => { expect( output ).toMatchSnapshot(); } ); + it( `should not try to replace 'body' in the middle of a classname`, () => { + const prefix = '.my-namespace'; + const input = `.has-body-text { color: red; }`; + const output = transformStyles( + [ + { + css: input, + }, + ], + prefix + ); + + expect( output ).toEqual( [ `${ prefix } ${ input }` ] ); + } ); + it( 'should ignore keyframes', () => { const input = ` @keyframes edit-post__fade-in-animation { @@ -210,6 +225,40 @@ describe( 'transformStyles', () => { expect( output ).toMatchSnapshot(); } ); + + it( 'should not try to wrap items within `:where` selectors', () => { + const input = `:where(.wp-element-button:active, .wp-block-button__link:active) { color: blue; }`; + const prefix = '.my-namespace'; + const expected = [ `${ prefix } ${ input }` ]; + + const output = transformStyles( + [ + { + css: input, + }, + ], + prefix + ); + + expect( output ).toEqual( expected ); + } ); + + it( 'should not try to prefix pseudo elements on `:where` selectors', () => { + const input = `:where(.wp-element-button, .wp-block-button__link)::before { color: blue; }`; + const prefix = '.my-namespace'; + const expected = [ `${ prefix } ${ input }` ]; + + const output = transformStyles( + [ + { + css: input, + }, + ], + prefix + ); + + expect( output ).toEqual( expected ); + } ); } ); it( 'should not break with data urls', () => { From 926e7389a95701c3eba4702d4b4ef7f5d4325ffb Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 13 Aug 2024 10:25:34 +0200 Subject: [PATCH 25/48] DataViews Extensibility: Allow unregistering the duplicate post action (#64441) Co-authored-by: youknowriad Co-authored-by: ntsekouras Co-authored-by: Mamaduka Co-authored-by: sirreal Co-authored-by: jsnajdr --- .../src/components/post-actions/actions.js | 219 +----------------- .../actions/duplicate-post.native.tsx | 3 + .../src/dataviews/actions/duplicate-post.tsx | 174 ++++++++++++++ .../src/dataviews/actions/reorder-page.tsx | 6 +- packages/editor/src/dataviews/fields/index.ts | 7 +- .../src/dataviews/store/private-actions.ts | 9 + packages/editor/src/dataviews/types.ts | 26 ++- 7 files changed, 213 insertions(+), 231 deletions(-) create mode 100644 packages/editor/src/dataviews/actions/duplicate-post.native.tsx create mode 100644 packages/editor/src/dataviews/actions/duplicate-post.tsx diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 0df73d1996deb5..627de5e8652e47 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -4,50 +4,15 @@ import { external } from '@wordpress/icons'; import { addQueryArgs } from '@wordpress/url'; import { useDispatch, useSelect } from '@wordpress/data'; -import { decodeEntities } from '@wordpress/html-entities'; import { store as coreStore } from '@wordpress/core-data'; -import { __, sprintf, _x } from '@wordpress/i18n'; -import { store as noticesStore } from '@wordpress/notices'; -import { useMemo, useState, useEffect } from '@wordpress/element'; -import { DataForm } from '@wordpress/dataviews'; -import { - Button, - __experimentalHStack as HStack, - __experimentalVStack as VStack, -} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { useMemo, useEffect } from '@wordpress/element'; /** * Internal dependencies */ -import { - TEMPLATE_PART_POST_TYPE, - TEMPLATE_POST_TYPE, - PATTERN_POST_TYPE, -} from '../../store/constants'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; -import { getItemTitle } from '../../dataviews/actions/utils'; - -// TODO: this should be shared with other components (see post-fields in edit-site). -const fields = [ - { - type: 'text', - id: 'title', - label: __( 'Title' ), - placeholder: __( 'No title' ), - getValue: ( { item } ) => item.title, - }, - { - type: 'integer', - id: 'menu_order', - label: __( 'Order' ), - description: __( 'Determines the order of pages.' ), - }, -]; - -const formDuplicateAction = { - fields: [ 'title' ], -}; const viewPostAction = { id: 'view-post', @@ -100,172 +65,6 @@ const postRevisionsAction = { }, }; -const useDuplicatePostAction = ( postType ) => { - const userCanCreatePost = useSelect( - ( select ) => { - return select( coreStore ).canUser( 'create', { - kind: 'postType', - name: postType, - } ); - }, - [ postType ] - ); - return useMemo( - () => - userCanCreatePost && { - id: 'duplicate-post', - label: _x( 'Duplicate', 'action label' ), - isEligible( { status } ) { - return status !== 'trash'; - }, - RenderModal: ( { items, closeModal, onActionPerformed } ) => { - const [ item, setItem ] = useState( { - ...items[ 0 ], - title: sprintf( - /* translators: %s: Existing template title */ - __( '%s (Copy)' ), - getItemTitle( items[ 0 ] ) - ), - } ); - - const [ isCreatingPage, setIsCreatingPage ] = - useState( false ); - - const { saveEntityRecord } = useDispatch( coreStore ); - const { createSuccessNotice, createErrorNotice } = - useDispatch( noticesStore ); - - async function createPage( event ) { - event.preventDefault(); - - if ( isCreatingPage ) { - return; - } - - const newItemOject = { - status: 'draft', - title: item.title, - slug: item.title || __( 'No title' ), - comment_status: item.comment_status, - content: - typeof item.content === 'string' - ? item.content - : item.content.raw, - excerpt: item.excerpt.raw, - meta: item.meta, - parent: item.parent, - password: item.password, - template: item.template, - format: item.format, - featured_media: item.featured_media, - menu_order: item.menu_order, - ping_status: item.ping_status, - }; - const assignablePropertiesPrefix = 'wp:action-assign-'; - // Get all the properties that the current user is able to assign normally author, categories, tags, - // and custom taxonomies. - const assignableProperties = Object.keys( - item?._links || {} - ) - .filter( ( property ) => - property.startsWith( - assignablePropertiesPrefix - ) - ) - .map( ( property ) => - property.slice( - assignablePropertiesPrefix.length - ) - ); - assignableProperties.forEach( ( property ) => { - if ( item[ property ] ) { - newItemOject[ property ] = item[ property ]; - } - } ); - setIsCreatingPage( true ); - try { - const newItem = await saveEntityRecord( - 'postType', - item.type, - newItemOject, - { throwOnError: true } - ); - - createSuccessNotice( - sprintf( - // translators: %s: Title of the created template e.g: "Category". - __( '"%s" successfully created.' ), - decodeEntities( - newItem.title?.rendered || item.title - ) - ), - { - id: 'duplicate-post-action', - type: 'snackbar', - } - ); - - if ( onActionPerformed ) { - onActionPerformed( [ newItem ] ); - } - } catch ( error ) { - const errorMessage = - error.message && error.code !== 'unknown_error' - ? error.message - : __( - 'An error occurred while duplicating the page.' - ); - - createErrorNotice( errorMessage, { - type: 'snackbar', - } ); - } finally { - setIsCreatingPage( false ); - closeModal(); - } - } - - return ( -
- - - setItem( { - ...item, - ...changes, - } ) - } - /> - - - - - -
- ); - }, - }, - [ userCanCreatePost ] - ); -}; - export function usePostActions( { postType, onActionPerformed, context } ) { const { defaultActions, postTypeObject } = useSelect( ( select ) => { @@ -284,12 +83,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { registerPostTypeActions( postType ); }, [ registerPostTypeActions, postType ] ); - const duplicatePostAction = useDuplicatePostAction( postType ); - const isTemplateOrTemplatePart = [ - TEMPLATE_POST_TYPE, - TEMPLATE_PART_POST_TYPE, - ].includes( postType ); - const isPattern = postType === PATTERN_POST_TYPE; const isLoaded = !! postTypeObject; const supportsRevisions = !! postTypeObject?.supports?.revisions; return useMemo( () => { @@ -300,11 +93,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { let actions = [ postTypeObject?.viewable && viewPostAction, supportsRevisions && postRevisionsAction, - globalThis.IS_GUTENBERG_PLUGIN - ? ! isTemplateOrTemplatePart && - ! isPattern && - duplicatePostAction - : false, ...defaultActions, ].filter( Boolean ); // Filter actions based on provided context. If not provided @@ -370,10 +158,7 @@ export function usePostActions( { postType, onActionPerformed, context } ) { return actions; }, [ defaultActions, - isTemplateOrTemplatePart, - isPattern, postTypeObject?.viewable, - duplicatePostAction, onActionPerformed, isLoaded, supportsRevisions, diff --git a/packages/editor/src/dataviews/actions/duplicate-post.native.tsx b/packages/editor/src/dataviews/actions/duplicate-post.native.tsx new file mode 100644 index 00000000000000..5468aa649abbd4 --- /dev/null +++ b/packages/editor/src/dataviews/actions/duplicate-post.native.tsx @@ -0,0 +1,3 @@ +const duplicatePost = undefined; + +export default duplicatePost; diff --git a/packages/editor/src/dataviews/actions/duplicate-post.tsx b/packages/editor/src/dataviews/actions/duplicate-post.tsx new file mode 100644 index 00000000000000..0979d30da39519 --- /dev/null +++ b/packages/editor/src/dataviews/actions/duplicate-post.tsx @@ -0,0 +1,174 @@ +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { decodeEntities } from '@wordpress/html-entities'; +import { store as coreStore } from '@wordpress/core-data'; +import { __, sprintf, _x } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { useState } from '@wordpress/element'; +import { DataForm } from '@wordpress/dataviews'; +import { + Button, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import type { Action } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import { getItemTitle } from '../../dataviews/actions/utils'; +import type { CoreDataError, BasePost } from '../types'; +import { titleField } from '../fields'; + +const fields = [ titleField ]; +const formDuplicateAction = { + fields: [ 'title' ], +}; + +const duplicatePost: Action< BasePost > = { + id: 'duplicate-post', + label: _x( 'Duplicate', 'action label' ), + isEligible( { status } ) { + return status !== 'trash'; + }, + RenderModal: ( { items, closeModal, onActionPerformed } ) => { + const [ item, setItem ] = useState< BasePost >( { + ...items[ 0 ], + title: sprintf( + /* translators: %s: Existing template title */ + __( '%s (Copy)' ), + getItemTitle( items[ 0 ] ) + ), + } ); + + const [ isCreatingPage, setIsCreatingPage ] = useState( false ); + const { saveEntityRecord } = useDispatch( coreStore ); + const { createSuccessNotice, createErrorNotice } = + useDispatch( noticesStore ); + + async function createPage( event: React.FormEvent ) { + event.preventDefault(); + + if ( isCreatingPage ) { + return; + } + + const newItemOject = { + status: 'draft', + title: item.title, + slug: item.title || __( 'No title' ), + comment_status: item.comment_status, + content: + typeof item.content === 'string' + ? item.content + : item.content.raw, + excerpt: + typeof item.excerpt === 'string' + ? item.excerpt + : item.excerpt?.raw, + meta: item.meta, + parent: item.parent, + password: item.password, + template: item.template, + format: item.format, + featured_media: item.featured_media, + menu_order: item.menu_order, + ping_status: item.ping_status, + }; + const assignablePropertiesPrefix = 'wp:action-assign-'; + // Get all the properties that the current user is able to assign normally author, categories, tags, + // and custom taxonomies. + const assignableProperties = Object.keys( item?._links || {} ) + .filter( ( property ) => + property.startsWith( assignablePropertiesPrefix ) + ) + .map( ( property ) => + property.slice( assignablePropertiesPrefix.length ) + ); + assignableProperties.forEach( ( property ) => { + if ( item.hasOwnProperty( property ) ) { + // @ts-ignore + newItemOject[ property ] = item[ property ]; + } + } ); + setIsCreatingPage( true ); + try { + const newItem = await saveEntityRecord( + 'postType', + item.type, + newItemOject, + { throwOnError: true } + ); + + createSuccessNotice( + sprintf( + // translators: %s: Title of the created template e.g: "Category". + __( '"%s" successfully created.' ), + decodeEntities( newItem.title?.rendered || item.title ) + ), + { + id: 'duplicate-post-action', + type: 'snackbar', + } + ); + + if ( onActionPerformed ) { + onActionPerformed( [ newItem ] ); + } + } catch ( error ) { + const typedError = error as CoreDataError; + const errorMessage = + typedError.message && typedError.code !== 'unknown_error' + ? typedError.message + : __( 'An error occurred while duplicating the page.' ); + + createErrorNotice( errorMessage, { + type: 'snackbar', + } ); + } finally { + setIsCreatingPage( false ); + closeModal?.(); + } + } + + return ( +
+ + + setItem( ( prev ) => ( { + ...prev, + ...changes, + } ) ) + } + /> + + + + + +
+ ); + }, +}; + +export default duplicatePost; diff --git a/packages/editor/src/dataviews/actions/reorder-page.tsx b/packages/editor/src/dataviews/actions/reorder-page.tsx index 1b9524123e7c95..1820884d8d8c73 100644 --- a/packages/editor/src/dataviews/actions/reorder-page.tsx +++ b/packages/editor/src/dataviews/actions/reorder-page.tsx @@ -17,7 +17,7 @@ import type { Action, RenderModalProps } from '@wordpress/dataviews'; /** * Internal dependencies */ -import type { CoreDataError, PostWithPageAttributesSupport } from '../types'; +import type { CoreDataError, BasePost } from '../types'; import { orderField } from '../fields'; const fields = [ orderField ]; @@ -29,7 +29,7 @@ function ReorderModal( { items, closeModal, onActionPerformed, -}: RenderModalProps< PostWithPageAttributesSupport > ) { +}: RenderModalProps< BasePost > ) { const [ item, setItem ] = useState( items[ 0 ] ); const orderInput = item.menu_order; const { editEntityRecord, saveEditedEntityRecord } = @@ -113,7 +113,7 @@ function ReorderModal( { ); } -const reorderPage: Action< PostWithPageAttributesSupport > = { +const reorderPage: Action< BasePost > = { id: 'order-pages', label: __( 'Order' ), isEligible( { status } ) { diff --git a/packages/editor/src/dataviews/fields/index.ts b/packages/editor/src/dataviews/fields/index.ts index ea30d15dab600b..b215172eaf7f02 100644 --- a/packages/editor/src/dataviews/fields/index.ts +++ b/packages/editor/src/dataviews/fields/index.ts @@ -7,17 +7,18 @@ import type { Field } from '@wordpress/dataviews'; /** * Internal dependencies */ -import type { BasePost, PostWithPageAttributesSupport } from '../types'; +import type { BasePost } from '../types'; +import { getItemTitle } from '../actions/utils'; export const titleField: Field< BasePost > = { type: 'text', id: 'title', label: __( 'Title' ), placeholder: __( 'No title' ), - getValue: ( { item } ) => item.title, + getValue: ( { item } ) => getItemTitle( item ), }; -export const orderField: Field< PostWithPageAttributesSupport > = { +export const orderField: Field< BasePost > = { type: 'integer', id: 'menu_order', label: __( 'Order' ), diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index 80449d1b7a0798..d5b299b012e364 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -21,6 +21,7 @@ import restorePost from '../actions/restore-post'; import type { PostType } from '../types'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import duplicatePost from '../actions/duplicate-post'; export function registerEntityAction< Item >( kind: string, @@ -87,6 +88,14 @@ export const registerPostTypeActions = .getCurrentTheme(); const actions = [ + // @ts-ignore + globalThis.IS_GUTENBERG_PLUGIN + ? ! [ 'wp_template', 'wp_block', 'wp_template_part' ].includes( + postTypeConfig.slug + ) && + canCreate && + duplicatePost + : undefined, postTypeConfig.slug === 'wp_template_part' && canCreate && currentTheme?.is_block_theme && diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index 5750ab96eeae81..514953d6691290 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -7,7 +7,7 @@ type PostStatus = | 'auto-draft' | 'trash'; -export interface BasePost { +export interface CommonPost { status?: PostStatus; title: string | { rendered: string } | { raw: string }; content: string | { raw: string; rendered: string }; @@ -16,7 +16,21 @@ export interface BasePost { blocks?: Object[]; } -export interface Template extends BasePost { +export interface BasePost extends CommonPost { + comment_status?: 'open' | 'closed'; + excerpt?: string | { raw: string; rendered: string }; + meta?: Record< string, any >; + parent?: number; + password?: string; + template?: string; + format?: string; + featured_media?: number; + menu_order?: number; + ping_status?: 'open' | 'closed'; + _links?: Record< string, { href: string }[] >; +} + +export interface Template extends CommonPost { type: 'wp_template'; is_custom: boolean; source: string; @@ -24,7 +38,7 @@ export interface Template extends BasePost { id: string; } -export interface TemplatePart extends BasePost { +export interface TemplatePart extends CommonPost { type: 'wp_template_part'; source: string; has_theme_file: boolean; @@ -32,16 +46,12 @@ export interface TemplatePart extends BasePost { area: string; } -export interface Pattern extends BasePost { +export interface Pattern extends CommonPost { slug: string; title: { raw: string }; wp_pattern_sync_status: string; } -export interface PostWithPageAttributesSupport extends BasePost { - menu_order: number; -} - export type Post = Template | TemplatePart | Pattern | BasePost; export type PostWithPermissions = Post & { From f9268debccb0fb53bb7552dd5723cece5c1178a2 Mon Sep 17 00:00:00 2001 From: Marco Ciampini Date: Tue, 13 Aug 2024 11:00:12 +0200 Subject: [PATCH 26/48] `Composite`: export `useCompositeStore`, add more focus-related props (#64450) * Export useCompositeStore from package * Add `accessibleWhenDisabled` prop to `Composite.Item` * Add `focusable`, `disabled`, `accessibleWhenDisabled`, and `onFocusVisible` props to `Composite` * CHANGELOG --- Co-authored-by: ciampo Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + packages/components/src/composite/README.md | 67 +++++++++++++++++ packages/components/src/composite/index.tsx | 6 +- .../src/composite/stories/index.story.tsx | 71 ++++++++++++++++++- packages/components/src/composite/types.ts | 67 +++++++++++++++++ packages/components/src/index.ts | 2 +- 6 files changed, 210 insertions(+), 4 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 57ba69197cfa98..761e6604a127a2 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,7 @@ - `Composite`: add stable version of the component ([#63564](https://github.com/WordPress/gutenberg/pull/63564)). - `Composite`: add `Hover` and `Typeahead` subcomponents ([#64399](https://github.com/WordPress/gutenberg/pull/64399)). +- `Composite`: export `useCompositeStore, add focus-related props to `Composite`and`Composite.Item` subcomponents ([#64450](https://github.com/WordPress/gutenberg/pull/64450)). ### Enhancements diff --git a/packages/components/src/composite/README.md b/packages/components/src/composite/README.md index 7bd12d0cabfa0c..3670e31b01e9df 100644 --- a/packages/components/src/composite/README.md +++ b/packages/components/src/composite/README.md @@ -147,6 +147,55 @@ Allows the component to be rendered as a different HTML element or React compone - Required: no +##### `focusable`: `boolean` + +Makes the component a focusable element. When this element gains keyboard focus, it gets a `data-focus-visible` attribute and triggers the `onFocusVisible` prop. + +The component supports the `disabled` prop even for those elements not supporting the native `disabled` attribute. Disabled elements may be still accessible via keyboard by using the the `accessibleWhenDisabled` prop. + +Non-native focusable elements will lose their focusability entirely. However, native focusable elements will retain their inherent focusability. + +- Required: no + +##### `disabled`: `boolean` + +Determines if the element is disabled. This sets the `aria-disabled` attribute accordingly, enabling support for all elements, including those that don't support the native `disabled` attribute. + +This feature can be combined with the `accessibleWhenDisabled` prop to +make disabled elements still accessible via keyboard. + +**Note**: For this prop to work, the `focusable` prop must be set to +`true`, if it's not set by default. + +- Required: no +- Default: `false` + +##### `accessibleWhenDisabled`: `boolean` + +Indicates whether the element should be focusable even when it is +`disabled`. + +This is important when discoverability is a concern. For example: + +> A toolbar in an editor contains a set of special smart paste functions +> that are disabled when the clipboard is empty or when the function is not +> applicable to the current content of the clipboard. It could be helpful to +> keep the disabled buttons focusable if the ability to discover their +> functionality is primarily via their presence on the toolbar. + +Learn more on [Focusability of disabled +controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols). + +- Required: no + +##### `onFocusVisible`: `(event: SyntheticEvent) => void` + +Custom event handler invoked when the element gains focus through keyboard interaction or a key press occurs while the element is in focus. This is the programmatic equivalent of the `data-focus-visible` attribute. + +**Note**: For this prop to work, the `focusable` prop must be set to `true` if it's not set by default. + +- Required: no + ##### `children`: `React.ReactNode` The contents of the component. @@ -189,6 +238,24 @@ The contents of the component. Renders a composite item. +##### `accessibleWhenDisabled`: `boolean` + +Indicates whether the element should be focusable even when it is +`disabled`. + +This is important when discoverability is a concern. For example: + +> A toolbar in an editor contains a set of special smart paste functions +> that are disabled when the clipboard is empty or when the function is not +> applicable to the current content of the clipboard. It could be helpful to +> keep the disabled buttons focusable if the ability to discover their +> functionality is primarily via their presence on the toolbar. + +Learn more on [Focusability of disabled +controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols). + +- Required: no + ##### `render`: `RenderProp & { ref?: React.Ref | undefined; }> | React.ReactElement>` Allows the component to be rendered as a different HTML element or React component. The value can be a React element or a function that takes in the original component props and gives back a React element with the props merged. diff --git a/packages/components/src/composite/index.tsx b/packages/components/src/composite/index.tsx index 4e87b9a55fa5bb..f5d92330cada3c 100644 --- a/packages/components/src/composite/index.tsx +++ b/packages/components/src/composite/index.tsx @@ -136,8 +136,10 @@ export const Composite = Object.assign( forwardRef< HTMLDivElement, WordPressComponentProps< CompositeProps, 'div', false > - >( function CompositeRow( props, ref ) { - return ; + >( function Composite( { disabled = false, ...props }, ref ) { + return ( + + ); } ), { displayName: 'Composite', diff --git a/packages/components/src/composite/stories/index.story.tsx b/packages/components/src/composite/stories/index.story.tsx index f1be53445f79ad..405962b92a761c 100644 --- a/packages/components/src/composite/stories/index.story.tsx +++ b/packages/components/src/composite/stories/index.story.tsx @@ -74,6 +74,28 @@ const meta: Meta< typeof UseCompositeStorePlaceholder > = { table: { type: { summary: 'React.ReactNode' } }, }, }; + const accessibleWhenDisabled = { + name: 'accessibleWhenDisabled', + description: `Indicates whether the element should be focusable even when it is +\`disabled\`. + +This is important when discoverability is a concern. For example: + +> A toolbar in an editor contains a set of special smart paste functions +> that are disabled when the clipboard is empty or when the function is not +> applicable to the current content of the clipboard. It could be helpful to +> keep the disabled buttons focusable if the ability to discover their +> functionality is primarily via their presence on the toolbar. + +Learn more on [Focusability of disabled +controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols).`, + table: { + type: { + summary: 'boolean', + }, + }, + }; + const argTypes = { useCompositeStore: { activeId: { @@ -226,11 +248,58 @@ This only affects the composite widget behavior. You still need to set \`dir="rt }, type: { required: true }, }, + focusable: { + name: 'focusable', + description: `Makes the component a focusable element. When this element gains keyboard focus, it gets a \`data-focus-visible\` attribute and triggers the \`onFocusVisible\` prop. + +The component supports the \`disabled\` prop even for those elements not supporting the native \`disabled\` attribute. Disabled elements may be still accessible via keyboard by using the the \`accessibleWhenDisabled\` prop. + +Non-native focusable elements will lose their focusability entirely. However, native focusable elements will retain their inherent focusability.`, + table: { + type: { + summary: 'boolean', + }, + }, + }, + disabled: { + name: 'disabled', + description: `Determines if the element is disabled. This sets the \`aria-disabled\` attribute accordingly, enabling support for all elements, including those that don't support the native \`disabled\` attribute. + +This feature can be combined with the \`accessibleWhenDisabled\` prop to +make disabled elements still accessible via keyboard. + +**Note**: For this prop to work, the \`focusable\` prop must be set to +\`true\`, if it's not set by default.`, + table: { + defaultValue: { + summary: 'false', + }, + type: { + summary: 'boolean', + }, + }, + }, + accessibleWhenDisabled, + onFocusVisible: { + name: 'onFocusVisible', + description: `Custom event handler invoked when the element gains focus through keyboard interaction or a key press occurs while the element is in focus. This is the programmatic equivalent of the \`data-focus-visible\` attribute. + +**Note**: For this prop to work, the \`focusable\` prop must be set to \`true\` if it's not set by default.`, + table: { + type: { + summary: + '(event: SyntheticEvent) => void', + }, + }, + }, }, 'Composite.Group': commonArgTypes, 'Composite.GroupLabel': commonArgTypes, 'Composite.Row': commonArgTypes, - 'Composite.Item': commonArgTypes, + 'Composite.Item': { + ...commonArgTypes, + accessibleWhenDisabled, + }, 'Composite.Hover': commonArgTypes, 'Composite.Typeahead': commonArgTypes, }; diff --git a/packages/components/src/composite/types.ts b/packages/components/src/composite/types.ts index 8bd4b447a83aef..5afe410f7582ba 100644 --- a/packages/components/src/composite/types.ts +++ b/packages/components/src/composite/types.ts @@ -131,6 +131,57 @@ export type CompositeProps = { * merged. */ render?: Ariakit.CompositeProps[ 'render' ]; + /** + * Makes the component a focusable element. When this element gains keyboard + * focus, it gets a `data-focus-visible` attribute and triggers the + * `onFocusVisible` prop. + * The component supports the `disabled` prop even for those elements not + * supporting the native `disabled` attribute. Disabled elements may be + * still accessible via keyboard by using the the `accessibleWhenDisabled` + * prop. + * Non-native focusable elements will lose their focusability entirely. + * However, native focusable elements will retain their inherent focusability. + */ + focusable?: Ariakit.CompositeProps[ 'focusable' ]; + /** + * Determines if the element is disabled. This sets the `aria-disabled` + * attribute accordingly, enabling support for all elements, including those + * that don't support the native `disabled` attribute. + * + * This feature can be combined with the `accessibleWhenDisabled` prop to + * make disabled elements still accessible via keyboard. + * + * **Note**: For this prop to work, the `focusable` prop must be set to + * `true`, if it's not set by default. + * + * @default false + */ + disabled?: Ariakit.CompositeProps[ 'disabled' ]; + /** + * Indicates whether the element should be focusable even when it is + * `disabled`. + * + * This is important when discoverability is a concern. For example: + * + * > A toolbar in an editor contains a set of special smart paste functions + * that are disabled when the clipboard is empty or when the function is not + * applicable to the current content of the clipboard. It could be helpful to + * keep the disabled buttons focusable if the ability to discover their + * functionality is primarily via their presence on the toolbar. + * + * Learn more on [Focusability of disabled + * controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols). + */ + accessibleWhenDisabled?: Ariakit.CompositeProps[ 'accessibleWhenDisabled' ]; + /** + * Custom event handler invoked when the element gains focus through keyboard + * interaction or a key press occurs while the element is in focus. This is + * the programmatic equivalent of the `data-focus-visible` attribute. + * + * **Note**: For this prop to work, the `focusable` prop must be set to `true` + * if it's not set by default. + */ + onFocusVisible?: Ariakit.CompositeProps[ 'onFocusVisible' ]; /** * The contents of the component. */ @@ -177,6 +228,22 @@ export type CompositeItemProps = { * The contents of the component. */ children?: Ariakit.CompositeItemProps[ 'children' ]; + /** + * Indicates whether the element should be focusable even when it is + * `disabled`. + * + * This is important when discoverability is a concern. For example: + * + * > A toolbar in an editor contains a set of special smart paste functions + * that are disabled when the clipboard is empty or when the function is not + * applicable to the current content of the clipboard. It could be helpful to + * keep the disabled buttons focusable if the ability to discover their + * functionality is primarily via their presence on the toolbar. + * + * Learn more on [Focusability of disabled + * controls](https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/#focusabilityofdisabledcontrols). + */ + accessibleWhenDisabled?: Ariakit.CompositeItemProps[ 'accessibleWhenDisabled' ]; }; export type CompositeRowProps = { diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 4c724a461e6775..cd6d2a77db9cb6 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -62,7 +62,7 @@ export { CompositeItem as __unstableCompositeItem, useCompositeState as __unstableUseCompositeState, } from './composite/legacy'; -export { Composite } from './composite'; +export { Composite, useCompositeStore } from './composite'; export { ConfirmDialog as __experimentalConfirmDialog } from './confirm-dialog'; export { default as CustomSelectControl } from './custom-select-control'; export { default as Dashicon } from './dashicon'; From ad37f2063021a9d151253ca1386c213ef300fe6d Mon Sep 17 00:00:00 2001 From: Ben Dwyer Date: Tue, 13 Aug 2024 10:03:02 +0100 Subject: [PATCH 27/48] Zoom Out: Defalt the inserter in the patterns tab when in zoom out (#64193) Co-authored-by: scruffian Co-authored-by: MaggieCabrera --- .../block-editor/src/components/inserter/menu.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index c5f41f9e3bf0a0..629765315c1d6b 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -73,9 +73,16 @@ function InserterMenu( const [ patternFilter, setPatternFilter ] = useState( 'all' ); const [ selectedMediaCategory, setSelectedMediaCategory ] = useState( null ); - const [ selectedTab, setSelectedTab ] = useState( - __experimentalInitialTab - ); + function getInitialTab() { + if ( __experimentalInitialTab ) { + return __experimentalInitialTab; + } + + if ( isZoomOutMode ) { + return 'patterns'; + } + } + const [ selectedTab, setSelectedTab ] = useState( getInitialTab() ); const [ destinationRootClientId, onInsertBlocks, onToggleInsertionPoint ] = useInsertionPoint( { From 943b72aecdfa160e925e43dad4cfbc9f9b277810 Mon Sep 17 00:00:00 2001 From: Hit Bhalodia <58802366+hbhalodia@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:39:56 +0530 Subject: [PATCH 28/48] Fix: Embed blocks: adding captions via toolbar (#64394) * Add the Caption component to be used for consistency on the embed block similar to audio and image * Change the label for the embed caption text Co-authored-by: hbhalodia Co-authored-by: Rishit30G Co-authored-by: t-hamano Co-authored-by: simison --- packages/block-library/src/embed/edit.js | 9 +++++ .../block-library/src/embed/embed-preview.js | 40 ++----------------- 2 files changed, 12 insertions(+), 37 deletions(-) diff --git a/packages/block-library/src/embed/edit.js b/packages/block-library/src/embed/edit.js index 12265883d6963c..0b589eb68ecf49 100644 --- a/packages/block-library/src/embed/edit.js +++ b/packages/block-library/src/embed/edit.js @@ -14,6 +14,7 @@ import { embedContentIcon } from './icons'; import EmbedLoading from './embed-loading'; import EmbedPlaceholder from './embed-placeholder'; import EmbedPreview from './embed-preview'; +import { Caption } from '../utils/caption'; /** * External dependencies @@ -277,6 +278,14 @@ const EmbedEdit = ( props ) => { label={ label } insertBlocksAfter={ insertBlocksAfter } /> + ); diff --git a/packages/block-library/src/embed/embed-preview.js b/packages/block-library/src/embed/embed-preview.js index ba079cc2e1e8b6..d53f1148cee13c 100644 --- a/packages/block-library/src/embed/embed-preview.js +++ b/packages/block-library/src/embed/embed-preview.js @@ -13,13 +13,8 @@ import clsx from 'clsx'; */ import { __, sprintf } from '@wordpress/i18n'; import { Placeholder, SandBox } from '@wordpress/components'; -import { - RichText, - BlockIcon, - __experimentalGetElementClassName, -} from '@wordpress/block-editor'; +import { BlockIcon } from '@wordpress/block-editor'; import { Component } from '@wordpress/element'; -import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; import { getAuthority } from '@wordpress/url'; /** @@ -57,19 +52,8 @@ class EmbedPreview extends Component { } render() { - const { - preview, - previewable, - url, - type, - caption, - onCaptionChange, - isSelected, - className, - icon, - label, - insertBlocksAfter, - } = this.props; + const { preview, previewable, url, type, className, icon, label } = + this.props; const { scripts } = preview; const { interactive } = this.state; @@ -139,24 +123,6 @@ class EmbedPreview extends Component {

) } - { ( ! RichText.isEmpty( caption ) || isSelected ) && ( - - insertBlocksAfter( - createBlock( getDefaultBlockName() ) - ) - } - /> - ) } ); } From 600aa54ca87ad53b8b7e81977651ba0e5fc9affc Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 13 Aug 2024 18:27:00 +0900 Subject: [PATCH 29/48] Table Block: Hide caption toolbar button on multiple selection (#64462) Co-authored-by: t-hamano Co-authored-by: Mamaduka --- packages/block-library/src/table/edit.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js index 5510d0031ff6be..95162f4e14c00b 100644 --- a/packages/block-library/src/table/edit.js +++ b/packages/block-library/src/table/edit.js @@ -95,7 +95,7 @@ function TableEdit( { attributes, setAttributes, insertBlocksAfter, - isSelected, + isSelected: isSingleSelected, } ) { const { hasFixedLayout, head, foot } = attributes; const [ initialRowCount, setInitialRowCount ] = useState( 2 ); @@ -340,10 +340,10 @@ function TableEdit( { } useEffect( () => { - if ( ! isSelected ) { + if ( ! isSingleSelected ) { setSelectedCell(); } - }, [ isSelected ] ); + }, [ isSingleSelected ] ); useEffect( () => { if ( hasTableCreated ) { @@ -565,9 +565,10 @@ function TableEdit( { ) } From 992bcde7651e93d3115f6c54e8bb485acd808bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Albert=20Juh=C3=A9=20Lluveras?= Date: Tue, 13 Aug 2024 11:57:24 +0200 Subject: [PATCH 30/48] Add plugin template registration API (#61577) Co-authored-by: Aljullu Co-authored-by: cbravobernal Co-authored-by: ntsekouras Co-authored-by: peterwilsoncc Co-authored-by: talldan Co-authored-by: youknowriad Co-authored-by: mtias Co-authored-by: gziolo Co-authored-by: SantosGuillamot Co-authored-by: TimothyBJacobs Co-authored-by: ellatrix Co-authored-by: ndiego Co-authored-by: carolinan Co-authored-by: nerrad Co-authored-by: annezazu --- backport-changelog/6.7/7125.md | 3 + lib/compat/wordpress-6.7/block-templates.php | 41 ++ ...utenberg-rest-templates-controller-6-7.php | 203 ++++++++++ .../class-wp-block-templates-registry.php | 256 +++++++++++++ lib/compat/wordpress-6.7/compat.php | 114 ++++++ lib/compat/wordpress-6.7/rest-api.php | 51 +++ lib/load.php | 4 + .../core-data/src/entity-types/wp-template.ts | 4 + .../plugins/block-template-registration.php | 72 ++++ .../src/utils/is-template-removable.js | 6 +- .../src/utils/is-template-revertable.js | 3 +- .../src/dataviews/actions/reset-post.tsx | 3 +- .../editor/src/dataviews/actions/utils.ts | 4 +- packages/editor/src/dataviews/types.ts | 3 + packages/editor/src/store/private-actions.js | 2 +- .../src/store/utils/is-template-revertable.js | 3 +- phpunit/block-template-test.php | 37 ++ ...tenberg-rest-templates-controller-test.php | 119 ++++++ ...class-wp-block-templates-registry-test.php | 192 ++++++++++ .../site-editor/template-registration.spec.js | 356 ++++++++++++++++++ 20 files changed, 1469 insertions(+), 7 deletions(-) create mode 100644 backport-changelog/6.7/7125.md create mode 100644 lib/compat/wordpress-6.7/block-templates.php create mode 100644 lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php create mode 100644 lib/compat/wordpress-6.7/class-wp-block-templates-registry.php create mode 100644 lib/compat/wordpress-6.7/compat.php create mode 100644 packages/e2e-tests/plugins/block-template-registration.php create mode 100644 phpunit/block-template-test.php create mode 100644 phpunit/class-gutenberg-rest-templates-controller-test.php create mode 100644 phpunit/class-wp-block-templates-registry-test.php create mode 100644 test/e2e/specs/site-editor/template-registration.spec.js diff --git a/backport-changelog/6.7/7125.md b/backport-changelog/6.7/7125.md new file mode 100644 index 00000000000000..ce208decd2d145 --- /dev/null +++ b/backport-changelog/6.7/7125.md @@ -0,0 +1,3 @@ +https://github.com/WordPress/wordpress-develop/pull/7125 + +* https://github.com/WordPress/gutenberg/pull/61577 diff --git a/lib/compat/wordpress-6.7/block-templates.php b/lib/compat/wordpress-6.7/block-templates.php new file mode 100644 index 00000000000000..e270ab226c1d9f --- /dev/null +++ b/lib/compat/wordpress-6.7/block-templates.php @@ -0,0 +1,41 @@ +register( $template_name, $args ); + } +} + +if ( ! function_exists( 'wp_unregister_block_template' ) ) { + /** + * Unregister a template. + * + * @param string $template_name Template name in the form of `plugin_uri//template_name`. + * @return true|WP_Error True on success, WP_Error on failure or if the template doesn't exist. + */ + function wp_unregister_block_template( $template_name ) { + return WP_Block_Templates_Registry::get_instance()->unregister( $template_name ); + } +} diff --git a/lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php b/lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php new file mode 100644 index 00000000000000..ed67dded75ecb1 --- /dev/null +++ b/lib/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php @@ -0,0 +1,203 @@ +post_type ); + } else { + $template = get_block_template( $request['id'], $this->post_type ); + } + + if ( ! $template ) { + return new WP_Error( 'rest_template_not_found', __( 'No templates exist with that id.' ), array( 'status' => 404 ) ); + } + + return $this->prepare_item_for_response( $template, $request ); + } + + /** + * Prepare a single template output for response + * + * @param WP_Block_Template $item Template instance. + * @param WP_REST_Request $request Request object. + * @return WP_REST_Response Response object. + */ + // @core-merge: Fix wrong author in plugin templates. + public function prepare_item_for_response( $item, $request ) { + $template = $item; + + $fields = $this->get_fields_for_response( $request ); + + if ( 'plugin' !== $item->origin ) { + return parent::prepare_item_for_response( $item, $request ); + } + $cloned_item = clone $item; + // Set the origin as theme when calling the previous `prepare_item_for_response()` to prevent warnings when generating the author text. + $cloned_item->origin = 'theme'; + $response = parent::prepare_item_for_response( $cloned_item, $request ); + $data = $response->data; + + if ( rest_is_field_included( 'origin', $fields ) ) { + $data['origin'] = 'plugin'; + } + + if ( rest_is_field_included( 'plugin', $fields ) ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $cloned_item->slug ); + if ( $registered_template ) { + $data['plugin'] = $registered_template->plugin; + } + } + + if ( rest_is_field_included( 'author_text', $fields ) ) { + $data['author_text'] = $this->get_wp_templates_author_text_field( $template ); + } + + if ( rest_is_field_included( 'original_source', $fields ) ) { + $data['original_source'] = $this->get_wp_templates_original_source_field( $template ); + } + + $response = rest_ensure_response( $data ); + + if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) { + $links = $this->prepare_links( $template->id ); + $response->add_links( $links ); + if ( ! empty( $links['self']['href'] ) ) { + $actions = $this->get_available_actions(); + $self = $links['self']['href']; + foreach ( $actions as $rel ) { + $response->add_link( $rel, $self ); + } + } + } + + return $response; + } + + /** + * Returns the source from where the template originally comes from. + * + * @param WP_Block_Template $template_object Template instance. + * @return string Original source of the template one of theme, plugin, site, or user. + */ + // @core-merge: Changed the comments format (from inline to multi-line) in the entire function. + private static function get_wp_templates_original_source_field( $template_object ) { + if ( 'wp_template' === $template_object->type || 'wp_template_part' === $template_object->type ) { + /* + * Added by theme. + * Template originally provided by a theme, but customized by a user. + * Templates originally didn't have the 'origin' field so identify + * older customized templates by checking for no origin and a 'theme' + * or 'custom' source. + */ + if ( $template_object->has_theme_file && + ( 'theme' === $template_object->origin || ( + empty( $template_object->origin ) && in_array( + $template_object->source, + array( + 'theme', + 'custom', + ), + true + ) ) + ) + ) { + return 'theme'; + } + + // Added by plugin. + // @core-merge: Removed `$template_object->has_theme_file` check from this if clause. + if ( 'plugin' === $template_object->origin ) { + return 'plugin'; + } + + /* + * Added by site. + * Template was created from scratch, but has no author. Author support + * was only added to templates in WordPress 5.9. Fallback to showing the + * site logo and title. + */ + if ( empty( $template_object->has_theme_file ) && 'custom' === $template_object->source && empty( $template_object->author ) ) { + return 'site'; + } + } + + // Added by user. + return 'user'; + } + + /** + * Returns a human readable text for the author of the template. + * + * @param WP_Block_Template $template_object Template instance. + * @return string Human readable text for the author. + */ + private static function get_wp_templates_author_text_field( $template_object ) { + $original_source = self::get_wp_templates_original_source_field( $template_object ); + switch ( $original_source ) { + case 'theme': + $theme_name = wp_get_theme( $template_object->theme )->get( 'Name' ); + return empty( $theme_name ) ? $template_object->theme : $theme_name; + case 'plugin': + // @core-merge: Prioritize plugin name instead of theme name for plugin-registered templates. + if ( ! function_exists( 'get_plugins' ) || ! function_exists( 'get_plugin_data' ) ) { + require_once ABSPATH . 'wp-admin/includes/plugin.php'; + } + if ( isset( $template_object->plugin ) ) { + $plugins = wp_get_active_and_valid_plugins(); + + foreach ( $plugins as $plugin_file ) { + $plugin_basename = plugin_basename( $plugin_file ); + // Split basename by '/' to get the plugin slug. + list( $plugin_slug, ) = explode( '/', $plugin_basename ); + + if ( $plugin_slug === $template_object->plugin ) { + $plugin_data = get_plugin_data( $plugin_file ); + + if ( ! empty( $plugin_data['Name'] ) ) { + return $plugin_data['Name']; + } + + break; + } + } + } + + /* + * Fall back to the theme name if the plugin is not defined. That's needed to keep backwards + * compatibility with templates that were registered before the plugin attribute was added. + */ + $plugins = get_plugins(); + $plugin_basename = plugin_basename( sanitize_text_field( $template_object->theme . '.php' ) ); + if ( isset( $plugins[ $plugin_basename ] ) && isset( $plugins[ $plugin_basename ]['Name'] ) ) { + return $plugins[ $plugin_basename ]['Name']; + } + return isset( $template_object->plugin ) ? + $template_object->plugin : + $template_object->theme; + // @core-merge: End of changes to merge in core. + case 'site': + return get_bloginfo( 'name' ); + case 'user': + $author = get_user_by( 'id', $template_object->author ); + if ( ! $author ) { + return __( 'Unknown author' ); + } + return $author->get( 'display_name' ); + } + } +} diff --git a/lib/compat/wordpress-6.7/class-wp-block-templates-registry.php b/lib/compat/wordpress-6.7/class-wp-block-templates-registry.php new file mode 100644 index 00000000000000..db53f735e13b3d --- /dev/null +++ b/lib/compat/wordpress-6.7/class-wp-block-templates-registry.php @@ -0,0 +1,256 @@ + $instance` pairs. + * + * @since 6.7.0 + * @var WP_Block_Template[] $registered_block_templates Registered templates. + */ + private $registered_templates = array(); + + /** + * Container for the main instance of the class. + * + * @since 6.7.0 + * @var WP_Block_Templates_Registry|null + */ + private static $instance = null; + + /** + * Registers a template. + * + * @since 6.7.0 + * + * @param string $template_name Template name including namespace. + * @param array $args Optional. Array of template arguments. + * @return WP_Block_Template|WP_Error The registered template on success, or false on failure. + */ + public function register( $template_name, $args = array() ) { + + $template = null; + + $error_message = ''; + $error_code = ''; + if ( ! is_string( $template_name ) ) { + $error_message = __( 'Template names must be strings.', 'gutenberg' ); + $error_code = 'template_name_no_string'; + } elseif ( preg_match( '/[A-Z]+/', $template_name ) ) { + $error_message = __( 'Template names must not contain uppercase characters.', 'gutenberg' ); + $error_code = 'template_name_no_uppercase'; + } elseif ( ! preg_match( '/^[a-z0-9-]+\/\/[a-z0-9-]+$/', $template_name ) ) { + $error_message = __( 'Template names must contain a namespace prefix. Example: my-plugin//my-custom-template', 'gutenberg' ); + $error_code = 'template_no_prefix'; + } elseif ( $this->is_registered( $template_name ) ) { + /* translators: %s: Template name. */ + $error_message = sprintf( __( 'Template "%s" is already registered.', 'gutenberg' ), $template_name ); + $error_code = 'template_already_registered'; + } + + if ( $error_message ) { + _doing_it_wrong( + __METHOD__, + $error_message, + '6.7.0' + ); + return new WP_Error( $error_code, $error_message ); + } + + if ( ! $template ) { + $theme_name = get_stylesheet(); + list( $plugin, $slug ) = explode( '//', $template_name ); + $default_template_types = get_default_block_template_types(); + + $template = new WP_Block_Template(); + $template->id = $theme_name . '//' . $slug; + $template->theme = $theme_name; + $template->plugin = $plugin; + $template->author = null; + $template->content = isset( $args['content'] ) ? $args['content'] : ''; + $template->source = 'plugin'; + $template->slug = $slug; + $template->type = 'wp_template'; + $template->title = isset( $args['title'] ) ? $args['title'] : $template_name; + $template->description = isset( $args['description'] ) ? $args['description'] : ''; + $template->status = 'publish'; + $template->origin = 'plugin'; + $template->is_custom = ! isset( $default_template_types[ $template_name ] ); + $template->post_types = isset( $args['post_types'] ) ? $args['post_types'] : array(); + } + + $this->registered_templates[ $template_name ] = $template; + + return $template; + } + + /** + * Retrieves all registered templates. + * + * @since 6.7.0 + * + * @return WP_Block_Template[]|false Associative array of `$template_name => $template` pairs. + */ + public function get_all_registered() { + return $this->registered_templates; + } + + /** + * Retrieves a registered template by its name. + * + * @since 6.7.0 + * + * @param string $template_name Template name including namespace. + * @return WP_Block_Template|null|false The registered template, or null if it is not registered. + */ + public function get_registered( $template_name ) { + if ( ! $this->is_registered( $template_name ) ) { + return null; + } + + return $this->registered_templates[ $template_name ]; + } + + /** + * Retrieves a registered template by its slug. + * + * @since 6.7.0 + * + * @param string $template_slug Slug of the template. + * @return WP_Block_Template|null The registered template, or null if it is not registered. + */ + public function get_by_slug( $template_slug ) { + $all_templates = $this->get_all_registered(); + + if ( ! $all_templates ) { + return null; + } + + foreach ( $all_templates as $template ) { + if ( $template->slug === $template_slug ) { + return $template; + } + } + + return null; + } + + /** + * Retrieves registered templates matching a query. + * + * @since 6.7.0 + * + * @param array $query { + * Arguments to retrieve templates. Optional, empty by default. + * + * @type string[] $slug__in List of slugs to include. + * @type string[] $slug__not_in List of slugs to skip. + * @type string $post_type Post type to get the templates for. + * } + */ + public function get_by_query( $query = array() ) { + $all_templates = $this->get_all_registered(); + + if ( ! $all_templates ) { + return array(); + } + + $query = wp_parse_args( + $query, + array( + 'slug__in' => array(), + 'slug__not_in' => array(), + 'post_type' => '', + ) + ); + $slugs_to_include = $query['slug__in']; + $slugs_to_skip = $query['slug__not_in']; + $post_type = $query['post_type']; + + $matching_templates = array(); + foreach ( $all_templates as $template_name => $template ) { + if ( $slugs_to_include && ! in_array( $template->slug, $slugs_to_include, true ) ) { + continue; + } + + if ( $slugs_to_skip && in_array( $template->slug, $slugs_to_skip, true ) ) { + continue; + } + + if ( $post_type && ! in_array( $post_type, $template->post_types, true ) ) { + continue; + } + + $matching_templates[ $template_name ] = $template; + } + + return $matching_templates; + } + + /** + * Checks if a template is registered. + * + * @since 6.7.0 + * + * @param string $template_name Template name. + * @return bool True if the template is registered, false otherwise. + */ + public function is_registered( $template_name ) { + return isset( $this->registered_templates[ $template_name ] ); + } + + /** + * Unregisters a template. + * + * @since 6.7.0 + * + * @param string $template_name Template name including namespace. + * @return WP_Block_Template|false The unregistered template on success, or false on failure. + */ + public function unregister( $template_name ) { + if ( ! $this->is_registered( $template_name ) ) { + _doing_it_wrong( + __METHOD__, + /* translators: %s: Template name. */ + sprintf( __( 'Template "%s" is not registered.', 'gutenberg' ), $template_name ), + '6.7.0' + ); + /* translators: %s: Template name. */ + return new WP_Error( 'template_not_registered', __( 'Template "%s" is not registered.', 'gutenberg' ) ); + } + + $unregistered_template = $this->registered_templates[ $template_name ]; + unset( $this->registered_templates[ $template_name ] ); + + return $unregistered_template; + } + + /** + * Utility method to retrieve the main instance of the class. + * + * The instance will be created if it does not exist yet. + * + * @since 6.7.0 + * + * @return WP_Block_Templates_Registry The main instance. + */ + public static function get_instance() { + if ( null === self::$instance ) { + self::$instance = new self(); + } + + return self::$instance; + } + } +} diff --git a/lib/compat/wordpress-6.7/compat.php b/lib/compat/wordpress-6.7/compat.php new file mode 100644 index 00000000000000..7021cab2053eff --- /dev/null +++ b/lib/compat/wordpress-6.7/compat.php @@ -0,0 +1,114 @@ + $value ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $query_result[ $key ]->slug ); + if ( $registered_template ) { + $query_result[ $key ]->plugin = $registered_template->plugin; + $query_result[ $key ]->origin = + 'theme' !== $query_result[ $key ]->origin && 'theme' !== $query_result[ $key ]->source ? + 'plugin' : + $query_result[ $key ]->origin; + } + } + + if ( ! isset( $query['wp_id'] ) ) { + $template_files = _gutenberg_get_block_templates_files( $template_type, $query ); + + /* + * Add templates registered in the template registry. Filtering out the ones which have a theme file. + */ + $registered_templates = WP_Block_Templates_Registry::get_instance()->get_by_query( $query ); + $matching_registered_templates = array_filter( + $registered_templates, + function ( $registered_template ) use ( $template_files ) { + foreach ( $template_files as $template_file ) { + if ( $template_file['slug'] === $registered_template->slug ) { + return false; + } + } + return true; + } + ); + $query_result = array_merge( $query_result, $matching_registered_templates ); + } + + return $query_result; +} +add_filter( 'get_block_templates', '_gutenberg_add_block_templates_from_registry', 10, 3 ); + +/** + * Hooks into `get_block_template` to add the `plugin` property when necessary. + * + * @param [WP_Block_Template|null] $block_template The found block template, or null if there isn’t one. + * @return [WP_Block_Template|null] The block template that was already found with the plugin property defined if it was reigstered by a plugin. + */ +function _gutenberg_add_block_template_plugin_attribute( $block_template ) { + if ( $block_template ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $block_template->slug ); + if ( $registered_template ) { + $block_template->plugin = $registered_template->plugin; + $block_template->origin = + 'theme' !== $block_template->origin && 'theme' !== $block_template->source ? + 'plugin' : + $block_template->origin; + } + } + + return $block_template; +} +add_filter( 'get_block_template', '_gutenberg_add_block_template_plugin_attribute', 10, 1 ); + +/** + * Hooks into `get_block_file_template` so templates from the registry are also returned. + * + * @param WP_Block_Template|null $block_template The found block template, or null if there is none. + * @param string $id Template unique identifier (example: 'theme_slug//template_slug'). + * @return WP_Block_Template|null The block template that was already found or from the registry. In case the template was already found, add the necessary details from the registry. + */ +function _gutenberg_add_block_file_templates_from_registry( $block_template, $id ) { + if ( $block_template ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $block_template->slug ); + if ( $registered_template ) { + $block_template->plugin = $registered_template->plugin; + $block_template->origin = + 'theme' !== $block_template->origin && 'theme' !== $block_template->source ? + 'plugin' : + $block_template->origin; + } + return $block_template; + } + + $parts = explode( '//', $id, 2 ); + + if ( count( $parts ) < 2 ) { + return $block_template; + } + + list( , $slug ) = $parts; + return WP_Block_Templates_Registry::get_instance()->get_by_slug( $slug ); +} +add_filter( 'get_block_file_template', '_gutenberg_add_block_file_templates_from_registry', 10, 2 ); diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index 713d31c4632c74..fe2aac9c2580ae 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -29,3 +29,54 @@ function gutenberg_block_editor_preload_paths_6_7( $paths, $context ) { return $paths; } add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_6_7', 10, 2 ); + +if ( ! function_exists( 'wp_api_template_registry' ) ) { + /** + * Hook in to the template and template part post types and modify the rest + * endpoint to include modifications to read templates from the + * BlockTemplatesRegistry. + * + * @param array $args Current registered post type args. + * @param string $post_type Name of post type. + * + * @return array + */ + function wp_api_template_registry( $args, $post_type ) { + if ( 'wp_template' === $post_type || 'wp_template_part' === $post_type ) { + $args['rest_controller_class'] = 'Gutenberg_REST_Templates_Controller_6_7'; + } + return $args; + } +} +add_filter( 'register_post_type_args', 'wp_api_template_registry', 10, 2 ); + +/** + * Adds `plugin` fields to WP_REST_Templates_Controller class. + */ +function gutenberg_register_wp_rest_templates_controller_plugin_field() { + + register_rest_field( + 'wp_template', + 'plugin', + array( + 'get_callback' => function ( $template_object ) { + if ( $template_object ) { + $registered_template = WP_Block_Templates_Registry::get_instance()->get_by_slug( $template_object['slug'] ); + if ( $registered_template ) { + return $registered_template->plugin; + } + } + + return; + }, + 'update_callback' => null, + 'schema' => array( + 'type' => 'string', + 'description' => __( 'Plugin that registered the template.', 'gutenberg' ), + 'readonly' => true, + 'context' => array( 'view', 'edit', 'embed' ), + ), + ) + ); +} +add_action( 'rest_api_init', 'gutenberg_register_wp_rest_templates_controller_plugin_field' ); diff --git a/lib/load.php b/lib/load.php index c5f12af1654df2..b501f0abd1c978 100644 --- a/lib/load.php +++ b/lib/load.php @@ -41,6 +41,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.6/rest-api.php'; // WordPress 6.7 compat. + require __DIR__ . '/compat/wordpress-6.7/class-gutenberg-rest-templates-controller-6-7.php'; require __DIR__ . '/compat/wordpress-6.7/rest-api.php'; // Plugin specific code. @@ -101,9 +102,12 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/compat/wordpress-6.6/post.php'; // WordPress 6.7 compat. +require __DIR__ . '/compat/wordpress-6.7/block-templates.php'; require __DIR__ . '/compat/wordpress-6.7/blocks.php'; require __DIR__ . '/compat/wordpress-6.7/block-bindings.php'; require __DIR__ . '/compat/wordpress-6.7/script-modules.php'; +require __DIR__ . '/compat/wordpress-6.7/class-wp-block-templates-registry.php'; +require __DIR__ . '/compat/wordpress-6.7/compat.php'; // Experimental features. require __DIR__ . '/experimental/block-editor-settings-mobile.php'; diff --git a/packages/core-data/src/entity-types/wp-template.ts b/packages/core-data/src/entity-types/wp-template.ts index ac6db09035f193..70d3e40c295dcf 100644 --- a/packages/core-data/src/entity-types/wp-template.ts +++ b/packages/core-data/src/entity-types/wp-template.ts @@ -73,6 +73,10 @@ declare module './base-entity-records' { * Post ID. */ wp_id: number; + /** + * Plugin that registered the template. + */ + plugin?: string; /** * Theme file exists. */ diff --git a/packages/e2e-tests/plugins/block-template-registration.php b/packages/e2e-tests/plugins/block-template-registration.php new file mode 100644 index 00000000000000..a7c75552849658 --- /dev/null +++ b/packages/e2e-tests/plugins/block-template-registration.php @@ -0,0 +1,72 @@ + 'Plugin Template', + 'description' => 'A template registered by a plugin.', + 'content' => '

This is a plugin-registered template.

', + 'post_types' => array( 'post' ), + ) + ); + add_action( + 'category_template_hierarchy', + function () { + return array( 'plugin-template' ); + } + ); + + // Custom template overridden by the theme. + wp_register_block_template( + 'gutenberg//custom-template', + array( + 'title' => 'Custom Template (overridden by the theme)', + 'description' => 'A custom template registered by a plugin and overridden by a theme.', + 'content' => '

This is a plugin-registered template and overridden by a theme.

', + 'post_types' => array( 'post' ), + ) + ); + + // Custom template used to test unregistration. + wp_register_block_template( + 'gutenberg//plugin-unregistered-template', + array( + 'title' => 'Plugin Unregistered Template', + 'description' => 'A plugin-registered template that is unregistered.', + 'content' => '

This is a plugin-registered template that is also unregistered.

', + ) + ); + wp_unregister_block_template( 'gutenberg//plugin-unregistered-template' ); + + // Custom template used to test overriding default WP templates. + wp_register_block_template( + 'gutenberg//page', + array( + 'title' => 'Plugin Page Template', + 'description' => 'A plugin-registered page template.', + 'content' => '

This is a plugin-registered page template.

', + ) + ); + + // Custom template used to test overriding default WP templates which can be created by the user. + wp_register_block_template( + 'gutenberg//author-admin', + array( + 'title' => 'Plugin Author Template', + 'description' => 'A plugin-registered author template.', + 'content' => '

This is a plugin-registered author template.

', + ) + ); + } +); diff --git a/packages/edit-site/src/utils/is-template-removable.js b/packages/edit-site/src/utils/is-template-removable.js index 9cb1de23daab75..f81cb74b022e73 100644 --- a/packages/edit-site/src/utils/is-template-removable.js +++ b/packages/edit-site/src/utils/is-template-removable.js @@ -7,7 +7,7 @@ import { TEMPLATE_ORIGINS } from './constants'; * Check if a template is removable. * * @param {Object} template The template entity to check. - * @return {boolean} Whether the template is revertable. + * @return {boolean} Whether the template is removable. */ export default function isTemplateRemovable( template ) { if ( ! template ) { @@ -15,6 +15,8 @@ export default function isTemplateRemovable( template ) { } return ( - template.source === TEMPLATE_ORIGINS.custom && ! template.has_theme_file + template.source === TEMPLATE_ORIGINS.custom && + ! Boolean( template.plugin ) && + ! template.has_theme_file ); } diff --git a/packages/edit-site/src/utils/is-template-revertable.js b/packages/edit-site/src/utils/is-template-revertable.js index a6274d07ebebb6..42413b06cd48ec 100644 --- a/packages/edit-site/src/utils/is-template-revertable.js +++ b/packages/edit-site/src/utils/is-template-revertable.js @@ -15,7 +15,8 @@ export default function isTemplateRevertable( template ) { } /* eslint-disable camelcase */ return ( - template?.source === TEMPLATE_ORIGINS.custom && template?.has_theme_file + template?.source === TEMPLATE_ORIGINS.custom && + ( Boolean( template?.plugin ) || template?.has_theme_file ) ); /* eslint-enable camelcase */ } diff --git a/packages/editor/src/dataviews/actions/reset-post.tsx b/packages/editor/src/dataviews/actions/reset-post.tsx index 59199555ddd4db..cc4cea8f5c82c0 100644 --- a/packages/editor/src/dataviews/actions/reset-post.tsx +++ b/packages/editor/src/dataviews/actions/reset-post.tsx @@ -32,7 +32,8 @@ const resetPost: Action< Post > = { return ( isTemplateOrTemplatePart( item ) && item?.source === TEMPLATE_ORIGINS.custom && - item?.has_theme_file + ( Boolean( item.type === 'wp_template' && item?.plugin ) || + item?.has_theme_file ) ); }, icon: backup, diff --git a/packages/editor/src/dataviews/actions/utils.ts b/packages/editor/src/dataviews/actions/utils.ts index 7da1f71728365b..33a2be16397f3f 100644 --- a/packages/editor/src/dataviews/actions/utils.ts +++ b/packages/editor/src/dataviews/actions/utils.ts @@ -57,6 +57,8 @@ export function isTemplateRemovable( template: Template | TemplatePart ) { return ( [ template.source, template.source ].includes( TEMPLATE_ORIGINS.custom - ) && ! template.has_theme_file + ) && + ! Boolean( template.type === 'wp_template' && template?.plugin ) && + ! template.has_theme_file ); } diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index 514953d6691290..d207410ca2b6a5 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -34,6 +34,8 @@ export interface Template extends CommonPost { type: 'wp_template'; is_custom: boolean; source: string; + origin: string; + plugin?: string; has_theme_file: boolean; id: string; } @@ -41,6 +43,7 @@ export interface Template extends CommonPost { export interface TemplatePart extends CommonPost { type: 'wp_template_part'; source: string; + origin: string; has_theme_file: boolean; id: string; area: string; diff --git a/packages/editor/src/store/private-actions.js b/packages/editor/src/store/private-actions.js index 0996d6eb8b9d32..e22929011256d5 100644 --- a/packages/editor/src/store/private-actions.js +++ b/packages/editor/src/store/private-actions.js @@ -269,7 +269,7 @@ export const revertTemplate = const fileTemplatePath = addQueryArgs( `${ templateEntityConfig.baseURL }/${ template.id }`, - { context: 'edit', source: 'theme' } + { context: 'edit', source: template.origin } ); const fileTemplate = await apiFetch( { path: fileTemplatePath } ); diff --git a/packages/editor/src/store/utils/is-template-revertable.js b/packages/editor/src/store/utils/is-template-revertable.js index a09715af875bc2..2cb674920e3e4c 100644 --- a/packages/editor/src/store/utils/is-template-revertable.js +++ b/packages/editor/src/store/utils/is-template-revertable.js @@ -18,6 +18,7 @@ export default function isTemplateRevertable( templateOrTemplatePart ) { return ( templateOrTemplatePart.source === TEMPLATE_ORIGINS.custom && - templateOrTemplatePart.has_theme_file + ( Boolean( templateOrTemplatePart?.plugin ) || + templateOrTemplatePart?.has_theme_file ) ); } diff --git a/phpunit/block-template-test.php b/phpunit/block-template-test.php new file mode 100644 index 00000000000000..6589aad90b8053 --- /dev/null +++ b/phpunit/block-template-test.php @@ -0,0 +1,37 @@ +assertArrayHasKey( $template_name, $templates ); + + wp_unregister_block_template( $template_name ); + } + + public function test_get_block_template_from_registry() { + $template_name = 'test-plugin//test-template'; + $args = array( + 'title' => 'Test Template', + ); + + wp_register_block_template( $template_name, $args ); + + $template = get_block_template( 'block-theme//test-template' ); + + $this->assertEquals( 'Test Template', $template->title ); + + wp_unregister_block_template( $template_name ); + } +} diff --git a/phpunit/class-gutenberg-rest-templates-controller-test.php b/phpunit/class-gutenberg-rest-templates-controller-test.php new file mode 100644 index 00000000000000..14735246c6fb20 --- /dev/null +++ b/phpunit/class-gutenberg-rest-templates-controller-test.php @@ -0,0 +1,119 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + } + + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + + $template_name = 'test-plugin//test-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Test Template', + 'description' => 'Description of test template', + 'post_types' => array( 'post', 'page' ), + ); + + wp_register_block_template( $template_name, $args ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/test-plugin//test-template' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertNotWPError( $response, "Fetching a registered template shouldn't cause an error." ); + + $data = $response->get_data(); + + $this->assertSame( 'default//test-template', $data['id'], 'Template ID mismatch.' ); + $this->assertSame( 'default', $data['theme'], 'Template theme mismatch.' ); + $this->assertSame( 'Template content', $data['content']['raw'], 'Template content mismatch.' ); + $this->assertSame( 'test-template', $data['slug'], 'Template slug mismatch.' ); + $this->assertSame( 'plugin', $data['source'], "Template source should be 'plugin'." ); + $this->assertSame( 'plugin', $data['origin'], "Template origin should be 'plugin'." ); + $this->assertSame( 'test-plugin', $data['author_text'], 'Template author text mismatch.' ); + $this->assertSame( 'Description of test template', $data['description'], 'Template description mismatch.' ); + $this->assertSame( 'Test Template', $data['title']['rendered'], 'Template title mismatch.' ); + $this->assertSame( 'test-plugin', $data['plugin'], 'Plugin name mismatch.' ); + + wp_unregister_block_template( $template_name ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/test-plugin//test-template' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertNotWPError( $response, "Fetching an unregistered template shouldn't cause an error." ); + $this->assertSame( 404, $response->get_status(), 'Fetching an unregistered template should return 404.' ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_register_routes() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_items() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_create_item() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_update_item() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_delete_item() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_item_schema() { + // Already present in core test class: Tests_REST_WpRestTemplatesController. + } +} diff --git a/phpunit/class-wp-block-templates-registry-test.php b/phpunit/class-wp-block-templates-registry-test.php new file mode 100644 index 00000000000000..fb8436eb6153d4 --- /dev/null +++ b/phpunit/class-wp-block-templates-registry-test.php @@ -0,0 +1,192 @@ +register( $template_name ); + + $this->assertSame( $template->slug, 'test-template' ); + + self::$registry->unregister( $template_name ); + } + + public function test_register_template_invalid_name() { + // Try to register a template with invalid name (non-string). + $template_name = array( 'invalid-template-name' ); + + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( $template_name ); + + $this->assertWPError( $result ); + $this->assertSame( 'template_name_no_string', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertSame( 'Template names must be strings.', $result->get_error_message(), 'Error message mismatch.' ); + } + + public function test_register_template_invalid_name_uppercase() { + // Try to register a template with uppercase characters in the name. + $template_name = 'test-plugin//Invalid-Template-Name'; + + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( $template_name ); + + $this->assertWPError( $result ); + $this->assertSame( 'template_name_no_uppercase', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertSame( 'Template names must not contain uppercase characters.', $result->get_error_message(), 'Error message mismatch.' ); + } + + public function test_register_template_no_prefix() { + // Try to register a template without a namespace. + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( 'template-no-plugin', array() ); + + $this->assertWPError( $result ); + $this->assertSame( 'template_no_prefix', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertSame( 'Template names must contain a namespace prefix. Example: my-plugin//my-custom-template', $result->get_error_message(), 'Error message mismatch.' ); + } + + public function test_register_template_already_exists() { + // Register the template for the first time. + $template_name = 'test-plugin//duplicate-template'; + self::$registry->register( $template_name ); + + // Try to register the same template again. + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( $template_name ); + + $this->assertWPError( $result ); + $this->assertSame( 'template_already_registered', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertStringContainsString( 'Template "test-plugin//duplicate-template" is already registered.', $result->get_error_message(), 'Error message mismatch.' ); + + self::$registry->unregister( $template_name ); + } + + public function test_get_all_registered() { + $template_name_1 = 'test-plugin//template-1'; + $template_name_2 = 'test-plugin//template-2'; + self::$registry->register( $template_name_1 ); + self::$registry->register( $template_name_2 ); + + $all_templates = self::$registry->get_all_registered(); + + $this->assertIsArray( $all_templates, 'Registered templates should be an array.' ); + $this->assertCount( 2, $all_templates, 'Registered templates should contain 2 items.' ); + $this->assertArrayHasKey( 'test-plugin//template-1', $all_templates, 'Registered templates should contain "test-plugin//template-1".' ); + $this->assertArrayHasKey( 'test-plugin//template-2', $all_templates, 'Registered templates should contain "test-plugin//template-2".' ); + + self::$registry->unregister( $template_name_1 ); + self::$registry->unregister( $template_name_2 ); + } + + public function test_get_registered() { + $template_name = 'test-plugin//registered-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Registered Template', + 'description' => 'Description of registered template', + 'post_types' => array( 'post', 'page' ), + ); + self::$registry->register( $template_name, $args ); + + $registered_template = self::$registry->get_registered( $template_name ); + + $this->assertSame( 'default', $registered_template->theme, 'Template theme mismatch.' ); + $this->assertSame( 'registered-template', $registered_template->slug, 'Template slug mismatch.' ); + $this->assertSame( 'default//registered-template', $registered_template->id, 'Template ID mismatch.' ); + $this->assertSame( 'Registered Template', $registered_template->title, 'Template title mismatch.' ); + $this->assertSame( 'Template content', $registered_template->content, 'Template content mismatch.' ); + $this->assertSame( 'Description of registered template', $registered_template->description, 'Template description mismatch.' ); + $this->assertSame( 'plugin', $registered_template->source, "Template source should be 'plugin'." ); + $this->assertSame( 'plugin', $registered_template->origin, "Template origin should be 'plugin'." ); + $this->assertEquals( array( 'post', 'page' ), $registered_template->post_types, 'Template post types mismatch.' ); + $this->assertSame( 'test-plugin', $registered_template->plugin, 'Plugin name mismatch.' ); + + self::$registry->unregister( $template_name ); + } + + public function test_get_by_slug() { + $slug = 'slug-template'; + $template_name = 'test-plugin//' . $slug; + $args = array( + 'content' => 'Template content', + 'title' => 'Slug Template', + ); + self::$registry->register( $template_name, $args ); + + $registered_template = self::$registry->get_by_slug( $slug ); + + $this->assertNotNull( $registered_template, 'Registered template should not be null.' ); + $this->assertSame( $slug, $registered_template->slug, 'Template slug mismatch.' ); + + self::$registry->unregister( $template_name ); + } + + public function test_get_by_query() { + $template_name_1 = 'test-plugin//query-template-1'; + $template_name_2 = 'test-plugin//query-template-2'; + $args_1 = array( + 'content' => 'Template content 1', + 'title' => 'Query Template 1', + ); + $args_2 = array( + 'content' => 'Template content 2', + 'title' => 'Query Template 2', + ); + self::$registry->register( $template_name_1, $args_1 ); + self::$registry->register( $template_name_2, $args_2 ); + + $query = array( + 'slug__in' => array( 'query-template-1' ), + ); + $results = self::$registry->get_by_query( $query ); + + $this->assertCount( 1, $results, 'Query result should contain 1 item.' ); + $this->assertArrayHasKey( $template_name_1, $results, 'Query result should contain "test-plugin//query-template-1".' ); + + self::$registry->unregister( $template_name_1 ); + self::$registry->unregister( $template_name_2 ); + } + + public function test_is_registered() { + $template_name = 'test-plugin//is-registered-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Is Registered Template', + ); + self::$registry->register( $template_name, $args ); + + $this->assertTrue( self::$registry->is_registered( $template_name ) ); + + self::$registry->unregister( $template_name ); + } + + public function test_unregister() { + $template_name = 'test-plugin//unregister-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Unregister Template', + ); + $template = self::$registry->register( $template_name, $args ); + + $unregistered_template = self::$registry->unregister( $template_name ); + + $this->assertEquals( $template, $unregistered_template, 'Unregistered template should be the same as the registered one.' ); + $this->assertFalse( self::$registry->is_registered( $template_name ), 'Template should not be registered after unregistering.' ); + } +} diff --git a/test/e2e/specs/site-editor/template-registration.spec.js b/test/e2e/specs/site-editor/template-registration.spec.js new file mode 100644 index 00000000000000..132e3a8c49a902 --- /dev/null +++ b/test/e2e/specs/site-editor/template-registration.spec.js @@ -0,0 +1,356 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.use( { + blockTemplateRegistrationUtils: async ( { editor, page }, use ) => { + await use( new BlockTemplateRegistrationUtils( { editor, page } ) ); + }, +} ); + +test.describe( 'Block template registration', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + await requestUtils.activatePlugin( + 'gutenberg-test-block-template-registration' + ); + } ); + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( + 'gutenberg-test-block-template-registration' + ); + } ); + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllTemplates( 'wp_template' ); + await requestUtils.deleteAllPosts(); + } ); + + test( 'templates can be registered and edited', async ( { + admin, + editor, + page, + blockTemplateRegistrationUtils, + } ) => { + // Verify template is applied to the frontend. + await page.goto( '/?cat=1' ); + await expect( + page.getByText( 'This is a plugin-registered template.' ) + ).toBeVisible(); + + // Verify template is listed in the Site Editor. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( + 'Plugin Template' + ); + await expect( page.getByText( 'Plugin Template' ) ).toBeVisible(); + await expect( + page.getByText( 'A template registered by a plugin.' ) + ).toBeVisible(); + await expect( page.getByText( 'AuthorGutenberg' ) ).toBeVisible(); + + // Verify the template contents are rendered in the editor. + await page.getByText( 'Plugin Template' ).click(); + await expect( + editor.canvas.getByText( 'This is a plugin-registered template.' ) + ).toBeVisible(); + + // Verify edits persist in the frontend. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'User-edited template' }, + } ); + await editor.saveSiteEditorEntities( { + isOnlyCurrentEntityDirty: true, + } ); + await page.goto( '/?cat=1' ); + await expect( page.getByText( 'User-edited template' ) ).toBeVisible(); + + // Verify template can be reset. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + const resetNotice = page + .getByLabel( 'Dismiss this notice' ) + .getByText( `"Plugin Template" reset.` ); + const savedButton = page.getByRole( 'button', { + name: 'Saved', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( + 'Plugin Template' + ); + const searchResults = page.getByLabel( 'Actions' ); + await searchResults.first().click(); + await page.getByRole( 'menuitem', { name: 'Reset' } ).click(); + await page.getByRole( 'button', { name: 'Reset' } ).click(); + + await expect( resetNotice ).toBeVisible(); + await expect( savedButton ).toBeVisible(); + await page.goto( '/?cat=1' ); + await expect( + page.getByText( 'Content edited template.' ) + ).toBeHidden(); + } ); + + test( 'registered templates are available in the Swap template screen', async ( { + admin, + editor, + page, + } ) => { + // Create a post. + await admin.visitAdminPage( '/post-new.php' ); + await page.getByLabel( 'Close', { exact: true } ).click(); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'User-created post.' }, + } ); + + // Swap template. + await page.getByRole( 'button', { name: 'Post' } ).click(); + await page.getByRole( 'button', { name: 'Template options' } ).click(); + await page.getByRole( 'menuitem', { name: 'Swap template' } ).click(); + await page.getByText( 'Plugin Template' ).click(); + + // Verify the template is applied. + const postId = await editor.publishPost(); + await page.goto( `?p=${ postId }` ); + await expect( + page.getByText( 'This is a plugin-registered template.' ) + ).toBeVisible(); + } ); + + test( 'themes can override registered templates', async ( { + admin, + editor, + page, + blockTemplateRegistrationUtils, + } ) => { + // Create a post. + await admin.visitAdminPage( '/post-new.php' ); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'User-created post.' }, + } ); + + // Swap template. + await page.getByRole( 'button', { name: 'Post' } ).click(); + await page.getByRole( 'button', { name: 'Template options' } ).click(); + await page.getByRole( 'menuitem', { name: 'Swap template' } ).click(); + await page.getByText( 'Custom', { exact: true } ).click(); + + // Verify the theme template is applied. + const postId = await editor.publishPost(); + await page.goto( `?p=${ postId }` ); + await expect( + page.getByText( 'Custom template for Posts' ) + ).toBeVisible(); + await expect( + page.getByText( + 'This is a plugin-registered template and overridden by a theme.' + ) + ).toBeHidden(); + + // Verify the plugin-registered template doesn't appear in the Site Editor. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( 'Custom' ); + await expect( + page.getByText( 'Custom Template (overridden by the theme)' ) + ).toBeHidden(); + // Verify the theme template shows the theme name as the author. + await expect( page.getByText( 'AuthorEmptytheme' ) ).toBeVisible(); + } ); + + test( 'templates can be deleted if the registered plugin is deactivated', async ( { + admin, + editor, + page, + requestUtils, + blockTemplateRegistrationUtils, + } ) => { + // Make an edit to the template. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( + 'Plugin Template' + ); + await page.getByText( 'Plugin Template' ).click(); + await expect( + editor.canvas.getByText( 'This is a plugin-registered template.' ) + ).toBeVisible(); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'User-customized template' }, + } ); + await editor.saveSiteEditorEntities( { + isOnlyCurrentEntityDirty: true, + } ); + + // Deactivate plugin. + await requestUtils.deactivatePlugin( + 'gutenberg-test-block-template-registration' + ); + + // Verify template can be deleted. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + const deletedNotice = page + .getByLabel( 'Dismiss this notice' ) + .getByText( `"Plugin Template" deleted.` ); + const savedButton = page.getByRole( 'button', { + name: 'Saved', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( + 'Plugin Template' + ); + const searchResults = page.getByLabel( 'Actions' ); + await searchResults.first().click(); + await page.getByRole( 'menuitem', { name: 'Delete' } ).click(); + await page.getByRole( 'button', { name: 'Delete' } ).click(); + + await expect( deletedNotice ).toBeVisible(); + await expect( savedButton ).toBeVisible(); + + // Expect template to no longer appear in the Site Editor. + await expect( page.getByLabel( 'Actions' ) ).toBeHidden(); + + // Reactivate plugin. + await requestUtils.activatePlugin( + 'gutenberg-test-block-template-registration' + ); + } ); + + test( 'registered templates can be unregistered', async ( { + admin, + page, + blockTemplateRegistrationUtils, + } ) => { + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( + 'Plugin Unregistered Template' + ); + await expect( + page.getByText( 'Plugin Unregistered Template' ) + ).toBeHidden(); + } ); + + test( 'WP default templates can be overridden by plugins', async ( { + page, + } ) => { + await page.goto( '?page_id=2' ); + await expect( + page.getByText( 'This is a plugin-registered page template.' ) + ).toBeVisible(); + } ); + + test( 'user-customized templates cannot be overridden by plugins', async ( { + admin, + editor, + page, + requestUtils, + blockTemplateRegistrationUtils, + } ) => { + await requestUtils.deactivatePlugin( + 'gutenberg-test-block-template-registration' + ); + + // Create an author template. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + await page.getByLabel( 'Add New Template' ).click(); + await page.getByRole( 'button', { name: 'Author Archives' } ).click(); + await page + .getByRole( 'button', { name: 'Author For a specific item' } ) + .click(); + await page.getByRole( 'option', { name: 'admin' } ).click(); + await expect( page.getByText( 'Choose a pattern' ) ).toBeVisible(); + await page.getByLabel( 'Close', { exact: true } ).click(); + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Author template customized by the user.' }, + } ); + await editor.saveSiteEditorEntities( { + isOnlyCurrentEntityDirty: true, + } ); + + await requestUtils.activatePlugin( + 'gutenberg-test-block-template-registration' + ); + + // Verify the template edited by the user has priority over the one registered by the theme. + await page.goto( '?author=1' ); + await expect( + page.getByText( 'Author template customized by the user.' ) + ).toBeVisible(); + await expect( + page.getByText( 'This is a plugin-registered author template.' ) + ).toBeHidden(); + + // Verify the template registered by the plugin is not visible in the Site Editor. + await admin.visitSiteEditor( { + postType: 'wp_template', + } ); + await blockTemplateRegistrationUtils.searchForTemplate( + 'Plugin Author Template' + ); + await expect( page.getByText( 'Plugin Author Template' ) ).toBeHidden(); + + // Reset the user-modified template. + const resetNotice = page + .getByLabel( 'Dismiss this notice' ) + .getByText( `"Author: Admin" reset.` ); + await page.getByPlaceholder( 'Search' ).fill( 'Author: admin' ); + await page.getByRole( 'link', { name: 'Author: Admin' } ).click(); + const actions = page.getByLabel( 'Actions' ); + await actions.first().click(); + await page.getByRole( 'menuitem', { name: 'Reset' } ).click(); + await page.getByRole( 'button', { name: 'Reset' } ).click(); + + await expect( resetNotice ).toBeVisible(); + + // Verify the template registered by the plugin is applied in the editor... + await expect( + editor.canvas.getByText( 'Author template customized by the user.' ) + ).toBeHidden(); + await expect( + editor.canvas.getByText( + 'This is a plugin-registered author template.' + ) + ).toBeVisible(); + + // ... and the frontend. + await page.goto( '?author=1' ); + await expect( + page.getByText( 'Author template customized by the user.' ) + ).toBeHidden(); + await expect( + page.getByText( 'This is a plugin-registered author template.' ) + ).toBeVisible(); + } ); +} ); + +class BlockTemplateRegistrationUtils { + constructor( { page } ) { + this.page = page; + } + + async searchForTemplate( searchTerm ) { + const searchResults = this.page.getByLabel( 'Actions' ); + await expect + .poll( async () => await searchResults.count() ) + .toBeGreaterThan( 0 ); + const initialSearchResultsCount = await searchResults.count(); + await this.page.getByPlaceholder( 'Search' ).fill( searchTerm ); + await expect + .poll( async () => await searchResults.count() ) + .toBeLessThan( initialSearchResultsCount ); + } +} From 7502c6a39ccb50df5fc1be7c11bac4d8e04aa48b Mon Sep 17 00:00:00 2001 From: Mario Santos <34552881+SantosGuillamot@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:13:46 +0200 Subject: [PATCH 31/48] Fit items into 100% width (#64465) Fix long keys overflow in bindings panel Co-authored-by: SantosGuillamot Co-authored-by: jasmussen --- packages/block-editor/src/hooks/block-bindings.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/hooks/block-bindings.scss b/packages/block-editor/src/hooks/block-bindings.scss index 73e7c490160d3e..603b2115623b8f 100644 --- a/packages/block-editor/src/hooks/block-bindings.scss +++ b/packages/block-editor/src/hooks/block-bindings.scss @@ -1,5 +1,5 @@ div.block-editor-bindings__panel { - grid-template-columns: auto; + grid-template-columns: repeat(auto-fit, minmax(100%, 1fr)); button:hover .block-editor-bindings__item-explanation { color: inherit; } From ea0bcf1ce1345afc840595040a6f4bf9be5faf9c Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 13 Aug 2024 12:35:17 +0200 Subject: [PATCH 32/48] DataViews Extensibility: Allow unregistering the view post revisions action (#64464) Co-authored-by: youknowriad Co-authored-by: ntsekouras --- packages/dataviews/src/types.ts | 6 +++ .../src/components/post-actions/actions.js | 40 +--------------- .../dataviews/actions/view-post-revisions.tsx | 47 +++++++++++++++++++ .../src/dataviews/store/private-actions.ts | 4 ++ packages/editor/src/dataviews/types.ts | 9 +++- 5 files changed, 66 insertions(+), 40 deletions(-) create mode 100644 packages/editor/src/dataviews/actions/view-post-revisions.tsx diff --git a/packages/dataviews/src/types.ts b/packages/dataviews/src/types.ts index 7bbbc8cb863c09..fa5cec8d7d0320 100644 --- a/packages/dataviews/src/types.ts +++ b/packages/dataviews/src/types.ts @@ -426,6 +426,12 @@ interface ActionBase< Item > { * Whether the action can be used as a bulk action. */ supportsBulk?: boolean; + + /** + * The context in which the action is visible. + * This is only a "meta" information for now. + */ + context?: 'list' | 'single'; } export interface RenderModalProps< Item > { diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 627de5e8652e47..831a4f5349869b 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -2,10 +2,9 @@ * WordPress dependencies */ import { external } from '@wordpress/icons'; -import { addQueryArgs } from '@wordpress/url'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { useMemo, useEffect } from '@wordpress/element'; /** @@ -31,40 +30,6 @@ const viewPostAction = { }, }; -const postRevisionsAction = { - id: 'view-post-revisions', - context: 'list', - label( items ) { - const revisionsCount = - items[ 0 ]._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0; - return sprintf( - /* translators: %s: number of revisions */ - __( 'View revisions (%s)' ), - revisionsCount - ); - }, - isEligible: ( post ) => { - if ( post.status === 'trash' ) { - return false; - } - const lastRevisionId = - post?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id ?? null; - const revisionsCount = - post?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0; - return lastRevisionId && revisionsCount > 1; - }, - callback( posts, { onActionPerformed } ) { - const post = posts[ 0 ]; - const href = addQueryArgs( 'revision.php', { - revision: post?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id, - } ); - document.location.href = href; - if ( onActionPerformed ) { - onActionPerformed( posts ); - } - }, -}; - export function usePostActions( { postType, onActionPerformed, context } ) { const { defaultActions, postTypeObject } = useSelect( ( select ) => { @@ -84,7 +49,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { }, [ registerPostTypeActions, postType ] ); const isLoaded = !! postTypeObject; - const supportsRevisions = !! postTypeObject?.supports?.revisions; return useMemo( () => { if ( ! isLoaded ) { return []; @@ -92,7 +56,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { let actions = [ postTypeObject?.viewable && viewPostAction, - supportsRevisions && postRevisionsAction, ...defaultActions, ].filter( Boolean ); // Filter actions based on provided context. If not provided @@ -161,7 +124,6 @@ export function usePostActions( { postType, onActionPerformed, context } ) { postTypeObject?.viewable, onActionPerformed, isLoaded, - supportsRevisions, context, ] ); } diff --git a/packages/editor/src/dataviews/actions/view-post-revisions.tsx b/packages/editor/src/dataviews/actions/view-post-revisions.tsx new file mode 100644 index 00000000000000..875b925b94f070 --- /dev/null +++ b/packages/editor/src/dataviews/actions/view-post-revisions.tsx @@ -0,0 +1,47 @@ +/** + * WordPress dependencies + */ +import { addQueryArgs } from '@wordpress/url'; +import { __, sprintf } from '@wordpress/i18n'; +import type { Action } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import type { Post } from '../types'; + +const viewPostRevisions: Action< Post > = { + id: 'view-post-revisions', + context: 'list', + label( items ) { + const revisionsCount = + items[ 0 ]._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0; + return sprintf( + /* translators: %s: number of revisions */ + __( 'View revisions (%s)' ), + revisionsCount + ); + }, + isEligible( post ) { + if ( post.status === 'trash' ) { + return false; + } + const lastRevisionId = + post?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id ?? null; + const revisionsCount = + post?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0; + return !! lastRevisionId && revisionsCount > 1; + }, + callback( posts, { onActionPerformed } ) { + const post = posts[ 0 ]; + const href = addQueryArgs( 'revision.php', { + revision: post?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id, + } ); + document.location.href = href; + if ( onActionPerformed ) { + onActionPerformed( posts ); + } + }, +}; + +export default viewPostRevisions; diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index d5b299b012e364..3ac121aea7393b 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -22,6 +22,7 @@ import type { PostType } from '../types'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; import duplicatePost from '../actions/duplicate-post'; +import viewPostRevisions from '../actions/view-post-revisions'; export function registerEntityAction< Item >( kind: string, @@ -88,6 +89,9 @@ export const registerPostTypeActions = .getCurrentTheme(); const actions = [ + !! postTypeConfig?.supports?.revisions + ? viewPostRevisions + : undefined, // @ts-ignore globalThis.IS_GUTENBERG_PLUGIN ? ! [ 'wp_template', 'wp_block', 'wp_template_part' ].includes( diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index d207410ca2b6a5..e5886792fb9c33 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -14,6 +14,13 @@ export interface CommonPost { type: string; id: string | number; blocks?: Object[]; + _links?: Links; +} + +interface Links { + 'predecessor-version'?: { href: string; id: number }[]; + 'version-history'?: { href: string; count: number }[]; + [ key: string ]: { href: string }[] | undefined; } export interface BasePost extends CommonPost { @@ -27,7 +34,6 @@ export interface BasePost extends CommonPost { featured_media?: number; menu_order?: number; ping_status?: 'open' | 'closed'; - _links?: Record< string, { href: string }[] >; } export interface Template extends CommonPost { @@ -69,6 +75,7 @@ export interface PostType { supports?: { 'page-attributes'?: boolean; title?: boolean; + revisions?: boolean; }; } From 64643ed8727d915557c4bef65f247ab287d72465 Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 13 Aug 2024 14:59:14 +0400 Subject: [PATCH 33/48] Block Editor: Refactor inner blocks appender components (#64470) * Block Editor: Refactor inner blocks appender components * Remove 'withClientId' HoC Co-authored-by: Mamaduka Co-authored-by: youknowriad Co-authored-by: tyxla --- .../inner-blocks/button-block-appender.js | 12 ++++----- .../inner-blocks/default-block-appender.js | 27 +++---------------- .../components/inner-blocks/with-client-id.js | 19 ------------- 3 files changed, 9 insertions(+), 49 deletions(-) delete mode 100644 packages/block-editor/src/components/inner-blocks/with-client-id.js diff --git a/packages/block-editor/src/components/inner-blocks/button-block-appender.js b/packages/block-editor/src/components/inner-blocks/button-block-appender.js index 500e59863db429..5bc788d58582f6 100644 --- a/packages/block-editor/src/components/inner-blocks/button-block-appender.js +++ b/packages/block-editor/src/components/inner-blocks/button-block-appender.js @@ -7,15 +7,15 @@ import clsx from 'clsx'; * Internal dependencies */ import BaseButtonBlockAppender from '../button-block-appender'; -import withClientId from './with-client-id'; +import { useBlockEditContext } from '../block-edit/context'; -export const ButtonBlockAppender = ( { - clientId, +export default function ButtonBlockAppender( { showSeparator, isFloating, onAddBlock, isToggle, -} ) => { +} ) { + const { clientId } = useBlockEditContext(); return ( ); -}; - -export default withClientId( ButtonBlockAppender ); +} diff --git a/packages/block-editor/src/components/inner-blocks/default-block-appender.js b/packages/block-editor/src/components/inner-blocks/default-block-appender.js index d2e137004d83bf..91e48a2854b513 100644 --- a/packages/block-editor/src/components/inner-blocks/default-block-appender.js +++ b/packages/block-editor/src/components/inner-blocks/default-block-appender.js @@ -1,29 +1,10 @@ -/** - * WordPress dependencies - */ -import { compose } from '@wordpress/compose'; -import { withSelect } from '@wordpress/data'; - /** * Internal dependencies */ import BaseDefaultBlockAppender from '../default-block-appender'; -import withClientId from './with-client-id'; -import { store as blockEditorStore } from '../../store'; +import { useBlockEditContext } from '../block-edit/context'; -export const DefaultBlockAppender = ( { clientId } ) => { +export default function DefaultBlockAppender() { + const { clientId } = useBlockEditContext(); return ; -}; - -export default compose( [ - withClientId, - withSelect( ( select, { clientId } ) => { - const { getBlockOrder } = select( blockEditorStore ); - - const blockClientIds = getBlockOrder( clientId ); - - return { - lastBlockClientId: blockClientIds[ blockClientIds.length - 1 ], - }; - } ), -] )( DefaultBlockAppender ); +} diff --git a/packages/block-editor/src/components/inner-blocks/with-client-id.js b/packages/block-editor/src/components/inner-blocks/with-client-id.js deleted file mode 100644 index 97c73ae2803934..00000000000000 --- a/packages/block-editor/src/components/inner-blocks/with-client-id.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * WordPress dependencies - */ -import { createHigherOrderComponent } from '@wordpress/compose'; - -/** - * Internal dependencies - */ -import { useBlockEditContext } from '../block-edit/context'; - -const withClientId = createHigherOrderComponent( - ( WrappedComponent ) => ( props ) => { - const { clientId } = useBlockEditContext(); - return ; - }, - 'withClientId' -); - -export default withClientId; From e8b45dacc23052952e01b4869027c9354818d74a Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 13 Aug 2024 13:14:17 +0200 Subject: [PATCH 34/48] DataViews Extensibility: Allow unregistering the view post action (#64467) Co-authored-by: youknowriad Co-authored-by: Mamaduka --- .../src/components/post-actions/actions.js | 43 ++----------------- .../src/dataviews/actions/view-post.tsx | 30 +++++++++++++ .../src/dataviews/store/private-actions.ts | 11 +++-- packages/editor/src/dataviews/types.ts | 2 + 4 files changed, 42 insertions(+), 44 deletions(-) create mode 100644 packages/editor/src/dataviews/actions/view-post.tsx diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js index 831a4f5349869b..e1c0ed1558193d 100644 --- a/packages/editor/src/components/post-actions/actions.js +++ b/packages/editor/src/components/post-actions/actions.js @@ -1,10 +1,7 @@ /** * WordPress dependencies */ -import { external } from '@wordpress/icons'; import { useDispatch, useSelect } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; -import { __ } from '@wordpress/i18n'; import { useMemo, useEffect } from '@wordpress/element'; /** @@ -13,30 +10,11 @@ import { useMemo, useEffect } from '@wordpress/element'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; -const viewPostAction = { - id: 'view-post', - label: __( 'View' ), - isPrimary: true, - icon: external, - isEligible( post ) { - return post.status !== 'trash'; - }, - callback( posts, { onActionPerformed } ) { - const post = posts[ 0 ]; - window.open( post.link, '_blank' ); - if ( onActionPerformed ) { - onActionPerformed( posts ); - } - }, -}; - export function usePostActions( { postType, onActionPerformed, context } ) { - const { defaultActions, postTypeObject } = useSelect( + const { defaultActions } = useSelect( ( select ) => { - const { getPostType } = select( coreStore ); const { getEntityActions } = unlock( select( editorStore ) ); return { - postTypeObject: getPostType( postType ), defaultActions: getEntityActions( 'postType', postType ), }; }, @@ -48,23 +26,14 @@ export function usePostActions( { postType, onActionPerformed, context } ) { registerPostTypeActions( postType ); }, [ registerPostTypeActions, postType ] ); - const isLoaded = !! postTypeObject; return useMemo( () => { - if ( ! isLoaded ) { - return []; - } - - let actions = [ - postTypeObject?.viewable && viewPostAction, - ...defaultActions, - ].filter( Boolean ); // Filter actions based on provided context. If not provided // all actions are returned. We'll have a single entry for getting the actions // and the consumer should provide the context to filter the actions, if needed. // Actions should also provide the `context` they support, if it's specific, to // compare with the provided context to get all the actions. // Right now the only supported context is `list`. - actions = actions.filter( ( action ) => { + const actions = defaultActions.filter( ( action ) => { if ( ! action.context ) { return true; } @@ -119,11 +88,5 @@ export function usePostActions( { postType, onActionPerformed, context } ) { } return actions; - }, [ - defaultActions, - postTypeObject?.viewable, - onActionPerformed, - isLoaded, - context, - ] ); + }, [ defaultActions, onActionPerformed, context ] ); } diff --git a/packages/editor/src/dataviews/actions/view-post.tsx b/packages/editor/src/dataviews/actions/view-post.tsx new file mode 100644 index 00000000000000..47eb1a66d019ad --- /dev/null +++ b/packages/editor/src/dataviews/actions/view-post.tsx @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { external } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; +import type { Action } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import type { BasePost } from '../types'; + +const viewPost: Action< BasePost > = { + id: 'view-post', + label: __( 'View' ), + isPrimary: true, + icon: external, + isEligible( post ) { + return post.status !== 'trash'; + }, + callback( posts, { onActionPerformed } ) { + const post = posts[ 0 ]; + window.open( post?.link, '_blank' ); + if ( onActionPerformed ) { + onActionPerformed( posts ); + } + }, +}; + +export default viewPost; diff --git a/packages/editor/src/dataviews/store/private-actions.ts b/packages/editor/src/dataviews/store/private-actions.ts index 3ac121aea7393b..a9101e57dd08b5 100644 --- a/packages/editor/src/dataviews/store/private-actions.ts +++ b/packages/editor/src/dataviews/store/private-actions.ts @@ -23,6 +23,7 @@ import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; import duplicatePost from '../actions/duplicate-post'; import viewPostRevisions from '../actions/view-post-revisions'; +import viewPost from '../actions/view-post'; export function registerEntityAction< Item >( kind: string, @@ -89,6 +90,7 @@ export const registerPostTypeActions = .getCurrentTheme(); const actions = [ + postTypeConfig.viewable ? viewPost : undefined, !! postTypeConfig?.supports?.revisions ? viewPostRevisions : undefined, @@ -101,9 +103,10 @@ export const registerPostTypeActions = duplicatePost : undefined, postTypeConfig.slug === 'wp_template_part' && - canCreate && - currentTheme?.is_block_theme && - duplicateTemplatePart, + canCreate && + currentTheme?.is_block_theme + ? duplicateTemplatePart + : undefined, canCreate && postTypeConfig.slug === 'wp_block' ? duplicatePattern : undefined, @@ -121,7 +124,7 @@ export const registerPostTypeActions = registry.batch( () => { actions.forEach( ( action ) => { - if ( action === undefined ) { + if ( ! action ) { return; } unlock( registry.dispatch( editorStore ) ).registerEntityAction( diff --git a/packages/editor/src/dataviews/types.ts b/packages/editor/src/dataviews/types.ts index e5886792fb9c33..664c2dd417201c 100644 --- a/packages/editor/src/dataviews/types.ts +++ b/packages/editor/src/dataviews/types.ts @@ -34,6 +34,7 @@ export interface BasePost extends CommonPost { featured_media?: number; menu_order?: number; ping_status?: 'open' | 'closed'; + link?: string; } export interface Template extends CommonPost { @@ -72,6 +73,7 @@ export type PostWithPermissions = Post & { export interface PostType { slug: string; + viewable: boolean; supports?: { 'page-attributes'?: boolean; title?: boolean; From 3ec1ced865757f6568c42c126acb3478b25fe2be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:43:15 +0200 Subject: [PATCH 35/48] Add `status` to quick edit (#64398) Co-authored-by: oandregal Co-authored-by: youknowriad Co-authored-by: ntsekouras --- .../src/components/post-edit/index.js | 29 +++++++++++++- .../src/components/post-fields/index.js | 40 +++++++++++++++---- 2 files changed, 59 insertions(+), 10 deletions(-) diff --git a/packages/edit-site/src/components/post-edit/index.js b/packages/edit-site/src/components/post-edit/index.js index 80304f16503705..0ec63589d97673 100644 --- a/packages/edit-site/src/components/post-edit/index.js +++ b/packages/edit-site/src/components/post-edit/index.js @@ -42,13 +42,38 @@ function PostEditForm( { postType, postId } ) { ); const [ multiEdits, setMultiEdits ] = useState( {} ); const { editEntityRecord } = useDispatch( coreDataStore ); - const { fields } = usePostFields(); + const { fields: _fields } = usePostFields(); + const fields = useMemo( + () => + _fields?.map( ( field ) => { + if ( field.id === 'status' ) { + return { + ...field, + elements: field.elements.filter( + ( element ) => element.value !== 'trash' + ), + }; + } + return field; + } ), + [ _fields ] + ); const form = { type: 'panel', - fields: [ 'title', 'author', 'date', 'comment_status' ], + fields: [ 'title', 'status', 'date', 'author', 'comment_status' ], }; const onChange = ( edits ) => { for ( const id of ids ) { + if ( + edits.status !== 'future' && + record.status === 'future' && + new Date( record.date ) > new Date() + ) { + edits.date = null; + } + if ( edits.status === 'private' && record.password ) { + edits.password = ''; + } editEntityRecord( 'postType', postType, id, edits ); if ( ids.length > 1 ) { setMultiEdits( ( prev ) => ( { diff --git a/packages/edit-site/src/components/post-fields/index.js b/packages/edit-site/src/components/post-fields/index.js index b03b2c6f5be3c4..9e59b23d61922d 100644 --- a/packages/edit-site/src/components/post-fields/index.js +++ b/packages/edit-site/src/components/post-fields/index.js @@ -42,11 +42,36 @@ import Media from '../media'; // See https://github.com/WordPress/gutenberg/issues/55886 // We do not support custom statutes at the moment. const STATUSES = [ - { value: 'draft', label: __( 'Draft' ), icon: drafts }, - { value: 'future', label: __( 'Scheduled' ), icon: scheduled }, - { value: 'pending', label: __( 'Pending Review' ), icon: pending }, - { value: 'private', label: __( 'Private' ), icon: notAllowed }, - { value: 'publish', label: __( 'Published' ), icon: published }, + { + value: 'draft', + label: __( 'Draft' ), + icon: drafts, + description: __( 'Not ready to publish.' ), + }, + { + value: 'future', + label: __( 'Scheduled' ), + icon: scheduled, + description: __( 'Publish automatically on a chosen date.' ), + }, + { + value: 'pending', + label: __( 'Pending Review' ), + icon: pending, + description: __( 'Waiting for review before publishing.' ), + }, + { + value: 'private', + label: __( 'Private' ), + icon: notAllowed, + description: __( 'Only visible to site admins and editors.' ), + }, + { + value: 'publish', + label: __( 'Published' ), + icon: published, + description: __( 'Visible to everyone.' ), + }, { value: 'trash', label: __( 'Trash' ), icon: trash }, ]; @@ -258,11 +283,10 @@ function usePostFields( viewType ) { { label: __( 'Status' ), id: 'status', - getValue: ( { item } ) => - STATUSES.find( ( { value } ) => value === item.status ) - ?.label ?? item.status, + type: 'text', elements: STATUSES, render: PostStatusField, + Edit: 'radio', enableSorting: false, filterBy: { operators: [ OPERATOR_IS_ANY ], From 9ca5628ed1cd11975086b25cd0cc318352b94619 Mon Sep 17 00:00:00 2001 From: JuanMa Date: Tue, 13 Aug 2024 13:33:04 +0100 Subject: [PATCH 36/48] Fix tip link in DataViews docs (#64469) Co-authored-by: juanmaguitar Co-authored-by: oandregal --- packages/dataviews/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 56cecedaef66ca..368880b69b14f0 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -30,8 +30,7 @@ const Example = () => { }; ``` -> [!TIP] -> At https://wordpress.github.io/gutenberg/?path=/docs/dataviews-dataviews--docs there's an example implementation of the Dataviews component." + ## Properties From 02d052033868ea554bbdfee9b18b766e77128a6b Mon Sep 17 00:00:00 2001 From: George Mamadashvili Date: Tue, 13 Aug 2024 17:18:12 +0400 Subject: [PATCH 37/48] Edit Post: Remove user pattern preloading (#64459) * Edit Post: Remove user pattern preloading * Update backport changelog Co-authored-by: Mamaduka Co-authored-by: tyxla --- backport-changelog/6.7/7179.md | 1 + lib/compat/wordpress-6.7/rest-api.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/backport-changelog/6.7/7179.md b/backport-changelog/6.7/7179.md index f359b6610a94e6..a6e9cc4efe2cd0 100644 --- a/backport-changelog/6.7/7179.md +++ b/backport-changelog/6.7/7179.md @@ -1,3 +1,4 @@ https://github.com/WordPress/wordpress-develop/pull/7179 * https://github.com/WordPress/gutenberg/pull/64401 +* https://github.com/WordPress/gutenberg/pull/64459 diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index fe2aac9c2580ae..2520a06fbb18be 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -26,6 +26,24 @@ function gutenberg_block_editor_preload_paths_6_7( $paths, $context ) { } } + if ( 'core/edit-post' === $context->name ) { + $reusable_blocks_key = array_search( + add_query_arg( + array( + 'context' => 'edit', + 'per_page' => -1, + ), + rest_get_route_for_post_type_items( 'wp_block' ) + ), + $paths, + true + ); + + if ( false !== $parts_key ) { + unset( $paths[ $reusable_blocks_key ] ); + } + } + return $paths; } add_filter( 'block_editor_rest_api_preload_paths', 'gutenberg_block_editor_preload_paths_6_7', 10, 2 ); From cedbe111a2cc2b806356a985df1f47034b7545df Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 13 Aug 2024 17:55:15 +0300 Subject: [PATCH 38/48] Update the copy of quick edit tooltip (#64475) Co-authored-by: ntsekouras Co-authored-by: youknowriad Co-authored-by: jasmussen --- packages/edit-site/src/components/post-list/index.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/edit-site/src/components/post-list/index.js b/packages/edit-site/src/components/post-list/index.js index 35ecaaa8424fd4..bbfece24518495 100644 --- a/packages/edit-site/src/components/post-list/index.js +++ b/packages/edit-site/src/components/post-list/index.js @@ -366,11 +366,7 @@ export default function PostList( { postType } ) { size="compact" isPressed={ quickEdit } icon={ drawerRight } - label={ - ! quickEdit - ? __( 'Show quick edit sidebar' ) - : __( 'Close quick edit sidebar' ) - } + label={ __( 'Toggle details panel' ) } onClick={ () => { history.push( { ...location.params, From b3ce94ea25990e329ce4c08f3d49eb07e7ec929b Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Tue, 13 Aug 2024 18:40:08 +0300 Subject: [PATCH 39/48] Edit Post: Fix user pattern preloading filter (#64477) * Edit Post: Fix user pattern preloading filter * Backport changelog Co-authored-by: tyxla Co-authored-by: Mamaduka --- backport-changelog/6.7/7179.md | 1 + lib/compat/wordpress-6.7/rest-api.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backport-changelog/6.7/7179.md b/backport-changelog/6.7/7179.md index a6e9cc4efe2cd0..d777eace2cb05e 100644 --- a/backport-changelog/6.7/7179.md +++ b/backport-changelog/6.7/7179.md @@ -2,3 +2,4 @@ https://github.com/WordPress/wordpress-develop/pull/7179 * https://github.com/WordPress/gutenberg/pull/64401 * https://github.com/WordPress/gutenberg/pull/64459 +* https://github.com/WordPress/gutenberg/pull/64477 diff --git a/lib/compat/wordpress-6.7/rest-api.php b/lib/compat/wordpress-6.7/rest-api.php index 2520a06fbb18be..081c22c8102914 100644 --- a/lib/compat/wordpress-6.7/rest-api.php +++ b/lib/compat/wordpress-6.7/rest-api.php @@ -39,7 +39,7 @@ function gutenberg_block_editor_preload_paths_6_7( $paths, $context ) { true ); - if ( false !== $parts_key ) { + if ( false !== $reusable_blocks_key ) { unset( $paths[ $reusable_blocks_key ] ); } } From 5dae25b15e82a59d28c6c065b5a3745f3f83980f Mon Sep 17 00:00:00 2001 From: Jan Pfeil Date: Tue, 13 Aug 2024 18:40:35 +0200 Subject: [PATCH 40/48] fix typo in block-wrapper.md (#64447) --- docs/getting-started/fundamentals/block-wrapper.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/fundamentals/block-wrapper.md b/docs/getting-started/fundamentals/block-wrapper.md index 39c80262d7bcbe..98c435f6ebe2f7 100644 --- a/docs/getting-started/fundamentals/block-wrapper.md +++ b/docs/getting-started/fundamentals/block-wrapper.md @@ -102,7 +102,7 @@ The [example block](https://github.com/WordPress/block-development-examples/tree ## Dynamic render markup -In dynamic blocks, where the font-end markup is rendered server-side, you can utilize the [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) function to output the necessary classes and attributes just like you would use `useBlockProps.save()` in the `save` function. (See [example](https://github.com/WordPress/block-development-examples/blob/f68640f42d993f0866d1879f67c73910285ca114/plugins/block-dynamic-rendering-64756b/src/render.php#L11)) +In dynamic blocks, where the front-end markup is rendered server-side, you can utilize the [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) function to output the necessary classes and attributes just like you would use `useBlockProps.save()` in the `save` function. (See [example](https://github.com/WordPress/block-development-examples/blob/f68640f42d993f0866d1879f67c73910285ca114/plugins/block-dynamic-rendering-64756b/src/render.php#L11)) ```php

> From 3be25a1db5915a94fe755f078ed35b63ebf7cc91 Mon Sep 17 00:00:00 2001 From: James Koster Date: Tue, 13 Aug 2024 20:52:40 +0100 Subject: [PATCH 41/48] Apply minimal variant to pagination dropdown (#63815) Co-authored-by: jameskoster Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: t-hamano Co-authored-by: ntsekouras Co-authored-by: swissspidy Co-authored-by: jasmussen --- .../components/dataviews-pagination/index.tsx | 51 +++++++++++++------ .../dataviews-pagination/style.scss | 13 +++-- 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/packages/dataviews/src/components/dataviews-pagination/index.tsx b/packages/dataviews/src/components/dataviews-pagination/index.tsx index f8ebf41469d949..f022b382cdb70d 100644 --- a/packages/dataviews/src/components/dataviews-pagination/index.tsx +++ b/packages/dataviews/src/components/dataviews-pagination/index.tsx @@ -21,10 +21,31 @@ function DataViewsPagination() { onChangeView, paginationInfo: { totalItems = 0, totalPages }, } = useContext( DataViewsContext ); + if ( ! totalItems || ! totalPages ) { return null; } + const currentPage = view.page ?? 1; + const pageSelectOptions = Array.from( Array( totalPages ) ).map( + ( _, i ) => { + const page = i + 1; + return { + value: page.toString(), + label: page.toString(), + 'aria-label': + currentPage === page + ? sprintf( + // translators: Current page number in total number of pages + __( 'Page %1$s of %2$s' ), + currentPage, + totalPages + ) + : page.toString(), + }; + } + ); + return ( !! totalItems && totalPages !== 1 && ( @@ -37,37 +58,35 @@ function DataViewsPagination() { { createInterpolateElement( sprintf( - // translators: %s: Total number of pages. - _x( 'Page of %s', 'paging' ), + // translators: 1: Current page number, 2: Total number of pages. + _x( + '

Page
%1$s
of %2$s
', + 'paging' + ), + '', totalPages ), { - CurrentPageControl: ( + div:
, + CurrentPage: ( { - const page = i + 1; - return { - value: page.toString(), - label: page.toString(), - }; - } ) } + value={ currentPage.toString() } + options={ pageSelectOptions } onChange={ ( newValue ) => { onChangeView( { ...view, page: +newValue, } ); } } - size="compact" + size="small" __nextHasNoMarginBottom + variant="minimal" /> ), } diff --git a/packages/dataviews/src/components/dataviews-pagination/style.scss b/packages/dataviews/src/components/dataviews-pagination/style.scss index 4e754ab90fa54a..16f064cc3a5178 100644 --- a/packages/dataviews/src/components/dataviews-pagination/style.scss +++ b/packages/dataviews/src/components/dataviews-pagination/style.scss @@ -5,17 +5,22 @@ background-color: $white; padding: $grid-unit-15 $grid-unit-60; border-top: $border-width solid $gray-100; - color: $gray-700; flex-shrink: 0; transition: padding ease-out 0.1s; @include reduce-motion("transition"); } -.dataviews-pagination__page-selection { +.dataviews-pagination__page-select { font-size: 11px; - text-transform: uppercase; font-weight: 500; - color: $gray-900; + text-transform: uppercase; + + @include break-small() { + .components-select-control__input { + font-size: 11px !important; + font-weight: 500; + } + } } /* stylelint-disable-next-line scss/at-rule-no-unknown -- '@container' not globally permitted */ From 562fff19e73a1416bdc533ef372b8117beeed436 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 14 Aug 2024 04:53:08 +0900 Subject: [PATCH 42/48] Start adding lint rules for 40px default size (#64410) * Start adding lint rules for 40px default size * Make stricter Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- .eslintrc.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 6143d62c28bc07..0dc184eabb8b04 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -278,7 +278,6 @@ module.exports = { }, }, { - // Temporary rules until we're ready to officially deprecate the bottom margins. files: [ 'packages/*/src/**/*.[tj]s?(x)' ], excludedFiles: [ 'packages/components/src/**/@(test|stories)/**', @@ -289,6 +288,7 @@ module.exports = { 'error', ...restrictedSyntax, ...restrictedSyntaxComponents, + // Temporary rules until we're ready to officially deprecate the bottom margins. ...[ 'BaseControl', 'CheckboxControl', @@ -309,6 +309,19 @@ module.exports = { componentName + ' should have the `__nextHasNoMarginBottom` prop to opt-in to the new margin-free styles.', } ) ), + // Temporary rules until we're ready to officially default to the new size. + ...[ + 'BorderBoxControl', + 'BorderControl', + 'DimensionControl', + 'FontSizePicker', + ].map( ( componentName ) => ( { + // Falsy `__next40pxDefaultSize` without a non-default `size` prop. + selector: `JSXOpeningElement[name.name="${ componentName }"]:not(:has(JSXAttribute[name.name="__next40pxDefaultSize"][value.expression.value!=false])):not(:has(JSXAttribute[name.name="size"][value.value!="default"]))`, + message: + componentName + + ' should have the `__next40pxDefaultSize` prop to opt-in to the new default size.', + } ) ), ], }, }, From 40b51b8619e120c96c3dfc6844ed72f3c84e2ca4 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 14 Aug 2024 05:28:37 +0900 Subject: [PATCH 43/48] QueryControls: Default to new 40px size (#64457) * QueryControls: Default to new 40px size * Add changelog Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 1 + packages/components/src/query-controls/index.tsx | 11 +++++------ packages/components/src/query-controls/types.ts | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 761e6604a127a2..6b95e23818c864 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,7 @@ ### Enhancements - `Composite`: improve Storybook examples and add interactive controls ([#64397](https://github.com/WordPress/gutenberg/pull/64397)). +- `QueryControls`: Default to new 40px size ([#64457](https://github.com/WordPress/gutenberg/pull/64457)). - `TimePicker`: add `hideLabelFromVision` prop ([#64267](https://github.com/WordPress/gutenberg/pull/64267)). - `DropdownMenuV2`: adopt elevation scale ([#64432](https://github.com/WordPress/gutenberg/pull/64432)). diff --git a/packages/components/src/query-controls/index.tsx b/packages/components/src/query-controls/index.tsx index 3557335ebac5a0..452dd303c778bb 100644 --- a/packages/components/src/query-controls/index.tsx +++ b/packages/components/src/query-controls/index.tsx @@ -60,7 +60,6 @@ function isMultipleCategorySelection( * ``` */ export function QueryControls( { - __next40pxDefaultSize = false, authorList, selectedAuthorId, numberOfItems, @@ -82,7 +81,7 @@ export function QueryControls( { onOrderChange && onOrderByChange && ( Date: Wed, 14 Aug 2024 06:32:10 +0900 Subject: [PATCH 44/48] TextControl: Add lint rule for 40px size prop usage (#64455) * TextControl: Add lint rule for 40px size prop usage * Fixup * Fixup formatting Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- .eslintrc.js | 8 ++++++++ .../src/components/date-format-picker/index.js | 2 ++ .../src/components/media-upload/index.native.js | 2 ++ .../src/components/url-popover/image-url-input-ui.js | 4 ++++ packages/block-library/src/button/edit.js | 2 ++ packages/block-library/src/form-input/edit.js | 2 ++ packages/block-library/src/form/edit.js | 2 ++ packages/block-library/src/image/image.js | 2 ++ .../src/navigation/edit/navigation-menu-name-control.js | 2 ++ packages/block-library/src/post-comment/edit.js | 2 ++ packages/block-library/src/post-featured-image/edit.js | 2 ++ packages/block-library/src/post-terms/edit.js | 2 ++ packages/block-library/src/post-title/edit.js | 2 ++ packages/block-library/src/social-link/edit.js | 4 ++++ .../src/template-part/edit/advanced-controls.js | 2 ++ packages/block-library/src/video/tracks-editor.js | 4 ++++ .../src/components/sidebar-dataviews/add-new-view.js | 2 ++ .../components/sidebar-dataviews/custom-dataviews-list.js | 2 ++ .../src/components/post-publish-panel/postpublish.js | 2 ++ packages/editor/src/components/post-slug/index.js | 2 ++ .../post-taxonomies/hierarchical-term-selector.js | 2 ++ .../reusable-block-convert-button.js | 2 ++ 22 files changed, 56 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 0dc184eabb8b04..eb2d2db47e4cab 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -322,6 +322,14 @@ module.exports = { componentName + ' should have the `__next40pxDefaultSize` prop to opt-in to the new default size.', } ) ), + // Temporary rules until all existing components have the `__next40pxDefaultSize` prop. + ...[ 'TextControl' ].map( ( componentName ) => ( { + // Not strict. Allows pre-existing __next40pxDefaultSize={ false } usage until they are all manually updated. + selector: `JSXOpeningElement[name.name="${ componentName }"]:not(:has(JSXAttribute[name.name="__next40pxDefaultSize"])):not(:has(JSXAttribute[name.name="size"]))`, + message: + componentName + + ' should have the `__next40pxDefaultSize` prop to opt-in to the new default size.', + } ) ), ], }, }, diff --git a/packages/block-editor/src/components/date-format-picker/index.js b/packages/block-editor/src/components/date-format-picker/index.js index 15beec4ac6ed54..63c977b111e01c 100644 --- a/packages/block-editor/src/components/date-format-picker/index.js +++ b/packages/block-editor/src/components/date-format-picker/index.js @@ -149,6 +149,8 @@ function NonDefaultControls( { format, onChange } ) { /> { isCustom && ( { isLinkTag && ( { ) } /> ( diff --git a/packages/block-library/src/post-featured-image/edit.js b/packages/block-library/src/post-featured-image/edit.js index c9f4645e5e8654..675580c71c1d7b 100644 --- a/packages/block-library/src/post-featured-image/edit.js +++ b/packages/block-library/src/post-featured-image/edit.js @@ -230,6 +230,8 @@ export default function PostFeaturedImageEdit( { checked={ linkTarget === '_blank' } /> onChange( { diff --git a/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js b/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js index aabb49c14a2ff7..69cca49fd84563 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js +++ b/packages/edit-site/src/components/sidebar-dataviews/add-new-view.js @@ -78,6 +78,8 @@ function AddNewItemModalContent( { type, setIsAdding } ) { >
{ showFilter && ( Date: Wed, 14 Aug 2024 07:15:24 +0900 Subject: [PATCH 45/48] Deprecate bottom margin on BaseControl-based components (#64408) * BaseControl: Deprecate bottom margin * Propagate to components * Missed spots * Add changelog * List all affected components in changelog Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla --- packages/components/CHANGELOG.md | 17 ++++++++++ .../components/src/base-control/index.tsx | 13 +++++++ packages/components/src/base-control/types.ts | 7 ++++ .../components/src/checkbox-control/index.tsx | 1 + .../checkbox-control/stories/index.story.tsx | 1 + .../src/checkbox-control/test/index.tsx | 9 ++++- .../components/src/combobox-control/index.tsx | 1 + .../combobox-control/stories/index.story.tsx | 7 ++-- .../src/combobox-control/test/index.tsx | 6 +++- .../src/dimension-control/index.tsx | 34 +++++++++++++------ .../dimension-control/stories/index.story.tsx | 1 + .../test/__snapshots__/index.test.js.snap | 16 --------- .../src/dimension-control/test/index.test.js | 6 +++- .../src/focal-point-picker/index.tsx | 1 + .../stories/index.story.tsx | 3 ++ .../src/focal-point-picker/test/index.tsx | 6 +++- .../components/src/range-control/index.tsx | 1 + .../src/range-control/stories/index.story.tsx | 7 ++++ .../src/range-control/test/index.tsx | 8 ++++- .../components/src/search-control/index.tsx | 11 +++--- .../search-control/stories/index.story.tsx | 1 + .../src/search-control/test/index.tsx | 1 + .../components/src/select-control/index.tsx | 1 + .../select-control/stories/index.story.tsx | 13 ++++--- .../select-control/test/select-control.tsx | 18 ++++++---- .../components/src/text-control/index.tsx | 1 + .../src/text-control/stories/index.story.tsx | 4 ++- .../src/text-control/test/text-control.tsx | 6 +++- .../components/src/textarea-control/index.tsx | 1 + .../textarea-control/stories/index.story.tsx | 1 + .../components/src/toggle-control/index.tsx | 9 +++++ .../toggle-control/stories/index.story.tsx | 1 + .../src/toggle-control/test/index.tsx | 8 ++++- .../test/__snapshots__/index.tsx.snap | 16 --------- .../src/toggle-group-control/test/index.tsx | 6 +++- .../toggle-group-control/component.tsx | 1 + packages/components/src/tree-select/index.tsx | 21 +++++++++--- .../src/tree-select/stories/index.story.tsx | 1 + 38 files changed, 191 insertions(+), 75 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6b95e23818c864..36b1c8fcc55d00 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,23 @@ ## Unreleased +### Deprecations + +- Deprecate bottom margin on the following `BaseControl`-based components. Set the `__nextHasNoMarginBottom` prop to true to start opting into the new styles, which will become the default in a future version ([#64408](https://github.com/WordPress/gutenberg/pull/64408)). + - `BaseControl` + - `CheckboxControl` + - `ComboboxControl` + - `DimensionControl` + - `FocalPointPicker` + - `RangeControl` + - `SearchControl` + - `SelectControl` + - `TextControl` + - `TextareaControl` + - `ToggleControl` + - `ToggleGroupControl` + - `TreeSelect` + ### New Features - `Composite`: add stable version of the component ([#63564](https://github.com/WordPress/gutenberg/pull/63564)). diff --git a/packages/components/src/base-control/index.tsx b/packages/components/src/base-control/index.tsx index 77899b6480daed..423636a92cd5f0 100644 --- a/packages/components/src/base-control/index.tsx +++ b/packages/components/src/base-control/index.tsx @@ -7,6 +7,7 @@ import type { ForwardedRef } from 'react'; /** * WordPress dependencies */ +import deprecated from '@wordpress/deprecated'; import { forwardRef } from '@wordpress/element'; /** @@ -31,6 +32,7 @@ const UnconnectedBaseControl = ( ) => { const { __nextHasNoMarginBottom = false, + __associatedWPComponentName = 'BaseControl', id, label, hideLabelFromVision = false, @@ -39,6 +41,17 @@ const UnconnectedBaseControl = ( children, } = useContextSystem( props, 'BaseControl' ); + if ( ! __nextHasNoMarginBottom ) { + deprecated( + `Bottom margin styles for wp.components.${ __associatedWPComponentName }`, + { + since: '6.7', + version: '7.0', + hint: 'Set the `__nextHasNoMarginBottom` prop to true to start opting into the new styles, which will become the default in a future version.', + } + ); + } + return ( = DefaultTemplate.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, label: 'Is author', help: 'Is the user an author or not?', }; diff --git a/packages/components/src/checkbox-control/test/index.tsx b/packages/components/src/checkbox-control/test/index.tsx index 899a9b100015b3..547f479184e862 100644 --- a/packages/components/src/checkbox-control/test/index.tsx +++ b/packages/components/src/checkbox-control/test/index.tsx @@ -20,13 +20,20 @@ const noop = () => {}; const getInput = () => screen.getByRole( 'checkbox' ) as HTMLInputElement; const CheckboxControl = ( props: Omit< CheckboxControlProps, 'onChange' > ) => { - return ; + return ( + + ); }; const ControlledCheckboxControl = ( { onChange }: CheckboxControlProps ) => { const [ isChecked, setChecked ] = useState( false ); return ( { setChecked( value ); diff --git a/packages/components/src/combobox-control/index.tsx b/packages/components/src/combobox-control/index.tsx index e3b1434be8c7c2..fc3ecccf0b6599 100644 --- a/packages/components/src/combobox-control/index.tsx +++ b/packages/components/src/combobox-control/index.tsx @@ -320,6 +320,7 @@ function ComboboxControl( props: ComboboxControlProps ) { = ( { }; export const Default = Template.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, allowReset: false, label: 'Select a country', options: countryOptions, @@ -135,8 +136,7 @@ const optionsWithDisabledOptions = countryOptions.map( ( option, index ) => ( { } ) ); WithDisabledOptions.args = { - allowReset: false, - label: 'Select a country', + ...Default.args, options: optionsWithDisabledOptions, }; @@ -148,8 +148,7 @@ WithDisabledOptions.args = { export const NotExpandOnFocus = Template.bind( {} ); NotExpandOnFocus.args = { - allowReset: false, - label: 'Select a country', + ...Default.args, options: countryOptions, expandOnFocus: false, }; diff --git a/packages/components/src/combobox-control/test/index.tsx b/packages/components/src/combobox-control/test/index.tsx index 76ce9cc4724c54..adc76590c24538 100644 --- a/packages/components/src/combobox-control/test/index.tsx +++ b/packages/components/src/combobox-control/test/index.tsx @@ -12,7 +12,7 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import ComboboxControl from '..'; +import _ComboboxControl from '..'; import type { ComboboxControlOption, ComboboxControlProps } from '../types'; const timezones = [ @@ -57,6 +57,10 @@ const getAllOptions = () => screen.getAllByRole( 'option' ); const getOptionSearchString = ( option: ComboboxControlOption ) => option.label.substring( 0, 11 ); +const ComboboxControl = ( props: ComboboxControlProps ) => { + return <_ComboboxControl { ...props } __nextHasNoMarginBottom />; +}; + const ControlledComboboxControl = ( { value: valueProp, onChange, diff --git a/packages/components/src/dimension-control/index.tsx b/packages/components/src/dimension-control/index.tsx index 114ebe987dd35d..52662f31c3f24c 100644 --- a/packages/components/src/dimension-control/index.tsx +++ b/packages/components/src/dimension-control/index.tsx @@ -16,6 +16,15 @@ import SelectControl from '../select-control'; import sizesTable, { findSizeBySlug } from './sizes'; import type { DimensionControlProps, Size } from './types'; import type { SelectControlSingleSelectionProps } from '../select-control/types'; +import { ContextSystemProvider } from '../context'; + +const CONTEXT_VALUE = { + BaseControl: { + // Temporary during deprecation grace period: Overrides the underlying `__associatedWPComponentName` + // via the context system to override the value set by SelectControl. + _overrides: { __associatedWPComponentName: 'DimensionControl' }, + }, +}; /** * `DimensionControl` is a component designed to provide a UI to control spacing and/or dimensions. @@ -87,16 +96,21 @@ export function DimensionControl( props: DimensionControlProps ) { ); return ( - + + + ); } diff --git a/packages/components/src/dimension-control/stories/index.story.tsx b/packages/components/src/dimension-control/stories/index.story.tsx index 33d5bad4ff4b19..3a6da44f461164 100644 --- a/packages/components/src/dimension-control/stories/index.story.tsx +++ b/packages/components/src/dimension-control/stories/index.story.tsx @@ -44,6 +44,7 @@ const Template: StoryFn< typeof DimensionControl > = ( args ) => ( export const Default = Template.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, label: 'Please select a size', sizes, }; diff --git a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap index 5990fbbd4a3f5f..658fe7febc02bc 100644 --- a/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap +++ b/packages/components/src/dimension-control/test/__snapshots__/index.test.js.snap @@ -13,10 +13,6 @@ exports[`DimensionControl rendering renders with custom sizes 1`] = ` box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } @@ -299,10 +295,6 @@ exports[`DimensionControl rendering renders with defaults 1`] = ` box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } @@ -595,10 +587,6 @@ exports[`DimensionControl rendering renders with icon and custom icon label 1`] box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } @@ -903,10 +891,6 @@ exports[`DimensionControl rendering renders with icon and default icon label 1`] box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } diff --git a/packages/components/src/dimension-control/test/index.test.js b/packages/components/src/dimension-control/test/index.test.js index 1d65dd86c7e7c8..1b34d2983ad0f1 100644 --- a/packages/components/src/dimension-control/test/index.test.js +++ b/packages/components/src/dimension-control/test/index.test.js @@ -12,7 +12,11 @@ import { plus } from '@wordpress/icons'; /** * Internal dependencies */ -import { DimensionControl } from '../'; +import { DimensionControl as _DimensionControl } from '../'; + +const DimensionControl = ( props ) => { + return <_DimensionControl { ...props } __nextHasNoMarginBottom />; +}; describe( 'DimensionControl', () => { const onChangeHandler = jest.fn(); diff --git a/packages/components/src/focal-point-picker/index.tsx b/packages/components/src/focal-point-picker/index.tsx index 4575108fe6a21d..8f299751be0d48 100644 --- a/packages/components/src/focal-point-picker/index.tsx +++ b/packages/components/src/focal-point-picker/index.tsx @@ -251,6 +251,7 @@ export function FocalPointPicker( { = ( { }; export const Default = Template.bind( {} ); +Default.args = { + __nextHasNoMarginBottom: true, +}; export const Image = Template.bind( {} ); Image.args = { diff --git a/packages/components/src/focal-point-picker/test/index.tsx b/packages/components/src/focal-point-picker/test/index.tsx index 1eccced32c70af..377ba6c4e9e6b3 100644 --- a/packages/components/src/focal-point-picker/test/index.tsx +++ b/packages/components/src/focal-point-picker/test/index.tsx @@ -7,12 +7,16 @@ import userEvent from '@testing-library/user-event'; /** * Internal dependencies */ -import Picker from '..'; +import _Picker from '..'; import type { FocalPointPickerProps } from '../types'; type Log = { name: string; args: unknown[] }; type EventLogger = ( name: string, args: unknown[] ) => void; +const Picker = ( props: React.ComponentProps< typeof _Picker > ) => { + return <_Picker { ...props } __nextHasNoMarginBottom />; +}; + const props: FocalPointPickerProps = { onChange: jest.fn(), url: 'test-url', diff --git a/packages/components/src/range-control/index.tsx b/packages/components/src/range-control/index.tsx index 5b4ecfa585679b..20d9712df9c251 100644 --- a/packages/components/src/range-control/index.tsx +++ b/packages/components/src/range-control/index.tsx @@ -213,6 +213,7 @@ function UnforwardedRangeControl( return ( = ( { onChange, ...args } ) => { export const Default: StoryFn< typeof RangeControl > = Template.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, help: 'Please select how transparent you would like this.', initialPosition: 50, label: 'Opacity', @@ -104,6 +105,7 @@ export const WithAnyStep: StoryFn< typeof RangeControl > = ( { ); }; WithAnyStep.args = { + __nextHasNoMarginBottom: true, label: 'Brightness', step: 'any', }; @@ -167,6 +169,7 @@ export const WithIntegerStepAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithIntegerStepAndMarks.args = { + __nextHasNoMarginBottom: true, label: 'Integer Step', marks: marksBase, max: 10, @@ -183,6 +186,7 @@ export const WithDecimalStepAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithDecimalStepAndMarks.args = { + __nextHasNoMarginBottom: true, marks: [ ...marksBase, { value: 3.5, label: '3.5' }, @@ -202,6 +206,7 @@ export const WithNegativeMinimumAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithNegativeMinimumAndMarks.args = { + __nextHasNoMarginBottom: true, marks: marksWithNegatives, max: 10, min: -10, @@ -217,6 +222,7 @@ export const WithNegativeRangeAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithNegativeRangeAndMarks.args = { + __nextHasNoMarginBottom: true, marks: marksWithNegatives, max: -1, min: -10, @@ -232,6 +238,7 @@ export const WithAnyStepAndMarks: StoryFn< typeof RangeControl > = MarkTemplate.bind( {} ); WithAnyStepAndMarks.args = { + __nextHasNoMarginBottom: true, marks: marksBase, max: 10, min: 0, diff --git a/packages/components/src/range-control/test/index.tsx b/packages/components/src/range-control/test/index.tsx index d843b615ed0078..a4c5d8c6f2bc7f 100644 --- a/packages/components/src/range-control/test/index.tsx +++ b/packages/components/src/range-control/test/index.tsx @@ -6,7 +6,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react'; /** * Internal dependencies */ -import RangeControl from '../'; +import _RangeControl from '../'; const getRangeInput = (): HTMLInputElement => screen.getByRole( 'slider' ); const getNumberInput = (): HTMLInputElement => screen.getByRole( 'spinbutton' ); @@ -15,6 +15,12 @@ const getResetButton = (): HTMLButtonElement => screen.getByRole( 'button' ); const fireChangeEvent = ( input: HTMLInputElement, value?: number | string ) => fireEvent.change( input, { target: { value } } ); +const RangeControl = ( + props: React.ComponentProps< typeof _RangeControl > +) => { + return <_RangeControl { ...props } __nextHasNoMarginBottom />; +}; + describe( 'RangeControl', () => { describe( '#render()', () => { it( 'should trigger change callback with numeric value', () => { diff --git a/packages/components/src/search-control/index.tsx b/packages/components/src/search-control/index.tsx index 08cb3b065c904e..aac905e137e025 100644 --- a/packages/components/src/search-control/index.tsx +++ b/packages/components/src/search-control/index.tsx @@ -77,10 +77,13 @@ function UnforwardedSearchControl( const contextValue = useMemo( () => ( { - // Overrides the underlying BaseControl `__nextHasNoMarginBottom` via the context system - // to provide backwards compatibile margin for SearchControl. - // (In a standard InputControl, the BaseControl `__nextHasNoMarginBottom` is always set to true.) - BaseControl: { _overrides: { __nextHasNoMarginBottom } }, + BaseControl: { + // Overrides the underlying BaseControl `__nextHasNoMarginBottom` via the context system + // to provide backwards compatibile margin for SearchControl. + // (In a standard InputControl, the BaseControl `__nextHasNoMarginBottom` is always set to true.) + _overrides: { __nextHasNoMarginBottom }, + __associatedWPComponentName: 'SearchControl', + }, // `isBorderless` is still experimental and not a public prop for InputControl yet. InputBase: { isBorderless: true }, } ), diff --git a/packages/components/src/search-control/stories/index.story.tsx b/packages/components/src/search-control/stories/index.story.tsx index 433d3eef655adf..215288bb67c9b6 100644 --- a/packages/components/src/search-control/stories/index.story.tsx +++ b/packages/components/src/search-control/stories/index.story.tsx @@ -48,6 +48,7 @@ const Template: StoryFn< typeof SearchControl > = ( { export const Default = Template.bind( {} ); Default.args = { help: 'Help text to explain the input.', + __nextHasNoMarginBottom: true, }; /** diff --git a/packages/components/src/search-control/test/index.tsx b/packages/components/src/search-control/test/index.tsx index f130cab1b2a7cd..c6637945adcf63 100644 --- a/packages/components/src/search-control/test/index.tsx +++ b/packages/components/src/search-control/test/index.tsx @@ -23,6 +23,7 @@ function ControlledSearchControl( { return ( { setValue( ...args ); diff --git a/packages/components/src/select-control/index.tsx b/packages/components/src/select-control/index.tsx index ca9966fc675b86..3686661b8a58dc 100644 --- a/packages/components/src/select-control/index.tsx +++ b/packages/components/src/select-control/index.tsx @@ -99,6 +99,7 @@ function UnforwardedSelectControl< V extends string >( help={ help } id={ id } __nextHasNoMarginBottom={ __nextHasNoMarginBottom } + __associatedWPComponentName="SelectControl" > = ( props ) => { export const Default = SelectControlWithState.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, options: [ { value: '', label: 'Select an Option', disabled: true }, { value: 'a', label: 'Option A' }, @@ -82,9 +83,11 @@ WithLabelAndHelpText.args = { * As an alternative to the `options` prop, `optgroup`s and `options` can be * passed in as `children` for more customizeability. */ -export const WithCustomChildren: StoryFn< typeof SelectControl > = ( args ) => { - return ( - +export const WithCustomChildren = SelectControlWithState.bind( {} ); +WithCustomChildren.args = { + __nextHasNoMarginBottom: true, + children: ( + <> - - ); + + ), }; export const Minimal = SelectControlWithState.bind( {} ); diff --git a/packages/components/src/select-control/test/select-control.tsx b/packages/components/src/select-control/test/select-control.tsx index 0e8a6891087043..47b684cd20e280 100644 --- a/packages/components/src/select-control/test/select-control.tsx +++ b/packages/components/src/select-control/test/select-control.tsx @@ -7,7 +7,13 @@ import userEvent from '@testing-library/user-event'; /** * Internal dependencies */ -import SelectControl from '..'; +import _SelectControl from '..'; + +const SelectControl = ( + props: React.ComponentProps< typeof _SelectControl > +) => { + return <_SelectControl { ...props } __nextHasNoMarginBottom />; +}; describe( 'SelectControl', () => { it( 'should not render when no options or children are provided', () => { @@ -123,7 +129,7 @@ describe( 'SelectControl', () => { onChange={ onChange } />; - { } ); it( 'should accept an explicit type argument', () => { - + <_SelectControl< 'narrow' | 'value' > // @ts-expect-error "string" is not "narrow" or "value" value="string" options={ [ @@ -166,7 +172,7 @@ describe( 'SelectControl', () => { value: ( 'foo' | 'bar' )[] ) => void = () => {}; - { onChange={ onChange } />; - { } ); it( 'should accept an explicit type argument', () => { - + <_SelectControl< 'narrow' | 'value' > multiple // @ts-expect-error "string" is not "narrow" or "value" value={ [ 'string' ] } diff --git a/packages/components/src/text-control/index.tsx b/packages/components/src/text-control/index.tsx index 1643c5bc37c347..ea2d2c17bb9cf6 100644 --- a/packages/components/src/text-control/index.tsx +++ b/packages/components/src/text-control/index.tsx @@ -41,6 +41,7 @@ function UnforwardedTextControl( return ( = ( { export const Default: StoryFn< typeof TextControl > = DefaultTemplate.bind( {} ); -Default.args = {}; +Default.args = { + __nextHasNoMarginBottom: true, +}; export const WithLabelAndHelpText: StoryFn< typeof TextControl > = DefaultTemplate.bind( {} ); diff --git a/packages/components/src/text-control/test/text-control.tsx b/packages/components/src/text-control/test/text-control.tsx index fc048b93992f08..19b17cae443614 100644 --- a/packages/components/src/text-control/test/text-control.tsx +++ b/packages/components/src/text-control/test/text-control.tsx @@ -6,7 +6,11 @@ import { render, screen } from '@testing-library/react'; /** * Internal dependencies */ -import TextControl from '..'; +import _TextControl from '..'; + +const TextControl = ( props: React.ComponentProps< typeof _TextControl > ) => { + return <_TextControl { ...props } __nextHasNoMarginBottom />; +}; const noop = () => {}; diff --git a/packages/components/src/textarea-control/index.tsx b/packages/components/src/textarea-control/index.tsx index 3b96e11b0621b5..e7528510667b75 100644 --- a/packages/components/src/textarea-control/index.tsx +++ b/packages/components/src/textarea-control/index.tsx @@ -35,6 +35,7 @@ function UnforwardedTextareaControl( return ( = ( { export const Default: StoryFn< typeof TextareaControl > = Template.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, label: 'Text', help: 'Enter some text', }; diff --git a/packages/components/src/toggle-control/index.tsx b/packages/components/src/toggle-control/index.tsx index 5c64d57d3d0249..d2ee234a9695f8 100644 --- a/packages/components/src/toggle-control/index.tsx +++ b/packages/components/src/toggle-control/index.tsx @@ -10,6 +10,7 @@ import clsx from 'clsx'; */ import { forwardRef } from '@wordpress/element'; import { useInstanceId } from '@wordpress/compose'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies @@ -48,6 +49,14 @@ function UnforwardedToggleControl( ! __nextHasNoMarginBottom && css( { marginBottom: space( 3 ) } ) ); + if ( ! __nextHasNoMarginBottom ) { + deprecated( 'Bottom margin styles for wp.components.ToggleControl', { + since: '6.7', + version: '7.0', + hint: 'Set the `__nextHasNoMarginBottom` prop to true to start opting into the new styles, which will become the default in a future version.', + } ); + } + let describedBy, helpLabel; if ( help ) { if ( typeof help === 'function' ) { diff --git a/packages/components/src/toggle-control/stories/index.story.tsx b/packages/components/src/toggle-control/stories/index.story.tsx index b8043b8f48e523..97723aa207a394 100644 --- a/packages/components/src/toggle-control/stories/index.story.tsx +++ b/packages/components/src/toggle-control/stories/index.story.tsx @@ -48,6 +48,7 @@ const Template: StoryFn< typeof ToggleControl > = ( { export const Default = Template.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, label: 'Enable something', }; diff --git a/packages/components/src/toggle-control/test/index.tsx b/packages/components/src/toggle-control/test/index.tsx index cc89031d9affa3..b0eec2aca6663d 100644 --- a/packages/components/src/toggle-control/test/index.tsx +++ b/packages/components/src/toggle-control/test/index.tsx @@ -6,7 +6,13 @@ import { render, screen } from '@testing-library/react'; /** * Internal dependencies */ -import ToggleControl from '..'; +import _ToggleControl from '..'; + +const ToggleControl = ( + props: React.ComponentProps< typeof _ToggleControl > +) => { + return <_ToggleControl { ...props } __nextHasNoMarginBottom />; +}; describe( 'ToggleControl', () => { it( 'should label the toggle', () => { diff --git a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap index 81afc7ac67b05f..d055ea5fcc9838 100644 --- a/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap +++ b/packages/components/src/toggle-group-control/test/__snapshots__/index.tsx.snap @@ -13,10 +13,6 @@ exports[`ToggleGroupControl controlled should render correctly with icons 1`] = box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } @@ -349,10 +345,6 @@ exports[`ToggleGroupControl controlled should render correctly with text options box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } @@ -573,10 +565,6 @@ exports[`ToggleGroupControl uncontrolled should render correctly with icons 1`] box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } @@ -903,10 +891,6 @@ exports[`ToggleGroupControl uncontrolled should render correctly with text optio box-sizing: inherit; } -.emotion-2 { - margin-bottom: calc(4px * 2); -} - .components-panel__row .emotion-2 { margin-bottom: inherit; } diff --git a/packages/components/src/toggle-group-control/test/index.tsx b/packages/components/src/toggle-group-control/test/index.tsx index 661bbb9fc37bab..170db01ae523c2 100644 --- a/packages/components/src/toggle-group-control/test/index.tsx +++ b/packages/components/src/toggle-group-control/test/index.tsx @@ -15,7 +15,7 @@ import { formatLowercase, formatUppercase } from '@wordpress/icons'; */ import Button from '../../button'; import { - ToggleGroupControl, + ToggleGroupControl as _ToggleGroupControl, ToggleGroupControlOption, ToggleGroupControlOptionIcon, } from '../index'; @@ -27,6 +27,10 @@ const hoverOutside = async () => { await hover( document.body, { clientX: 10, clientY: 10 } ); }; +const ToggleGroupControl = ( props: ToggleGroupControlProps ) => { + return <_ToggleGroupControl { ...props } __nextHasNoMarginBottom />; +}; + const ControlledToggleGroupControl = ( { value: valueProp, onChange, diff --git a/packages/components/src/toggle-group-control/toggle-group-control/component.tsx b/packages/components/src/toggle-group-control/toggle-group-control/component.tsx index 8138b76505fe50..1c86c93548f6df 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control/component.tsx +++ b/packages/components/src/toggle-group-control/toggle-group-control/component.tsx @@ -72,6 +72,7 @@ function UnconnectedToggleGroupControl( { ! hideLabelFromVision && ( diff --git a/packages/components/src/tree-select/index.tsx b/packages/components/src/tree-select/index.tsx index 599dee4402ec72..bd92807bff4cc9 100644 --- a/packages/components/src/tree-select/index.tsx +++ b/packages/components/src/tree-select/index.tsx @@ -10,6 +10,15 @@ import { decodeEntities } from '@wordpress/html-entities'; import { SelectControl } from '../select-control'; import type { TreeSelectProps, Tree, Truthy } from './types'; import { useDeprecated36pxDefaultSizeProp } from '../utils/use-deprecated-props'; +import { ContextSystemProvider } from '../context'; + +const CONTEXT_VALUE = { + BaseControl: { + // Temporary during deprecation grace period: Overrides the underlying `__associatedWPComponentName` + // via the context system to override the value set by SelectControl. + _overrides: { __associatedWPComponentName: 'TreeSelect' }, + }, +}; function getSelectOptions( tree: Tree[], @@ -91,11 +100,13 @@ export function TreeSelect( props: TreeSelectProps ) { }, [ noOptionLabel, tree ] ); return ( - + + + ); } diff --git a/packages/components/src/tree-select/stories/index.story.tsx b/packages/components/src/tree-select/stories/index.story.tsx index 0a4212dc791227..33103786bbc541 100644 --- a/packages/components/src/tree-select/stories/index.story.tsx +++ b/packages/components/src/tree-select/stories/index.story.tsx @@ -48,6 +48,7 @@ const TreeSelectWithState: StoryFn< typeof TreeSelect > = ( props ) => { export const Default = TreeSelectWithState.bind( {} ); Default.args = { + __nextHasNoMarginBottom: true, label: 'Label Text', noOptionLabel: 'No parent page', help: 'Help text to explain the select control.', From e0760706272e4d6af3f1b0c7f90ab4ea9e305256 Mon Sep 17 00:00:00 2001 From: Amit Raj <77401999+amitraj2203@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:16:04 +0530 Subject: [PATCH 46/48] chore: Add label prop to SizeControl component (#64428) Co-authored-by: amitraj2203 Co-authored-by: mirka <0mirka00@git.wordpress.org> --- .../src/components/global-styles/size-control/index.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/size-control/index.js b/packages/edit-site/src/components/global-styles/size-control/index.js index 28fa64c643cbbd..06ea0bb5617e31 100644 --- a/packages/edit-site/src/components/global-styles/size-control/index.js +++ b/packages/edit-site/src/components/global-styles/size-control/index.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -26,7 +25,7 @@ function SizeControl( { ...props } ) { const { baseControlProps } = useBaseControlProps( props ); - const { value, onChange, fallbackValue, disabled } = props; + const { value, onChange, fallbackValue, disabled, label } = props; const units = useCustomUnits( { availableUnits: DEFAULT_UNITS, @@ -55,7 +54,7 @@ function SizeControl( { Date: Wed, 14 Aug 2024 08:23:24 +0100 Subject: [PATCH 47/48] Zoon Out: Fix scale calculations (#64478) --- packages/block-editor/src/components/iframe/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index e7af77920ea127..3b0bce6d56b403 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -242,8 +242,10 @@ function Iframe( { const isZoomedOut = scale !== 1; useEffect( () => { - prevContainerWidth.current = containerWidth; - }, [ containerWidth ] ); + if ( ! isZoomedOut ) { + prevContainerWidth.current = containerWidth; + } + }, [ containerWidth, isZoomedOut ] ); const disabledRef = useDisabled( { isDisabled: ! readonly } ); const bodyRef = useMergeRefs( [ From c20273736e30e2bfc54f82faaeaad4912b25cd29 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 14 Aug 2024 12:18:10 +0000 Subject: [PATCH 48/48] Bump plugin version to 19.0.0 --- gutenberg.php | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 6ec0a56f00e74d..66f0aa31a65baa 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.5 * Requires PHP: 7.2 - * Version: 19.0.0-rc.1 + * Version: 19.0.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 471d7b845d24f8..ddfec3a5dddc63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "19.0.0-rc.1", + "version": "19.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "19.0.0-rc.1", + "version": "19.0.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index e4466eb4f470e1..ee78f197a43e28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "19.0.0-rc.1", + "version": "19.0.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors",