From 5a4a4ef254ec497e786da822498d90b16d0e5ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 26 Aug 2025 18:52:23 +0200 Subject: [PATCH 01/20] Revert "DataForm: Streamline validation behavior (#71345)" (#71359) This reverts commit 55d79f3c6fda4bfa3071bbe92ed06b1ef5011d5f. --- packages/dataviews/CHANGELOG.md | 4 --- packages/dataviews/README.md | 4 +-- .../src/components/dataform/index.tsx | 23 +---------------- .../dataform/stories/index.story.tsx | 14 +++++------ packages/dataviews/src/test/dataform.tsx | 25 ++++++------------- packages/dataviews/src/types.ts | 5 +--- packages/fields/src/actions/reorder-page.tsx | 15 ++++++----- 7 files changed, 25 insertions(+), 65 deletions(-) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 72ee147e674032..5dac1b82db9fc2 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -10,10 +10,6 @@ - DataViews: Fix incorrect documentation for `defaultLayouts` prop. [#71334](https://github.com/WordPress/gutenberg/pull/71334) -### Enhancements - -- DataForm: Add second argument to the `onChange` callback that contains an `isValid` boolean property and remove `isItemValid` utility. [#71345](https://github.com/WordPress/gutenberg/pull/71345) - ## 7.0.0 (2025-08-20) ### Breaking changes diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 7d4edd31538aec..da831d39a8a14c 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -586,7 +586,7 @@ const form = { #### `onChange`: `function` -Callback function that receives an object with the edits done by the user. It also receives a second parameter that indicates whether the current item is valid or not according to the current fields and form configuration. +Callback function that receives an object with the edits done by the user. Example: @@ -598,7 +598,7 @@ const data = { date: '2012-04-23T18:25:43.511Z', }; -const onChange = ( edits, { isValid } ) => { +const onChange = ( edits ) => { /* * edits will contain user edits. * For example, if the user edited the title diff --git a/packages/dataviews/src/components/dataform/index.tsx b/packages/dataviews/src/components/dataform/index.tsx index 39e88050b8da80..b359ddba74381e 100644 --- a/packages/dataviews/src/components/dataform/index.tsx +++ b/packages/dataviews/src/components/dataform/index.tsx @@ -10,7 +10,6 @@ import type { DataFormProps } from '../../types'; import { DataFormProvider } from '../dataform-context'; import { normalizeFields } from '../../normalize-fields'; import { DataFormLayout } from '../../dataforms-layouts/data-form-layout'; -import { isItemValid } from '../../validation'; export default function DataForm< Item >( { data, @@ -23,33 +22,13 @@ export default function DataForm< Item >( { [ fields ] ); - const onChangeWithValidation = ( updatedData: Partial< Item > ) => { - if ( ! onChange ) { - return; - } - - const isValid = isItemValid( - { - ...data, - ...updatedData, - }, - fields, - form - ); - onChange( updatedData, { isValid } ); - }; - if ( ! form.fields ) { return null; } return ( - + ); } diff --git a/packages/dataviews/src/components/dataform/stories/index.story.tsx b/packages/dataviews/src/components/dataform/stories/index.story.tsx index 6a2e566bc47d50..b3c581d6a20198 100644 --- a/packages/dataviews/src/components/dataform/stories/index.story.tsx +++ b/packages/dataviews/src/components/dataform/stories/index.story.tsx @@ -12,6 +12,7 @@ import { * Internal dependencies */ import DataForm from '../index'; +import { isItemValid } from '../../../validation'; import type { Field, Form, @@ -402,7 +403,6 @@ const ValidationComponent = ( { boolean: true, customEdit: 'custom control', } ); - const [ isPostValid, setIsValid ] = useState( true ); const customTextRule = ( value: ValidatedItem ) => { if ( ! /^[a-zA-Z ]+$/.test( value.text ) ) { @@ -483,6 +483,8 @@ const ValidationComponent = ( { fields: [ 'text', 'email', 'integer', 'boolean', 'customEdit' ], }; + const canSave = isItemValid( post, _fields, form ); + return (
@@ -490,19 +492,17 @@ const ValidationComponent = ( { data={ post } fields={ _fields } form={ form } - onChange={ ( edits, { isValid } ) => { + onChange={ ( edits ) => setPost( ( prev ) => ( { ...prev, ...edits, - } ) ); - - setIsValid( isValid ); - } } + } ) ) + } /> From 81e9364f7bfe75ff49138bb579714383b18982a3 Mon Sep 17 00:00:00 2001 From: Darren Ethier Date: Tue, 26 Aug 2025 16:02:43 -0400 Subject: [PATCH 02/20] Add wp-env SPX profiler option (#70693) * Changes from background composer bc-8937f5ee-00f5-4b89-8d22-7c1930bce6dd * fixing configuration of spx in config * only execute with non cli service * install missing dependency * fix linting errors * fix linting and remove superfluous test * split out apt-get commands. * Use simpler check for value. --------- Co-authored-by: Cursor Agent --- packages/env/README.md | 28 ++++++++++++ packages/env/lib/cli.js | 7 +++ packages/env/lib/commands/start.js | 3 ++ packages/env/lib/init-config.js | 60 +++++++++++++++++++++++++ packages/env/lib/parse-spx-mode.js | 41 +++++++++++++++++ packages/env/lib/test/parse-spx-mode.js | 32 +++++++++++++ 6 files changed, 171 insertions(+) create mode 100644 packages/env/lib/parse-spx-mode.js create mode 100644 packages/env/lib/test/parse-spx-mode.js diff --git a/packages/env/README.md b/packages/env/README.md index fe3e5dea3bf2b2..1c2b9adb5cb1fb 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -289,6 +289,11 @@ Options: them in a comma-separated list: `--xdebug=develop,coverage`. See https://xdebug.org/docs/all_settings#mode for information about Xdebug modes. [string] + --spx Enables SPX profiling. If not passed, SPX is turned off. If no + mode is set, uses "enabled". SPX is a simple profiling extension + with a built-in web UI. See + https://github.com/NoiseByNorthwest/php-spx for more information. + [string] --scripts Execute any configured lifecycle scripts. [boolean] [default: true] ``` @@ -756,6 +761,29 @@ php_value memory_limit 2G This is useful if there are options you'd like to add to `php.ini`, which is difficult to access in this environment. +### Using SPX Profiling + +SPX is a simple profiling extension for PHP that provides low-overhead profiling with a built-in web UI. When enabled with `--spx`, you can access the SPX profiling interface to analyze your application's performance. + +To enable SPX profiling: + +```sh +wp-env start --spx +``` + +Once enabled, you can access the SPX web UI by visiting any page in your WordPress environment with the query parameters `?SPX_KEY=dev&SPX_UI_URI=/`. For example: + +- Development site: `http://localhost:8888/?SPX_KEY=dev&SPX_UI_URI=/` +- Test site: `http://localhost:8889/?SPX_KEY=dev&SPX_UI_URI=/` + +From the SPX interface, you can: +- Enable profiling for subsequent requests +- View flame graphs and performance metrics +- Analyze function call timelines +- Examine memory usage and other performance data + +SPX provides a more lightweight alternative to Xdebug for profiling, with minimal performance overhead and an intuitive web-based interface. + ## Contributing to this package This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. diff --git a/packages/env/lib/cli.js b/packages/env/lib/cli.js index 896df6cd59fed0..e206972b8511d4 100644 --- a/packages/env/lib/cli.js +++ b/packages/env/lib/cli.js @@ -14,6 +14,7 @@ const { execSync } = require( 'child_process' ); const pkg = require( '../package.json' ); const env = require( './env' ); const parseXdebugMode = require( './parse-xdebug-mode' ); +const parseSpxMode = require( './parse-spx-mode' ); const { RUN_CONTAINERS, validateRunContainer, @@ -139,6 +140,12 @@ module.exports = function cli() { coerce: parseXdebugMode, type: 'string', } ); + args.option( 'spx', { + describe: + 'Enables SPX profiling. If not passed, SPX is turned off. If no mode is set, uses "enabled". SPX is a simple profiling extension with a built-in web UI. See https://github.com/NoiseByNorthwest/php-spx for more information.', + coerce: parseSpxMode, + type: 'string', + } ); args.option( 'scripts', { type: 'boolean', describe: 'Execute any configured lifecycle scripts.', diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index db05b82060d2c5..c50a05719f8898 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -46,6 +46,7 @@ const CONFIG_CACHE_KEY = 'config_checksum'; * @param {Object} options.spinner A CLI spinner which indicates progress. * @param {boolean} options.update If true, update sources. * @param {string} options.xdebug The Xdebug mode to set. + * @param {string} options.spx The SPX mode to set. * @param {boolean} options.scripts Indicates whether or not lifecycle scripts should be executed. * @param {boolean} options.debug True if debug mode is enabled. */ @@ -53,6 +54,7 @@ module.exports = async function start( { spinner, update, xdebug, + spx, scripts, debug, } ) { @@ -63,6 +65,7 @@ module.exports = async function start( { spinner, debug, xdebug, + spx, writeChanges: true, } ); diff --git a/packages/env/lib/init-config.js b/packages/env/lib/init-config.js index 946a7800de1fea..9c6885d5f9a0aa 100644 --- a/packages/env/lib/init-config.js +++ b/packages/env/lib/init-config.js @@ -26,6 +26,7 @@ const buildDockerComposeConfig = require( './build-docker-compose-config' ); * @param {Object} options.spinner A CLI spinner which indicates progress. * @param {boolean} options.debug True if debug mode is enabled. * @param {string} options.xdebug The Xdebug mode to set. Defaults to "off". + * @param {string} options.spx The SPX mode to set. Defaults to "off". * @param {boolean} options.writeChanges If true, writes the parsed config to the * required docker files like docker-compose * and Dockerfile. By default, this is false @@ -37,6 +38,7 @@ module.exports = async function initConfig( { spinner, debug, xdebug = 'off', + spx = 'off', writeChanges = false, } ) { const config = await loadConfig( path.resolve( '.' ) ); @@ -47,6 +49,11 @@ module.exports = async function initConfig( { // so that Docker will rebuild the image whenever the xdebug flag changes. config.xdebug = xdebug; + // Adding this to the config allows the start command to understand that the + // config has changed when only the spx param has changed. This is needed + // so that Docker will rebuild the image whenever the spx flag changes. + config.spx = spx; + const dockerComposeConfig = buildDockerComposeConfig( config ); if ( config.debug ) { @@ -238,6 +245,12 @@ RUN echo "#$HOST_UID ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers`; config.env[ env ].phpVersion ); + dockerFileContent += getSpxConfig( + config.spx, + config.env[ env ].phpVersion, + service + ); + // Add better PHP settings. dockerFileContent += ` RUN echo 'upload_max_filesize = 1G' >> /usr/local/etc/php/php.ini @@ -309,3 +322,50 @@ RUN echo 'xdebug.start_with_request=yes' >> /usr/local/etc/php/php.ini RUN echo 'xdebug.mode=${ xdebugMode }' >> /usr/local/etc/php/php.ini RUN echo 'xdebug.client_host="host.docker.internal"' >> /usr/local/etc/php/php.ini`; } + +/** + * Gets the SPX config based on the options in the config object. + * + * @param {string} spxMode The SPX mode set in the config. + * @param {string} phpVersion The php version set in the environment. + * @param {string} service The service name. + * @return {string} The SPX config -- can be an empty string when it's not used. + */ +function getSpxConfig( spxMode = 'off', phpVersion, service ) { + if ( spxMode === 'off' || service === 'cli' ) { + return ''; + } + + if ( phpVersion ) { + const versionTokens = phpVersion.split( '.' ); + const majorVer = parseInt( versionTokens[ 0 ] ); + const minorVer = parseInt( versionTokens[ 1 ] ); + + if ( isNaN( majorVer ) || isNaN( minorVer ) ) { + throw new ValidationError( + 'Something went wrong when parsing the PHP version.' + ); + } + + // SPX requires PHP 5.4 or higher + if ( majorVer < 5 || ( majorVer === 5 && minorVer < 4 ) ) { + throw new ValidationError( + `Cannot use SPX with PHP < 5.4. Your PHP version is ${ phpVersion }.` + ); + } + } + + return ` +# Install SPX profiler +RUN apt-get update -qy +RUN apt-get install -qy git zlib1g-dev +RUN cd /tmp && git clone https://github.com/NoiseByNorthwest/php-spx.git +RUN cd /tmp/php-spx && git checkout release/latest +RUN cd /tmp/php-spx && phpize && ./configure && make && make install +RUN docker-php-ext-enable spx +RUN echo 'spx.http_enabled=1' >> /usr/local/etc/php/php.ini +RUN echo 'spx.http_key="dev"' >> /usr/local/etc/php/php.ini +RUN echo 'spx.http_ip_whitelist="*"' >> /usr/local/etc/php/php.ini +RUN echo 'spx.data_dir="/tmp/spx"' >> /usr/local/etc/php/php.ini +RUN mkdir -p /tmp/spx && chmod 777 /tmp/spx`; +} diff --git a/packages/env/lib/parse-spx-mode.js b/packages/env/lib/parse-spx-mode.js new file mode 100644 index 00000000000000..68e2f61b5c23ca --- /dev/null +++ b/packages/env/lib/parse-spx-mode.js @@ -0,0 +1,41 @@ +'use strict'; + +// SPX is a simple profiling extension for PHP +// See https://github.com/NoiseByNorthwest/php-spx +const SPX_MODES = [ 'off', 'enabled' ]; + +/** + * Custom parsing for the SPX mode set via yargs. This function ensures three things: + * 1. If the --spx flag was not set, set it to 'off'. + * 2. If the --spx flag was set by itself, default to 'enabled'. + * 3. If the --spx flag includes modes, make sure they are accepted by SPX. + * + * @param {string|undefined} value The user-set mode of SPX; undefined if there is no --spx flag. + * @return {string} The SPX mode to use with defaults applied. + */ +module.exports = function parseSpxMode( value ) { + if ( value === undefined ) { + return 'off'; + } + if ( typeof value !== 'string' ) { + throwSpxModeError( value ); + } + + if ( value.length === 0 || value === 'undefined' ) { + return 'enabled'; + } + + if ( ! SPX_MODES.includes( value ) ) { + throwSpxModeError( value ); + } + + return value; +}; + +function throwSpxModeError( value ) { + throw new Error( + `"${ value }" is not a mode recognized by SPX. Valid modes are: ${ SPX_MODES.join( + ', ' + ) }` + ); +} diff --git a/packages/env/lib/test/parse-spx-mode.js b/packages/env/lib/test/parse-spx-mode.js new file mode 100644 index 00000000000000..2999f56179838c --- /dev/null +++ b/packages/env/lib/test/parse-spx-mode.js @@ -0,0 +1,32 @@ +'use strict'; + +/** + * Internal dependencies + */ +const parseSpxMode = require( '../parse-spx-mode' ); + +describe( 'parseSpxMode', () => { + it( 'errors with invalid values', () => { + const errorMessage = 'is not a mode recognized by SPX'; + expect( () => parseSpxMode( true ) ).toThrow( errorMessage ); + expect( () => parseSpxMode( false ) ).toThrow( errorMessage ); + expect( () => parseSpxMode( 1 ) ).toThrow( errorMessage ); + } ); + + it( 'sets the SPX mode to "off" if no --spx flag is passed', () => { + const result = parseSpxMode( undefined ); + expect( result ).toBe( 'off' ); + } ); + + it( 'sets the SPX mode to "enabled" if no mode is specified', () => { + const result = parseSpxMode( '' ); + expect( result ).toBe( 'enabled' ); + } ); + + it( 'errors with a mix of valid and invalid modes', () => { + const fakeMode = 'invalidmode'; + expect( () => parseSpxMode( `enabled,${ fakeMode }` ) ).toThrow( + fakeMode + ); + } ); +} ); From 2e1abebe4304a28bb2ebca80497242aef133bd87 Mon Sep 17 00:00:00 2001 From: Lena Morita Date: Wed, 27 Aug 2025 05:46:24 +0900 Subject: [PATCH 03/20] ControlWithError: Show validating state when transitioning from error state (#71260) * Add tests * ControlWithError: Show validating state when transitioning from error state * Add changelog * Mimic a stable server response time * Prefer `toBeVisible` in tests * Fix useEffect return type weirdness https://github.com/WordPress/gutenberg/pull/71184#discussion_r2287608784 * Add test for deferring local validity state until server response is received * Add play function * Wait for async response before showing local state * Don't export test story Co-authored-by: mirka <0mirka00@git.wordpress.org> Co-authored-by: tyxla Co-authored-by: jsnajdr Co-authored-by: oandregal Co-authored-by: jameskoster --- package-lock.json | 50 +++- package.json | 3 +- packages/components/CHANGELOG.md | 1 + .../components/stories/overview.story.tsx | 136 ++++++++--- .../control-with-error.tsx | 37 +-- .../test/control-with-error.tsx | 224 ++++++++++++++++++ storybook/main.js | 1 + 7 files changed, 395 insertions(+), 57 deletions(-) create mode 100644 packages/components/src/validated-form-controls/test/control-with-error.tsx diff --git a/package-lock.json b/package-lock.json index ec4790fb459c24..1b871f9f49b427 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "@storybook/addon-actions": "8.4.7", "@storybook/addon-controls": "8.4.7", "@storybook/addon-docs": "8.4.7", + "@storybook/addon-interactions": "8.4.7", "@storybook/addon-toolbars": "8.4.7", "@storybook/addon-viewport": "8.4.7", "@storybook/addon-webpack5-compiler-babel": "3.0.3", @@ -100,7 +101,7 @@ "eslint-plugin-prettier": "5.0.0", "eslint-plugin-react-compiler": "19.0.0-beta-0dec889-20241115", "eslint-plugin-ssr-friendly": "1.0.6", - "eslint-plugin-storybook": "0.6.13", + "eslint-plugin-storybook": "0.9.0", "eslint-plugin-testing-library": "6.0.2", "execa": "4.0.2", "fast-glob": "3.2.7", @@ -5034,15 +5035,19 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -11724,6 +11729,27 @@ "storybook": "^8.4.7" } }, + "node_modules/@storybook/addon-interactions": { + "version": "8.4.7", + "resolved": "https://registry.npmjs.org/@storybook/addon-interactions/-/addon-interactions-8.4.7.tgz", + "integrity": "sha512-fnufT3ym8ht3HHUIRVXAH47iOJW/QOb0VSM+j269gDuvyDcY03D1civCu1v+eZLGaXPKJ8vtjr0L8zKQ/4P0JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/instrumenter": "8.4.7", + "@storybook/test": "8.4.7", + "polished": "^4.2.2", + "ts-dedent": "^2.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^8.4.7" + } + }, "node_modules/@storybook/addon-toolbars": { "version": "8.4.7", "resolved": "https://registry.npmjs.org/@storybook/addon-toolbars/-/addon-toolbars-8.4.7.tgz", @@ -23765,18 +23791,19 @@ } }, "node_modules/eslint-plugin-storybook": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.6.13.tgz", - "integrity": "sha512-smd+CS0WH1jBqUEJ3znGS7DU4ayBE9z6lkQAK2yrSUv1+rq8BT/tiI5C/rKE7rmiqiAfojtNYZRhzo5HrulccQ==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-0.9.0.tgz", + "integrity": "sha512-qOT/2vQBo0VqrG/BhZv8IdSsKQiyzJw+2Wqq+WFCiblI/PfxLSrGkF/buiXF+HumwfsCyBdaC94UhqhmYFmAvA==", "dev": true, + "license": "MIT", "dependencies": { "@storybook/csf": "^0.0.1", - "@typescript-eslint/utils": "^5.45.0", - "requireindex": "^1.1.0", + "@typescript-eslint/utils": "^5.62.0", + "requireindex": "^1.2.0", "ts-dedent": "^2.2.0" }, "engines": { - "node": "12.x || 14.x || >= 16" + "node": ">= 18" }, "peerDependencies": { "eslint": ">=6" @@ -23787,6 +23814,7 @@ "resolved": "https://registry.npmjs.org/@storybook/csf/-/csf-0.0.1.tgz", "integrity": "sha512-USTLkZze5gkel8MYCujSRBVIrUQ3YPBrLOx7GNk/0wttvVtlzWXAq9eLbQ4p/NicGxP+3T7KPEMVV//g+yubpw==", "dev": true, + "license": "MIT", "dependencies": { "lodash": "^4.17.15" } diff --git a/package.json b/package.json index 0b6fb42ba96177..8806164074f54e 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@storybook/addon-actions": "8.4.7", "@storybook/addon-controls": "8.4.7", "@storybook/addon-docs": "8.4.7", + "@storybook/addon-interactions": "8.4.7", "@storybook/addon-toolbars": "8.4.7", "@storybook/addon-viewport": "8.4.7", "@storybook/addon-webpack5-compiler-babel": "3.0.3", @@ -109,7 +110,7 @@ "eslint-plugin-prettier": "5.0.0", "eslint-plugin-react-compiler": "19.0.0-beta-0dec889-20241115", "eslint-plugin-ssr-friendly": "1.0.6", - "eslint-plugin-storybook": "0.6.13", + "eslint-plugin-storybook": "0.9.0", "eslint-plugin-testing-library": "6.0.2", "execa": "4.0.2", "fast-glob": "3.2.7", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 17d65b4453b656..41b3b7b6399069 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -15,6 +15,7 @@ ### Internal - Validated form controls: Add support for async validation. This is a breaking API change that splits the `customValidator` prop into an `onValidate` callback and a `customValidity` object. ([#71184](https://github.com/WordPress/gutenberg/pull/71184)). +- Validated form controls: Fix bug where "validating" state was not shown when transitioning from error state ([#71260](https://github.com/WordPress/gutenberg/pull/71260)). - `DateCalendar`, `DateRangeCalendar`: use `px` instead of `rem` units. ([#71248](https://github.com/WordPress/gutenberg/pull/71248)). ## 30.1.0 (2025-08-07) diff --git a/packages/components/src/validated-form-controls/components/stories/overview.story.tsx b/packages/components/src/validated-form-controls/components/stories/overview.story.tsx index 6f5e9ac174167c..2267a383961ea5 100644 --- a/packages/components/src/validated-form-controls/components/stories/overview.story.tsx +++ b/packages/components/src/validated-form-controls/components/stories/overview.story.tsx @@ -1,12 +1,14 @@ /** - * WordPress dependencies + * External dependencies */ -import { useRef, useCallback, useState } from '@wordpress/element'; +import type { Meta, StoryObj } from '@storybook/react'; +import { expect, userEvent, waitFor, within } from '@storybook/test'; /** - * External dependencies + * WordPress dependencies */ -import type { Meta, StoryObj } from '@storybook/react'; +import { useRef, useCallback, useState } from '@wordpress/element'; +import { debounce } from '@wordpress/compose'; /** * Internal dependencies @@ -14,7 +16,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { ValidatedInputControl } from '..'; import { formDecorator } from './story-utils'; import type { ControlWithError } from '../../control-with-error'; -import { debounce } from '@wordpress/compose'; const meta: Meta< typeof ControlWithError > = { title: 'Components/Selection & Input/Validated Form Controls/Overview', @@ -166,24 +167,19 @@ export const AsyncValidation: StoryObj< typeof ValidatedInputControl > = { } ); clearTimeout( timeoutRef.current ); - timeoutRef.current = setTimeout( - () => { - if ( v?.toString().toLowerCase() === 'error' ) { - setCustomValidity( { - type: 'invalid', - message: 'The word "error" is not allowed.', - } ); - } else { - setCustomValidity( { - type: 'valid', - message: 'Validated', - } ); - } - }, - // Mimics a random server response time. - // eslint-disable-next-line no-restricted-syntax - Math.random() < 0.5 ? 1500 : 300 - ); + timeoutRef.current = setTimeout( () => { + if ( v?.toString().toLowerCase() === 'error' ) { + setCustomValidity( { + type: 'invalid', + message: 'The word "error" is not allowed.', + } ); + } else { + setCustomValidity( { + type: 'valid', + message: 'Validated', + } ); + } + }, 1500 ); }, 500 ), [] ); @@ -200,9 +196,95 @@ export const AsyncValidation: StoryObj< typeof ValidatedInputControl > = { /> ); }, + args: { + label: 'Text', + help: 'The word "error" will trigger an error asynchronously.', + required: true, + }, }; -AsyncValidation.args = { - label: 'Text', - help: 'The word "error" will trigger an error asynchronously.', - required: true, + +// Not exported - Only for testing purposes. +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const AsyncValidationWithTest: StoryObj< typeof ValidatedInputControl > = { + ...AsyncValidation, + play: async ( { canvasElement } ) => { + const canvas = within( canvasElement ); + await userEvent.click( canvas.getByRole( 'textbox' ) ); + await userEvent.type( canvas.getByRole( 'textbox' ), 'valid text', { + delay: 10, + } ); + await userEvent.tab(); + + await waitFor( + () => { + expect( canvas.getByText( 'Validated' ) ).toBeVisible(); + }, + { timeout: 2500 } + ); + + await new Promise( ( resolve ) => setTimeout( resolve, 500 ) ); + await userEvent.clear( canvas.getByRole( 'textbox' ) ); + + // Should show validating state when transitioning from valid to invalid. + await waitFor( + () => { + expect( canvas.getByText( 'Validating...' ) ).toBeVisible(); + }, + { timeout: 2500 } + ); + + await waitFor( + () => { + expect( + canvas.getByText( 'Please fill out this field.' ) + ).toBeVisible(); + }, + { timeout: 2500 } + ); + + // Should not show validating state if there were no changes + // after a valid/invalid state was already shown. + await new Promise( ( resolve ) => setTimeout( resolve, 1500 ) ); + await expect( + canvas.queryByText( 'Validating...' ) + ).not.toBeInTheDocument(); + + await userEvent.type( canvas.getByRole( 'textbox' ), 'e', { + delay: 10, + } ); + + // Should not show valid state if server has not yet responded. + await expect( + canvas.queryByText( 'Validated' ) + ).not.toBeInTheDocument(); + + // Should show validating state when transitioning from invalid to valid. + await waitFor( + () => { + expect( canvas.getByText( 'Validating...' ) ).toBeVisible(); + }, + { timeout: 2500 } + ); + + await waitFor( + () => { + expect( canvas.getByText( 'Validated' ) ).toBeVisible(); + }, + { timeout: 2500 } + ); + + await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) ); + await userEvent.type( canvas.getByRole( 'textbox' ), 'rror', { + delay: 10, + } ); + + await waitFor( + () => { + expect( + canvas.getByText( 'The word "error" is not allowed.' ) + ).toBeVisible(); + }, + { timeout: 2500 } + ); + }, }; diff --git a/packages/components/src/validated-form-controls/control-with-error.tsx b/packages/components/src/validated-form-controls/control-with-error.tsx index aa6027a74c9edc..76cd207f0b19b2 100644 --- a/packages/components/src/validated-form-controls/control-with-error.tsx +++ b/packages/components/src/validated-form-controls/control-with-error.tsx @@ -1,11 +1,8 @@ /** * WordPress dependencies */ +import { usePrevious } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; - -/** - * External dependencies - */ import { cloneElement, forwardRef, @@ -98,6 +95,7 @@ function UnforwardedControlWithError< C extends React.ReactElement >( | undefined >(); const [ isTouched, setIsTouched ] = useState( false ); + const previousCustomValidityType = usePrevious( customValidity?.type ); // Ensure that error messages are visible after user attemps to submit a form // with multiple invalid fields. @@ -116,7 +114,7 @@ function UnforwardedControlWithError< C extends React.ReactElement >( }; } ); - useEffect( () => { + useEffect( (): ReturnType< React.EffectCallback > => { if ( ! isTouched ) { return; } @@ -134,6 +132,9 @@ function UnforwardedControlWithError< C extends React.ReactElement >( case 'validating': { // Wait before showing a validating state. const timer = setTimeout( () => { + validityTarget?.setCustomValidity( '' ); + setErrorMessage( undefined ); + setStatusMessage( { type: 'validating', message: customValidity.message, @@ -143,6 +144,12 @@ function UnforwardedControlWithError< C extends React.ReactElement >( return () => clearTimeout( timer ); } case 'valid': { + // Ensures that we wait for any async responses before showing + // a synchronously valid state. + if ( previousCustomValidityType === 'valid' ) { + break; + } + validityTarget?.setCustomValidity( '' ); setErrorMessage( validityTarget?.validationMessage ); @@ -150,7 +157,7 @@ function UnforwardedControlWithError< C extends React.ReactElement >( type: 'valid', message: customValidity.message, } ); - return; + break; } case 'invalid': { validityTarget?.setCustomValidity( @@ -159,7 +166,7 @@ function UnforwardedControlWithError< C extends React.ReactElement >( setErrorMessage( validityTarget?.validationMessage ); setStatusMessage( undefined ); - return undefined; + break; } } }, [ @@ -167,9 +174,14 @@ function UnforwardedControlWithError< C extends React.ReactElement >( customValidity?.type, customValidity?.message, getValidityTarget, + previousCustomValidityType, ] ); const onBlur = ( event: React.FocusEvent< HTMLDivElement > ) => { + if ( isTouched ) { + return; + } + // Only consider "blurred from the component" if focus has fully left the wrapping div. // This prevents unnecessary blurs from components with multiple focusable elements. if ( @@ -177,17 +189,6 @@ function UnforwardedControlWithError< C extends React.ReactElement >( ! event.currentTarget.contains( event.relatedTarget ) ) { setIsTouched( true ); - - const validityTarget = getValidityTarget(); - - // Prevents a double flash of the native error tooltip when the control is already showing one. - if ( ! validityTarget?.validity.valid ) { - if ( ! errorMessage ) { - setErrorMessage( validityTarget?.validationMessage ); - } - return; - } - onValidate?.(); } }; diff --git a/packages/components/src/validated-form-controls/test/control-with-error.tsx b/packages/components/src/validated-form-controls/test/control-with-error.tsx new file mode 100644 index 00000000000000..8d04834c00f51a --- /dev/null +++ b/packages/components/src/validated-form-controls/test/control-with-error.tsx @@ -0,0 +1,224 @@ +/** + * External dependencies + */ +import { render, screen, waitFor, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * WordPress dependencies + */ +import { useState, useCallback } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { ValidatedInputControl } from '../components'; + +describe( 'ControlWithError', () => { + describe( 'Async Validation', () => { + beforeEach( () => { + jest.useFakeTimers(); + } ); + + afterEach( () => { + jest.useRealTimers(); + } ); + + const AsyncValidatedInputControl = ( { + serverDelayMs, + ...restProps + }: { + serverDelayMs: number; + } & React.ComponentProps< typeof ValidatedInputControl > ) => { + const [ text, setText ] = useState( '' ); + const [ customValidity, setCustomValidity ] = + useState< + React.ComponentProps< + typeof ValidatedInputControl + >[ 'customValidity' ] + >( undefined ); + + const onValidate = useCallback( + ( value?: string ) => { + setCustomValidity( { + type: 'validating', + message: 'Validating...', + } ); + + // Simulate delayed server response + setTimeout( () => { + if ( value?.toLowerCase() === 'error' ) { + setCustomValidity( { + type: 'invalid', + message: 'The word "error" is not allowed.', + } ); + } else { + setCustomValidity( { + type: 'valid', + message: 'Validated', + } ); + } + }, serverDelayMs ); + }, + [ serverDelayMs ] + ); + + return ( + { + setText( newValue ?? '' ); + } } + onValidate={ onValidate } + customValidity={ customValidity } + { ...restProps } + /> + ); + }; + + it( 'should not show "validating" state if it takes less than 1000ms', async () => { + const user = userEvent.setup( { + advanceTimers: jest.advanceTimersByTime, + } ); + render( ); + + const input = screen.getByRole( 'textbox' ); + + await user.type( input, 'valid text' ); + + // Blur to trigger validation + await user.tab(); + + // Fast-forward to right before the server response + act( () => jest.advanceTimersByTime( 499 ) ); + + // The validating state should not be shown + await waitFor( () => { + expect( + screen.queryByText( 'Validating...' ) + ).not.toBeInTheDocument(); + } ); + + // Fast-forward past the server delay to show validation result + act( () => jest.advanceTimersByTime( 1 ) ); + + await waitFor( () => { + expect( screen.getByText( 'Validated' ) ).toBeVisible(); + } ); + } ); + + it( 'should show "validating" state if it takes more than 1000ms', async () => { + const user = userEvent.setup( { + advanceTimers: jest.advanceTimersByTime, + } ); + render( ); + + const input = screen.getByRole( 'textbox' ); + + await user.type( input, 'valid text' ); + + // Blur to trigger validation + await user.tab(); + + // Initially, no validating message should be shown (before 1s delay) + expect( + screen.queryByText( 'Validating...' ) + ).not.toBeInTheDocument(); + + // Fast-forward past the 1s delay to show validating state + act( () => jest.advanceTimersByTime( 1000 ) ); + + await waitFor( () => { + expect( screen.getByText( 'Validating...' ) ).toBeVisible(); + } ); + + // Fast-forward past the server delay to show validation result + act( () => jest.advanceTimersByTime( 200 ) ); + + await waitFor( () => { + expect( screen.getByText( 'Validated' ) ).toBeVisible(); + } ); + + // Test error case + await user.clear( input ); + await user.type( input, 'error' ); + + // Blur to trigger validation + await user.tab(); + + act( () => jest.advanceTimersByTime( 1000 ) ); + + await waitFor( () => { + expect( screen.getByText( 'Validating...' ) ).toBeVisible(); + } ); + + act( () => jest.advanceTimersByTime( 200 ) ); + + await waitFor( () => { + expect( + screen.getByText( 'The word "error" is not allowed.' ) + ).toBeVisible(); + } ); + + // Test editing after error + await user.type( input, '{backspace}' ); + + act( () => jest.advanceTimersByTime( 1000 ) ); + + await waitFor( () => { + expect( screen.getByText( 'Validating...' ) ).toBeVisible(); + } ); + + act( () => jest.advanceTimersByTime( 200 ) ); + + await waitFor( () => { + expect( screen.getByText( 'Validated' ) ).toBeVisible(); + } ); + } ); + + it( 'should not show a "valid" state until the server response is received, even if locally valid', async () => { + const user = userEvent.setup( { + advanceTimers: jest.advanceTimersByTime, + } ); + render( + + ); + + const input = screen.getByRole( 'textbox' ); + + await user.type( input, 'valid text' ); + + await user.tab(); + act( () => jest.advanceTimersByTime( 1200 ) ); + + await waitFor( () => { + expect( screen.getByText( 'Validated' ) ).toBeVisible(); + } ); + + await user.clear( input ); + + act( () => jest.advanceTimersByTime( 1000 ) ); + + await waitFor( () => { + expect( + screen.getByText( 'Constraints not satisfied' ) + ).toBeVisible(); + } ); + + await user.type( input, 'error' ); + + act( () => jest.advanceTimersByTime( 200 ) ); + + expect( screen.queryByText( 'Validated' ) ).not.toBeInTheDocument(); + + act( () => jest.advanceTimersByTime( 1000 ) ); + + await waitFor( () => { + expect( + screen.getByText( 'The word "error" is not allowed.' ) + ).toBeVisible(); + } ); + } ); + } ); +} ); diff --git a/storybook/main.js b/storybook/main.js index 29f24c223ccdfe..7e9ddb7342d2cf 100644 --- a/storybook/main.js +++ b/storybook/main.js @@ -54,6 +54,7 @@ module.exports = { '@storybook/addon-a11y', '@storybook/addon-toolbars', '@storybook/addon-actions', + '@storybook/addon-interactions', '@storybook/addon-webpack5-compiler-babel', 'storybook-source-link', '@geometricpanda/storybook-addon-badges', From 25bd8b49bfe64053c8fd0cc2299a2fa300f818c5 Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Tue, 26 Aug 2025 17:33:37 -0700 Subject: [PATCH 04/20] Convert date package to TypeScript (#67665) Co-authored-by: manzoorwanijk Co-authored-by: dmsnell --- packages/date/CHANGELOG.md | 4 + packages/date/README.md | 36 ++-- packages/date/src/{index.js => index.ts} | 256 ++++++++++------------- packages/date/src/types.ts | 138 ++++++++++++ 4 files changed, 275 insertions(+), 159 deletions(-) rename packages/date/src/{index.js => index.ts} (60%) create mode 100644 packages/date/src/types.ts diff --git a/packages/date/CHANGELOG.md b/packages/date/CHANGELOG.md index e6067ffe7bab0e..f8d713014b1ebb 100644 --- a/packages/date/CHANGELOG.md +++ b/packages/date/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- Improved TypeScript definitions ([67573](https://github.com/WordPress/gutenberg/pull/67573)) + ## 5.29.0 (2025-08-20) ## 5.28.0 (2025-08-07) diff --git a/packages/date/README.md b/packages/date/README.md index a7543df01cd631..075fc3ee061ee0 100644 --- a/packages/date/README.md +++ b/packages/date/README.md @@ -28,8 +28,8 @@ _Related_ _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). -- _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. -- _timezone_ `string | number | undefined`: Timezone to output result in or a UTC offset. Defaults to timezone from site. +- _dateValue_ `Moment | Date | string | number`: Date object or string, parsable by moment.js. +- _timezone_ `string`: Timezone to output result in or a UTC offset. Defaults to timezone from site. _Returns_ @@ -49,12 +49,12 @@ _Related_ _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). -- _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. -- _timezone_ `string | number | boolean | undefined=`: Timezone to output result in or a UTC offset. Defaults to timezone from site. Notice: `boolean` is effectively deprecated, but still supported for backward compatibility reasons. +- _dateValue_ `Moment | Date | string | number`: Date object or string, parsable by moment.js. +- _timezone_ `string | number | boolean`: Timezone to output result in or a UTC offset. Defaults to timezone from site. Notice: `boolean` is effectively deprecated, but still supported for backward compatibility reasons. _Returns_ -- `string`: Formatted date. +- Formatted date. ### format @@ -63,11 +63,11 @@ Formats a date. Does not alter the date's timezone. _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). -- _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. +- _dateValue_ `Moment | Date | string | number`: Date object or string, parsable by moment.js. _Returns_ -- `string`: Formatted date. +- Formatted date. ### getDate @@ -75,11 +75,11 @@ Create and return a JavaScript Date Object from a date string in the WP timezone _Parameters_ -- _dateString_ `?string`: Date formatted in the WP timezone. +- _dateString_ `string | null`: Date formatted in the WP timezone. _Returns_ -- `Date`: Date +- Date ### getSettings @@ -96,11 +96,11 @@ Formats a date (like `date()` in PHP), in the UTC timezone. _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). -- _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. +- _dateValue_ `Moment | Date | string | number`: Date object or string, parsable by moment.js. _Returns_ -- `string`: Formatted date in English. +- Formatted date in English. ### gmdateI18n @@ -109,11 +109,11 @@ Formats a date (like `wp_date()` in PHP), translating it into site's locale and _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See [php.net/date](https://www.php.net/manual/en/function.date.php). -- _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. +- _dateValue_ `Moment | Date | string | number`: Date object or string, parsable by moment.js. _Returns_ -- `string`: Formatted date. +- Formatted date. ### humanTimeDiff @@ -121,12 +121,12 @@ Returns a human-readable time difference between two dates, like human_time_diff _Parameters_ -- _from_ `Moment | Date | string`: From date, in the WP timezone. -- _to_ `Moment | Date | string | undefined`: To date, formatted in the WP timezone. +- _from_ `Moment | Date | string | number`: From date, in the WP timezone. +- _to_ `Moment | Date | string | number`: To date, formatted in the WP timezone. _Returns_ -- `string`: Human-readable time difference. +- Human-readable time difference. ### isInTheFuture @@ -134,11 +134,11 @@ Check whether a date is considered in the future according to the WordPress sett _Parameters_ -- _dateValue_ `string`: Date String or Date object in the Defined WP Timezone. +- _dateValue_ `Date | string | number`: Date String or Date object in the Defined WP Timezone. _Returns_ -- `boolean`: Is in the future. +- Is in the future. ### setSettings diff --git a/packages/date/src/index.js b/packages/date/src/index.ts similarity index 60% rename from packages/date/src/index.js rename to packages/date/src/index.ts index 5af8b5f4780e89..d5950c57602d91 100644 --- a/packages/date/src/index.js +++ b/packages/date/src/index.ts @@ -1,6 +1,7 @@ /** * External dependencies */ +import type { Moment } from 'moment'; import momentLib from 'moment'; import 'moment-timezone/moment-timezone'; import 'moment-timezone/moment-timezone-utils'; @@ -9,54 +10,12 @@ import 'moment-timezone/moment-timezone-utils'; * WordPress dependencies */ import deprecated from '@wordpress/deprecated'; - -/** @typedef {import('moment').Moment} Moment */ -/** @typedef {import('moment').LocaleSpecification} MomentLocaleSpecification */ - /** - * @typedef MeridiemConfig - * @property {string} am Lowercase AM. - * @property {string} AM Uppercase AM. - * @property {string} pm Lowercase PM. - * @property {string} PM Uppercase PM. + * Internal dependencies */ +import type { DateSettings } from './types'; -/** - * @typedef FormatsConfig - * @property {string} time Time format. - * @property {string} date Date format. - * @property {string} datetime Datetime format. - * @property {string} datetimeAbbreviated Abbreviated datetime format. - */ - -/** - * @typedef TimezoneConfig - * @property {string} offset Offset setting. - * @property {string} offsetFormatted Offset setting with decimals formatted to minutes. - * @property {string} string The timezone as a string (e.g., `'America/Los_Angeles'`). - * @property {string} abbr Abbreviation for the timezone. - */ - -/* eslint-disable jsdoc/valid-types */ -/** - * @typedef L10nSettings - * @property {string} locale Moment locale. - * @property {MomentLocaleSpecification['months']} months Locale months. - * @property {MomentLocaleSpecification['monthsShort']} monthsShort Locale months short. - * @property {MomentLocaleSpecification['weekdays']} weekdays Locale weekdays. - * @property {MomentLocaleSpecification['weekdaysShort']} weekdaysShort Locale weekdays short. - * @property {MeridiemConfig} meridiem Meridiem config. - * @property {MomentLocaleSpecification['relativeTime']} relative Relative time config. - * @property {0|1|2|3|4|5|6} startOfWeek Day that the week starts on. - */ -/* eslint-enable jsdoc/valid-types */ - -/** - * @typedef DateSettings - * @property {L10nSettings} l10n Localization settings. - * @property {FormatsConfig} formats Date/time formats config. - * @property {TimezoneConfig} timezone Timezone settings. - */ +export * from './types'; const WP_ZONE = 'WP'; @@ -66,8 +25,7 @@ const VALID_UTC_OFFSET = /^[+-][0-1][0-9](:?[0-9][0-9])?$/; // Changes made here will likely need to be synced with Core in the file // src/wp-includes/script-loader.php in `wp_default_packages_inline_scripts()`. -/** @type {DateSettings} */ -let settings = { +let settings: DateSettings = { l10n: { locale: 'en', months: [ @@ -139,9 +97,9 @@ let settings = { /** * Adds a locale to moment, using the format supplied by `wp_localize_script()`. * - * @param {DateSettings} dateSettings Settings, including locale data. + * @param dateSettings Settings, including locale data. */ -export function setSettings( dateSettings ) { +export function setSettings( dateSettings: DateSettings ) { settings = dateSettings; setupWPTimezone(); @@ -259,31 +217,25 @@ function setupWPTimezone() { // Date constants. /** * Number of seconds in one minute. - * - * @type {number} */ const MINUTE_IN_SECONDS = 60; /** * Number of minutes in one hour. - * - * @type {number} */ const HOUR_IN_MINUTES = 60; /** * Number of seconds in one hour. - * - * @type {number} */ const HOUR_IN_SECONDS = 60 * MINUTE_IN_SECONDS; /** * Map of PHP formats to Moment.js formats. * - * These are used internally by {@link wp.date.format}, and are either + * These are used internally by {@link format}, and are either * a string representing the corresponding Moment.js format code, or a * function which returns the formatted string. * - * This should only be used through {@link wp.date.format}, not + * This should only be used through {@link format}, not * directly. */ const formatMap = { @@ -297,11 +249,11 @@ const formatMap = { /** * Gets the ordinal suffix. * - * @param {Moment} momentDate Moment instance. + * @param momentDate Moment instance. * - * @return {string} Formatted date. + * @return Formatted date. */ - S( momentDate ) { + S( momentDate: Moment ) { // Do - D. const num = momentDate.format( 'D' ); const withOrdinal = momentDate.format( 'Do' ); @@ -312,11 +264,11 @@ const formatMap = { /** * Gets the day of the year (zero-indexed). * - * @param {Moment} momentDate Moment instance. + * @param momentDate Moment instance. * - * @return {string} Formatted date. + * @return Formatted date. */ - z( momentDate ) { + z( momentDate: Moment ) { // DDD - 1. return ( parseInt( momentDate.format( 'DDD' ), 10 ) - 1 ).toString(); }, @@ -332,11 +284,11 @@ const formatMap = { /** * Gets the days in the month. * - * @param {Moment} momentDate Moment instance. + * @param momentDate Moment instance. * - * @return {number} Formatted date. + * @return Formatted date. */ - t( momentDate ) { + t( momentDate: Moment ) { return momentDate.daysInMonth(); }, @@ -344,11 +296,11 @@ const formatMap = { /** * Gets whether the current year is a leap year. * - * @param {Moment} momentDate Moment instance. + * @param momentDate Moment instance. * - * @return {string} Formatted date. + * @return Formatted date. */ - L( momentDate ) { + L( momentDate: Moment ) { return momentDate.isLeapYear() ? '1' : '0'; }, o: 'GGGG', @@ -361,11 +313,11 @@ const formatMap = { /** * Gets the current time in Swatch Internet Time (.beats). * - * @param {Moment} momentDate Moment instance. + * @param momentDate Moment instance. * - * @return {number} Formatted date. + * @return Formatted date. */ - B( momentDate ) { + B( momentDate: Moment ) { const timezoned = momentLib( momentDate ).utcOffset( 60 ); const seconds = parseInt( timezoned.format( 's' ), 10 ), minutes = parseInt( timezoned.format( 'm' ), 10 ), @@ -393,11 +345,11 @@ const formatMap = { /** * Gets whether the timezone is in DST currently. * - * @param {Moment} momentDate Moment instance. + * @param momentDate Moment instance. * - * @return {string} Formatted date. + * @return Formatted date. */ - I( momentDate ) { + I( momentDate: Moment ) { return momentDate.isDST() ? '1' : '0'; }, O: 'ZZ', @@ -406,11 +358,11 @@ const formatMap = { /** * Gets the timezone offset in seconds. * - * @param {Moment} momentDate Moment instance. + * @param momentDate Moment instance. * - * @return {number} Formatted date. + * @return Formatted date. */ - Z( momentDate ) { + Z( momentDate: Moment ) { // Timezone offset in seconds. const offset = momentDate.format( 'Z' ); const sign = offset[ 0 ] === '-' ? -1 : 1; @@ -429,11 +381,11 @@ const formatMap = { /** * Formats the date as RFC2822. * - * @param {Moment} momentDate Moment instance. + * @param momentDate Moment instance. * - * @return {string} Formatted date. + * @return Formatted date. */ - r( momentDate ) { + r( momentDate: Moment ) { return momentDate .locale( 'en' ) .format( 'ddd, DD MMM YYYY HH:mm:ss ZZ' ); @@ -444,14 +396,17 @@ const formatMap = { /** * Formats a date. Does not alter the date's timezone. * - * @param {string} dateFormat PHP-style formatting string. - * See [php.net/date](https://www.php.net/manual/en/function.date.php). - * @param {Moment | Date | string | undefined} dateValue Date object or string, - * parsable by moment.js. + * @param dateFormat PHP-style formatting string. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). + * @param dateValue Date object or string, + * parsable by moment.js. * - * @return {string} Formatted date. + * @return Formatted date. */ -export function format( dateFormat, dateValue = new Date() ) { +export function format( + dateFormat: string, + dateValue: Moment | Date | string | number = new Date() +) { let i, char; const newFormat = []; const momentDate = momentLib( dateValue ); @@ -465,8 +420,7 @@ export function format( dateFormat, dateValue = new Date() ) { continue; } if ( char in formatMap ) { - const formatter = - formatMap[ /** @type {keyof formatMap} */ ( char ) ]; + const formatter = formatMap[ char as keyof typeof formatMap ]; if ( typeof formatter !== 'string' ) { // If the format is a function, call it. newFormat.push( '[' + formatter( momentDate ) + ']' ); @@ -486,20 +440,24 @@ export function format( dateFormat, dateValue = new Date() ) { /** * Formats a date (like `date()` in PHP). * - * @param {string} dateFormat PHP-style formatting string. - * See [php.net/date](https://www.php.net/manual/en/function.date.php). - * @param {Moment | Date | string | undefined} dateValue Date object or string, parsable - * by moment.js. - * @param {string | number | undefined} timezone Timezone to output result in or a - * UTC offset. Defaults to timezone from - * site. + * @param dateFormat PHP-style formatting string. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). + * @param dateValue Date object or string, parsable + * by moment.js. + * @param timezone Timezone to output result in or a + * UTC offset. Defaults to timezone from + * site. * * @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones * @see https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC * * @return {string} Formatted date in English. */ -export function date( dateFormat, dateValue = new Date(), timezone ) { +export function date( + dateFormat: string, + dateValue: Moment | Date | string | number = new Date(), + timezone?: string +) { const dateMoment = buildMoment( dateValue, timezone ); return format( dateFormat, dateMoment ); } @@ -507,14 +465,17 @@ export function date( dateFormat, dateValue = new Date(), timezone ) { /** * Formats a date (like `date()` in PHP), in the UTC timezone. * - * @param {string} dateFormat PHP-style formatting string. - * See [php.net/date](https://www.php.net/manual/en/function.date.php). - * @param {Moment | Date | string | undefined} dateValue Date object or string, - * parsable by moment.js. + * @param dateFormat PHP-style formatting string. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). + * @param dateValue Date object or string, + * parsable by moment.js. * - * @return {string} Formatted date in English. + * @return Formatted date in English. */ -export function gmdate( dateFormat, dateValue = new Date() ) { +export function gmdate( + dateFormat: string, + dateValue: Moment | Date | string | number = new Date() +) { const dateMoment = momentLib( dateValue ).utc(); return format( dateFormat, dateMoment ); } @@ -525,22 +486,26 @@ export function gmdate( dateFormat, dateValue = new Date() ) { * Backward Compatibility Notice: if `timezone` is set to `true`, the function * behaves like `gmdateI18n`. * - * @param {string} dateFormat PHP-style formatting string. - * See [php.net/date](https://www.php.net/manual/en/function.date.php). - * @param {Moment | Date | string | undefined} dateValue Date object or string, parsable by - * moment.js. - * @param {string | number | boolean | undefined=} timezone Timezone to output result in or a - * UTC offset. Defaults to timezone from - * site. Notice: `boolean` is effectively - * deprecated, but still supported for - * backward compatibility reasons. + * @param dateFormat PHP-style formatting string. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). + * @param dateValue Date object or string, parsable by + * moment.js. + * @param timezone Timezone to output result in or a + * UTC offset. Defaults to timezone from + * site. Notice: `boolean` is effectively + * deprecated, but still supported for + * backward compatibility reasons. * * @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones * @see https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC * - * @return {string} Formatted date. + * @return Formatted date. */ -export function dateI18n( dateFormat, dateValue = new Date(), timezone ) { +export function dateI18n( + dateFormat: string, + dateValue: Moment | Date | string | number = new Date(), + timezone?: string | number | boolean +) { if ( true === timezone ) { return gmdateI18n( dateFormat, dateValue ); } @@ -558,14 +523,17 @@ export function dateI18n( dateFormat, dateValue = new Date(), timezone ) { * Formats a date (like `wp_date()` in PHP), translating it into site's locale * and using the UTC timezone. * - * @param {string} dateFormat PHP-style formatting string. - * See [php.net/date](https://www.php.net/manual/en/function.date.php). - * @param {Moment | Date | string | undefined} dateValue Date object or string, - * parsable by moment.js. + * @param dateFormat PHP-style formatting string. + * See [php.net/date](https://www.php.net/manual/en/function.date.php). + * @param dateValue Date object or string, + * parsable by moment.js. * - * @return {string} Formatted date. + * @return Formatted date. */ -export function gmdateI18n( dateFormat, dateValue = new Date() ) { +export function gmdateI18n( + dateFormat: string, + dateValue: Moment | Date | string | number = new Date() +) { const dateMoment = momentLib( dateValue ).utc(); dateMoment.locale( settings.l10n.locale ); return format( dateFormat, dateMoment ); @@ -574,11 +542,11 @@ export function gmdateI18n( dateFormat, dateValue = new Date() ) { /** * Check whether a date is considered in the future according to the WordPress settings. * - * @param {string} dateValue Date String or Date object in the Defined WP Timezone. + * @param dateValue Date String or Date object in the Defined WP Timezone. * - * @return {boolean} Is in the future. + * @return Is in the future. */ -export function isInTheFuture( dateValue ) { +export function isInTheFuture( dateValue: Date | string | number ) { const now = momentLib.tz( WP_ZONE ); const momentObject = momentLib.tz( dateValue, WP_ZONE ); @@ -588,11 +556,11 @@ export function isInTheFuture( dateValue ) { /** * Create and return a JavaScript Date Object from a date string in the WP timezone. * - * @param {?string} dateString Date formatted in the WP timezone. + * @param dateString Date formatted in the WP timezone. * - * @return {Date} Date + * @return Date */ -export function getDate( dateString ) { +export function getDate( dateString?: string | null ) { if ( ! dateString ) { return momentLib.tz( WP_ZONE ).toDate(); } @@ -603,12 +571,15 @@ export function getDate( dateString ) { /** * Returns a human-readable time difference between two dates, like human_time_diff() in PHP. * - * @param {Moment | Date | string} from From date, in the WP timezone. - * @param {Moment | Date | string | undefined} to To date, formatted in the WP timezone. + * @param from From date, in the WP timezone. + * @param to To date, formatted in the WP timezone. * - * @return {string} Human-readable time difference. + * @return Human-readable time difference. */ -export function humanTimeDiff( from, to ) { +export function humanTimeDiff( + from: Moment | Date | string | number, + to?: Moment | Date | string | number +) { const fromMoment = momentLib.tz( from, WP_ZONE ); const toMoment = to ? momentLib.tz( to, WP_ZONE ) : momentLib.tz( WP_ZONE ); return fromMoment.from( toMoment ); @@ -617,23 +588,26 @@ export function humanTimeDiff( from, to ) { /** * Creates a moment instance using the given timezone or, if none is provided, using global settings. * - * @param {Moment | Date | string | undefined} dateValue Date object or string, parsable - * by moment.js. - * @param {string | number | undefined} timezone Timezone to output result in or a - * UTC offset. Defaults to timezone from - * site. + * @param dateValue Date object or string, parsable + * by moment.js. + * @param timezone Timezone to output result in or a + * UTC offset. Defaults to timezone from + * site. * * @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones * @see https://en.wikipedia.org/wiki/ISO_8601#Time_offsets_from_UTC * - * @return {Moment} a moment instance. + * @return A moment instance. */ -function buildMoment( dateValue, timezone = '' ) { +function buildMoment( + dateValue?: Moment | Date | string | number, + timezone: string | number = '' +) { const dateMoment = momentLib( dateValue ); if ( timezone && ! isUTCOffset( timezone ) ) { // The ! isUTCOffset() check guarantees that timezone is a string. - return dateMoment.tz( /** @type {string} */ ( timezone ) ); + return dateMoment.tz( timezone as string ); } if ( timezone && isUTCOffset( timezone ) ) { @@ -650,11 +624,11 @@ function buildMoment( dateValue, timezone = '' ) { /** * Returns whether a certain UTC offset is valid or not. * - * @param {number|string} offset a UTC offset. + * @param offset a UTC offset. * - * @return {boolean} whether a certain UTC offset is valid or not. + * @return whether a certain UTC offset is valid or not. */ -function isUTCOffset( offset ) { +function isUTCOffset( offset: number | string ) { if ( 'number' === typeof offset ) { return true; } diff --git a/packages/date/src/types.ts b/packages/date/src/types.ts new file mode 100644 index 00000000000000..ace3c14162b2c6 --- /dev/null +++ b/packages/date/src/types.ts @@ -0,0 +1,138 @@ +/** + * External dependencies + */ +import type { LocaleSpecification as MomentLocaleSpecification } from 'moment'; + +export type MeridiemConfig = { + /** + * Lowercase AM. + */ + am: string; + + /** + * Uppercase AM. + */ + AM: string; + + /** + * Lowercase PM. + */ + pm: string; + + /** + * Uppercase PM. + */ + PM: string; +}; + +export type FormatsConfig = { + /** + * Time format. + */ + time: string; + + /** + * Date format. + */ + date: string; + + /** + * Datetime format. + */ + datetime: string; + + /** + * Abbreviated datetime format. + */ + datetimeAbbreviated: string; +}; + +export type TimezoneConfig = { + /** + * Offset setting. + */ + offset: string; + + /** + * Offset setting with decimals formatted to minutes. + */ + offsetFormatted: string; + + /** + * The timezone as a string (e.g., `'America/Los_Angeles'`). + */ + string: string; + + /** + * Abbreviation for the timezone. + */ + abbr: string; +}; + +export type L10nSettings = { + /** + * Moment locale. + */ + locale: string; + + /** + * Locale months. + * + * @example + * ['January', 'February', ... ] + */ + months: MomentLocaleSpecification[ 'months' ]; + + /** + * Locale months short. + * + * @example + * ['Jan', 'Feb', ... ] + */ + monthsShort: MomentLocaleSpecification[ 'monthsShort' ]; + + /** + * Locale weekdays. + * + * @example + * ['Sunday', 'Monday', ... ] + */ + weekdays: MomentLocaleSpecification[ 'weekdays' ]; + + /** + * Locale weekdays short. + */ + weekdaysShort: MomentLocaleSpecification[ 'weekdaysShort' ]; + + /** + * Meridiem config. + */ + meridiem: MeridiemConfig; + + /** + * Relative time config. + */ + relative: MomentLocaleSpecification[ 'relativeTime' ]; + + /** + * Day that the week starts on. + */ + startOfWeek: 0 | 1 | 2 | 3 | 4 | 5 | 6; +}; + +export type DateSettings = { + /** + * Localization settings. + */ + l10n: L10nSettings; + + /** + * Date/time formats config. + */ + formats: FormatsConfig; + + /** + * Timezone settings. + */ + timezone: TimezoneConfig; +}; From d919b2923ac4646b1c0f38b818074d35078548cc Mon Sep 17 00:00:00 2001 From: Manzoor Wani Date: Tue, 26 Aug 2025 17:34:00 -0700 Subject: [PATCH 05/20] Convert api-fetch package to TypeScript (#67669) Co-authored-by: manzoorwanijk Co-authored-by: dmsnell --- packages/api-fetch/src/{index.js => index.ts} | 95 ++++++++++--------- ...-middleware.js => fetch-all-middleware.ts} | 44 +++++---- .../middlewares/{http-v1.js => http-v1.ts} | 14 +-- .../{media-upload.js => media-upload.ts} | 19 ++-- ...pace-endpoint.js => namespace-endpoint.ts} | 9 +- .../src/middlewares/{nonce.js => nonce.ts} | 22 +++-- .../{preloading.js => preloading.ts} | 32 ++++--- .../api-fetch/src/middlewares/root-url.js | 44 --------- .../api-fetch/src/middlewares/root-url.ts | 47 +++++++++ ...-middleware.js => fetch-all-middleware.ts} | 2 +- .../test/{http-v1.js => http-v1.ts} | 11 ++- .../test/{media-upload.js => media-upload.ts} | 3 +- ...pace-endpoint.js => namespace-endpoint.ts} | 3 +- .../middlewares/test/{nonce.js => nonce.ts} | 9 +- .../test/{preloading.js => preloading.ts} | 42 +++++--- .../test/{root-url.js => root-url.ts} | 3 +- .../test/{user-locale.js => user-locale.ts} | 17 ++-- .../src/middlewares/theme-preview.js | 42 -------- .../src/middlewares/theme-preview.ts | 60 ++++++++++++ .../{user-locale.js => user-locale.ts} | 6 +- packages/api-fetch/src/types.ts | 15 ++- .../src/utils/{response.js => response.ts} | 33 ++++--- 22 files changed, 332 insertions(+), 240 deletions(-) rename packages/api-fetch/src/{index.js => index.ts} (69%) rename packages/api-fetch/src/middlewares/{fetch-all-middleware.js => fetch-all-middleware.ts} (71%) rename packages/api-fetch/src/middlewares/{http-v1.js => http-v1.ts} (81%) rename packages/api-fetch/src/middlewares/{media-upload.js => media-upload.ts} (82%) rename packages/api-fetch/src/middlewares/{namespace-endpoint.js => namespace-endpoint.ts} (77%) rename packages/api-fetch/src/middlewares/{nonce.js => nonce.ts} (62%) rename packages/api-fetch/src/middlewares/{preloading.js => preloading.ts} (79%) delete mode 100644 packages/api-fetch/src/middlewares/root-url.js create mode 100644 packages/api-fetch/src/middlewares/root-url.ts rename packages/api-fetch/src/middlewares/test/{fetch-all-middleware.js => fetch-all-middleware.ts} (95%) rename packages/api-fetch/src/middlewares/test/{http-v1.js => http-v1.ts} (73%) rename packages/api-fetch/src/middlewares/test/{media-upload.js => media-upload.ts} (88%) rename packages/api-fetch/src/middlewares/test/{namespace-endpoint.js => namespace-endpoint.ts} (84%) rename packages/api-fetch/src/middlewares/test/{nonce.js => nonce.ts} (73%) rename packages/api-fetch/src/middlewares/test/{preloading.js => preloading.ts} (90%) rename packages/api-fetch/src/middlewares/test/{root-url.js => root-url.ts} (83%) rename packages/api-fetch/src/middlewares/test/{user-locale.js => user-locale.ts} (85%) delete mode 100644 packages/api-fetch/src/middlewares/theme-preview.js create mode 100644 packages/api-fetch/src/middlewares/theme-preview.ts rename packages/api-fetch/src/middlewares/{user-locale.js => user-locale.ts} (76%) rename packages/api-fetch/src/utils/{response.js => response.ts} (68%) diff --git a/packages/api-fetch/src/index.js b/packages/api-fetch/src/index.ts similarity index 69% rename from packages/api-fetch/src/index.js rename to packages/api-fetch/src/index.ts index 408f2af0901f15..a5cd76253f3ebd 100644 --- a/packages/api-fetch/src/index.js +++ b/packages/api-fetch/src/index.ts @@ -19,14 +19,17 @@ import { parseResponseAndNormalizeError, parseAndThrowError, } from './utils/response'; +import type { + APIFetchMiddleware, + APIFetchOptions, + FetchHandler, +} from './types'; /** * Default set of header values which should be sent with every request unless * explicitly provided through apiFetch options. - * - * @type {Record} */ -const DEFAULT_HEADERS = { +const DEFAULT_HEADERS: APIFetchOptions[ 'headers' ] = { // The backend uses the Accept header as a condition for considering an // incoming request as a REST request. // @@ -37,20 +40,12 @@ const DEFAULT_HEADERS = { /** * Default set of fetch option values which should be sent with every request * unless explicitly provided through apiFetch options. - * - * @type {Object} */ -const DEFAULT_OPTIONS = { +const DEFAULT_OPTIONS: APIFetchOptions = { credentials: 'include', }; -/** @typedef {import('./types').APIFetchMiddleware} APIFetchMiddleware */ -/** @typedef {import('./types').APIFetchOptions} APIFetchOptions */ - -/** - * @type {import('./types').APIFetchMiddleware[]} - */ -const middlewares = [ +const middlewares: Array< APIFetchMiddleware > = [ userLocaleMiddleware, namespaceEndpointMiddleware, httpV1Middleware, @@ -60,9 +55,9 @@ const middlewares = [ /** * Register a middleware * - * @param {import('./types').APIFetchMiddleware} middleware + * @param middleware */ -function registerMiddleware( middleware ) { +function registerMiddleware( middleware: APIFetchMiddleware ) { middlewares.unshift( middleware ); } @@ -70,10 +65,10 @@ function registerMiddleware( middleware ) { * Checks the status of a response, throwing the Response as an error if * it is outside the 200 range. * - * @param {Response} response - * @return {Response} The response if the status is in the 200 range. + * @param response + * @return The response if the status is in the 200 range. */ -const checkStatus = ( response ) => { +const checkStatus = ( response: Response ) => { if ( response.status >= 200 && response.status < 300 ) { return response; } @@ -81,12 +76,7 @@ const checkStatus = ( response ) => { throw response; }; -/** @typedef {(options: import('./types').APIFetchOptions) => Promise} FetchHandler*/ - -/** - * @type {FetchHandler} - */ -const defaultFetchHandler = ( nextOptions ) => { +const defaultFetchHandler: FetchHandler = ( nextOptions ) => { const { url, path, data, parse = true, ...remainingOptions } = nextOptions; let { body, headers } = nextOptions; @@ -134,32 +124,48 @@ const defaultFetchHandler = ( nextOptions ) => { ); }; -/** @type {FetchHandler} */ let fetchHandler = defaultFetchHandler; /** * Defines a custom fetch handler for making the requests that will override * the default one using window.fetch * - * @param {FetchHandler} newFetchHandler The new fetch handler + * @param newFetchHandler The new fetch handler */ -function setFetchHandler( newFetchHandler ) { +function setFetchHandler( newFetchHandler: FetchHandler ) { fetchHandler = newFetchHandler; } +interface apiFetch { + < T, Parse extends boolean = true >( + options: APIFetchOptions< Parse > + ): Promise< Parse extends true ? T : Response >; + nonceEndpoint?: string; + nonceMiddleware?: ReturnType< typeof createNonceMiddleware >; + use: ( middleware: APIFetchMiddleware ) => void; + setFetchHandler: ( newFetchHandler: FetchHandler ) => void; + createNonceMiddleware: typeof createNonceMiddleware; + createPreloadingMiddleware: typeof createPreloadingMiddleware; + createRootURLMiddleware: typeof createRootURLMiddleware; + fetchAllMiddleware: typeof fetchAllMiddleware; + mediaUploadMiddleware: typeof mediaUploadMiddleware; + createThemePreviewMiddleware: typeof createThemePreviewMiddleware; +} + /** - * @template T - * @param {import('./types').APIFetchOptions} options - * @return {Promise} A promise representing the request processed via the registered middlewares. + * Fetch + * + * @param options The options for the fetch. + * @return A promise representing the request processed via the registered middlewares. */ -function apiFetch( options ) { +const apiFetch: apiFetch = ( options ) => { // creates a nested function chain that calls all middlewares and finally the `fetchHandler`, // converting `middlewares = [ m1, m2, m3 ]` into: // ``` // opts1 => m1( opts1, opts2 => m2( opts2, opts3 => m3( opts3, fetchHandler ) ) ); // ``` - const enhancedHandler = middlewares.reduceRight( - ( /** @type {FetchHandler} */ next, middleware ) => { + const enhancedHandler = middlewares.reduceRight< FetchHandler >( + ( next, middleware ) => { return ( workingOptions ) => middleware( workingOptions, next ); }, fetchHandler @@ -171,20 +177,16 @@ function apiFetch( options ) { } // If the nonce is invalid, refresh it and try again. - return ( - window - // @ts-ignore - .fetch( apiFetch.nonceEndpoint ) - .then( checkStatus ) - .then( ( data ) => data.text() ) - .then( ( text ) => { - // @ts-ignore - apiFetch.nonceMiddleware.nonce = text; - return apiFetch( options ); - } ) - ); + return window + .fetch( apiFetch.nonceEndpoint! ) + .then( checkStatus ) + .then( ( data ) => data.text() ) + .then( ( text ) => { + apiFetch.nonceMiddleware!.nonce = text; + return apiFetch( options ); + } ); } ); -} +}; apiFetch.use = registerMiddleware; apiFetch.setFetchHandler = setFetchHandler; @@ -197,3 +199,4 @@ apiFetch.mediaUploadMiddleware = mediaUploadMiddleware; apiFetch.createThemePreviewMiddleware = createThemePreviewMiddleware; export default apiFetch; +export * from './types'; diff --git a/packages/api-fetch/src/middlewares/fetch-all-middleware.js b/packages/api-fetch/src/middlewares/fetch-all-middleware.ts similarity index 71% rename from packages/api-fetch/src/middlewares/fetch-all-middleware.js rename to packages/api-fetch/src/middlewares/fetch-all-middleware.ts index 30834353fde811..8f53a91d7037f6 100644 --- a/packages/api-fetch/src/middlewares/fetch-all-middleware.js +++ b/packages/api-fetch/src/middlewares/fetch-all-middleware.ts @@ -7,15 +7,19 @@ import { addQueryArgs } from '@wordpress/url'; * Internal dependencies */ import apiFetch from '..'; +import type { APIFetchMiddleware, APIFetchOptions } from '../types'; /** * Apply query arguments to both URL and Path, whichever is present. * - * @param {import('../types').APIFetchOptions} props - * @param {Record} queryArgs - * @return {import('../types').APIFetchOptions} The request with the modified query args + * @param {APIFetchOptions} props The request options + * @param {Record< string, string | number >} queryArgs + * @return The request with the modified query args */ -const modifyQuery = ( { path, url, ...options }, queryArgs ) => ( { +const modifyQuery = ( + { path, url, ...options }: APIFetchOptions, + queryArgs: Record< string, string | number > +): APIFetchOptions => ( { ...options, url: url && addQueryArgs( url, queryArgs ), path: path && addQueryArgs( path, queryArgs ), @@ -24,17 +28,17 @@ const modifyQuery = ( { path, url, ...options }, queryArgs ) => ( { /** * Duplicates parsing functionality from apiFetch. * - * @param {Response} response - * @return {Promise} Parsed response json. + * @param response + * @return Parsed response json. */ -const parseResponse = ( response ) => +const parseResponse = ( response: Response ) => response.json ? response.json() : Promise.reject( response ); /** - * @param {string | null} linkHeader - * @return {{ next?: string }} The parsed link header. + * @param linkHeader + * @return The parsed link header. */ -const parseLinkHeader = ( linkHeader ) => { +const parseLinkHeader = ( linkHeader: string | null ) => { if ( ! linkHeader ) { return {}; } @@ -47,19 +51,19 @@ const parseLinkHeader = ( linkHeader ) => { }; /** - * @param {Response} response - * @return {string | undefined} The next page URL. + * @param response + * @return The next page URL. */ -const getNextPageUrl = ( response ) => { +const getNextPageUrl = ( response: Response ) => { const { next } = parseLinkHeader( response.headers.get( 'link' ) ); return next; }; /** - * @param {import('../types').APIFetchOptions} options - * @return {boolean} True if the request contains an unbounded query. + * @param options + * @return True if the request contains an unbounded query. */ -const requestContainsUnboundedQuery = ( options ) => { +const requestContainsUnboundedQuery = ( options: APIFetchOptions ) => { const pathIsUnbounded = !! options.path && options.path.indexOf( 'per_page=-1' ) !== -1; const urlIsUnbounded = @@ -71,10 +75,10 @@ const requestContainsUnboundedQuery = ( options ) => { * The REST API enforces an upper limit on the per_page option. To handle large * collections, apiFetch consumers can pass `per_page=-1`; this middleware will * then recursively assemble a full response array from all available pages. - * - * @type {import('../types').APIFetchMiddleware} + * @param options + * @param next */ -const fetchAllMiddleware = async ( options, next ) => { +const fetchAllMiddleware: APIFetchMiddleware = async ( options, next ) => { if ( options.parse === false ) { // If a consumer has opted out of parsing, do not apply middleware. return next( options ); @@ -108,7 +112,7 @@ const fetchAllMiddleware = async ( options, next ) => { } // Iteratively fetch all remaining pages until no "next" header is found. - let mergedResults = /** @type {any[]} */ ( [] ).concat( results ); + let mergedResults = ( [] as Array< any > ).concat( results ); while ( nextPage ) { const nextResponse = await apiFetch( { ...options, diff --git a/packages/api-fetch/src/middlewares/http-v1.js b/packages/api-fetch/src/middlewares/http-v1.ts similarity index 81% rename from packages/api-fetch/src/middlewares/http-v1.js rename to packages/api-fetch/src/middlewares/http-v1.ts index 953fc26ff70c8a..aa232568afe23b 100644 --- a/packages/api-fetch/src/middlewares/http-v1.js +++ b/packages/api-fetch/src/middlewares/http-v1.ts @@ -1,7 +1,10 @@ +/** + * Internal dependencies + */ +import type { APIFetchMiddleware } from '../types'; + /** * Set of HTTP methods which are eligible to be overridden. - * - * @type {Set} */ const OVERRIDE_METHODS = new Set( [ 'PATCH', 'PUT', 'DELETE' ] ); @@ -12,8 +15,6 @@ const OVERRIDE_METHODS = new Set( [ 'PATCH', 'PUT', 'DELETE' ] ); * is `GET`." * * @see https://fetch.spec.whatwg.org/#requests - * - * @type {string} */ const DEFAULT_METHOD = 'GET'; @@ -21,9 +22,10 @@ const DEFAULT_METHOD = 'GET'; * API Fetch middleware which overrides the request method for HTTP v1 * compatibility leveraging the REST API X-HTTP-Method-Override header. * - * @type {import('../types').APIFetchMiddleware} + * @param options + * @param next */ -const httpV1Middleware = ( options, next ) => { +const httpV1Middleware: APIFetchMiddleware = ( options, next ) => { const { method = DEFAULT_METHOD } = options; if ( OVERRIDE_METHODS.has( method.toUpperCase() ) ) { options = { diff --git a/packages/api-fetch/src/middlewares/media-upload.js b/packages/api-fetch/src/middlewares/media-upload.ts similarity index 82% rename from packages/api-fetch/src/middlewares/media-upload.js rename to packages/api-fetch/src/middlewares/media-upload.ts index ddd0be4e4ab436..a5bbc7bc802683 100644 --- a/packages/api-fetch/src/middlewares/media-upload.js +++ b/packages/api-fetch/src/middlewares/media-upload.ts @@ -10,12 +10,13 @@ import { parseAndThrowError, parseResponseAndNormalizeError, } from '../utils/response'; +import type { APIFetchOptions, APIFetchMiddleware } from '../types'; /** - * @param {import('../types').APIFetchOptions} options - * @return {boolean} True if the request is for media upload. + * @param options + * @return True if the request is for media upload. */ -function isMediaUploadRequest( options ) { +function isMediaUploadRequest( options: APIFetchOptions ) { const isCreateMethod = !! options.method && options.method === 'POST'; const isMediaEndpoint = ( !! options.path && options.path.indexOf( '/wp/v2/media' ) !== -1 ) || @@ -26,10 +27,10 @@ function isMediaUploadRequest( options ) { /** * Middleware handling media upload failures and retries. - * - * @type {import('../types').APIFetchMiddleware} + * @param options + * @param next */ -const mediaUploadMiddleware = ( options, next ) => { +const mediaUploadMiddleware: APIFetchMiddleware = ( options, next ) => { if ( ! isMediaUploadRequest( options ) ) { return next( options ); } @@ -38,10 +39,10 @@ const mediaUploadMiddleware = ( options, next ) => { const maxRetries = 5; /** - * @param {string} attachmentId - * @return {Promise} Processed post response. + * @param attachmentId + * @return Processed post response. */ - const postProcess = ( attachmentId ) => { + const postProcess = ( attachmentId: string ): Promise< any > => { retries++; return next( { path: `/wp/v2/media/${ attachmentId }/post-process`, diff --git a/packages/api-fetch/src/middlewares/namespace-endpoint.js b/packages/api-fetch/src/middlewares/namespace-endpoint.ts similarity index 77% rename from packages/api-fetch/src/middlewares/namespace-endpoint.js rename to packages/api-fetch/src/middlewares/namespace-endpoint.ts index bfb0e824187f35..e8ac61a543c685 100644 --- a/packages/api-fetch/src/middlewares/namespace-endpoint.js +++ b/packages/api-fetch/src/middlewares/namespace-endpoint.ts @@ -1,7 +1,12 @@ /** - * @type {import('../types').APIFetchMiddleware} + * Internal dependencies */ -const namespaceAndEndpointMiddleware = ( options, next ) => { +import type { APIFetchMiddleware } from '../types'; + +const namespaceAndEndpointMiddleware: APIFetchMiddleware = ( + options, + next +) => { let path = options.path; let namespaceTrimmed, endpointTrimmed; diff --git a/packages/api-fetch/src/middlewares/nonce.js b/packages/api-fetch/src/middlewares/nonce.ts similarity index 62% rename from packages/api-fetch/src/middlewares/nonce.js rename to packages/api-fetch/src/middlewares/nonce.ts index 60914823661ed9..19b43b1f41d65e 100644 --- a/packages/api-fetch/src/middlewares/nonce.js +++ b/packages/api-fetch/src/middlewares/nonce.ts @@ -1,12 +1,20 @@ /** - * @param {string} nonce - * @return {import('../types').APIFetchMiddleware & { nonce: string }} A middleware to enhance a request with a nonce. + * Internal dependencies */ -function createNonceMiddleware( nonce ) { - /** - * @type {import('../types').APIFetchMiddleware & { nonce: string }} - */ - const middleware = ( options, next ) => { +import type { APIFetchMiddleware } from '../types'; + +/** + * @param nonce + * + * @return A middleware to enhance a request with a nonce. + */ +function createNonceMiddleware( + nonce: string +): APIFetchMiddleware & { nonce: string } { + const middleware: APIFetchMiddleware & { nonce: string } = ( + options, + next + ) => { const { headers = {} } = options; // If an 'X-WP-Nonce' header (or any case-insensitive variation diff --git a/packages/api-fetch/src/middlewares/preloading.js b/packages/api-fetch/src/middlewares/preloading.ts similarity index 79% rename from packages/api-fetch/src/middlewares/preloading.js rename to packages/api-fetch/src/middlewares/preloading.ts index 3eaab48dc8392c..096f68e9b6d50d 100644 --- a/packages/api-fetch/src/middlewares/preloading.js +++ b/packages/api-fetch/src/middlewares/preloading.ts @@ -4,10 +4,17 @@ import { addQueryArgs, getQueryArgs, normalizePath } from '@wordpress/url'; /** - * @param {Record} preloadedData - * @return {import('../types').APIFetchMiddleware} Preloading middleware. + * Internal dependencies */ -function createPreloadingMiddleware( preloadedData ) { +import type { APIFetchMiddleware } from '../types'; + +/** + * @param preloadedData + * @return Preloading middleware. + */ +function createPreloadingMiddleware( + preloadedData: Record< string, any > +): APIFetchMiddleware { const cache = Object.fromEntries( Object.entries( preloadedData ).map( ( [ path, data ] ) => [ normalizePath( path ), @@ -17,7 +24,6 @@ function createPreloadingMiddleware( preloadedData ) { return ( options, next ) => { const { parse = true } = options; - /** @type {string | void} */ let rawPath = options.path; if ( ! rawPath && options.url ) { const { rest_route: pathFromQuery, ...queryArgs } = getQueryArgs( @@ -63,11 +69,14 @@ function createPreloadingMiddleware( preloadedData ) { /** * This is a helper function that sends a success response. * - * @param {Record} responseData - * @param {boolean} parse - * @return {Promise} Promise with the response. + * @param responseData + * @param parse + * @return Promise with the response. */ -function prepareResponse( responseData, parse ) { +function prepareResponse( + responseData: Record< string, any >, + parse: boolean +) { if ( parse ) { return Promise.resolve( responseData.body ); } @@ -82,12 +91,13 @@ function prepareResponse( responseData, parse ) { ); } catch { // See: https://github.com/WordPress/gutenberg/issues/67358#issuecomment-2621163926. - Object.entries( responseData.headers ).forEach( ( [ key, value ] ) => { + Object.entries( + responseData.headers as Record< string, string > + ).forEach( ( [ key, value ] ) => { if ( key.toLowerCase() === 'link' ) { responseData.headers[ key ] = value.replace( /<([^>]+)>/, - ( /** @type {any} */ _, /** @type {string} */ url ) => - `<${ encodeURI( url ) }>` + ( _, url ) => `<${ encodeURI( url ) }>` ); } } ); diff --git a/packages/api-fetch/src/middlewares/root-url.js b/packages/api-fetch/src/middlewares/root-url.js deleted file mode 100644 index cde855f3f9504f..00000000000000 --- a/packages/api-fetch/src/middlewares/root-url.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Internal dependencies - */ -import namespaceAndEndpointMiddleware from './namespace-endpoint'; - -/** - * @param {string} rootURL - * @return {import('../types').APIFetchMiddleware} Root URL middleware. - */ -const createRootURLMiddleware = ( rootURL ) => ( options, next ) => { - return namespaceAndEndpointMiddleware( options, ( optionsWithPath ) => { - let url = optionsWithPath.url; - let path = optionsWithPath.path; - let apiRoot; - - if ( typeof path === 'string' ) { - apiRoot = rootURL; - - if ( -1 !== rootURL.indexOf( '?' ) ) { - path = path.replace( '?', '&' ); - } - - path = path.replace( /^\//, '' ); - - // API root may already include query parameter prefix if site is - // configured to use plain permalinks. - if ( - 'string' === typeof apiRoot && - -1 !== apiRoot.indexOf( '?' ) - ) { - path = path.replace( '?', '&' ); - } - - url = apiRoot + path; - } - - return next( { - ...optionsWithPath, - url, - } ); - } ); -}; - -export default createRootURLMiddleware; diff --git a/packages/api-fetch/src/middlewares/root-url.ts b/packages/api-fetch/src/middlewares/root-url.ts new file mode 100644 index 00000000000000..1e617a300692d0 --- /dev/null +++ b/packages/api-fetch/src/middlewares/root-url.ts @@ -0,0 +1,47 @@ +/** + * Internal dependencies + */ +import type { APIFetchMiddleware } from '../types'; +import namespaceAndEndpointMiddleware from './namespace-endpoint'; + +/** + * @param rootURL + * @return Root URL middleware. + */ +const createRootURLMiddleware = + ( rootURL: string ): APIFetchMiddleware => + ( options, next ) => { + return namespaceAndEndpointMiddleware( options, ( optionsWithPath ) => { + let url = optionsWithPath.url; + let path = optionsWithPath.path; + let apiRoot; + + if ( typeof path === 'string' ) { + apiRoot = rootURL; + + if ( -1 !== rootURL.indexOf( '?' ) ) { + path = path.replace( '?', '&' ); + } + + path = path.replace( /^\//, '' ); + + // API root may already include query parameter prefix if site is + // configured to use plain permalinks. + if ( + 'string' === typeof apiRoot && + -1 !== apiRoot.indexOf( '?' ) + ) { + path = path.replace( '?', '&' ); + } + + url = apiRoot + path; + } + + return next( { + ...optionsWithPath, + url, + } ); + } ); + }; + +export default createRootURLMiddleware; diff --git a/packages/api-fetch/src/middlewares/test/fetch-all-middleware.js b/packages/api-fetch/src/middlewares/test/fetch-all-middleware.ts similarity index 95% rename from packages/api-fetch/src/middlewares/test/fetch-all-middleware.js rename to packages/api-fetch/src/middlewares/test/fetch-all-middleware.ts index 08b2c21bb0358f..d49def8066f136 100644 --- a/packages/api-fetch/src/middlewares/test/fetch-all-middleware.js +++ b/packages/api-fetch/src/middlewares/test/fetch-all-middleware.ts @@ -19,7 +19,7 @@ describe( 'Fetch All Middleware', () => { expect.hasAssertions(); const originalOptions = { url: '/posts?per_page=-1' }; let counter = 1; - jest.doMock( '../../index.js', () => ( options ) => { + jest.doMock( '../../index.ts', () => ( options ) => { const expectedUrl = counter === 1 ? '/posts?per_page=100' diff --git a/packages/api-fetch/src/middlewares/test/http-v1.js b/packages/api-fetch/src/middlewares/test/http-v1.ts similarity index 73% rename from packages/api-fetch/src/middlewares/test/http-v1.js rename to packages/api-fetch/src/middlewares/test/http-v1.ts index 6be47038ae83fa..06ea3a168a6e14 100644 --- a/packages/api-fetch/src/middlewares/test/http-v1.js +++ b/packages/api-fetch/src/middlewares/test/http-v1.ts @@ -1,15 +1,18 @@ /** * Internal dependencies */ +import type { FetchHandler } from '../../types'; import httpV1Middleware from '../http-v1'; describe( 'HTTP v1 Middleware', () => { it( 'should use a POST for a PUT requests', () => { expect.hasAssertions(); - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options.method ).toBe( 'POST' ); - expect( options.headers[ 'X-HTTP-Method-Override' ] ).toBe( 'PUT' ); + expect( options.headers![ 'X-HTTP-Method-Override' ] ).toBe( + 'PUT' + ); }; httpV1Middleware( { method: 'PUT', data: {} }, callback ); @@ -19,7 +22,7 @@ describe( 'HTTP v1 Middleware', () => { expect.hasAssertions(); const requestOptions = { method: 'GET', path: '/wp/v2/posts' }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options ).toBe( requestOptions ); }; @@ -30,7 +33,7 @@ describe( 'HTTP v1 Middleware', () => { expect.hasAssertions(); const requestOptions = { path: '/wp/v2/posts' }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options ).toBe( requestOptions ); }; diff --git a/packages/api-fetch/src/middlewares/test/media-upload.js b/packages/api-fetch/src/middlewares/test/media-upload.ts similarity index 88% rename from packages/api-fetch/src/middlewares/test/media-upload.js rename to packages/api-fetch/src/middlewares/test/media-upload.ts index c0c5b721f67bbb..7c9be9bf61f2d4 100644 --- a/packages/api-fetch/src/middlewares/test/media-upload.js +++ b/packages/api-fetch/src/middlewares/test/media-upload.ts @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import type { FetchHandler } from '../../types'; import mediaUploadMiddleware from '../media-upload'; describe( 'Media Upload Middleware', () => { @@ -8,7 +9,7 @@ describe( 'Media Upload Middleware', () => { expect.hasAssertions(); const originalOptions = { path: '/wp/v2/media' }; - const next = ( options ) => { + const next: FetchHandler = async ( options ) => { expect( options ).toBe( originalOptions ); }; diff --git a/packages/api-fetch/src/middlewares/test/namespace-endpoint.js b/packages/api-fetch/src/middlewares/test/namespace-endpoint.ts similarity index 84% rename from packages/api-fetch/src/middlewares/test/namespace-endpoint.js rename to packages/api-fetch/src/middlewares/test/namespace-endpoint.ts index 9e4b088f6089f0..c4256335396547 100644 --- a/packages/api-fetch/src/middlewares/test/namespace-endpoint.js +++ b/packages/api-fetch/src/middlewares/test/namespace-endpoint.ts @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import type { FetchHandler } from '../../types'; import namespaceEndpointMiddleware from '../namespace-endpoint'; describe( 'Namespace & Endpoint middleware', () => { @@ -12,7 +13,7 @@ describe( 'Namespace & Endpoint middleware', () => { namespace: '/wp/v2', endpoint: '/posts', }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options.path ).toBe( 'wp/v2/posts' ); expect( options.namespace ).toBeUndefined(); expect( options.endpoint ).toBeUndefined(); diff --git a/packages/api-fetch/src/middlewares/test/nonce.js b/packages/api-fetch/src/middlewares/test/nonce.ts similarity index 73% rename from packages/api-fetch/src/middlewares/test/nonce.js rename to packages/api-fetch/src/middlewares/test/nonce.ts index 81d32240be079d..aa32ee9514dd30 100644 --- a/packages/api-fetch/src/middlewares/test/nonce.js +++ b/packages/api-fetch/src/middlewares/test/nonce.ts @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import type { FetchHandler } from '../../types'; import createNonceMiddleware from '../nonce'; describe( 'Nonce middleware', () => { @@ -13,8 +14,8 @@ describe( 'Nonce middleware', () => { method: 'GET', path: '/wp/v2/posts', }; - const callback = ( options ) => { - expect( options.headers[ 'X-WP-Nonce' ] ).toBe( nonce ); + const callback: FetchHandler = async ( options ) => { + expect( options.headers![ 'X-WP-Nonce' ] ).toBe( nonce ); }; nonceMiddleware( requestOptions, callback ); @@ -31,8 +32,8 @@ describe( 'Nonce middleware', () => { headers: { 'X-WP-Nonce': 'existing nonce' }, }; - const callback = ( options ) => { - expect( options.headers[ 'X-WP-Nonce' ] ).toBe( 'new nonce' ); + const callback: FetchHandler = async ( options ) => { + expect( options.headers![ 'X-WP-Nonce' ] ).toBe( 'new nonce' ); }; nonceMiddleware( requestOptions, callback ); diff --git a/packages/api-fetch/src/middlewares/test/preloading.js b/packages/api-fetch/src/middlewares/test/preloading.ts similarity index 90% rename from packages/api-fetch/src/middlewares/test/preloading.js rename to packages/api-fetch/src/middlewares/test/preloading.ts index a3ffedbdef46d5..e6bc5661e141a6 100644 --- a/packages/api-fetch/src/middlewares/test/preloading.js +++ b/packages/api-fetch/src/middlewares/test/preloading.ts @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import type { FetchHandler } from '../../types'; import createPreloadingMiddleware from '../preloading'; describe( 'Preloading Middleware', () => { @@ -23,7 +24,10 @@ describe( 'Preloading Middleware', () => { path: 'wp/v2/posts', }; - const response = preloadingMiddleware( requestOptions ); + const response = preloadingMiddleware( + requestOptions, + async () => {} + ); return response.then( ( value ) => { expect( value ).toEqual( body ); } ); @@ -60,9 +64,12 @@ describe( 'Preloading Middleware', () => { const noResponseMock = 'undefined' === typeof window.Response; if ( noResponseMock ) { + // @ts-expect-error window.Response = class { constructor( body, options ) { + // @ts-expect-error this.body = JSON.parse( body ); + // @ts-expect-error this.headers = options.headers; } }; @@ -92,8 +99,12 @@ describe( 'Preloading Middleware', () => { parse: false, }; - const response = preloadingMiddleware( requestOptions ); + const response = preloadingMiddleware( + requestOptions, + async () => {} + ); if ( noResponseMock ) { + // @ts-expect-error delete window.Response; } return response.then( ( value ) => { @@ -126,7 +137,10 @@ describe( 'Preloading Middleware', () => { parse: true, }; - const response = preloadingMiddleware( requestOptions ); + const response = preloadingMiddleware( + requestOptions, + async () => {} + ); return response.then( ( value ) => { expect( value ).toEqual( body ); } ); @@ -172,7 +186,10 @@ describe( 'Preloading Middleware', () => { path: 'wp/v2/demo-reverse-alphabetical?baz=quux&foo=bar', }; - let value = await preloadingMiddleware( requestOptions, () => {} ); + let value = await preloadingMiddleware( + requestOptions, + async () => {} + ); expect( value ).toEqual( body ); requestOptions = { @@ -180,7 +197,7 @@ describe( 'Preloading Middleware', () => { path: 'wp/v2/demo-alphabetical?foo=bar&baz=quux', }; - value = await preloadingMiddleware( requestOptions, () => {} ); + value = await preloadingMiddleware( requestOptions, async () => {} ); expect( value ).toEqual( body ); } ); @@ -196,7 +213,7 @@ describe( 'Preloading Middleware', () => { method: 'GET', path: '/?_fields=foo%2Cbar', }, - () => {} + async () => {} ); expect( response ).toEqual( body ); @@ -214,7 +231,7 @@ describe( 'Preloading Middleware', () => { method: 'GET', url: '/index.php?rest_route=%2F', }, - () => {} + async () => {} ); expect( response ).toEqual( body ); @@ -232,7 +249,7 @@ describe( 'Preloading Middleware', () => { method: 'GET', url: '/index.php?rest_route=%2F&_fields=foo%2Cbar', }, - () => {} + async () => {} ); expect( response ).toEqual( body ); @@ -321,7 +338,7 @@ describe( 'Preloading Middleware', () => { [ 'all empty', {} ], [ 'method empty', { [ method ]: {} } ], ] )( '%s', ( label, preloadedData ) => { - it( 'should move to the next middleware if no preloaded data', () => { + it( 'should move to the next middleware if no preloaded data', async () => { const preloadingMiddleware = createPreloadingMiddleware( preloadedData ); const requestOptions = { @@ -329,12 +346,15 @@ describe( 'Preloading Middleware', () => { path: 'wp/v2/posts', }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options ).toBe( requestOptions ); return true; }; - const ret = preloadingMiddleware( requestOptions, callback ); + const ret = await preloadingMiddleware( + requestOptions, + callback + ); expect( ret ).toBe( true ); } ); } ); diff --git a/packages/api-fetch/src/middlewares/test/root-url.js b/packages/api-fetch/src/middlewares/test/root-url.ts similarity index 83% rename from packages/api-fetch/src/middlewares/test/root-url.js rename to packages/api-fetch/src/middlewares/test/root-url.ts index 921e6af8bb01e9..2014550719afe4 100644 --- a/packages/api-fetch/src/middlewares/test/root-url.js +++ b/packages/api-fetch/src/middlewares/test/root-url.ts @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import type { FetchHandler } from '../../types'; import createRootUrlMiddleware from '../root-url'; describe( 'Root URL middleware', () => { @@ -13,7 +14,7 @@ describe( 'Root URL middleware', () => { method: 'GET', path: '/wp/v2/posts', }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options.url ).toBe( 'http://wp.org/wp-admin/rest/wp/v2/posts' ); diff --git a/packages/api-fetch/src/middlewares/test/user-locale.js b/packages/api-fetch/src/middlewares/test/user-locale.ts similarity index 85% rename from packages/api-fetch/src/middlewares/test/user-locale.js rename to packages/api-fetch/src/middlewares/test/user-locale.ts index 9626819f3e0860..2291ca7160b680 100644 --- a/packages/api-fetch/src/middlewares/test/user-locale.js +++ b/packages/api-fetch/src/middlewares/test/user-locale.ts @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import type { FetchHandler } from '../../types'; import userLocaleMiddleware from '../user-locale'; describe( 'User locale middleware', () => { @@ -12,7 +13,7 @@ describe( 'User locale middleware', () => { path: '/wp/v2/posts', }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options.path ).toBe( '/wp/v2/posts?_locale=user' ); }; @@ -27,7 +28,7 @@ describe( 'User locale middleware', () => { path: '/wp/v2/posts?foo=bar', }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options.path ).toBe( '/wp/v2/posts?foo=bar&_locale=user' ); }; @@ -42,7 +43,7 @@ describe( 'User locale middleware', () => { path: '/wp/v2/posts?_locale=foo', }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options.path ).toBe( '/wp/v2/posts?_locale=foo' ); }; @@ -57,7 +58,7 @@ describe( 'User locale middleware', () => { path: '/wp/v2/posts?foo=bar&_locale=foo', }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options.path ).toBe( '/wp/v2/posts?foo=bar&_locale=foo' ); }; @@ -72,7 +73,7 @@ describe( 'User locale middleware', () => { url: 'http://wp.org/wp-json/wp/v2/posts', }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options.url ).toBe( 'http://wp.org/wp-json/wp/v2/posts?_locale=user' ); @@ -89,7 +90,7 @@ describe( 'User locale middleware', () => { url: 'http://wp.org/wp-json/wp/v2/posts?foo=bar', }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options.url ).toBe( 'http://wp.org/wp-json/wp/v2/posts?foo=bar&_locale=user' ); @@ -106,7 +107,7 @@ describe( 'User locale middleware', () => { url: 'http://wp.org/wp-json/wp/v2/posts?_locale=foo', }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options.url ).toBe( 'http://wp.org/wp-json/wp/v2/posts?_locale=foo' ); @@ -123,7 +124,7 @@ describe( 'User locale middleware', () => { url: 'http://wp.org/wp-json/wp/v2/posts?foo=bar&_locale=foo', }; - const callback = ( options ) => { + const callback: FetchHandler = async ( options ) => { expect( options.url ).toBe( 'http://wp.org/wp-json/wp/v2/posts?foo=bar&_locale=foo' ); diff --git a/packages/api-fetch/src/middlewares/theme-preview.js b/packages/api-fetch/src/middlewares/theme-preview.js deleted file mode 100644 index 56098c88cb351f..00000000000000 --- a/packages/api-fetch/src/middlewares/theme-preview.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * WordPress dependencies - */ -import { addQueryArgs, getQueryArg, removeQueryArgs } from '@wordpress/url'; - -/** - * This appends a `wp_theme_preview` parameter to the REST API request URL if - * the admin URL contains a `theme` GET parameter. - * - * If the REST API request URL has contained the `wp_theme_preview` parameter as `''`, - * then bypass this middleware. - * - * @param {Record} themePath - * @return {import('../types').APIFetchMiddleware} Preloading middleware. - */ -const createThemePreviewMiddleware = ( themePath ) => ( options, next ) => { - if ( typeof options.url === 'string' ) { - const wpThemePreview = getQueryArg( options.url, 'wp_theme_preview' ); - if ( wpThemePreview === undefined ) { - options.url = addQueryArgs( options.url, { - wp_theme_preview: themePath, - } ); - } else if ( wpThemePreview === '' ) { - options.url = removeQueryArgs( options.url, 'wp_theme_preview' ); - } - } - - if ( typeof options.path === 'string' ) { - const wpThemePreview = getQueryArg( options.path, 'wp_theme_preview' ); - if ( wpThemePreview === undefined ) { - options.path = addQueryArgs( options.path, { - wp_theme_preview: themePath, - } ); - } else if ( wpThemePreview === '' ) { - options.path = removeQueryArgs( options.path, 'wp_theme_preview' ); - } - } - - return next( options ); -}; - -export default createThemePreviewMiddleware; diff --git a/packages/api-fetch/src/middlewares/theme-preview.ts b/packages/api-fetch/src/middlewares/theme-preview.ts new file mode 100644 index 00000000000000..5feeaf061622b7 --- /dev/null +++ b/packages/api-fetch/src/middlewares/theme-preview.ts @@ -0,0 +1,60 @@ +/** + * WordPress dependencies + */ +import { addQueryArgs, getQueryArg, removeQueryArgs } from '@wordpress/url'; +/** + * Internal dependencies + */ +import type { APIFetchMiddleware } from '../types'; + +/** + * This appends a `wp_theme_preview` parameter to the REST API request URL if + * the admin URL contains a `theme` GET parameter. + * + * If the REST API request URL has contained the `wp_theme_preview` parameter as `''`, + * then bypass this middleware. + * + * @param themePath + * @return Preloading middleware. + */ +const createThemePreviewMiddleware = + ( themePath: Record< string, any > ): APIFetchMiddleware => + ( options, next ) => { + if ( typeof options.url === 'string' ) { + const wpThemePreview = getQueryArg( + options.url, + 'wp_theme_preview' + ); + if ( wpThemePreview === undefined ) { + options.url = addQueryArgs( options.url, { + wp_theme_preview: themePath, + } ); + } else if ( wpThemePreview === '' ) { + options.url = removeQueryArgs( + options.url, + 'wp_theme_preview' + ); + } + } + + if ( typeof options.path === 'string' ) { + const wpThemePreview = getQueryArg( + options.path, + 'wp_theme_preview' + ); + if ( wpThemePreview === undefined ) { + options.path = addQueryArgs( options.path, { + wp_theme_preview: themePath, + } ); + } else if ( wpThemePreview === '' ) { + options.path = removeQueryArgs( + options.path, + 'wp_theme_preview' + ); + } + } + + return next( options ); + }; + +export default createThemePreviewMiddleware; diff --git a/packages/api-fetch/src/middlewares/user-locale.js b/packages/api-fetch/src/middlewares/user-locale.ts similarity index 76% rename from packages/api-fetch/src/middlewares/user-locale.js rename to packages/api-fetch/src/middlewares/user-locale.ts index 18057cb41fa06e..9ab3db62b5bfe0 100644 --- a/packages/api-fetch/src/middlewares/user-locale.js +++ b/packages/api-fetch/src/middlewares/user-locale.ts @@ -4,9 +4,11 @@ import { addQueryArgs, hasQueryArg } from '@wordpress/url'; /** - * @type {import('../types').APIFetchMiddleware} + * Internal dependencies */ -const userLocaleMiddleware = ( options, next ) => { +import type { APIFetchMiddleware } from '../types'; + +const userLocaleMiddleware: APIFetchMiddleware = ( options, next ) => { if ( typeof options.url === 'string' && ! hasQueryArg( options.url, '_locale' ) diff --git a/packages/api-fetch/src/types.ts b/packages/api-fetch/src/types.ts index e7f478d69a1d26..f40b86e6bf5bd2 100644 --- a/packages/api-fetch/src/types.ts +++ b/packages/api-fetch/src/types.ts @@ -1,4 +1,5 @@ -export interface APIFetchOptions extends RequestInit { +export interface APIFetchOptions< Parse extends boolean = boolean > + extends RequestInit { // Override headers, we only accept it as an object due to the `nonce` middleware headers?: Record< string, string >; path?: string; @@ -6,13 +7,17 @@ export interface APIFetchOptions extends RequestInit { /** * @default true */ - parse?: boolean; + parse?: Parse; data?: any; namespace?: string; endpoint?: string; } -export type APIFetchMiddleware = ( - options: APIFetchOptions, - next: ( nextOptions: APIFetchOptions ) => Promise< any > +export type FetchHandler< Parse extends boolean = boolean > = ( + nextOptions: APIFetchOptions< Parse > +) => Promise< any >; + +export type APIFetchMiddleware< Parse extends boolean = boolean > = ( + options: APIFetchOptions< Parse >, + next: FetchHandler< Parse > ) => Promise< any >; diff --git a/packages/api-fetch/src/utils/response.js b/packages/api-fetch/src/utils/response.ts similarity index 68% rename from packages/api-fetch/src/utils/response.js rename to packages/api-fetch/src/utils/response.ts index b5be8009fd1ac9..665d440c1c816b 100644 --- a/packages/api-fetch/src/utils/response.js +++ b/packages/api-fetch/src/utils/response.ts @@ -6,12 +6,12 @@ import { __ } from '@wordpress/i18n'; /** * Parses the apiFetch response. * - * @param {Response} response - * @param {boolean} shouldParseResponse + * @param response + * @param shouldParseResponse * - * @return {Promise | null | Response} Parsed response. + * @return Parsed response. */ -const parseResponse = ( response, shouldParseResponse = true ) => { +const parseResponse = ( response: Response, shouldParseResponse = true ) => { if ( shouldParseResponse ) { if ( response.status === 204 ) { return null; @@ -27,10 +27,10 @@ const parseResponse = ( response, shouldParseResponse = true ) => { * Calls the `json` function on the Response, throwing an error if the response * doesn't have a json function or if parsing the json itself fails. * - * @param {Response} response - * @return {Promise} Parsed response. + * @param response + * @return Parsed response. */ -const parseJsonAndNormalizeError = ( response ) => { +const parseJsonAndNormalizeError = ( response: Response ) => { const invalidJsonError = { code: 'invalid_json', message: __( 'The response is not a valid JSON response.' ), @@ -48,13 +48,13 @@ const parseJsonAndNormalizeError = ( response ) => { /** * Parses the apiFetch response properly and normalize response errors. * - * @param {Response} response - * @param {boolean} shouldParseResponse + * @param response + * @param shouldParseResponse * - * @return {Promise} Parsed response. + * @return Parsed response. */ export const parseResponseAndNormalizeError = ( - response, + response: Response, shouldParseResponse = true ) => { return Promise.resolve( @@ -65,11 +65,14 @@ export const parseResponseAndNormalizeError = ( /** * Parses a response, throwing an error if parsing the response fails. * - * @param {Response} response - * @param {boolean} shouldParseResponse - * @return {Promise} Parsed response. + * @param response + * @param shouldParseResponse + * @return Parsed response. */ -export function parseAndThrowError( response, shouldParseResponse = true ) { +export function parseAndThrowError( + response: Response, + shouldParseResponse = true +) { if ( ! shouldParseResponse ) { throw response; } From 65708348aae6f213d72b4e29cd3658f6bf06bef9 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:36:33 +1000 Subject: [PATCH 06/20] Audio and Video: Hide caption controls in contentOnly mode (#71368) * Audio and Video: Hide caption and text tracks controls in contentOnly mode * Reinstate Text tracks control Co-authored-by: andrewserong Co-authored-by: tellthemachines --- packages/block-library/src/audio/edit.js | 7 ++++++- packages/block-library/src/video/edit.js | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/audio/edit.js b/packages/block-library/src/audio/edit.js index 5b2737873fce44..2c36f2272571b6 100644 --- a/packages/block-library/src/audio/edit.js +++ b/packages/block-library/src/audio/edit.js @@ -22,6 +22,7 @@ import { MediaPlaceholder, MediaReplaceFlow, useBlockProps, + useBlockEditingMode, } from '@wordpress/block-editor'; import { __, _x } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; @@ -51,6 +52,8 @@ function AudioEdit( { } ) { const { id, autoplay, loop, preload, src } = attributes; const [ temporaryURL, setTemporaryURL ] = useState( attributes.blob ); + const blockEditingMode = useBlockEditingMode(); + const hasNonContentControls = blockEditingMode === 'default'; useUploadMediaFromBlobURL( { url: temporaryURL, @@ -264,7 +267,9 @@ function AudioEdit( { isSelected={ isSingleSelected } insertBlocksAfter={ insertBlocksAfter } label={ __( 'Audio caption text' ) } - showToolbarButton={ isSingleSelected } + showToolbarButton={ + isSingleSelected && hasNonContentControls + } /> diff --git a/packages/block-library/src/video/edit.js b/packages/block-library/src/video/edit.js index fbd724b2160a09..125b2cbc78741e 100644 --- a/packages/block-library/src/video/edit.js +++ b/packages/block-library/src/video/edit.js @@ -20,6 +20,7 @@ import { MediaPlaceholder, MediaReplaceFlow, useBlockProps, + useBlockEditingMode, } from '@wordpress/block-editor'; import { useRef, useEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -55,6 +56,8 @@ function VideoEdit( { const { id, controls, poster, src, tracks } = attributes; const [ temporaryURL, setTemporaryURL ] = useState( attributes.blob ); const dropdownMenuProps = useToolsPanelDropdownMenuProps(); + const blockEditingMode = useBlockEditingMode(); + const hasNonContentControls = blockEditingMode === 'default'; useUploadMediaFromBlobURL( { url: temporaryURL, @@ -251,7 +254,9 @@ function VideoEdit( { isSelected={ isSingleSelected } insertBlocksAfter={ insertBlocksAfter } label={ __( 'Video caption text' ) } - showToolbarButton={ isSingleSelected } + showToolbarButton={ + isSingleSelected && hasNonContentControls + } /> From e058cba00b4cb2b938e4505d907afab7ac1171c6 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Wed, 27 Aug 2025 16:22:08 +0900 Subject: [PATCH 07/20] Global Styles: Remove unecessary useEffect from ScreenStyleVariations (#71344) Co-authored-by: t-hamano Co-authored-by: Mamaduka --- .../components/global-styles/screen-style-variations.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/screen-style-variations.js b/packages/edit-site/src/components/global-styles/screen-style-variations.js index f6036aaace4358..a3feecd347c7a3 100644 --- a/packages/edit-site/src/components/global-styles/screen-style-variations.js +++ b/packages/edit-site/src/components/global-styles/screen-style-variations.js @@ -6,10 +6,8 @@ import { store as blockEditorStore, } from '@wordpress/block-editor'; import { Card, CardBody } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { store as editorStore } from '@wordpress/editor'; -import { useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -27,11 +25,7 @@ function ScreenStyleVariations() { const isPreviewMode = useSelect( ( select ) => { return select( blockEditorStore ).getSettings().isPreviewMode; }, [] ); - const { setDeviceType } = useDispatch( editorStore ); useZoomOut( ! isPreviewMode ); - useEffect( () => { - setDeviceType( 'desktop' ); - }, [ setDeviceType ] ); return ( <> From a128b6676a61546ce20e1d4783e548b959bdd215 Mon Sep 17 00:00:00 2001 From: Aditya Singh <92062352+Adi-ty@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:34:05 +0530 Subject: [PATCH 08/20] Create Block: Add lifecycle script execution support (#71072) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Create Block: Add lifecycle script execution support - Execute postinstall script after npm install if present in package.json Fixes #71069 * Refactor(create-block): streamline preinstall and postinstall script execution * refactor(create-block): enhance dependency checking and installation process to support lifecycle scripts * fix: update dependency assignment to use saveSpec for accurate versioning * Add changelog entry --------- Co-authored-by: Greg Ziółkowski Co-authored-by: Adi-ty Co-authored-by: gziolo Co-authored-by: ryanwelcher Co-authored-by: bacoords --- packages/create-block/CHANGELOG.md | 6 + .../create-block/lib/init-package-json.js | 126 ++++++++++-------- 2 files changed, 79 insertions(+), 53 deletions(-) diff --git a/packages/create-block/CHANGELOG.md b/packages/create-block/CHANGELOG.md index dd5c1e0fd576d8..2d185b082c5e61 100644 --- a/packages/create-block/CHANGELOG.md +++ b/packages/create-block/CHANGELOG.md @@ -2,10 +2,16 @@ ## Unreleased +### Enhancement + +- Add lifecycle script execution support during npm packages installation ([#71072](https://github.com/WordPress/gutenberg/pull/71072)). + ## 4.72.0 (2025-08-20) ## 4.71.0 (2025-08-07) +### Enhancement + - Add support for template variants to define their own template files ([#70855](https://github.com/WordPress/gutenberg/pull/70855)) ## 4.70.0 (2025-07-23) diff --git a/packages/create-block/lib/init-package-json.js b/packages/create-block/lib/init-package-json.js index e3404122b438c8..26d492304512f7 100644 --- a/packages/create-block/lib/init-package-json.js +++ b/packages/create-block/lib/init-package-json.js @@ -29,6 +29,55 @@ module.exports = async ( { info( '' ); info( 'Creating a "package.json" file.' ); + /** + * Helper to determine if we can install this package. + * + * @param {string} packageArg The package to install. + */ + function checkDependency( packageArg ) { + const { type } = npmPackageArg( packageArg ); + if ( + ! [ 'git', 'tag', 'version', 'range', 'remote' ].includes( type ) + ) { + throw new Error( + `Provided package type "${ type }" is not supported.` + ); + } + } + + const dependencies = {}; + const devDependencies = {}; + + if ( npmDependencies && npmDependencies.length ) { + for ( const packageArg of npmDependencies ) { + try { + checkDependency( packageArg ); + const parsed = npmPackageArg( packageArg ); + dependencies[ parsed.name ] = parsed.saveSpec || 'latest'; + } catch ( { message } ) { + info( '' ); + info( `Skipping "${ packageArg }" npm dependency. Reason:` ); + error( message ); + } + } + } + + if ( npmDevDependencies && npmDevDependencies.length ) { + for ( const packageArg of npmDevDependencies ) { + try { + checkDependency( packageArg ); + const parsed = npmPackageArg( packageArg ); + devDependencies[ parsed.name ] = parsed.saveSpec || 'latest'; + } catch ( { message } ) { + info( '' ); + info( + `Skipping "${ packageArg }" npm dev dependency. Reason:` + ); + error( message ); + } + } + } + await writePkg( rootDirectory, Object.fromEntries( @@ -59,71 +108,42 @@ module.exports = async ( { ...( wpEnv && { env: 'wp-env' } ), ...customScripts, }, + dependencies: + Object.keys( dependencies ).length > 0 + ? dependencies + : undefined, + devDependencies: + Object.keys( devDependencies ).length > 0 + ? devDependencies + : undefined, ...customPackageJSON, } ).filter( ( [ , value ] ) => !! value ) ) ); - /** - * Helper to determine if we can install this package. - * - * @param {string} packageArg The package to install. - */ - function checkDependency( packageArg ) { - const { type } = npmPackageArg( packageArg ); + if ( wpScripts ) { if ( - ! [ 'git', 'tag', 'version', 'range', 'remote' ].includes( type ) + Object.keys( dependencies ).length > 0 || + Object.keys( devDependencies ).length > 0 ) { - throw new Error( - `Provided package type "${ type }" is not supported.` - ); - } - } - - if ( wpScripts ) { - if ( npmDependencies && npmDependencies.length ) { info( '' ); info( 'Installing npm dependencies. It might take a couple of minutes...' ); - for ( const packageArg of npmDependencies ) { - try { - checkDependency( packageArg ); - info( '' ); - info( `Installing "${ packageArg }".` ); - await command( `npm install ${ packageArg }`, { - cwd: rootDirectory, - } ); - } catch ( { message } ) { - info( '' ); - info( - `Skipping "${ packageArg }" npm dependency. Reason:` - ); - error( message ); - } - } - } - if ( npmDevDependencies && npmDevDependencies.length ) { - info( '' ); - info( - 'Installing npm devDependencies. It might take a couple of minutes...' - ); - for ( const packageArg of npmDevDependencies ) { - try { - checkDependency( packageArg ); - info( '' ); - info( `Installing "${ packageArg }".` ); - await command( `npm install ${ packageArg } --save-dev`, { - cwd: rootDirectory, - } ); - } catch ( { message } ) { - info( '' ); - info( - `Skipping "${ packageArg }" npm dev dependency. Reason:` - ); - error( message ); - } + try { + await command( 'npm install', { + cwd: rootDirectory, + } ); + + info( '' ); + info( + 'Successfully installed dependencies and ran lifecycle scripts.' + ); + } catch ( { message } ) { + info( '' ); + info( 'Warning: Failed to install dependencies:' ); + error( message ); } } } From 78b1abb62ee004bdc1d51680ccdf2ef4ed6cf3b6 Mon Sep 17 00:00:00 2001 From: Vipul Gupta <55375170+vipul0425@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:43:31 +0530 Subject: [PATCH 09/20] Query Loop Block: Fix broken placeholder (#70840) * feat: Add resize-observer to chnage the placeholder content for compact container. * feat: Adds styles for compact containers. * feat: Add blcok controls to the common component. * feat: Remove redundant code. * refactor: QueryEdit and QueryPlaceholder components to integrate block-controls. * refactor: Restore useBlockProp Co-authored-by: vipul0425 Co-authored-by: jasmussen Co-authored-by: t-hamano --- .../block-library/src/query/edit/index.js | 1 + .../src/query/edit/query-content.js | 12 ++-- .../src/query/edit/query-placeholder.js | 64 ++++++++++++++----- .../src/query/edit/query-toolbar.js | 12 +++- packages/block-library/src/query/editor.scss | 7 +- 5 files changed, 72 insertions(+), 24 deletions(-) diff --git a/packages/block-library/src/query/edit/index.js b/packages/block-library/src/query/edit/index.js index 60fc2ebd6170b3..3c91cdc7e3d712 100644 --- a/packages/block-library/src/query/edit/index.js +++ b/packages/block-library/src/query/edit/index.js @@ -22,6 +22,7 @@ const QueryEdit = ( props ) => { [ clientId ] ); const Component = hasInnerBlocks ? QueryContent : QueryPlaceholder; + return ( <> + + + - - - { + setContainerWidth( entry.contentRect.width ); + } ); + + const SMALL_CONTAINER_BREAKPOINT = 160; + + const isSmallContainer = + containerWidth > 0 && containerWidth < SMALL_CONTAINER_BREAKPOINT; + const { blockType, activeBlockVariation } = useSelect( ( select ) => { const { getActiveBlockVariation, getBlockType } = @@ -49,6 +63,10 @@ export default function QueryPlaceholder( { activeBlockVariation?.icon || blockType?.icon?.src; const label = activeBlockVariation?.title || blockType?.title; + const blockProps = useBlockProps( { + ref: resizeObserverRef, + } ); + if ( isStartingBlank ) { return ( + + + - { !! hasPatterns && ( + { !! hasPatterns && ! isSmallContainer && ( + { ! isSmallContainer && ( + + ) } ); diff --git a/packages/block-library/src/query/edit/query-toolbar.js b/packages/block-library/src/query/edit/query-toolbar.js index 25e087ebe1559c..9eafc5aa760709 100644 --- a/packages/block-library/src/query/edit/query-toolbar.js +++ b/packages/block-library/src/query/edit/query-toolbar.js @@ -14,12 +14,20 @@ import { __ } from '@wordpress/i18n'; */ import PatternSelection, { useBlockPatterns } from './pattern-selection'; -export default function QueryToolbar( { clientId, attributes } ) { +export default function QueryToolbar( { + clientId, + attributes, + hasInnerBlocks, +} ) { const hasPatterns = useBlockPatterns( clientId, attributes ).length; if ( ! hasPatterns ) { return null; } + const buttonLabel = hasInnerBlocks + ? __( 'Change design' ) + : __( 'Choose pattern' ); + return ( @@ -33,7 +41,7 @@ export default function QueryToolbar( { clientId, attributes } ) { aria-expanded={ isOpen } onClick={ onToggle } > - { __( 'Change design' ) } + { buttonLabel } ) } renderContent={ () => ( diff --git a/packages/block-library/src/query/editor.scss b/packages/block-library/src/query/editor.scss index ab6b361ed9e98e..4c67489cbb5fcc 100644 --- a/packages/block-library/src/query/editor.scss +++ b/packages/block-library/src/query/editor.scss @@ -1,5 +1,4 @@ .block-library-query-pattern__selection-modal { - .block-editor-block-patterns-list { column-count: 2; column-gap: $grid-unit-30; @@ -53,3 +52,9 @@ margin-bottom: 0; } } + +// Provide special styling for the placeholder. +// @todo this particular minimal style of placeholder could be componentized further. +.wp-block-query > .block-editor-media-placeholder.is-small { + min-height: 60px; +} From 5a822c95a6a727e904c65992bd2eaa9b795df400 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 27 Aug 2025 09:36:53 +0000 Subject: [PATCH 10/20] Bump plugin version to 21.5.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 0dca34add104ac..67c177d7705b48 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.7 * Requires PHP: 7.2 - * Version: 21.5.0-rc.1 + * Version: 21.5.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 1b871f9f49b427..6b929a290896f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "21.5.0-rc.1", + "version": "21.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "21.5.0-rc.1", + "version": "21.5.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "workspaces": [ diff --git a/package.json b/package.json index 8806164074f54e..24926e093a03cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "21.5.0-rc.1", + "version": "21.5.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From 0aa1bceff2c92203439f7a0eacfb5f90340c3dfe Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation Date: Wed, 27 Aug 2025 09:51:20 +0000 Subject: [PATCH 11/20] Update Changelog for 21.5.0 --- changelog.txt | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/changelog.txt b/changelog.txt index 6457698af6b5c7..01b6c07cf0cb8b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,13 +1,14 @@ == Changelog == -= 21.5.0-rc.1 = - += 21.5.0 = ## Changelog ### Features #### Block Library + +- Add Accordions Block. ([64119](https://github.com/WordPress/gutenberg/pull/64119)) - Query Title: Add "Post Type Label" variation. ([71167](https://github.com/WordPress/gutenberg/pull/71167)) @@ -114,7 +115,9 @@ #### Widgets Editor - Widget Area Block: Apply API version 3. ([71110](https://github.com/WordPress/gutenberg/pull/71110)) - +#### Icons +- Deprecate moreHorizontalMobile. ([71172](https://github.com/WordPress/gutenberg/pull/71172)) +- ### Tools #### Testing @@ -126,16 +129,6 @@ #### Build Tooling - Dynamically set `node-version-file` input. ([71090](https://github.com/WordPress/gutenberg/pull/71090)) - -### Various - -#### Icons -- Deprecate moreHorizontalMobile. ([71172](https://github.com/WordPress/gutenberg/pull/71172)) - -#### Block Library -- Add Accordions Block. ([64119](https://github.com/WordPress/gutenberg/pull/64119)) - - ## First-time contributors The following PRs were merged by first-time contributors: @@ -150,6 +143,8 @@ The following contributors merged PRs in this release: @andrewserong @BogdanUngureanu @BugReportOnWeb @ciampo @desrosj @elazzabi @getdave @gigitux @im3dabasia @jasmussen @jffng @karthikeya-io @Mamaduka @mikachan @mikejolley @mirka @mrleemon @oandregal @p-jackson @priethor @R1shabh-Gupta @ramonjd @shimotmk @Sukhendu2002 @t-hamano @tellthemachines @USERSATOSHI @yogeshbhutkar @youknowriad + + = 21.4.0 = ## Changelog From dfba7c91b0247270486c6df853d61f07d69126c6 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 27 Aug 2025 12:17:20 +0200 Subject: [PATCH 12/20] Components: Fix Tab font size when used outside WP (#71346) --- packages/components/CHANGELOG.md | 7 ++++++- packages/components/src/tabs/styles.ts | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 41b3b7b6399069..d42b65d7aef13b 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,9 +2,14 @@ ## Unreleased +### Bug Fixes + +- `Tabs`: Ensure font size inheritance for tab buttons in all contexts ([#71346](https://github.com/WordPress/gutenberg/pull/71346)). + + ## 30.2.0 (2025-08-20) -### Bug fixes +### Bug Fixes - `MenuItem`: make accessible when disabled ([#71251](https://github.com/WordPress/gutenberg/pull/71251)). diff --git a/packages/components/src/tabs/styles.ts b/packages/components/src/tabs/styles.ts index 717316227ddb3c..7e0c2534942a41 100644 --- a/packages/components/src/tabs/styles.ts +++ b/packages/components/src/tabs/styles.ts @@ -7,7 +7,7 @@ import * as Ariakit from '@ariakit/react'; /** * Internal dependencies */ -import { COLORS, CONFIG } from '../utils'; +import { COLORS, CONFIG, font } from '../utils'; import { space } from '../utils/space'; import Icon from '../icon'; @@ -163,6 +163,7 @@ export const Tab = styled( Ariakit.Tab )` cursor: pointer; line-height: 1.2; // Characters in some languages (e.g. Japanese) may have a native higher line-height. font-weight: 400; + font-size: ${ font( 'default.fontSize' ) }; color: ${ COLORS.theme.foreground }; &[aria-disabled='true'] { From e5d85afaa24fe527d16a319404c15a55be157f51 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 27 Aug 2025 13:35:30 +0200 Subject: [PATCH 13/20] Scripts: do not access `window` global (#71348) --- packages/scripts/scripts/test-playwright.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/scripts/scripts/test-playwright.js b/packages/scripts/scripts/test-playwright.js index c0f1ac6783633d..d48623c0e9c6be 100644 --- a/packages/scripts/scripts/test-playwright.js +++ b/packages/scripts/scripts/test-playwright.js @@ -42,7 +42,8 @@ try { // First, try to load the package installed from among the optional peerDependencies. loadConfig = require( '@wordpress/env/lib/config/load-config' ); } catch ( error ) { - window.console.log( + // eslint-disable-next-line no-console + console.log( 'Notice: Could not find @wordpress/env package. Using WP_BASE_URL environment variable or else the default http://localhost:8889 URL for tests.' ); } From 06ce64d529ba10548945984989dc7125a9a939ea Mon Sep 17 00:00:00 2001 From: Karol Manijak <20098064+kmanijak@users.noreply.github.com> Date: Wed, 27 Aug 2025 15:06:29 +0200 Subject: [PATCH 14/20] Editor: Update types of getCurrentPostId (#71347) Co-authored-by: kmanijak Co-authored-by: im3dabasia Co-authored-by: Mamaduka --- docs/reference-guides/data/data-core-editor.md | 2 +- packages/editor/src/store/selectors.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index 43fba216c4ce08..3191956f8d99b0 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -218,7 +218,7 @@ _Parameters_ _Returns_ -- `?number`: ID of current post. +- `?(number|string)`: The current post ID (number) or template slug (string). ### getCurrentPostLastRevisionId diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 7f2f22d2978b4b..435eab23f0b878 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -195,7 +195,7 @@ export function getCurrentPostType( state ) { * * @param {Object} state Global application state. * - * @return {?number} ID of current post. + * @return {?(number|string)} The current post ID (number) or template slug (string). */ export function getCurrentPostId( state ) { return state.postId; From 775f0cea9ea3b0a5ea8a39982475f82a9a6ddae0 Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:24:19 +0300 Subject: [PATCH 15/20] Types: Update to be compatible with React 19 (#71374) * Fix deprecated types: React.ReactChild * Fix deprecated types: React.ReactNodeArray * Fix deprecated types: React.ReactFragment * Fix deprecated types: React.ReactText Co-authored-by: tyxla Co-authored-by: Mamaduka --- packages/components/src/context/context-connect.ts | 4 ++-- packages/components/src/tools-panel/stories/index.story.tsx | 6 +++--- packages/components/src/utils/font-size.ts | 4 ++-- packages/components/src/utils/get-valid-children.ts | 6 ++++-- packages/element/src/serialize.js | 6 +++--- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/components/src/context/context-connect.ts b/packages/components/src/context/context-connect.ts index bbab68222abb9a..b7ec73f433045e 100644 --- a/packages/components/src/context/context-connect.ts +++ b/packages/components/src/context/context-connect.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ForwardedRef, ReactChild, ReactNode } from 'react'; +import type { ForwardedRef, ReactElement, ReactNode } from 'react'; /** * WordPress dependencies @@ -114,7 +114,7 @@ function _contextConnect< * @return The connected namespaces. */ export function getConnectNamespace( - Component: ReactChild | undefined | {} + Component: ReactElement | number | string | undefined | {} ): string[] { if ( ! Component ) { return []; diff --git a/packages/components/src/tools-panel/stories/index.story.tsx b/packages/components/src/tools-panel/stories/index.story.tsx index 811afaa3473fd0..611cae70fb2313 100644 --- a/packages/components/src/tools-panel/stories/index.story.tsx +++ b/packages/components/src/tools-panel/stories/index.story.tsx @@ -54,7 +54,7 @@ export const Default: StoryFn< typeof ToolsPanel > = ( { const [ height, setHeight ] = useState< string | undefined >(); const [ minHeight, setMinHeight ] = useState< string | undefined >(); const [ width, setWidth ] = useState< string | undefined >(); - const [ scale, setScale ] = useState< React.ReactText | undefined >(); + const [ scale, setScale ] = useState< number | string | undefined >(); const resetAll: typeof resetAllProp = ( filters ) => { setHeight( undefined ); @@ -414,7 +414,7 @@ export const WithConditionalDefaultControl: StoryFn< typeof ToolsPanel > = ( { } ) => { const [ attributes, setAttributes ] = useState< { height?: string; - scale?: React.ReactText; + scale?: number | string; } >( {} ); const { height, scale } = attributes; @@ -512,7 +512,7 @@ export const WithConditionallyRenderedControl: StoryFn< > = ( { resetAll: resetAllProp, panelId, ...props } ) => { const [ attributes, setAttributes ] = useState< { height?: string; - scale?: React.ReactText; + scale?: number | string; } >( {} ); const { height, scale } = attributes; diff --git a/packages/components/src/utils/font-size.ts b/packages/components/src/utils/font-size.ts index c9a3b044b55a22..c21b50f7452dd2 100644 --- a/packages/components/src/utils/font-size.ts +++ b/packages/components/src/utils/font-size.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { CSSProperties, ReactText } from 'react'; +import type { CSSProperties } from 'react'; /** * Internal dependencies @@ -61,7 +61,7 @@ export function getFontSize( return `calc(${ ratio } * ${ CONFIG.fontSize })`; } -export function getHeadingFontSize( size: ReactText = 3 ): string { +export function getHeadingFontSize( size: number | string = 3 ): string { if ( ! HEADING_FONT_SIZES.includes( size as HeadingSize ) ) { return getFontSize( size ); } diff --git a/packages/components/src/utils/get-valid-children.ts b/packages/components/src/utils/get-valid-children.ts index 07d4aa038e8a27..30d32dbafd4ab1 100644 --- a/packages/components/src/utils/get-valid-children.ts +++ b/packages/components/src/utils/get-valid-children.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import type { ReactNode, ReactChild, ReactFragment, ReactPortal } from 'react'; +import type { ReactNode, ReactElement, ReactPortal } from 'react'; /** * WordPress dependencies @@ -17,7 +17,9 @@ import { Children, isValidElement } from '@wordpress/element'; */ export function getValidChildren( children: ReactNode -): Array< ReactChild | ReactFragment | ReactPortal > { +): Array< + ReactElement | number | string | Iterable< ReactNode > | ReactPortal +> { if ( typeof children === 'string' ) { return [ children ]; } diff --git a/packages/element/src/serialize.js b/packages/element/src/serialize.js index d16f954d325ff0..c967e1fa565f35 100644 --- a/packages/element/src/serialize.js +++ b/packages/element/src/serialize.js @@ -718,9 +718,9 @@ export function renderComponent( /** * Serializes an array of children to string. * - * @param {import('react').ReactNodeArray} children Children to serialize. - * @param {Object} [context] Context object. - * @param {Object} [legacyContext] Legacy context object. + * @param {ReadonlyArray} children Children to serialize. + * @param {Object} [context] Context object. + * @param {Object} [legacyContext] Legacy context object. * * @return {string} Serialized children. */ From 82768d0e018b44007e3e3a25636c13375b33e7e7 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 28 Aug 2025 21:13:18 -0400 Subject: [PATCH 16/20] Avoid using `*-latest` for runner images (#71201) * Avoid using `*-latest` for runner images While using the `ubuntu-latest`, `macos-latest`, and `windows-latest` runner image tags is convenient, it has proven to be problematic in a number of instances as the runners are slowly updated (see Core-62808 and Core-62843). Additionally, GitHub uses warnings on workflow runs using the `-latest` tags when there are upcoming changes to the images that the alias points to. There is no way to acknowledge and dismiss these notices, and they can be quite noisy and alarming. This switches all workflows to using specific version tags representing the latest non-preview versions, which currently are as follows: - `ubuntu-24.04` - `windows-2025` - `macos-15` * Only reference OS type in job name This makes helps avoid needing to change the required check every time the runner is updated. * Use `matrix`, not `inputs` as the context --------- Co-authored-by: desrosj Co-authored-by: johnbillion --- .github/workflows/build-plugin-zip.yml | 12 ++++++------ .github/workflows/bundle-size.yml | 2 +- .github/workflows/check-backport-changelog.yml | 2 +- .../workflows/check-components-changelog.yml | 2 +- .../workflows/check-dataviews-changelog.yml | 2 +- .github/workflows/cherry-pick-wp-release.yml | 2 +- .github/workflows/create-block.yml | 4 ++-- .github/workflows/end2end-test.yml | 6 +++--- .github/workflows/enforce-pr-labels.yml | 2 +- .../workflows/gradle-wrapper-validation.yml | 2 +- .github/workflows/performance.yml | 2 +- .github/workflows/props-bot.yml | 2 +- .github/workflows/publish-npm-packages.yml | 2 +- .github/workflows/pull-request-automation.yml | 2 +- .github/workflows/reusable-workflow-lint.yml | 2 +- .github/workflows/stale-issue-gardening.yml | 2 +- .github/workflows/static-checks.yml | 2 +- .github/workflows/storybook-check.yml | 2 +- .github/workflows/storybook-pages.yml | 2 +- .../workflows/sync-assets-to-plugin-repo.yml | 2 +- .github/workflows/sync-backport-changelog.yml | 2 +- .github/workflows/unit-test.yml | 18 +++++++++--------- .../upload-release-to-plugin-repo.yml | 10 +++++----- 23 files changed, 43 insertions(+), 43 deletions(-) diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml index 358be382815f87..ba7111f9578700 100644 --- a/.github/workflows/build-plugin-zip.yml +++ b/.github/workflows/build-plugin-zip.yml @@ -27,7 +27,7 @@ permissions: {} jobs: compute-stable-branches: name: Compute current and next stable release branches - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read if: ${{ github.event_name == 'workflow_dispatch' }} @@ -55,7 +55,7 @@ jobs: bump-version: name: Bump version - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: write needs: compute-stable-branches @@ -180,7 +180,7 @@ jobs: build: name: Build Release Artifact - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read needs: bump-version @@ -244,7 +244,7 @@ jobs: revert-version-bump: name: Revert version bump if build failed needs: [bump-version, build] - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: write if: | @@ -300,7 +300,7 @@ jobs: create-release: name: Create Release Draft and Attach Asset needs: [bump-version, build] - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: write @@ -347,7 +347,7 @@ jobs: npm-publish: name: Publish WordPress packages to npm - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read environment: WordPress packages diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml index 063cf996529038..f6e5c2d50596d6 100644 --- a/.github/workflows/bundle-size.yml +++ b/.github/workflows/bundle-size.yml @@ -38,7 +38,7 @@ permissions: {} jobs: build: name: Check - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read pull-requests: write diff --git a/.github/workflows/check-backport-changelog.yml b/.github/workflows/check-backport-changelog.yml index 85e36fb8e66713..c3acd6d1a40755 100644 --- a/.github/workflows/check-backport-changelog.yml +++ b/.github/workflows/check-backport-changelog.yml @@ -26,7 +26,7 @@ permissions: {} jobs: check: name: Check for a Core backport changelog entry - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read if: ${{ !contains(github.event.pull_request.labels.*.name, 'No Core Sync Required') && !contains(github.event.pull_request.labels.*.name, 'Backport from WordPress Core') }} diff --git a/.github/workflows/check-components-changelog.yml b/.github/workflows/check-components-changelog.yml index 7e51425c0be5f6..4f1621d12f5ee5 100644 --- a/.github/workflows/check-components-changelog.yml +++ b/.github/workflows/check-components-changelog.yml @@ -22,7 +22,7 @@ permissions: {} jobs: check: name: Check CHANGELOG diff - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read steps: diff --git a/.github/workflows/check-dataviews-changelog.yml b/.github/workflows/check-dataviews-changelog.yml index b454daaaf1ecc7..98c14b91e14f73 100644 --- a/.github/workflows/check-dataviews-changelog.yml +++ b/.github/workflows/check-dataviews-changelog.yml @@ -22,7 +22,7 @@ permissions: {} jobs: check: name: Check CHANGELOG diff - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read steps: diff --git a/.github/workflows/cherry-pick-wp-release.yml b/.github/workflows/cherry-pick-wp-release.yml index a703332b9edf68..ada27216c5f8af 100644 --- a/.github/workflows/cherry-pick-wp-release.yml +++ b/.github/workflows/cherry-pick-wp-release.yml @@ -22,7 +22,7 @@ permissions: {} jobs: cherry-pick: - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: write issues: write diff --git a/.github/workflows/create-block.yml b/.github/workflows/create-block.yml index 6cdfd7754bd611..b2a62a5bd9aa6a 100644 --- a/.github/workflows/create-block.yml +++ b/.github/workflows/create-block.yml @@ -18,7 +18,7 @@ permissions: {} jobs: checks: - name: Checks w/Node.js ${{ matrix.node }} on ${{ matrix.os }} + name: Checks w/Node.js ${{ matrix.node }} on ${{ contains( matrix.os, 'macos-' ) && 'MacOS' || contains( matrix.os, 'windows-' ) && 'Windows' || 'Linux' }} runs-on: ${{ matrix.os }} permissions: contents: read @@ -27,7 +27,7 @@ jobs: fail-fast: false matrix: node: ['20', '22', '24'] - os: ['macos-latest', 'ubuntu-latest', 'windows-latest'] + os: ['macos-15', 'ubuntu-24.04', 'windows-2025'] steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index cdfce5d0c5ee0f..c7816a5f0839de 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -22,7 +22,7 @@ permissions: {} jobs: e2e-playwright: name: Playwright - ${{ matrix.part }} - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} @@ -80,7 +80,7 @@ jobs: name: Merge Artifacts if: ${{ !cancelled() }} needs: [e2e-playwright] - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: {} outputs: has-flaky-test-report: ${{ !!steps.merge-flaky-tests-reports.outputs.artifact-id }} @@ -108,7 +108,7 @@ jobs: name: Report to GitHub needs: [merge-artifacts] if: ${{ needs.merge-artifacts.outputs.has-flaky-test-report == 'true' }} - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read issues: write diff --git a/.github/workflows/enforce-pr-labels.yml b/.github/workflows/enforce-pr-labels.yml index 9c81710b6d3e22..e018ac9c9d719c 100644 --- a/.github/workflows/enforce-pr-labels.yml +++ b/.github/workflows/enforce-pr-labels.yml @@ -9,7 +9,7 @@ permissions: {} jobs: type-related-labels: - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: pull-requests: write steps: diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index c4a289645104ec..d9cdbdc0bff1bb 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -8,7 +8,7 @@ permissions: {} jobs: validation: name: 'Validation' - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read steps: diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 6a7be0419130ab..d97e0c45b5553d 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -31,7 +31,7 @@ jobs: performance: timeout-minutes: 60 name: Run performance tests - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read if: ${{ github.repository == 'WordPress/gutenberg' }} diff --git a/.github/workflows/props-bot.yml b/.github/workflows/props-bot.yml index 574aea9f9ae948..b91c4cc2e3c606 100644 --- a/.github/workflows/props-bot.yml +++ b/.github/workflows/props-bot.yml @@ -48,7 +48,7 @@ jobs: # - Removes the props-bot label, if necessary. props-bot: name: Generate a list of props - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: # The action needs permission `write` permission for PRs in order to add a comment. pull-requests: write diff --git a/.github/workflows/publish-npm-packages.yml b/.github/workflows/publish-npm-packages.yml index 5e58eccdeee2ee..5a3cc9272682fb 100644 --- a/.github/workflows/publish-npm-packages.yml +++ b/.github/workflows/publish-npm-packages.yml @@ -31,7 +31,7 @@ permissions: {} jobs: release: name: Release - ${{ github.event.inputs.release_type }} - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read environment: WordPress packages diff --git a/.github/workflows/pull-request-automation.yml b/.github/workflows/pull-request-automation.yml index 1c6ecf90469201..cf9f5891a51dc2 100644 --- a/.github/workflows/pull-request-automation.yml +++ b/.github/workflows/pull-request-automation.yml @@ -10,7 +10,7 @@ permissions: {} jobs: pull-request-automation: - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read issues: write diff --git a/.github/workflows/reusable-workflow-lint.yml b/.github/workflows/reusable-workflow-lint.yml index fc2e7752211c19..b9ff2d1e0557b5 100644 --- a/.github/workflows/reusable-workflow-lint.yml +++ b/.github/workflows/reusable-workflow-lint.yml @@ -17,7 +17,7 @@ jobs: # - Runs actionlint. actionlint: name: Run actionlint - runs-on: ubuntu-24.04 + runs-on: 'ubuntu-24.04' permissions: contents: read timeout-minutes: 5 diff --git a/.github/workflows/stale-issue-gardening.yml b/.github/workflows/stale-issue-gardening.yml index 5caeee49198dba..60e7bbc0c624c4 100644 --- a/.github/workflows/stale-issue-gardening.yml +++ b/.github/workflows/stale-issue-gardening.yml @@ -11,7 +11,7 @@ permissions: {} jobs: issue-gardening: name: ${{ matrix.name }} - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: issues: write pull-requests: write diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml index 86cbacd6d960a9..07a979989f62d2 100644 --- a/.github/workflows/static-checks.yml +++ b/.github/workflows/static-checks.yml @@ -22,7 +22,7 @@ permissions: {} jobs: check: name: All - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} diff --git a/.github/workflows/storybook-check.yml b/.github/workflows/storybook-check.yml index aa06f847851c68..60f6c5b93e558e 100644 --- a/.github/workflows/storybook-check.yml +++ b/.github/workflows/storybook-check.yml @@ -15,7 +15,7 @@ permissions: {} jobs: check: - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} diff --git a/.github/workflows/storybook-pages.yml b/.github/workflows/storybook-pages.yml index 9defe8d0ef89d5..ddf459b784b78d 100644 --- a/.github/workflows/storybook-pages.yml +++ b/.github/workflows/storybook-pages.yml @@ -11,7 +11,7 @@ permissions: {} jobs: deploy: - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: write if: ${{ github.repository == 'WordPress/gutenberg' }} diff --git a/.github/workflows/sync-assets-to-plugin-repo.yml b/.github/workflows/sync-assets-to-plugin-repo.yml index 745bf37c208bed..d4107cf6c7322b 100644 --- a/.github/workflows/sync-assets-to-plugin-repo.yml +++ b/.github/workflows/sync-assets-to-plugin-repo.yml @@ -14,7 +14,7 @@ permissions: {} jobs: sync-assets: name: Sync assets to WordPress.org plugin repo - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read environment: wp.org plugin diff --git a/.github/workflows/sync-backport-changelog.yml b/.github/workflows/sync-backport-changelog.yml index a42a2f1e8be7be..a9ff869f562f39 100644 --- a/.github/workflows/sync-backport-changelog.yml +++ b/.github/workflows/sync-backport-changelog.yml @@ -14,7 +14,7 @@ permissions: {} jobs: sync-backport-changelog: name: Sync Core Backport Issue - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read issues: write diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 76dc35cf89d188..1054f5db3f2043 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -26,7 +26,7 @@ permissions: {} jobs: unit-js: name: JavaScript (Node.js ${{ matrix.node }}) ${{ matrix.shard }} - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} @@ -71,7 +71,7 @@ jobs: unit-js-date: name: JavaScript Date Tests (Node.js ${{ matrix.node }}) - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} @@ -109,7 +109,7 @@ jobs: compute-previous-wordpress-version: name: Compute previous WordPress version - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: {} outputs: previous-wordpress-version: ${{ steps.get-previous-wordpress-version.outputs.previous-wordpress-version }} @@ -136,7 +136,7 @@ jobs: build-assets: name: Build JavaScript assets for PHP unit tests - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read steps: @@ -160,9 +160,9 @@ jobs: ./build-module/ test-php: - name: PHP ${{ matrix.php }}${{ matrix.multisite && ' multisite' || '' }}${{ matrix.wordpress != '' && format( ' (WP {0}) ', matrix.wordpress ) || '' }} on ubuntu-latest + name: PHP ${{ matrix.php }}${{ matrix.multisite && ' multisite' || '' }}${{ matrix.wordpress != '' && format( ' (WP {0}) ', matrix.wordpress ) || '' }} on 'ubuntu-24.04' needs: [compute-previous-wordpress-version, build-assets] - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read timeout-minutes: 20 @@ -297,7 +297,7 @@ jobs: phpcs: name: PHP coding standards - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read timeout-minutes: 20 @@ -353,7 +353,7 @@ jobs: # This job is deprecated but be present for compatibility reasons. unit-php: name: PHP - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: {} needs: [test-php, phpcs] if: ${{ always() }} @@ -372,7 +372,7 @@ jobs: mobile-unit-js: name: Mobile - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: read # React 19 breaks these tests, and upgrading React Native to a compatible version is paused. diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml index 6590001c27a2a4..4303d001aa9784 100644 --- a/.github/workflows/upload-release-to-plugin-repo.yml +++ b/.github/workflows/upload-release-to-plugin-repo.yml @@ -11,7 +11,7 @@ permissions: {} jobs: compute-should-update-trunk: name: Decide if trunk or tag - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: {} # Skip this job if the release is a release candidate. This will in turn skip # the upload jobs, which are only relevant for non-RC releases. @@ -69,7 +69,7 @@ jobs: get-release-branch: name: Get release branch name - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: {} outputs: release_branch: ${{ steps.get_release_branch.outputs.release_branch }} @@ -86,7 +86,7 @@ jobs: update-changelog: name: Update Changelog on ${{ matrix.branch }} branch - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: contents: write if: | @@ -165,7 +165,7 @@ jobs: upload: name: Publish as trunk (and tag) - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: {} environment: wp.org plugin needs: [compute-should-update-trunk, update-changelog] @@ -225,7 +225,7 @@ jobs: upload-tag: name: Publish as tag - runs-on: ubuntu-latest + runs-on: 'ubuntu-24.04' permissions: {} environment: wp.org plugin needs: [compute-should-update-trunk, update-changelog] From 52db0dfce7f62d0b56f5b4548456180779a363d1 Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <359867+desrosj@users.noreply.github.com> Date: Thu, 28 Aug 2025 22:27:07 -0400 Subject: [PATCH 17/20] Fix PHP unit testing jobs to not include version (#71396) Co-authored-by: desrosj Co-authored-by: peterwilsoncc --- .github/workflows/unit-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 1054f5db3f2043..450b77209a92ad 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -160,7 +160,7 @@ jobs: ./build-module/ test-php: - name: PHP ${{ matrix.php }}${{ matrix.multisite && ' multisite' || '' }}${{ matrix.wordpress != '' && format( ' (WP {0}) ', matrix.wordpress ) || '' }} on 'ubuntu-24.04' + name: PHP ${{ matrix.php }}${{ matrix.multisite && ' multisite' || '' }}${{ matrix.wordpress != '' && format( ' (WP {0}) ', matrix.wordpress ) || '' }} on Linux needs: [compute-previous-wordpress-version, build-assets] runs-on: 'ubuntu-24.04' permissions: From 169eb065ba87ed9378b21feb647f74236cfe0b5b Mon Sep 17 00:00:00 2001 From: im3dabasia Date: Fri, 8 Aug 2025 15:46:09 +0530 Subject: [PATCH 18/20] feat: Add Context displaynames in React dev tools for improved dev exp --- packages/block-editor/src/components/block-context/index.js | 1 + packages/block-editor/src/components/block-edit/context.js | 2 ++ packages/block-editor/src/components/block-list/index.js | 2 ++ packages/block-editor/src/components/block-list/layout.js | 1 + .../src/components/block-list/private-block-context.js | 1 + .../block-editor/src/components/block-popover/inbetween.js | 1 + .../src/components/block-toolbar/block-name-context.js | 1 + .../src/components/block-tools/insertion-point.js | 1 + .../block-editor/src/components/global-styles/context.js | 1 + .../block-editor/src/components/image-editor/context.js | 1 + .../block-editor/src/components/inserter-listbox/context.js | 1 + packages/block-editor/src/components/list-view/context.js | 1 + .../src/components/provider/block-refs-provider.js | 1 + .../block-editor/src/components/recursion-provider/index.js | 1 + packages/block-editor/src/components/rich-text/index.js | 3 +++ packages/components/src/card/context.ts | 2 ++ .../circular-option-picker-context.tsx | 1 + packages/components/src/composite/context.tsx | 1 + packages/components/src/context/context-system-provider.js | 2 ++ .../src/custom-select-control-v2/custom-select.tsx | 1 + packages/components/src/disabled/index.tsx | 2 ++ packages/components/src/item-group/context.ts | 1 + packages/components/src/menu/context.tsx | 1 + packages/components/src/modal/index.tsx | 1 + packages/components/src/navigation/context.tsx | 3 +++ packages/components/src/navigation/group/context.tsx | 1 + packages/components/src/navigation/menu/context.tsx | 2 ++ packages/components/src/navigator/context.ts | 1 + packages/components/src/popover/index.tsx | 1 + packages/components/src/radio-group/context.tsx | 1 + .../src/slot-fill/bubbles-virtually/slot-fill-context.ts | 1 + packages/components/src/slot-fill/context.ts | 1 + packages/components/src/tabs/context.ts | 1 + packages/components/src/toggle-group-control/context.ts | 2 ++ packages/components/src/toolbar/toolbar-context/index.ts | 1 + packages/components/src/tools-panel/context.ts | 1 + packages/components/src/tooltip/index.tsx | 1 + .../components/src/tree-grid/roving-tab-index-context.ts | 2 ++ packages/compose/src/hooks/use-viewport-match/index.js | 1 + packages/core-data/src/entity-context.js | 1 + .../customize-widgets/src/components/focus-control/index.js | 1 + .../src/components/sidebar-controls/index.js | 1 + packages/data/src/components/async-mode-provider/context.js | 1 + packages/data/src/components/registry-provider/context.js | 1 + .../dataviews/src/components/dataform-context/index.tsx | 1 + .../dataviews/src/components/dataviews-context/index.ts | 2 ++ .../components/global-styles/font-library-modal/context.js | 1 + packages/edit-site/src/components/sidebar/index.js | 2 ++ packages/element/src/serialize.js | 6 +++++- packages/interactivity/src/hooks.tsx | 1 + packages/keyboard-shortcuts/src/context.js | 2 ++ packages/plugins/src/components/plugin-context/index.tsx | 1 + packages/react-i18n/src/index.tsx | 1 + packages/router/src/router.tsx | 3 +++ 54 files changed, 75 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-context/index.js b/packages/block-editor/src/components/block-context/index.js index 67ca1960e71fd4..329aa3f843bfcf 100644 --- a/packages/block-editor/src/components/block-context/index.js +++ b/packages/block-editor/src/components/block-context/index.js @@ -15,6 +15,7 @@ import { createContext, useContext, useMemo } from '@wordpress/element'; /** @type {import('react').Context>} */ const Context = createContext( {} ); +Context.displayName = 'BlockContext'; /** * Component which merges passed value with current consumed block context. diff --git a/packages/block-editor/src/components/block-edit/context.js b/packages/block-editor/src/components/block-edit/context.js index 6bb1861abd0a2b..e1a1c1646eb53a 100644 --- a/packages/block-editor/src/components/block-edit/context.js +++ b/packages/block-editor/src/components/block-edit/context.js @@ -15,6 +15,8 @@ export const DEFAULT_BLOCK_EDIT_CONTEXT = { }; const Context = createContext( DEFAULT_BLOCK_EDIT_CONTEXT ); +Context.displayName = 'BlockEditContext'; + const { Provider } = Context; export { Provider as BlockEditContextProvider }; diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index 61c5cc277c1355..2a632dca1b1531 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -40,6 +40,8 @@ import { ZoomOutSeparator } from './zoom-out-separator'; import { unlock } from '../../lock-unlock'; export const IntersectionObserver = createContext(); +IntersectionObserver.displayName = 'IntersectionObserverContext'; + const pendingBlockVisibilityUpdatesPerRegistry = new WeakMap(); function Root( { className, ...settings } ) { diff --git a/packages/block-editor/src/components/block-list/layout.js b/packages/block-editor/src/components/block-list/layout.js index a7de26cc5a0fa8..b0ca07b6e0b622 100644 --- a/packages/block-editor/src/components/block-list/layout.js +++ b/packages/block-editor/src/components/block-list/layout.js @@ -12,6 +12,7 @@ import { useSettings } from '../use-settings'; export const defaultLayout = { type: 'default' }; const Layout = createContext( defaultLayout ); +Layout.displayName = 'BlockLayoutContext'; /** * Allows to define the layout. diff --git a/packages/block-editor/src/components/block-list/private-block-context.js b/packages/block-editor/src/components/block-list/private-block-context.js index 4ff3caa43a7241..b1da31376e9cb6 100644 --- a/packages/block-editor/src/components/block-list/private-block-context.js +++ b/packages/block-editor/src/components/block-list/private-block-context.js @@ -4,3 +4,4 @@ import { createContext } from '@wordpress/element'; export const PrivateBlockContext = createContext( {} ); +PrivateBlockContext.displayName = 'PrivateBlockContext'; diff --git a/packages/block-editor/src/components/block-popover/inbetween.js b/packages/block-editor/src/components/block-popover/inbetween.js index 1d7c1766732409..8089ffc328d180 100644 --- a/packages/block-editor/src/components/block-popover/inbetween.js +++ b/packages/block-editor/src/components/block-popover/inbetween.js @@ -26,6 +26,7 @@ import usePopoverScroll from './use-popover-scroll'; const MAX_POPOVER_RECOMPUTE_COUNTER = Number.MAX_SAFE_INTEGER; export const InsertionPointOpenRef = createContext(); +InsertionPointOpenRef.displayName = 'InsertionPointOpenRefContext'; function BlockPopoverInbetween( { previousClientId, diff --git a/packages/block-editor/src/components/block-toolbar/block-name-context.js b/packages/block-editor/src/components/block-toolbar/block-name-context.js index 4537f6f861559d..ec7e8512f5cab8 100644 --- a/packages/block-editor/src/components/block-toolbar/block-name-context.js +++ b/packages/block-editor/src/components/block-toolbar/block-name-context.js @@ -4,5 +4,6 @@ import { createContext } from '@wordpress/element'; const __unstableBlockNameContext = createContext( '' ); +__unstableBlockNameContext.displayName = '__unstableBlockNameContext'; export default __unstableBlockNameContext; diff --git a/packages/block-editor/src/components/block-tools/insertion-point.js b/packages/block-editor/src/components/block-tools/insertion-point.js index 6c54993b23bcaa..c6547a4339da50 100644 --- a/packages/block-editor/src/components/block-tools/insertion-point.js +++ b/packages/block-editor/src/components/block-tools/insertion-point.js @@ -21,6 +21,7 @@ import BlockDropZonePopover from '../block-popover/drop-zone'; import { unlock } from '../../lock-unlock'; export const InsertionPointOpenRef = createContext(); +InsertionPointOpenRef.displayName = 'InsertionPointOpenRefContext'; function InbetweenInsertionPointPopover( { __unstablePopoverSlot, diff --git a/packages/block-editor/src/components/global-styles/context.js b/packages/block-editor/src/components/global-styles/context.js index 630b5a2b9059d5..0f5ebeddb6fe37 100644 --- a/packages/block-editor/src/components/global-styles/context.js +++ b/packages/block-editor/src/components/global-styles/context.js @@ -13,3 +13,4 @@ export const DEFAULT_GLOBAL_STYLES_CONTEXT = { export const GlobalStylesContext = createContext( DEFAULT_GLOBAL_STYLES_CONTEXT ); +GlobalStylesContext.displayName = 'GlobalStylesContext'; diff --git a/packages/block-editor/src/components/image-editor/context.js b/packages/block-editor/src/components/image-editor/context.js index fc81c349d8d014..bd466a571c500f 100644 --- a/packages/block-editor/src/components/image-editor/context.js +++ b/packages/block-editor/src/components/image-editor/context.js @@ -10,6 +10,7 @@ import useSaveImage from './use-save-image'; import useTransformImage from './use-transform-image'; const ImageEditingContext = createContext( {} ); +ImageEditingContext.displayName = 'ImageEditingContext'; export const useImageEditingContext = () => useContext( ImageEditingContext ); diff --git a/packages/block-editor/src/components/inserter-listbox/context.js b/packages/block-editor/src/components/inserter-listbox/context.js index aaaac34f05f61f..664bdcf190e53b 100644 --- a/packages/block-editor/src/components/inserter-listbox/context.js +++ b/packages/block-editor/src/components/inserter-listbox/context.js @@ -4,5 +4,6 @@ import { createContext } from '@wordpress/element'; const InserterListboxContext = createContext(); +InserterListboxContext.displayName = 'InserterListboxContext'; export default InserterListboxContext; diff --git a/packages/block-editor/src/components/list-view/context.js b/packages/block-editor/src/components/list-view/context.js index c837dce9ca23fd..340411060e45c3 100644 --- a/packages/block-editor/src/components/list-view/context.js +++ b/packages/block-editor/src/components/list-view/context.js @@ -4,5 +4,6 @@ import { createContext, useContext } from '@wordpress/element'; export const ListViewContext = createContext( {} ); +ListViewContext.displayName = 'ListViewContext'; export const useListViewContext = () => useContext( ListViewContext ); diff --git a/packages/block-editor/src/components/provider/block-refs-provider.js b/packages/block-editor/src/components/provider/block-refs-provider.js index e54680356efda2..0842a5b08d8743 100644 --- a/packages/block-editor/src/components/provider/block-refs-provider.js +++ b/packages/block-editor/src/components/provider/block-refs-provider.js @@ -5,6 +5,7 @@ import { createContext, useMemo } from '@wordpress/element'; import { observableMap } from '@wordpress/compose'; export const BlockRefs = createContext( { refsMap: observableMap() } ); +BlockRefs.displayName = 'BlockRefsContext'; export function BlockRefsProvider( { children } ) { const value = useMemo( () => ( { refsMap: observableMap() } ), [] ); diff --git a/packages/block-editor/src/components/recursion-provider/index.js b/packages/block-editor/src/components/recursion-provider/index.js index 98a2b5c546f2bf..e3ca1416e6b935 100644 --- a/packages/block-editor/src/components/recursion-provider/index.js +++ b/packages/block-editor/src/components/recursion-provider/index.js @@ -10,6 +10,7 @@ import deprecated from '@wordpress/deprecated'; import { useBlockEditContext } from '../block-edit/context'; const RenderedRefsContext = createContext( {} ); +RenderedRefsContext.displayName = 'RenderedRefsContext'; /** * Immutably adds an unique identifier to a set scoped for a given block type. diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 310b2a0ce63264..ac33f7f98d2968 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -43,7 +43,10 @@ import { canBindBlock } from '../../utils/block-bindings'; import BlockContext from '../block-context'; export const keyboardShortcutContext = createContext(); +keyboardShortcutContext.displayName = 'keyboardShortcutContext'; + export const inputEventContext = createContext(); +inputEventContext.displayName = 'inputEventContext'; const instanceIdKey = Symbol( 'instanceId' ); diff --git a/packages/components/src/card/context.ts b/packages/components/src/card/context.ts index 2bcc31eaf65d4e..5ad31be70c04c7 100644 --- a/packages/components/src/card/context.ts +++ b/packages/components/src/card/context.ts @@ -4,4 +4,6 @@ import { createContext, useContext } from '@wordpress/element'; export const CardContext = createContext( {} ); +CardContext.displayName = 'CardContext'; + export const useCardContext = () => useContext( CardContext ); diff --git a/packages/components/src/circular-option-picker/circular-option-picker-context.tsx b/packages/components/src/circular-option-picker/circular-option-picker-context.tsx index cafb0b90e3017f..1a41893e8a0f88 100644 --- a/packages/components/src/circular-option-picker/circular-option-picker-context.tsx +++ b/packages/components/src/circular-option-picker/circular-option-picker-context.tsx @@ -10,3 +10,4 @@ import type { CircularOptionPickerContextProps } from './types'; export const CircularOptionPickerContext = createContext< CircularOptionPickerContextProps >( {} ); +CircularOptionPickerContext.displayName = 'CircularOptionPickerContext'; diff --git a/packages/components/src/composite/context.tsx b/packages/components/src/composite/context.tsx index 4a8b22a712b031..32f638a66162ce 100644 --- a/packages/components/src/composite/context.tsx +++ b/packages/components/src/composite/context.tsx @@ -9,5 +9,6 @@ import { createContext, useContext } from '@wordpress/element'; import type { CompositeContextProps } from './types'; export const CompositeContext = createContext< CompositeContextProps >( {} ); +CompositeContext.displayName = 'CompositeContext'; export const useCompositeContext = () => useContext( CompositeContext ); diff --git a/packages/components/src/context/context-system-provider.js b/packages/components/src/context/context-system-provider.js index 741d2d833227fa..7114372e0eb2a5 100644 --- a/packages/components/src/context/context-system-provider.js +++ b/packages/components/src/context/context-system-provider.js @@ -25,6 +25,8 @@ import { useUpdateEffect } from '../utils'; export const ComponentsContext = createContext( /** @type {Record} */ ( {} ) ); +ComponentsContext.displayName = 'ComponentsContext'; + export const useComponentsContext = () => useContext( ComponentsContext ); /** diff --git a/packages/components/src/custom-select-control-v2/custom-select.tsx b/packages/components/src/custom-select-control-v2/custom-select.tsx index 79acfc949a5485..9b4bd0616cbb16 100644 --- a/packages/components/src/custom-select-control-v2/custom-select.tsx +++ b/packages/components/src/custom-select-control-v2/custom-select.tsx @@ -28,6 +28,7 @@ import BaseControl from '../base-control'; export const CustomSelectContext = createContext< CustomSelectContextType >( undefined ); +CustomSelectContext.displayName = 'CustomSelectContext'; function defaultRenderSelectedValue( value: CustomSelectButtonProps[ 'value' ] diff --git a/packages/components/src/disabled/index.tsx b/packages/components/src/disabled/index.tsx index cc55a4d2e6d672..25fd72f1983bf5 100644 --- a/packages/components/src/disabled/index.tsx +++ b/packages/components/src/disabled/index.tsx @@ -12,6 +12,8 @@ import type { WordPressComponentProps } from '../context'; import { useCx } from '../utils'; const Context = createContext< boolean >( false ); +Context.displayName = 'DisabledContext'; + const { Consumer, Provider } = Context; /** diff --git a/packages/components/src/item-group/context.ts b/packages/components/src/item-group/context.ts index 3c27afa64ef30a..cccd9b608eabe1 100644 --- a/packages/components/src/item-group/context.ts +++ b/packages/components/src/item-group/context.ts @@ -11,5 +11,6 @@ import type { ItemGroupContext as Context } from './types'; export const ItemGroupContext = createContext( { size: 'medium', } as Context ); +ItemGroupContext.displayName = 'ItemGroupContext'; export const useItemGroupContext = () => useContext( ItemGroupContext ); diff --git a/packages/components/src/menu/context.tsx b/packages/components/src/menu/context.tsx index fa38f2c75aea61..83ee2e5348610f 100644 --- a/packages/components/src/menu/context.tsx +++ b/packages/components/src/menu/context.tsx @@ -9,3 +9,4 @@ import { createContext } from '@wordpress/element'; import type { ContextProps } from './types'; export const Context = createContext< ContextProps | undefined >( undefined ); +Context.displayName = 'MenuContext'; diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx index 2dec0f65240f9b..fe7a20dc71042d 100644 --- a/packages/components/src/modal/index.tsx +++ b/packages/components/src/modal/index.tsx @@ -44,6 +44,7 @@ type Dismissers = Set< React.RefObject< ModalProps[ 'onRequestClose' ] | undefined > >; const ModalContext = createContext< Dismissers >( new Set() ); +ModalContext.displayName = 'ModalContext'; // Used to track body class names applied while modals are open. const bodyOpenClasses = new Map< string, number >(); diff --git a/packages/components/src/navigation/context.tsx b/packages/components/src/navigation/context.tsx index 3b14faf47d315d..342b2d0c22b5a5 100644 --- a/packages/components/src/navigation/context.tsx +++ b/packages/components/src/navigation/context.tsx @@ -34,4 +34,7 @@ export const NavigationContext = createContext< NavigationContextType >( { isMenuEmpty: defaultIsEmpty, }, } ); + +NavigationContext.displayName = 'NavigationContext'; + export const useNavigationContext = () => useContext( NavigationContext ); diff --git a/packages/components/src/navigation/group/context.tsx b/packages/components/src/navigation/group/context.tsx index 822a0bb067c882..920bf31988f06a 100644 --- a/packages/components/src/navigation/group/context.tsx +++ b/packages/components/src/navigation/group/context.tsx @@ -10,6 +10,7 @@ import type { NavigationGroupContext as NavigationGroupContextType } from '../ty export const NavigationGroupContext = createContext< NavigationGroupContextType >( { group: undefined } ); +NavigationGroupContext.displayName = 'NavigationGroupContext'; export const useNavigationGroupContext = () => useContext( NavigationGroupContext ); diff --git a/packages/components/src/navigation/menu/context.tsx b/packages/components/src/navigation/menu/context.tsx index 53b71e4ccc368f..c21a70da2e16f1 100644 --- a/packages/components/src/navigation/menu/context.tsx +++ b/packages/components/src/navigation/menu/context.tsx @@ -14,5 +14,7 @@ export const NavigationMenuContext = createContext< NavigationMenuContextType >( search: '', } ); +NavigationMenuContext.displayName = 'NavigationMenuContext'; + export const useNavigationMenuContext = () => useContext( NavigationMenuContext ); diff --git a/packages/components/src/navigator/context.ts b/packages/components/src/navigator/context.ts index e195621b03d205..9b7d26eae54c0d 100644 --- a/packages/components/src/navigator/context.ts +++ b/packages/components/src/navigator/context.ts @@ -18,3 +18,4 @@ const initialContextValue: NavigatorContextType = { params: {}, }; export const NavigatorContext = createContext( initialContextValue ); +NavigatorContext.displayName = 'NavigatorContext'; diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index 2dc4015c860919..ac03ae882531d7 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -102,6 +102,7 @@ const ArrowTriangle = () => ( ); const slotNameContext = createContext< string | undefined >( undefined ); +slotNameContext.displayName = '__unstableSlotNameContext'; const fallbackContainerClassname = 'components-popover__fallback-container'; const getPopoverFallbackContainer = () => { diff --git a/packages/components/src/radio-group/context.tsx b/packages/components/src/radio-group/context.tsx index be03d8d3254a4e..88fc4930e2d48f 100644 --- a/packages/components/src/radio-group/context.tsx +++ b/packages/components/src/radio-group/context.tsx @@ -15,3 +15,4 @@ export const RadioGroupContext = createContext< { store: undefined, disabled: undefined, } ); +RadioGroupContext.displayName = 'RadioGroupContext'; diff --git a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-context.ts b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-context.ts index a144a7dc33f464..d012b1d73a0fab 100644 --- a/packages/components/src/slot-fill/bubbles-virtually/slot-fill-context.ts +++ b/packages/components/src/slot-fill/bubbles-virtually/slot-fill-context.ts @@ -29,5 +29,6 @@ const initialContextValue: SlotFillBubblesVirtuallyContext = { }; const SlotFillContext = createContext( initialContextValue ); +SlotFillContext.displayName = 'SlotFillContext'; export default SlotFillContext; diff --git a/packages/components/src/slot-fill/context.ts b/packages/components/src/slot-fill/context.ts index b1f0718180e9eb..73763ab5263cfa 100644 --- a/packages/components/src/slot-fill/context.ts +++ b/packages/components/src/slot-fill/context.ts @@ -19,5 +19,6 @@ const initialValue: BaseSlotFillContext = { updateFill: () => {}, }; export const SlotFillContext = createContext( initialValue ); +SlotFillContext.displayName = 'SlotFillContext'; export default SlotFillContext; diff --git a/packages/components/src/tabs/context.ts b/packages/components/src/tabs/context.ts index cc6184d827138f..1621c5d0e686df 100644 --- a/packages/components/src/tabs/context.ts +++ b/packages/components/src/tabs/context.ts @@ -9,5 +9,6 @@ import { createContext, useContext } from '@wordpress/element'; import type { TabsContextProps } from './types'; export const TabsContext = createContext< TabsContextProps >( undefined ); +TabsContext.displayName = 'TabsContext'; export const useTabsContext = () => useContext( TabsContext ); diff --git a/packages/components/src/toggle-group-control/context.ts b/packages/components/src/toggle-group-control/context.ts index a686cee2d97d64..8a18ee26097c3f 100644 --- a/packages/components/src/toggle-group-control/context.ts +++ b/packages/components/src/toggle-group-control/context.ts @@ -11,6 +11,8 @@ import type { ToggleGroupControlContextProps } from './types'; const ToggleGroupControlContext = createContext( {} as ToggleGroupControlContextProps ); +ToggleGroupControlContext.displayName = 'ToggleGroupControlContext'; + export const useToggleGroupControlContext = () => useContext( ToggleGroupControlContext ); export default ToggleGroupControlContext; diff --git a/packages/components/src/toolbar/toolbar-context/index.ts b/packages/components/src/toolbar/toolbar-context/index.ts index 33d407eac5c180..234cf8414c1d17 100644 --- a/packages/components/src/toolbar/toolbar-context/index.ts +++ b/packages/components/src/toolbar/toolbar-context/index.ts @@ -11,5 +11,6 @@ import { createContext } from '@wordpress/element'; const ToolbarContext = createContext< Ariakit.ToolbarStore | undefined >( undefined ); +ToolbarContext.displayName = 'ToolbarContext'; export default ToolbarContext; diff --git a/packages/components/src/tools-panel/context.ts b/packages/components/src/tools-panel/context.ts index d39c49c04ef203..f2f0899424c5a5 100644 --- a/packages/components/src/tools-panel/context.ts +++ b/packages/components/src/tools-panel/context.ts @@ -22,6 +22,7 @@ export const ToolsPanelContext = createContext< ToolsPanelContextType >( { deregisterResetAllFilter: noop, areAllOptionalControlsHidden: true, } ); +ToolsPanelContext.displayName = 'ToolsPanelContext'; export const useToolsPanelContext = () => useContext< ToolsPanelContextType >( ToolsPanelContext ); diff --git a/packages/components/src/tooltip/index.tsx b/packages/components/src/tooltip/index.tsx index b7184579ceca91..df2c49b65a9bf3 100644 --- a/packages/components/src/tooltip/index.tsx +++ b/packages/components/src/tooltip/index.tsx @@ -30,6 +30,7 @@ import { positionToPlacement } from '../popover/utils'; const TooltipInternalContext = createContext< TooltipInternalContextType >( { isNestedInTooltip: false, } ); +TooltipInternalContext.displayName = 'TooltipInternalContext'; /** * Time over anchor to wait before showing tooltip diff --git a/packages/components/src/tree-grid/roving-tab-index-context.ts b/packages/components/src/tree-grid/roving-tab-index-context.ts index a0ff4ae486df45..9735abbf7faf87 100644 --- a/packages/components/src/tree-grid/roving-tab-index-context.ts +++ b/packages/components/src/tree-grid/roving-tab-index-context.ts @@ -12,6 +12,8 @@ const RovingTabIndexContext = createContext< } | undefined >( undefined ); +RovingTabIndexContext.displayName = 'RovingTabIndexContext'; + export const useRovingTabIndexContext = () => useContext( RovingTabIndexContext ); export const RovingTabIndexProvider = RovingTabIndexContext.Provider; diff --git a/packages/compose/src/hooks/use-viewport-match/index.js b/packages/compose/src/hooks/use-viewport-match/index.js index 4186045f7d08c4..8d0dbcf838da52 100644 --- a/packages/compose/src/hooks/use-viewport-match/index.js +++ b/packages/compose/src/hooks/use-viewport-match/index.js @@ -57,6 +57,7 @@ const OPERATOR_EVALUATORS = { const ViewportMatchWidthContext = createContext( /** @type {null | number} */ ( null ) ); +ViewportMatchWidthContext.displayName = 'ViewportMatchWidthContext'; /** * Returns true if the viewport matches the given query, or false otherwise. diff --git a/packages/core-data/src/entity-context.js b/packages/core-data/src/entity-context.js index 9a2e597527065a..7b3ba0ebcae414 100644 --- a/packages/core-data/src/entity-context.js +++ b/packages/core-data/src/entity-context.js @@ -4,3 +4,4 @@ import { createContext } from '@wordpress/element'; export const EntityContext = createContext( {} ); +EntityContext.displayName = 'EntityContext'; diff --git a/packages/customize-widgets/src/components/focus-control/index.js b/packages/customize-widgets/src/components/focus-control/index.js index 682f3b2a20b662..1103bea3b448aa 100644 --- a/packages/customize-widgets/src/components/focus-control/index.js +++ b/packages/customize-widgets/src/components/focus-control/index.js @@ -16,6 +16,7 @@ import { import { settingIdToWidgetId } from '../../utils'; const FocusControlContext = createContext(); +FocusControlContext.displayName = 'FocusControlContext'; export default function FocusControl( { api, sidebarControls, children } ) { const [ focusedWidgetIdRef, setFocusedWidgetIdRef ] = useState( { diff --git a/packages/customize-widgets/src/components/sidebar-controls/index.js b/packages/customize-widgets/src/components/sidebar-controls/index.js index 3f39b2bb419fa3..e1cdfdc379194c 100644 --- a/packages/customize-widgets/src/components/sidebar-controls/index.js +++ b/packages/customize-widgets/src/components/sidebar-controls/index.js @@ -4,6 +4,7 @@ import { createContext, useMemo, useContext } from '@wordpress/element'; export const SidebarControlsContext = createContext(); +SidebarControlsContext.displayName = 'SidebarControlsContext'; export default function SidebarControls( { sidebarControls, diff --git a/packages/data/src/components/async-mode-provider/context.js b/packages/data/src/components/async-mode-provider/context.js index fe0c8acc99333b..5d3d23ebb5a523 100644 --- a/packages/data/src/components/async-mode-provider/context.js +++ b/packages/data/src/components/async-mode-provider/context.js @@ -4,6 +4,7 @@ import { createContext } from '@wordpress/element'; export const Context = createContext( false ); +Context.displayName = 'AsyncModeContext'; const { Consumer, Provider } = Context; diff --git a/packages/data/src/components/registry-provider/context.js b/packages/data/src/components/registry-provider/context.js index 04a24974a6ff25..28db67391dec89 100644 --- a/packages/data/src/components/registry-provider/context.js +++ b/packages/data/src/components/registry-provider/context.js @@ -9,6 +9,7 @@ import { createContext } from '@wordpress/element'; import defaultRegistry from '../../default-registry'; export const Context = createContext( defaultRegistry ); +Context.displayName = 'RegistryProviderContext'; const { Consumer, Provider } = Context; diff --git a/packages/dataviews/src/components/dataform-context/index.tsx b/packages/dataviews/src/components/dataform-context/index.tsx index 72fbf7e0f42ab6..518b74ce107200 100644 --- a/packages/dataviews/src/components/dataform-context/index.tsx +++ b/packages/dataviews/src/components/dataform-context/index.tsx @@ -15,6 +15,7 @@ type DataFormContextType< Item > = { const DataFormContext = createContext< DataFormContextType< any > >( { fields: [], } ); +DataFormContext.displayName = 'DataFormContext'; export function DataFormProvider< Item >( { fields, diff --git a/packages/dataviews/src/components/dataviews-context/index.ts b/packages/dataviews/src/components/dataviews-context/index.ts index a4800151f9af51..f7dd439afee461 100644 --- a/packages/dataviews/src/components/dataviews-context/index.ts +++ b/packages/dataviews/src/components/dataviews-context/index.ts @@ -88,4 +88,6 @@ const DataViewsContext = createContext< DataViewsContextType< any > >( { }, } ); +DataViewsContext.displayName = 'DataViewsContext'; + export default DataViewsContext; diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index abfb5484e44bf5..6bcdb8a05baaed 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -38,6 +38,7 @@ import { toggleFont } from './utils/toggleFont'; import setNestedValue from '../../../utils/set-nested-value'; export const FontLibraryContext = createContext( {} ); +FontLibraryContext.displayName = 'FontLibraryContext'; function FontLibraryProvider( { children } ) { const { saveEntityRecord } = useDispatch( coreStore ); diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index a95caf929b4736..ba8ba1c3654979 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -16,6 +16,8 @@ import { import { focus } from '@wordpress/dom'; export const SidebarNavigationContext = createContext( () => {} ); +SidebarNavigationContext.displayName = 'SidebarNavigationContext'; + // Focus a sidebar element after a navigation. The element to focus is either // specified by `focusSelector` (when navigating back) or it is the first // tabbable element (usually the "Back" button). diff --git a/packages/element/src/serialize.js b/packages/element/src/serialize.js index c967e1fa565f35..67ddd86675027f 100644 --- a/packages/element/src/serialize.js +++ b/packages/element/src/serialize.js @@ -48,7 +48,11 @@ import RawHTML from './raw-html'; /** @typedef {import('react').ReactElement} ReactElement */ -const { Provider, Consumer } = createContext( undefined ); +const Context = createContext( undefined ); +Context.displayName = 'ElementContext'; + +const { Provider, Consumer } = Context; + const ForwardRef = forwardRef( () => { return null; } ); diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index d2560537523410..a0936abe8b9753 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -114,6 +114,7 @@ interface DirectivesProps { // Main context. const context = createContext< any >( { client: {}, server: {} } ); +context.displayName = 'InteractivityContext'; // WordPress Directives. const directiveCallbacks: Record< string, DirectiveCallback > = {}; diff --git a/packages/keyboard-shortcuts/src/context.js b/packages/keyboard-shortcuts/src/context.js index f2f8846751291f..9c376ef03f4942 100644 --- a/packages/keyboard-shortcuts/src/context.js +++ b/packages/keyboard-shortcuts/src/context.js @@ -24,3 +24,5 @@ export const context = createContext( { } }, } ); + +context.displayName = 'KeyboardShortcutsContext'; diff --git a/packages/plugins/src/components/plugin-context/index.tsx b/packages/plugins/src/components/plugin-context/index.tsx index 3e2d687229011c..dc23561dd5f5c1 100644 --- a/packages/plugins/src/components/plugin-context/index.tsx +++ b/packages/plugins/src/components/plugin-context/index.tsx @@ -19,6 +19,7 @@ const Context = createContext< PluginContext >( { name: null, icon: null, } ); +Context.displayName = 'PluginContext'; export const PluginContextProvider = Context.Provider; diff --git a/packages/react-i18n/src/index.tsx b/packages/react-i18n/src/index.tsx index 4d68e50c66c2c0..5600bc463048c5 100644 --- a/packages/react-i18n/src/index.tsx +++ b/packages/react-i18n/src/index.tsx @@ -46,6 +46,7 @@ function makeContextValue( i18n: I18n ): I18nContextProps { } const I18nContext = createContext( makeContextValue( defaultI18n ) ); +I18nContext.displayName = 'I18nContext'; type I18nProviderProps = PropsWithChildren< { i18n: I18n } >; diff --git a/packages/router/src/router.tsx b/packages/router/src/router.tsx index c1eb6b46699404..c4cd798e05e737 100644 --- a/packages/router/src/router.tsx +++ b/packages/router/src/router.tsx @@ -66,7 +66,10 @@ export interface NavigationOptions { } const RoutesContext = createContext< Match | null >( null ); +RoutesContext.displayName = 'RoutesContext'; + export const ConfigContext = createContext< Config >( { pathArg: 'p' } ); +ConfigContext.displayName = 'ConfigContext'; const locationMemo = new WeakMap(); function getLocationWithQuery() { From 53335c2e40d1123e9497a3140f94f9fe94b6ff18 Mon Sep 17 00:00:00 2001 From: im3dabasia Date: Thu, 21 Aug 2025 12:03:58 +0530 Subject: [PATCH 19/20] fix: Remove the context in interactivity hooks.tsx --- packages/interactivity/src/hooks.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index a0936abe8b9753..d2560537523410 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -114,7 +114,6 @@ interface DirectivesProps { // Main context. const context = createContext< any >( { client: {}, server: {} } ); -context.displayName = 'InteractivityContext'; // WordPress Directives. const directiveCallbacks: Record< string, DirectiveCallback > = {}; From 841df2f53f082987976b1d386d052a3de55e2682 Mon Sep 17 00:00:00 2001 From: im3dabasia Date: Tue, 26 Aug 2025 15:30:30 +0530 Subject: [PATCH 20/20] chore: Add changelog entries --- packages/components/CHANGELOG.md | 4 ++++ packages/dataviews/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d42b65d7aef13b..0428afe72e631e 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,10 @@ - `Tabs`: Ensure font size inheritance for tab buttons in all contexts ([#71346](https://github.com/WordPress/gutenberg/pull/71346)). +### Internal + +- Display names for Context providers [#71208](https://github.com/WordPress/gutenberg/pull/71208) + ## 30.2.0 (2025-08-20) diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 5dac1b82db9fc2..9500421b8816b0 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -6,6 +6,10 @@ - Revert the ability to hide the view config via `config` prop and export a `DataViews.Footer` component to support the "Minimal UI" story. [#71276](https://github.com/WordPress/gutenberg/pull/71276) +### Internal + +- Display names for Context providers [#71208](https://github.com/WordPress/gutenberg/pull/71208) + ### Bug Fixes - DataViews: Fix incorrect documentation for `defaultLayouts` prop. [#71334](https://github.com/WordPress/gutenberg/pull/71334)