diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 22a1f4852b1434..4391fa3fd2b83f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,7 +1,7 @@ # Documentation -/docs @youknowriad @chrisvanpatten @ajitbohra @notnownikki -/docs/designers-developers/developers @youknowriad @gziolo @chrisvanpatten @mkaz @ajitbohra @notnownikki -/docs/designers-developers/designers @youknowriad @chrisvanpatten @mkaz @ajitbohra @notnownikki +/docs @youknowriad @chrisvanpatten @ajitbohra +/docs/designers-developers/developers @youknowriad @gziolo @chrisvanpatten @mkaz @ajitbohra +/docs/designers-developers/designers @youknowriad @chrisvanpatten @mkaz @ajitbohra # Data /packages/api-fetch @youknowriad @aduth @nerrad @mmtr @@ -10,7 +10,7 @@ /packages/redux-routine @youknowriad @aduth @nerrad # Blocks -/packages/block-library @youknowriad @gziolo @Soean @ajitbohra @jorgefilipecosta @talldan @notnownikki +/packages/block-library @youknowriad @gziolo @Soean @ajitbohra @jorgefilipecosta @talldan # Editor /packages/annotations @youknowriad @aduth @atimmer @ellatrix @@ -29,7 +29,7 @@ # Tooling /bin @youknowriad @gziolo @aduth @ntwb @nerrad @ajitbohra @nosolosw -/docs/tool @youknowriad @gziolo @chrisvanpatten @ajitbohra @nosolosw @notnownikki +/docs/tool @youknowriad @gziolo @chrisvanpatten @ajitbohra @nosolosw /packages/babel-plugin-import-jsx-pragma @youknowriad @gziolo @aduth @ntwb @nerrad @ajitbohra @nosolosw /packages/babel-plugin-makepot @youknowriad @gziolo @aduth @ntwb @nerrad @ajitbohra /packages/babel-preset-default @youknowriad @gziolo @aduth @ntwb @nerrad @ajitbohra @nosolosw diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index cb60c1c2f0354d..bef89730704cbb 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,4 +19,4 @@ - [ ] My code follows the WordPress code style. - [ ] My code follows the accessibility standards. - [ ] My code has proper inline documentation. -- [ ] I've included developer documentation if appropriate. +- [ ] I've included developer documentation if appropriate. diff --git a/.travis.yml b/.travis.yml index 60a40cec454eb7..9e42ed6163f3a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -160,3 +160,16 @@ jobs: env: WP_VERSION=latest SWITCH_TO_PHP=5.2 script: - ./bin/run-wp-unit-tests.sh + +before_deploy: + - npm install + - npm run playground:build -- --public-url '/gutenberg' + +deploy: + provider: pages + skip_cleanup: true + github_token: $GITHUB_TOKEN + keep_history: true + local_dir: playground/dist + on: + branch: master diff --git a/bin/packages/build.js b/bin/packages/build.js index 9639f8b232f70e..e1a6bd7142f80d 100755 --- a/bin/packages/build.js +++ b/bin/packages/build.js @@ -64,6 +64,43 @@ function createStyleEntryTransform() { } ); } +/** + * Returns a stream transform which maps an individual block.json to the + * index.js that imports it. Presently, babel resolves the import of json + * files by inlining them as a JavaScript primitive in the importing file. + * This transform ensures the importing file is rebuilt. + * + * @return {Transform} Stream transform instance. + */ +function createBlockJsonEntryTransform() { + const blocks = new Set; + + return new Transform( { + objectMode: true, + async transform( file, encoding, callback ) { + const matches = /block-library[\/\\]src[\/\\](.*)[\/\\]block.json$/.exec( file ); + const blockName = matches ? matches[ 1 ] : undefined; + + // Only block.json files in the block-library folder are subject to this transform. + if ( ! blockName ) { + this.push( file ); + callback(); + return; + } + + // Only operate once per block, assuming entries are common. + if ( blockName && blocks.has( blockName ) ) { + callback(); + return; + } + + blocks.add( blockName ); + this.push( file.replace( 'block.json', 'index.js' ) ); + callback(); + }, + } ); +} + let onFileComplete = () => {}; let stream; @@ -72,7 +109,9 @@ if ( files.length ) { stream = new Readable( { encoding: 'utf8' } ); files.forEach( ( file ) => stream.push( file ) ); stream.push( null ); - stream = stream.pipe( createStyleEntryTransform() ); + stream = stream + .pipe( createStyleEntryTransform() ) + .pipe( createBlockJsonEntryTransform() ); } else { const bar = new ProgressBar( 'Build Progress: [:bar] :percent', { width: 30, diff --git a/composer.json b/composer.json index 864a34d9c092f7..e869eb3ccc7fff 100644 --- a/composer.json +++ b/composer.json @@ -11,10 +11,10 @@ "issues": "https://github.com/WordPress/gutenberg/issues" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.4", - "squizlabs/php_codesniffer": "^3.1", - "wimg/php-compatibility": "^8", - "wp-coding-standards/wpcs": "^1.0.0" + "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", + "squizlabs/php_codesniffer": "^3.4.2", + "phpcompatibility/php-compatibility": "^9.2.0", + "wp-coding-standards/wpcs": "^2.1.1" }, "require": { "composer/installers": "~1.0" diff --git a/composer.lock b/composer.lock index a7c30dab21f451..e6bdc5bf4f1473 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "14614d0ab7be7d1f70eeae5bcd5f125d", + "content-hash": "78aec8b4f7bcfa500026de9ddb1abace", "packages": [ { "name": "composer/installers", - "version": "v1.5.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/composer/installers.git", - "reference": "049797d727261bf27f2690430d935067710049c2" + "reference": "cfcca6b1b60bc4974324efb5783c13dca6932b5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/installers/zipball/049797d727261bf27f2690430d935067710049c2", - "reference": "049797d727261bf27f2690430d935067710049c2", + "url": "https://api.github.com/repos/composer/installers/zipball/cfcca6b1b60bc4974324efb5783c13dca6932b5b", + "reference": "cfcca6b1b60bc4974324efb5783c13dca6932b5b", "shasum": "" }, "require": { @@ -124,35 +124,33 @@ "zend", "zikula" ], - "time": "2017-12-29T09:13:20+00:00" + "time": "2018-08-27T06:10:37+00:00" } ], "packages-dev": [ { "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v0.4.4", + "version": "v0.5.0", "source": { "type": "git", "url": "https://github.com/Dealerdirect/phpcodesniffer-composer-installer.git", - "reference": "2e41850d5f7797cbb1af7b030d245b3b24e63a08" + "reference": "e749410375ff6fb7a040a68878c656c2e610b132" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/2e41850d5f7797cbb1af7b030d245b3b24e63a08", - "reference": "2e41850d5f7797cbb1af7b030d245b3b24e63a08", + "url": "https://api.github.com/repos/Dealerdirect/phpcodesniffer-composer-installer/zipball/e749410375ff6fb7a040a68878c656c2e610b132", + "reference": "e749410375ff6fb7a040a68878c656c2e610b132", "shasum": "" }, "require": { "composer-plugin-api": "^1.0", "php": "^5.3|^7", - "squizlabs/php_codesniffer": "*" + "squizlabs/php_codesniffer": "^2|^3" }, "require-dev": { "composer/composer": "*", - "wimg/php-compatibility": "^8.0" - }, - "suggest": { - "dealerdirect/qa-tools": "All the PHP QA tools you'll need" + "phpcompatibility/php-compatibility": "^9.0", + "sensiolabs/security-checker": "^4.1.0" }, "type": "composer-plugin", "extra": { @@ -170,13 +168,13 @@ "authors": [ { "name": "Franck Nijhof", - "email": "f.nijhof@dealerdirect.nl", - "homepage": "http://workingatdealerdirect.eu", - "role": "Developer" + "email": "franck.nijhof@dealerdirect.com", + "homepage": "http://www.frenck.nl", + "role": "Developer / IT Manager" } ], "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "homepage": "http://workingatdealerdirect.eu", + "homepage": "http://www.dealerdirect.com", "keywords": [ "PHPCodeSniffer", "PHP_CodeSniffer", @@ -194,135 +192,142 @@ "stylecheck", "tests" ], - "time": "2017-12-06T16:27:17+00:00" + "time": "2018-10-26T13:21:45+00:00" }, { - "name": "squizlabs/php_codesniffer", - "version": "3.3.1", + "name": "phpcompatibility/php-compatibility", + "version": "9.2.0", "source": { "type": "git", - "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "628a481780561150481a9ec74709092b9759b3ec" + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "3db1bf1e28123fd574a4ae2e9a84072826d51b5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/628a481780561150481a9ec74709092b9759b3ec", - "reference": "628a481780561150481a9ec74709092b9759b3ec", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/3db1bf1e28123fd574a4ae2e9a84072826d51b5e", + "reference": "3db1bf1e28123fd574a4ae2e9a84072826d51b5e", "shasum": "" }, "require": { - "ext-simplexml": "*", - "ext-tokenizer": "*", - "ext-xmlwriter": "*", - "php": ">=5.4.0" + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" }, "require-dev": { - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" }, - "bin": [ - "bin/phpcs", - "bin/phpcbf" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." }, + "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "LGPL-3.0-or-later" ], "authors": [ { - "name": "Greg Sherwood", + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + }, + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", "role": "lead" } ], - "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", - "homepage": "http://www.squizlabs.com/php-codesniffer", + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", "keywords": [ + "compatibility", "phpcs", "standards" ], - "time": "2018-07-26T23:47:18+00:00" + "time": "2019-06-27T19:58:56+00:00" }, { - "name": "wimg/php-compatibility", - "version": "8.2.0", + "name": "squizlabs/php_codesniffer", + "version": "3.4.2", "source": { "type": "git", - "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", - "reference": "eaf613c1a8265bcfd7b0ab690783f2aef519f78a" + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/eaf613c1a8265bcfd7b0ab690783f2aef519f78a", - "reference": "eaf613c1a8265bcfd7b0ab690783f2aef519f78a", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8", + "reference": "b8a7362af1cc1aadb5bd36c3defc4dda2cf5f0a8", "shasum": "" }, "require": { - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" - }, - "conflict": { - "squizlabs/php_codesniffer": "2.6.2" + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" }, "require-dev": { - "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" - }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", - "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, - "type": "phpcodesniffer-standard", - "autoload": { - "psr-4": { - "PHPCompatibility\\": "PHPCompatibility/" + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0-or-later" + "BSD-3-Clause" ], "authors": [ { - "name": "Wim Godden", + "name": "Greg Sherwood", "role": "lead" } ], - "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP version compatibility.", - "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", "keywords": [ - "compatibility", "phpcs", "standards" ], - "time": "2018-07-17T13:42:26+00:00" + "time": "2019-04-10T23:49:02+00:00" }, { "name": "wp-coding-standards/wpcs", - "version": "1.0.0", + "version": "2.1.1", "source": { "type": "git", - "url": "https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards.git", - "reference": "539c6d74e6207daa22b7ea754d6f103e9abb2755" + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "bd9c33152115e6741e3510ff7189605b35167908" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress-Coding-Standards/WordPress-Coding-Standards/zipball/539c6d74e6207daa22b7ea754d6f103e9abb2755", - "reference": "539c6d74e6207daa22b7ea754d6f103e9abb2755", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/bd9c33152115e6741e3510ff7189605b35167908", + "reference": "bd9c33152115e6741e3510ff7189605b35167908", "shasum": "" }, "require": { - "php": ">=5.3", - "squizlabs/php_codesniffer": "^2.9.0 || ^3.0.2" + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.3.1" }, "require-dev": { - "phpcompatibility/php-compatibility": "*" + "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", + "phpcompatibility/php-compatibility": "^9.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." + "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." }, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", @@ -341,7 +346,7 @@ "standards", "wordpress" ], - "time": "2018-07-25T18:10:35+00:00" + "time": "2019-05-21T02:50:00+00:00" } ], "aliases": [], diff --git a/docs/contributors/release.md b/docs/contributors/release.md index bfbc84636b3a50..c9d4a99715dd9c 100644 --- a/docs/contributors/release.md +++ b/docs/contributors/release.md @@ -6,18 +6,40 @@ To release Gutenberg, you need commit access to the [WordPress.org plugin reposi ## Plugin Releases -### Versioning +### Schedule We release a new major version approximately every two weeks. The current and next versions are [tracked in GitHub milestones](https://github.com/WordPress/gutenberg/milestones), along with each version's tagging date. -### Release Candidates - On the date of the current milestone, we publish a release candidate and make it available for plugin authors and users to test. If any regressions are found with a release candidate, a new release candidate can be published. The date in the milestone is the date of **tagging the release candidate**. On this date, all remaining PRs on the milestone are moved automatically to the next release. Release candidates should be versioned incrementally, starting with `-rc.1`, then `-rc.2`, and so on. +Two days after the first release candidate, the stable version is created based on the last release candidate and any necessary regression fixes. + +Once the stable version is released, a post [like this](https://make.wordpress.org/core/2019/06/26/whats-new-in-gutenberg-26th-june/) describing the changes and performing a performance audit should be published. + +If critical bugs are discovered on stable versions of the plugin, patch versions can be released at any time. + +### Release Tool + +The plugin release process is entirely automated. To release the RC version of the plugin, run the following command and follow the instructions: (Note that at the time of writing, the tool doesn't support releasing multiple consecutive RC releases) + +```bash +./bin/commander.js rc +``` + +To release a stable version, run: + +```bash +./bin.commander.js stable +``` + +It is possible to run the "stable" release CLI in a consecutive way to release patch releases following the first stable release. + +### Manual Release Process + #### Creating the first Release Candidate Releasing the first release candidate for this milestone (`x.x`) involves: @@ -71,10 +93,6 @@ git push origin release/x.x Here's an example [release candidate page](https://github.com/WordPress/gutenberg/releases/tag/v4.6.0-rc.1); yours should look like that when you're finished. -##### Publishing the Call For Testing - -Ping @karmatosed in the `[#core-test](https://wordpress.slack.com/messages/C03B0H5J0)` team to publish a call for testing post. Here's an [example call for testing post.](https://make.wordpress.org/test/2019/01/04/call-for-testing-gutenberg-4-8/) - #### Creating Release Candidate Patches (done via `git cherry-pick`) If a bug is found in a release candidate and a fix is committed to `master`, we should include that fix in a new release candidate. To do this you'll need to use `git cherry-pick` to add these changes to the milestone's release branch. This way only fixes are added to the release candidate and not all the new code that has landed on `master` since tagging: diff --git a/docs/designers-developers/developers/block-api/block-registration.md b/docs/designers-developers/developers/block-api/block-registration.md index 7986be28bee4bc..9d78c4247e4ec4 100644 --- a/docs/designers-developers/developers/block-api/block-registration.md +++ b/docs/designers-developers/developers/block-api/block-registration.md @@ -520,7 +520,7 @@ transforms: { * **Type:** `Array` -Blocks are able to be inserted into blocks that use [`InnerBlocks`](/packages/block-editor/src/components/inner-blocks/README.md) as nested content. Sometimes it is useful to restrict a block so that it is only available as a nested block. For example, you might want to allow an 'Add to Cart' block to only be available within a 'Product' block. +Blocks are able to be inserted into blocks that use [`InnerBlocks`](https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/inner-blocks/README.md) as nested content. Sometimes it is useful to restrict a block so that it is only available as a nested block. For example, you might want to allow an 'Add to Cart' block to only be available within a 'Product' block. Setting `parent` lets a block require that it is only available when nested within the specified blocks. diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md b/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md index 73448eadf0d023..f21c3b3f09988e 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/creating-dynamic-blocks.md @@ -1,6 +1,17 @@ # Creating dynamic blocks -Dynamic blocks are blocks that can change their content even if the post is not saved. One example from WordPress itself is the latest posts block. This block will update everywhere it is used when a new post is published. +Dynamic blocks are blocks that build their structure and content on the fly when the block is rendered on the front end. + +There are two primary uses for dynamic blocks: + +1. Blocks where content should change even if a post has not been updated. One example from WordPress itself is the latest posts block. This block will update everywhere it is used when a new post is published. +2. Blocks where updates to the code (HTML, CSS, JS) should be immediately shown on the front end of the website. For example, if you update the HTML structure of a block by adding a new class, adding a div, or changing the layout in any other way, if you want these changes to be applied immediately on all occurrences of that block across the site, a dynamic block should be used. (If a dynamic block is not used then when block code is updated Guterberg's [validation process](https://developer.wordpress.org/block-editor/developers/block-api/block-edit-save/#validation) applies, causing users to see the validation message, "This block appears to have been modified externally"). + +For many dynamic blocks, the `save` callback function should be returned as `null`, which tells the editor to save only the [block attributes](https://developer.wordpress.org/block-editor/developers/block-api/block-attributes/) to the database. These attributes are then passed into the server-side rendering callback, so you can decide how to display the block on the front end of your site. When you return `null`, the editor will skip the block markup validation process, avoiding issues with frequently-changing markup. + +You can also save an HTML representation of the block. If you provide a server-side rendering callback, this HTML will be replaced with the output of your callback, but will be rendered if your block is deactivated or your render callback is removed. + +Block attributes can be used for any content or setting you want to save for that block. In the first example above, with the latest posts block, the number of latest posts you want to show could be saved as an attribute. Or in the second example, attributes can be used for each piece of content you want to show in the front end - such as heading text, paragraph text, an image, a URL, etc. The following code example shows how to create a dynamic block that shows only the last post as a link. diff --git a/gutenberg.php b/gutenberg.php index 5a03abdad7c309..977083ad622016 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the new block editor in core. - * Version: 6.0.0-rc.1 + * Version: 6.0.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/client-assets.php b/lib/client-assets.php index 5e4d2651bb603d..724cf26f9c4023 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -628,7 +628,7 @@ function gutenberg_extend_block_editor_preload_paths( $preload_paths, $post ) { $rest_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; $autosaves_path = sprintf( '/wp/v2/%s/%d/autosaves?context=edit', $rest_base, $post->ID ); - if ( ! in_array( $autosaves_path, $preload_paths ) ) { + if ( ! in_array( $autosaves_path, $preload_paths, true ) ) { $preload_paths[] = $autosaves_path; } } @@ -644,7 +644,7 @@ function gutenberg_extend_block_editor_preload_paths( $preload_paths, $post ) { */ $blocks_path = array( '/wp/v2/blocks', 'OPTIONS' ); - if ( ! in_array( $blocks_path, $preload_paths ) ) { + if ( ! in_array( $blocks_path, $preload_paths, true ) ) { $preload_paths[] = $blocks_path; } diff --git a/package-lock.json b/package-lock.json index 7b34929e4cd4c9..54148d3de97777 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "6.0.0-rc.1", + "version": "6.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2910,31 +2910,31 @@ } }, "@tannin/compile": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tannin/compile/-/compile-1.0.1.tgz", - "integrity": "sha512-ymd9icvnkQin8UG4eRU3+xBc7gqTn/Kv5+EMY3ALWVwIl6j/7McWbCkxB8MgU40UaHJk8kLCk06wiKszXLdXWQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tannin/compile/-/compile-1.0.3.tgz", + "integrity": "sha512-OkPHvaM/hIHdSco3+ZO1hzkOtfEddn5a0veWft2aDLvKnbdj9VusiLKNdEE9by3hCZIIcb9aWF+iBorhfrQOfw==", "requires": { - "@tannin/evaluate": "^1.0.0", - "@tannin/postfix": "^1.0.0" + "@tannin/evaluate": "^1.1.1", + "@tannin/postfix": "^1.0.2" } }, "@tannin/evaluate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@tannin/evaluate/-/evaluate-1.0.0.tgz", - "integrity": "sha512-gO7YbJsD8sj5/nqUbFZv71Meu2++D9n4DZov/cWwp3YJbBwKShPlWwwlXr/0vz4vuxm/gys+3NiGbZkmhlXf0Q==" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@tannin/evaluate/-/evaluate-1.1.1.tgz", + "integrity": "sha512-ALuSZHjrLHGnw0WxsHDHde74FJ2WW0Ck4rg3QBxFBCmxd6Wsac+e0HXfJ++Qion15LIOCmFhyVpWzawMgeBA8Q==" }, "@tannin/plural-forms": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@tannin/plural-forms/-/plural-forms-1.0.1.tgz", - "integrity": "sha512-SXutT+XLbMOECvmWDBSqIOHhS5hzWG9875HCFGKYgp8ghGPrJ4HZ325Xc0hsRThdjgrWMEQixlbpWl4SXOQTig==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tannin/plural-forms/-/plural-forms-1.0.3.tgz", + "integrity": "sha512-IUr9+FiCnzCiB9aRio3FVNR8TNL9SmX2zkV6tmfWWwSclX4uTCykoGsDhTGKK+sZnMrdPCTmb/OxbtGNdVyV4g==", "requires": { - "@tannin/compile": "^1.0.0" + "@tannin/compile": "^1.0.3" } }, "@tannin/postfix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.0.0.tgz", - "integrity": "sha512-59/mWwU7sXHfoU2kI3RcWRki2Jjbz5nEVJNBN4MUyIhPjXTebAcZqgsQACvlk+sjKVOTMEMHcrFrKQbaxz/1Dw==" + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.0.2.tgz", + "integrity": "sha512-Nggtk7/ljfNPpAX8CjxxLkMKuO6u2gH1ozmTvGclWF2pNcxTf6YGghYNYNWZRKrimXGhQ8yZqvAHep7h80K04g==" }, "@types/babel__core": { "version": "7.1.1", @@ -3285,6 +3285,7 @@ "classnames": "^2.2.5", "dom-scroll-into-view": "^1.2.1", "lodash": "^4.17.10", + "react-spring": "^8.0.19", "redux-multi": "^0.1.12", "refx": "^3.0.0", "rememo": "^3.0.0", @@ -3381,7 +3382,7 @@ "memize": "^1.0.5", "moment": "^2.22.1", "mousetrap": "^1.6.2", - "re-resizable": "^4.7.1", + "re-resizable": "^5.0.1", "react-click-outside": "^3.0.0", "react-dates": "^17.1.1", "react-spring": "^8.0.20", @@ -3649,7 +3650,7 @@ "lodash": "^4.17.11", "memize": "^1.0.5", "sprintf-js": "^1.1.1", - "tannin": "^1.0.1" + "tannin": "^1.1.0" } }, "@wordpress/is-shallow-equal": { @@ -9375,6 +9376,11 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-memoize": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.1.tgz", + "integrity": "sha512-xdmw296PCL01tMOXx9mdJSmWY29jQgxyuZdq0rEHMu+Tpe1eOEtCycoG6chzlcrWsNgpZP7oL8RiQr7+G6Bl6g==" + }, "fastparse": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", @@ -18366,9 +18372,12 @@ } }, "re-resizable": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-4.7.1.tgz", - "integrity": "sha512-pLJkPbZCe+3ml+9Q15z+R69qYZDsluj0KwrdFb8kSNaqDzYAveDUblf7voHH9hNTdKIiIvP8iIdGFFKSgffVaQ==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-5.0.1.tgz", + "integrity": "sha512-Iy8v5li7bhNBDxCN1DbA4l6G2Hk8NCZtcExoI1D+5pfvKyQcH8LH2P5h3DGoEfHhs0uyyRC1Qx8bHBomfrmxgA==", + "requires": { + "fast-memoize": "^2.5.1" + } }, "react": { "version": "16.8.4", @@ -21146,11 +21155,11 @@ } }, "tannin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.0.1.tgz", - "integrity": "sha512-dDtnwHQ63bS/Gz0ZLY+E+JCdRoTZkmoKDoC64y3hzAD2X2qrp8jSuWNUjtiYHA48mtj4Ens9xl4knAOm1t+rfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.1.0.tgz", + "integrity": "sha512-LxhcXqpMHEOVeVKmuG5aCPPsTXFlO373vrWkqN7FSJBVLS6lFOAg8ZGzIyGhrOf7Ho3xB4jdGedY1gi/8J1FCA==", "requires": { - "@tannin/plural-forms": "^1.0.0" + "@tannin/plural-forms": "^1.0.3" } }, "tapable": { diff --git a/package.json b/package.json index 64a7a54a098d70..1087de7094828a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "6.0.0-rc.1", + "version": "6.0.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index ff1a8579a0b016..f279d2994600d3 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -387,6 +387,19 @@ _Type_ - `Object` +# **transformStyles** + +Applies a series of CSS rule transforms to wrap selectors inside a given class and/or rewrite URLs depending on the parameters passed. + +_Parameters_ + +- _styles_ `Array`: CSS rules. +- _wrapperClassName_ `string`: Wrapper Class Name. + +_Returns_ + +- `Array`: converted rules. + # **URLInput** _Related_ diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index da701fc0c08669..5add09430524e1 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -44,6 +44,7 @@ "classnames": "^2.2.5", "dom-scroll-into-view": "^1.2.1", "lodash": "^4.17.10", + "react-spring": "^8.0.19", "redux-multi": "^0.1.12", "refx": "^3.0.0", "rememo": "^3.0.0", diff --git a/packages/block-editor/src/components/block-list/block-mobile-toolbar.native.js b/packages/block-editor/src/components/block-list/block-mobile-toolbar.native.js index e5ac71ee27ed34..96395c41c835f2 100644 --- a/packages/block-editor/src/components/block-list/block-mobile-toolbar.native.js +++ b/packages/block-editor/src/components/block-list/block-mobile-toolbar.native.js @@ -55,15 +55,12 @@ export default compose( order: getBlockIndex( clientId ), }; } ), - withDispatch( ( dispatch, { clientId, rootClientId, onDelete } ) => { + withDispatch( ( dispatch, { clientId, rootClientId } ) => { const { removeBlock } = dispatch( 'core/block-editor' ); return { - onDelete() { + onDelete: () => { Keyboard.dismiss(); removeBlock( clientId, rootClientId ); - if ( onDelete ) { - onDelete( clientId ); - } }, }; } ), diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index aac68e2e6faa88..3556b829ba188d 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -3,6 +3,7 @@ */ import classnames from 'classnames'; import { get, reduce, size, first, last } from 'lodash'; +import { animated } from 'react-spring/web.cjs'; /** * WordPress dependencies @@ -50,6 +51,7 @@ import InserterWithShortcuts from '../inserter-with-shortcuts'; import Inserter from '../inserter'; import useHoveredArea from './hover-area'; import { isInsideRootBlock } from '../../utils/dom'; +import useMovingAnimation from './moving-animation'; /** * Prevents default dragging behavior within a block to allow for multi- @@ -97,6 +99,8 @@ function BlockListBlock( { toggleSelection, onShiftSelection, onSelectionStart, + animateOnChange, + enableAnimation, } ) { // Random state used to rerender the component if needed, ideally we don't need this const [ , updateRerenderState ] = useState( {} ); @@ -247,6 +251,9 @@ function BlockListBlock( { } }, [ isFirstMultiSelected ] ); + // Block Reordering animation + const style = useMovingAnimation( wrapper, isSelected || isPartOfMultiSelection, enableAnimation, animateOnChange ); + // Other event handlers /** @@ -458,6 +465,8 @@ function BlockListBlock( { tabIndex="0" aria-label={ blockLabel } childHandledEvents={ [ 'onDragStart', 'onMouseDown' ] } + tagName={ animated.div } + style={ style } { ...blockWrapperProps } > { shouldShowInsertionPoint && ( diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index 79cc1b29273375..3e65a52fd3b2ec 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -3,7 +3,6 @@ */ import { findLast, - map, invert, mapValues, sortBy, @@ -29,6 +28,12 @@ import BlockListBlock from './block'; import BlockListAppender from '../block-list-appender'; import { getBlockDOMNode } from '../../utils/dom'; +/** + * If the block count exceeds the threshold, we disable the reordering animation + * to avoid laginess. + */ +const BLOCK_ANIMATION_THRESHOLD = 200; + const forceSyncUpdates = ( WrappedComponent ) => ( props ) => { return ( @@ -36,6 +41,7 @@ const forceSyncUpdates = ( WrappedComponent ) => ( props ) => { ); }; + class BlockList extends Component { constructor( props ) { super( props ); @@ -198,11 +204,12 @@ class BlockList extends Component { multiSelectedBlockClientIds, hasMultiSelection, renderAppender, + enableAnimation, } = this.props; return (
- { map( blockClientIds, ( clientId ) => { + { blockClientIds.map( ( clientId ) => { const isBlockInSelection = hasMultiSelection ? multiSelectedBlockClientIds.includes( clientId ) : selectedBlockClientId === clientId; @@ -214,11 +221,17 @@ class BlockList extends Component { isBlockInSelection={ isBlockInSelection } > ); @@ -248,6 +261,7 @@ export default compose( [ getSelectedBlockClientId, getMultiSelectedBlockClientIds, hasMultiSelection, + getGlobalBlockCount, } = select( 'core/block-editor' ); const { rootClientId } = ownProps; @@ -261,6 +275,7 @@ export default compose( [ selectedBlockClientId: getSelectedBlockClientId(), multiSelectedBlockClientIds: getMultiSelectedBlockClientIds(), hasMultiSelection: hasMultiSelection(), + enableAnimation: getGlobalBlockCount() <= BLOCK_ANIMATION_THRESHOLD, }; } ), withDispatch( ( dispatch ) => { diff --git a/packages/block-editor/src/components/block-list/moving-animation.js b/packages/block-editor/src/components/block-list/moving-animation.js new file mode 100644 index 00000000000000..e0fdf75ed6d6b3 --- /dev/null +++ b/packages/block-editor/src/components/block-list/moving-animation.js @@ -0,0 +1,85 @@ +/** + * External dependencies + */ +import { useSpring, interpolate } from 'react-spring/web.cjs'; + +/** + * WordPress dependencies + */ +import { useState, useLayoutEffect } from '@wordpress/element'; +import { useReducedMotion } from '@wordpress/compose'; + +/** + * Hook used to compute the styles required to move a div into a new position. + * + * The way this animation works is the following: + * - It first renders the element as if there was no animation. + * - It takes a snapshot of the position of the block to use it + * as a destination point for the animation. + * - It restores the element to the previous position using a CSS transform + * - It uses the "resetAnimation" flag to reset the animation + * from the beginning in order to animate to the new destination point. + * + * @param {Object} ref Reference to the element to animate. + * @param {boolean} isSelected Whether it's the current block or not. + * @param {boolean} enableAnimation Enable/Disable animation. + * @param {*} triggerAnimationOnChange Variable used to trigger the animation if it changes. + * + * @return {Object} Style object. + */ +function useMovingAnimation( ref, isSelected, enableAnimation, triggerAnimationOnChange ) { + const prefersReducedMotion = useReducedMotion() || ! enableAnimation; + const [ resetAnimation, setResetAnimation ] = useState( false ); + const [ transform, setTransform ] = useState( { x: 0, y: 0 } ); + + const previous = ref.current ? ref.current.getBoundingClientRect() : null; + useLayoutEffect( () => { + if ( prefersReducedMotion ) { + return; + } + ref.current.style.transform = 'none'; + const destination = ref.current.getBoundingClientRect(); + const newTransform = { + x: previous ? previous.left - destination.left : 0, + y: previous ? previous.top - destination.top : 0, + }; + ref.current.style.transform = `translate3d(${ newTransform.x }px,${ newTransform.y }px,0)`; + setResetAnimation( true ); + setTransform( newTransform ); + }, [ triggerAnimationOnChange ] ); + useLayoutEffect( () => { + if ( resetAnimation ) { + setResetAnimation( false ); + } + }, [ resetAnimation ] ); + const animationProps = useSpring( { + from: transform, + to: { + x: 0, + y: 0, + }, + reset: resetAnimation, + config: { mass: 5, tension: 2000, friction: 200 }, + immediate: prefersReducedMotion, + } ); + + return { + transformOrigin: 'center', + transform: interpolate( + [ + animationProps.x, + animationProps.y, + ], + ( x, y ) => x === 0 && y === 0 ? undefined : `translate3d(${ x }px,${ y }px,0)` + ), + zIndex: interpolate( + [ + animationProps.x, + animationProps.y, + ], + ( x, y ) => ! isSelected || ( x === 0 && y === 0 ) ? undefined : `1` + ), + }; +} + +export default useMovingAnimation; diff --git a/packages/block-editor/src/components/ignore-nested-events/index.js b/packages/block-editor/src/components/ignore-nested-events/index.js index cb707785bcb65e..42f902b3e61c95 100644 --- a/packages/block-editor/src/components/ignore-nested-events/index.js +++ b/packages/block-editor/src/components/ignore-nested-events/index.js @@ -6,7 +6,7 @@ import { reduce } from 'lodash'; /** * WordPress dependencies */ -import { Component, forwardRef } from '@wordpress/element'; +import { Component, forwardRef, createElement } from '@wordpress/element'; /** * Component which renders a div with passed props applied except the optional @@ -66,7 +66,7 @@ export class IgnoreNestedEvents extends Component { } render() { - const { childHandledEvents = [], forwardedRef, ...props } = this.props; + const { childHandledEvents = [], forwardedRef, tagName = 'div', ...props } = this.props; const eventHandlers = reduce( [ ...childHandledEvents, @@ -96,7 +96,7 @@ export class IgnoreNestedEvents extends Component { return result; }, {} ); - return
; + return createElement( tagName, { ref: forwardedRef, ...props, ...eventHandlers } ); } } diff --git a/packages/block-editor/src/components/inner-blocks/style.scss b/packages/block-editor/src/components/inner-blocks/style.scss index 0f8576fd5ecf34..e7bd479e04e02e 100644 --- a/packages/block-editor/src/components/inner-blocks/style.scss +++ b/packages/block-editor/src/components/inner-blocks/style.scss @@ -39,19 +39,23 @@ .block-editor-inner-blocks__template-picker-options.block-editor-inner-blocks__template-picker-options { display: flex; + justify-content: center; flex-direction: row; - flex-wrap: nowrap; + flex-wrap: wrap; width: 100%; - margin: $grid-size-large 0; + margin: $grid-size-small 0; list-style: none; > li { list-style: none; - flex-basis: 100%; + margin: $grid-size; flex-shrink: 1; - margin: 0 $grid-size; max-width: 100px; } + + .block-editor-inner-blocks__template-picker-option { + padding: $grid-size; + } } .block-editor-inner-blocks__template-picker-option { diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index baca2921d1677f..8f96935d549895 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -112,8 +112,10 @@ function RichTextWraper( { ) } - { inlineToolbar && ( - + { isSelected && inlineToolbar && ( + ) } diff --git a/packages/block-editor/src/utils/index.js b/packages/block-editor/src/utils/index.js index cb35b8d3420d15..95241bd4fa7673 100644 --- a/packages/block-editor/src/utils/index.js +++ b/packages/block-editor/src/utils/index.js @@ -1,4 +1 @@ -/** - * Internal dependencies - */ -export { default as __experimentalTransformStyles } from './transform-styles'; +export { default as transformStyles } from './transform-styles'; diff --git a/packages/block-editor/src/utils/transform-styles/index.js b/packages/block-editor/src/utils/transform-styles/index.js index 5d98c856ed1160..b0f1d66fe55111 100644 --- a/packages/block-editor/src/utils/transform-styles/index.js +++ b/packages/block-editor/src/utils/transform-styles/index.js @@ -16,7 +16,7 @@ import urlRewrite from './transforms/url-rewrite'; import wrap from './transforms/wrap'; /** - * Convert css rules. + * Applies a series of CSS rule transforms to wrap selectors inside a given class and/or rewrite URLs depending on the parameters passed. * * @param {Array} styles CSS rules. * @param {string} wrapperClassName Wrapper Class Name. diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 784850095f97d2..7e8652b23a3bf2 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -51,7 +51,7 @@ // Provide every block with a default base margin. This margin provides a consistent spacing // between blocks in the editor. -[data-block] { +.editor-styles-wrapper [data-block] { margin-top: $default-block-margin; margin-bottom: $default-block-margin; } diff --git a/packages/block-library/src/html/edit.js b/packages/block-library/src/html/edit.js index 4ddc146ccf3d66..4f1ab45a073dfe 100644 --- a/packages/block-library/src/html/edit.js +++ b/packages/block-library/src/html/edit.js @@ -6,7 +6,7 @@ import { Component } from '@wordpress/element'; import { BlockControls, PlainText, - __experimentalTransformStyles, + transformStyles, } from '@wordpress/block-editor'; import { Disabled, SandBox } from '@wordpress/components'; import { withSelect } from '@wordpress/data'; @@ -38,7 +38,7 @@ class HTMLEdit extends Component { this.setState( { styles: [ defaultStyles, - ...__experimentalTransformStyles( styles ), + ...transformStyles( styles ), ] } ); } diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index faabe881a04c94..b22d63da20cb9f 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -415,7 +415,7 @@ class ImageEdit extends Component { const src = isExternal ? url : undefined; const labels = { title: ! url ? __( 'Image' ) : __( 'Edit image' ), - instructions: __( 'Upload an image, pick one from your media library, or add one with a URL.' ), + instructions: __( 'Upload an image file, pick one from your media library, or add one with a URL.' ), }; const mediaPreview = ( !! url && {post_excerpt; if ( ! ( $post_excerpt ) ) { $post_excerpt = $post->post_content; @@ -77,7 +77,7 @@ function render_block_core_latest_posts( $attributes ) { } if ( isset( $attributes['displayPostContent'] ) && $attributes['displayPostContent'] - && isset( $attributes['displayPostContentRadio'] ) && 'full_post' == $attributes['displayPostContentRadio'] ) { + && isset( $attributes['displayPostContentRadio'] ) && 'full_post' === $attributes['displayPostContentRadio'] ) { $list_items_markup .= sprintf( '
%1$s
', wp_kses_post( html_entity_decode( $post->post_content, ENT_QUOTES, get_option( 'blog_charset' ) ) ) diff --git a/packages/block-library/src/rss/index.php b/packages/block-library/src/rss/index.php index 1bf217be3a9b59..53879d5bb2716b 100644 --- a/packages/block-library/src/rss/index.php +++ b/packages/block-library/src/rss/index.php @@ -69,7 +69,7 @@ function render_block_core_rss( $attributes ) { $excerpt = esc_attr( wp_trim_words( $excerpt, $attributes['excerptLength'], ' […]' ) ); // Change existing [...] to […]. - if ( '[...]' == substr( $excerpt, -5 ) ) { + if ( '[...]' === substr( $excerpt, -5 ) ) { $excerpt = substr( $excerpt, 0, -5 ) . '[…]'; } diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js index 9277c4bb6de705..e183fc029cee31 100644 --- a/packages/block-library/src/video/edit.native.js +++ b/packages/block-library/src/video/edit.native.js @@ -11,7 +11,6 @@ import { mediaUploadSync, requestImageFailedRetryDialog, requestImageUploadCancelDialog, - requestImageUploadCancel, } from 'react-native-gutenberg-bridge'; /** @@ -33,6 +32,7 @@ import { } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; import { isURL } from '@wordpress/url'; +import { doAction, hasAction } from '@wordpress/hooks'; /** * Internal dependencies @@ -70,8 +70,9 @@ class VideoEdit extends React.Component { } componentWillUnmount() { - if ( this.state.isUploadInProgress ) { - requestImageUploadCancel( this.props.attributes.id ); + // this action will only exist if the user pressed the trash button on the block holder + if ( hasAction( 'blocks.onRemoveBlockCheckUpload' ) && this.state.isUploadInProgress ) { + doAction( 'blocks.onRemoveBlockCheckUpload', this.props.attributes.id ); } } diff --git a/packages/blocks/src/api/parser.js b/packages/blocks/src/api/parser.js index 162c5cec83c508..b40532957c5f05 100644 --- a/packages/blocks/src/api/parser.js +++ b/packages/blocks/src/api/parser.js @@ -169,45 +169,6 @@ export function isAmbiguousStringSource( attributeSchema ) { return isStringSource && isSingleType; } -/** - * Returns value coerced to the specified JSON schema type string. - * - * @see http://json-schema.org/latest/json-schema-validation.html#rfc.section.6.25 - * - * @param {*} value Original value. - * @param {string} type Type to coerce. - * - * @return {*} Coerced value. - */ -export function asType( value, type ) { - switch ( type ) { - case 'string': - return String( value ); - - case 'boolean': - return Boolean( value ); - - case 'object': - return Object( value ); - - case 'null': - return null; - - case 'array': - if ( Array.isArray( value ) ) { - return value; - } - - return Array.from( value ); - - case 'integer': - case 'number': - return Number( value ); - } - - return value; -} - /** * Returns an hpq matcher given a source object. * @@ -238,7 +199,7 @@ export function matcherFromSource( sourceConfig ) { case 'tag': return flow( [ prop( sourceConfig.selector, 'nodeName' ), - ( value ) => value.toLowerCase(), + ( nodeName ) => nodeName ? nodeName.toLowerCase() : undefined, ] ); default: // eslint-disable-next-line no-console diff --git a/packages/blocks/src/api/test/parser.js b/packages/blocks/src/api/test/parser.js index f6c1569a420436..8c0e50feee3eec 100644 --- a/packages/blocks/src/api/test/parser.js +++ b/packages/blocks/src/api/test/parser.js @@ -10,7 +10,6 @@ import deepFreeze from 'deep-freeze'; import { getBlockAttribute, getBlockAttributes, - asType, createBlockWithFallback, getMigratedBlock, default as parsePegjs, @@ -104,44 +103,6 @@ describe( 'block parser', () => { } ); } ); - describe( 'asType()', () => { - it( 'gracefully handles undefined type', () => { - expect( asType( 5 ) ).toBe( 5 ); - } ); - - it( 'gracefully handles unhandled type', () => { - expect( asType( 5, '__UNHANDLED__' ) ).toBe( 5 ); - } ); - - it( 'returns expected coerced values', () => { - const arr = []; - const obj = {}; - - expect( asType( '5', 'string' ) ).toBe( '5' ); - expect( asType( 5, 'string' ) ).toBe( '5' ); - - expect( asType( 5, 'integer' ) ).toBe( 5 ); - expect( asType( '5', 'integer' ) ).toBe( 5 ); - - expect( asType( 5, 'number' ) ).toBe( 5 ); - expect( asType( '5', 'number' ) ).toBe( 5 ); - - expect( asType( true, 'boolean' ) ).toBe( true ); - expect( asType( false, 'boolean' ) ).toBe( false ); - expect( asType( '5', 'boolean' ) ).toBe( true ); - expect( asType( 0, 'boolean' ) ).toBe( false ); - - expect( asType( null, 'null' ) ).toBe( null ); - expect( asType( 0, 'null' ) ).toBe( null ); - - expect( asType( arr, 'array' ) ).toBe( arr ); - expect( asType( new Set( [ 1, 2, 3 ] ), 'array' ) ).toEqual( [ 1, 2, 3 ] ); - - expect( asType( obj, 'object' ) ).toBe( obj ); - expect( asType( {}, 'object' ) ).toEqual( {} ); - } ); - } ); - describe( 'isOfType()', () => { it( 'gracefully handles unhandled type', () => { expect( isOfType( 5, '__UNHANDLED__' ) ).toBe( true ); @@ -282,6 +243,32 @@ describe( 'block parser', () => { ); expect( value ).toBe( false ); } ); + + describe( 'source: tag', () => { + it( 'returns tag name of matching selector', () => { + const value = parseWithAttributeSchema( + '
', + { + source: 'tag', + selector: ':nth-child(1)', + } + ); + + expect( value ).toBe( 'div' ); + } ); + + it( 'returns undefined when no element matches selector', () => { + const value = parseWithAttributeSchema( + '
', + { + source: 'tag', + selector: ':nth-child(2)', + } + ); + + expect( value ).toBe( undefined ); + } ); + } ); } ); describe( 'getBlockAttribute', () => { diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index b54dcec4b412bc..a2ce1b818505e4 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -1,9 +1,12 @@ -## Next release +## Master ### New Features - Added a new `popoverProps` prop to the `Dropdown` component which allows users of the `Dropdown` component to pass props directly to the `PopOver` component. +### Bug Fixes + +- The `Button` component will no longer assign default styling (`is-default` class) when explicitly assigned as primary (the `isPrimary` prop). This should resolve potential conflicts affecting a combination of `isPrimary`, `isDefault`, and `isLarge` / `isSmall`, where the busy animation would appear with incorrect coloring. ## 8.0.0 (2019-06-12) diff --git a/packages/components/package.json b/packages/components/package.json index 99c44a249b7e42..f82a996cda1e65 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -40,7 +40,7 @@ "memize": "^1.0.5", "moment": "^2.22.1", "mousetrap": "^1.6.2", - "re-resizable": "^4.7.1", + "re-resizable": "^5.0.1", "react-click-outside": "^3.0.0", "react-dates": "^17.1.1", "react-spring": "^8.0.20", diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js index 3d6f9a2cb251c8..da3441e7f2982a 100644 --- a/packages/components/src/button/index.js +++ b/packages/components/src/button/index.js @@ -28,7 +28,7 @@ export function Button( props, ref ) { const classes = classnames( 'components-button', className, { 'is-button': isDefault || isPrimary || isLarge || isSmall, - 'is-default': isDefault || isLarge || isSmall, + 'is-default': isDefault || ( ! isPrimary && ( isLarge || isSmall ) ), 'is-primary': isPrimary, 'is-large': isLarge, 'is-small': isSmall, diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss index 69776322ede4ee..ba13650e492dab 100644 --- a/packages/components/src/button/style.scss +++ b/packages/components/src/button/style.scss @@ -90,7 +90,7 @@ box-shadow: inset 0 -1px 0 color(theme(button) shade(50%)), 0 0 0 1px $white, - 0 0 0 3px $blue-medium-focus; + 0 0 0 3px color(theme(button) shade(5%)); } &:active:enabled { diff --git a/packages/components/src/button/test/index.js b/packages/components/src/button/test/index.js index 6140b00b56b7c5..05e3580bfacbe6 100644 --- a/packages/components/src/button/test/index.js +++ b/packages/components/src/button/test/index.js @@ -27,14 +27,14 @@ describe( 'Button', () => { expect( button.type() ).toBe( 'button' ); } ); - it( 'should render a button element with button-primary class', () => { + it( 'should render a button element with is-primary class', () => { const button = shallow(
Sidebar title plugin
"`; diff --git a/packages/e2e-tests/specs/plugins/plugins-api.test.js b/packages/e2e-tests/specs/plugins/plugins-api.test.js index d264bb736ed082..904fcaf0914e0d 100644 --- a/packages/e2e-tests/specs/plugins/plugins-api.test.js +++ b/packages/e2e-tests/specs/plugins/plugins-api.test.js @@ -76,4 +76,12 @@ describe( 'Using Plugins API', () => { expect( pluginSidebarClosed ).toBeNull(); } ); } ); + + describe( 'Document Setting Custom Panel', () => { + it( 'Should render a custom panel inside Document Setting sidebar', async () => { + await openDocumentSettingsSidebar(); + const pluginDocumentSettingsText = await page.$eval( '.edit-post-sidebar .my-document-setting-plugin', ( el ) => el.innerText ); + expect( pluginDocumentSettingsText ).toMatchSnapshot(); + } ); + } ); } ); diff --git a/packages/edit-post/README.md b/packages/edit-post/README.md index 4c2c8e46b2cfbc..6b895c79e75fb5 100644 --- a/packages/edit-post/README.md +++ b/packages/edit-post/README.md @@ -100,6 +100,60 @@ _Returns_ - `WPElement`: The WPElement to be rendered. +# **PluginDocumentSettingPanel** + +Renders items below the Status & Availability panel in the Document Sidebar. + +_Usage_ + +```js +// Using ES5 syntax +var el = wp.element.createElement; +var __ = wp.i18n.__; +var registerPlugin = wp.plugins.registerPlugin; +var PluginDocumentSettingPanel = wp.editPost.PluginDocumentSettingPanel; + +function MyDocumentSettingPlugin() { + return el( + PluginDocumentSettingPanel, + { + className: 'my-document-setting-plugin', + }, + __( 'My Document Setting Panel' ) + ); +} + +registerPlugin( 'my-document-setting-plugin', { + render: MyDocumentSettingPlugin +} ); +``` + +```jsx +// Using ESNext syntax +const { registerPlugin } = wp.plugins; +const { PluginDocumentSettingPanel } = wp.editPost; + +const MyDocumentSettingTest = () => ( + +

My Document Setting Panel

+ +); + + registerPlugin( 'document-setting-test', { render: MyDocumentSettingTest } ); +``` + +_Parameters_ + +- _props_ `Object`: Component properties. +- _props.name_ `[string]`: The machine-friendly name for the panel. +- _props.className_ `[string]`: An optional class name added to the row. +- _props.title_ `[string]`: The title of the panel +- _props.icon_ `[(string|Element)]`: The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element, to be rendered when the sidebar is pinned to toolbar. + +_Returns_ + +- `WPElement`: The WPElement to be rendered. + # **PluginMoreMenuItem** Renders a menu item in `Plugins` group in `More Menu` drop down, and can be used to as a button or link depending on the props provided. diff --git a/packages/edit-post/src/components/sidebar/plugin-document-setting-panel/index.js b/packages/edit-post/src/components/sidebar/plugin-document-setting-panel/index.js new file mode 100644 index 00000000000000..a4ba4b924273ba --- /dev/null +++ b/packages/edit-post/src/components/sidebar/plugin-document-setting-panel/index.js @@ -0,0 +1,106 @@ +/** + * Defines as extensibility slot for the Settings sidebar + */ + +/** + * WordPress dependencies + */ +import { createSlotFill, PanelBody } from '@wordpress/components'; +import { compose } from '@wordpress/compose'; +import { withPluginContext } from '@wordpress/plugins'; +import { withDispatch, withSelect } from '@wordpress/data'; + +export const { Fill, Slot } = createSlotFill( 'PluginDocumentSettingPanel' ); + +const PluginDocumentSettingFill = ( { isEnabled, opened, onToggle, className, title, icon, children } ) => { + if ( ! isEnabled ) { + return null; + } + return ( + + + { children } + + + ); +}; + +/** + * Renders items below the Status & Availability panel in the Document Sidebar. + * + * @param {Object} props Component properties. + * @param {string} [props.name] The machine-friendly name for the panel. + * @param {string} [props.className] An optional class name added to the row. + * @param {string} [props.title] The title of the panel + * @param {string|Element} [props.icon=inherits from the plugin] The [Dashicon](https://developer.wordpress.org/resource/dashicons/) icon slug string, or an SVG WP element, to be rendered when the sidebar is pinned to toolbar. + * + * @example ES5 + * ```js + * // Using ES5 syntax + * var el = wp.element.createElement; + * var __ = wp.i18n.__; + * var registerPlugin = wp.plugins.registerPlugin; + * var PluginDocumentSettingPanel = wp.editPost.PluginDocumentSettingPanel; + * + * function MyDocumentSettingPlugin() { + * return el( + * PluginDocumentSettingPanel, + * { + * className: 'my-document-setting-plugin', + * }, + * __( 'My Document Setting Panel' ) + * ); + * } + * + * registerPlugin( 'my-document-setting-plugin', { + * render: MyDocumentSettingPlugin + * } ); + * ``` + * + * @example ESNext + * ```jsx + * // Using ESNext syntax + * const { registerPlugin } = wp.plugins; + * const { PluginDocumentSettingPanel } = wp.editPost; + * + * const MyDocumentSettingTest = () => ( + * + *

My Document Setting Panel

+ * + * ); + * + * registerPlugin( 'document-setting-test', { render: MyDocumentSettingTest } ); + * ``` + * + * @return {WPElement} The WPElement to be rendered. + */ +const PluginDocumentSettingPanel = compose( + withPluginContext( ( context, ownProps ) => { + return { + icon: ownProps.icon || context.icon, + panelName: `${ context.name }/${ ownProps.name }`, + }; + } ), + withSelect( ( select, { panelName } ) => { + return ( + { + opened: select( 'core/edit-post' ).isEditorPanelOpened( panelName ), + isEnabled: select( 'core/edit-post' ).isEditorPanelEnabled( panelName ), + } + ); + } ), + withDispatch( ( dispatch, { panelName } ) => ( { + onToggle() { + return dispatch( 'core/edit-post' ).toggleEditorPanelOpened( panelName ); + }, + } ) ), +)( PluginDocumentSettingFill ); + +PluginDocumentSettingPanel.Slot = Slot; +export default PluginDocumentSettingPanel; diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index 81e8a0ba87eb03..49bb45eec0ce38 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -20,6 +20,7 @@ import PostLink from '../post-link'; import DiscussionPanel from '../discussion-panel'; import PageAttributes from '../page-attributes'; import MetaBoxes from '../../meta-boxes'; +import PluginDocumentSettingPanel from '../plugin-document-setting-panel'; const SettingsSidebar = ( { sidebarName } ) => ( ( { sidebarName === 'edit-post/document' && ( <> + diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 66d8c753b46ad4..8027695313a6f0 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -88,6 +88,7 @@ export function initializeEditor( id, postType, postId, settings, initialEdits ) } export { default as PluginBlockSettingsMenuItem } from './components/block-settings-menu/plugin-block-settings-menu-item'; +export { default as PluginDocumentSettingPanel } from './components/sidebar/plugin-document-setting-panel'; export { default as PluginMoreMenuItem } from './components/header/plugin-more-menu-item'; export { default as PluginPostPublishPanel } from './components/sidebar/plugin-post-publish-panel'; export { default as PluginPostStatusInfo } from './components/sidebar/plugin-post-status-info'; diff --git a/packages/edit-post/src/index.native.js b/packages/edit-post/src/index.native.js index 1ffdcb13477335..1240c84ccb7e2f 100644 --- a/packages/edit-post/src/index.native.js +++ b/packages/edit-post/src/index.native.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { Platform } from 'react-native'; + /** * WordPress dependencies */ @@ -31,6 +36,11 @@ export function initializeEditor() { // eslint-disable-next-line no-undef if ( typeof __DEV__ === 'undefined' || ! __DEV__ ) { unregisterBlockType( 'core/code' ); + + // Disable Video block except for iOS for now. + if ( Platform.OS !== 'ios' ) { + unregisterBlockType( 'core/video' ); + } } blocksRegistered = true; diff --git a/packages/edit-post/src/plugins/copy-content-menu-item/index.js b/packages/edit-post/src/plugins/copy-content-menu-item/index.js index af93ecb542ba6e..a46355cde7f969 100644 --- a/packages/edit-post/src/plugins/copy-content-menu-item/index.js +++ b/packages/edit-post/src/plugins/copy-content-menu-item/index.js @@ -8,27 +8,29 @@ import { withState, compose } from '@wordpress/compose'; function CopyContentMenuItem( { createNotice, editedPostContent, hasCopied, setState } ) { return ( - { - setState( { hasCopied: true } ); - createNotice( - 'info', - 'All content copied.', - { - isDismissible: true, - type: 'snackbar', - } - ); - } } - onFinishCopy={ () => setState( { hasCopied: false } ) } - > - { hasCopied ? - __( 'Copied!' ) : - __( 'Copy All Content' ) } - + editedPostContent.length > 0 && ( + { + setState( { hasCopied: true } ); + createNotice( + 'info', + 'All content copied.', + { + isDismissible: true, + type: 'snackbar', + } + ); + } } + onFinishCopy={ () => setState( { hasCopied: false } ) } + > + { hasCopied ? + __( 'Copied!' ) : + __( 'Copy All Content' ) } + + ) ); } diff --git a/packages/editor/src/components/convert-to-group-buttons/convert-button.js b/packages/editor/src/components/convert-to-group-buttons/convert-button.js index 6a7299ed259c98..2aa2fc9dcd6b19 100644 --- a/packages/editor/src/components/convert-to-group-buttons/convert-button.js +++ b/packages/editor/src/components/convert-to-group-buttons/convert-button.js @@ -72,8 +72,8 @@ export default compose( [ ! isSingleContainerBlock ); - // Do we have a single Group Block selected? - const isUngroupable = isSingleContainerBlock; + // Do we have a single Group Block selected and does that group have inner blocks? + const isUngroupable = isSingleContainerBlock && !! blocksSelection[ 0 ].innerBlocks.length; return { isGroupable, diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index 0418f9b17c6062..ff3bedf5a79fb5 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -92,7 +92,6 @@ export class PostPublishButton extends Component { 'aria-disabled': isButtonDisabled, className: 'editor-post-publish-button', isBusy: isSaving && isPublished, - isLarge: true, isPrimary: true, onClick: onClickButton, }; diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 0e959f80c7b866..cc9533a0a79a7b 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -11,7 +11,7 @@ import { compose } from '@wordpress/compose'; import { Component } from '@wordpress/element'; import { withDispatch, withSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { BlockEditorProvider, __experimentalTransformStyles } from '@wordpress/block-editor'; +import { BlockEditorProvider, transformStyles } from '@wordpress/block-editor'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; import { decodeEntities } from '@wordpress/html-entities'; @@ -112,7 +112,7 @@ class EditorProvider extends Component { return; } - const updatedStyles = __experimentalTransformStyles( this.props.settings.styles, '.editor-styles-wrapper' ); + const updatedStyles = transformStyles( this.props.settings.styles, '.editor-styles-wrapper' ); map( updatedStyles, ( updatedCSS ) => { if ( updatedCSS ) { diff --git a/packages/editor/src/index.js b/packages/editor/src/index.js index eb54a38859b135..67de1923a013d9 100644 --- a/packages/editor/src/index.js +++ b/packages/editor/src/index.js @@ -22,4 +22,4 @@ export { storeConfig } from './store'; /* * Backward compatibility */ -export { __experimentalTransformStyles as transformStyles } from '@wordpress/block-editor'; +export { transformStyles } from '@wordpress/block-editor'; diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index ab698ea82a7315..dd78100a4385db 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -337,7 +337,7 @@ describe( 'Post generator actions', () => { dispatch( 'core/notices', 'createErrorNotice', - ...[ 'Updating failed', { id: 'SAVE_POST_NOTICE_ID' } ] + ...[ 'Updating failed.', { id: 'SAVE_POST_NOTICE_ID' } ] ) ); }, diff --git a/packages/editor/src/store/utils/notice-builder.js b/packages/editor/src/store/utils/notice-builder.js index 9732cff6fa7cfd..eb8172e9801dda 100644 --- a/packages/editor/src/store/utils/notice-builder.js +++ b/packages/editor/src/store/utils/notice-builder.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -97,14 +97,19 @@ export function getNotificationArgumentsForSaveFail( data ) { // If the post was being published, we show the corresponding publish error message // Unless we publish an "updating failed" message const messages = { - publish: __( 'Publishing failed' ), - private: __( 'Publishing failed' ), - future: __( 'Scheduling failed' ), + publish: __( 'Publishing failed.' ), + private: __( 'Publishing failed.' ), + future: __( 'Scheduling failed.' ), }; - const noticeMessage = ! isPublished && publishStatus.indexOf( edits.status ) !== -1 ? + let noticeMessage = ! isPublished && publishStatus.indexOf( edits.status ) !== -1 ? messages[ edits.status ] : - __( 'Updating failed' ); + __( 'Updating failed.' ); + // Check if message string contains HTML. Notice text is currently only + // supported as plaintext, and stripping the tags may muddle the meaning. + if ( error.message && ! ( /<\/?[^>]*>/.test( error.message ) ) ) { + noticeMessage = sprintf( __( '%1$s Error message: %2$s' ), noticeMessage, error.message ); + } return [ noticeMessage, { id: SAVE_POST_NOTICE_ID, } ]; diff --git a/packages/editor/src/store/utils/test/notice-builder.js b/packages/editor/src/store/utils/test/notice-builder.js index e67215113c1f19..3e6abab360bca7 100644 --- a/packages/editor/src/store/utils/test/notice-builder.js +++ b/packages/editor/src/store/utils/test/notice-builder.js @@ -93,7 +93,7 @@ describe( 'getNotificationArgumentsForSaveSuccess()', () => { } ); } ); describe( 'getNotificationArgumentsForSaveFail()', () => { - const error = { code: '42' }; + const error = { code: '42', message: 'Something went wrong.' }; const post = { status: 'publish' }; const edits = { status: 'publish' }; const defaultExpectedAction = { id: SAVE_POST_NOTICE_ID }; @@ -108,25 +108,25 @@ describe( 'getNotificationArgumentsForSaveFail()', () => { 'when post is not published and edits is published', '', [ 'draft', 'publish' ], - [ 'Publishing failed', defaultExpectedAction ], + [ 'Publishing failed. Error message: Something went wrong.', defaultExpectedAction ], ], [ 'when post is published and edits is privately published', '', [ 'draft', 'private' ], - [ 'Publishing failed', defaultExpectedAction ], + [ 'Publishing failed. Error message: Something went wrong.', defaultExpectedAction ], ], [ 'when post is published and edits is scheduled to be published', '', [ 'draft', 'future' ], - [ 'Scheduling failed', defaultExpectedAction ], + [ 'Scheduling failed. Error message: Something went wrong.', defaultExpectedAction ], ], [ 'when post is published and edits is published', '', [ 'publish', 'publish' ], - [ 'Updating failed', defaultExpectedAction ], + [ 'Updating failed. Error message: Something went wrong.', defaultExpectedAction ], ], ].forEach( ( [ description, diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 28de7846b5f212..abe9728d926313 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -4,6 +4,7 @@ - Fixed custom regular expression for the `no-restricted-syntax` rule enforcing translate function arguments. [#15839](https://github.com/WordPress/gutenberg/pull/15839). - Fixed arguments checking of `_nx` for the `no-restricted-syntax` rule enforcing translate function arguments. [#15839](https://github.com/WordPress/gutenberg/pull/15839). +- Fixed false positive with `react-no-unsafe-timeout` which would wrongly flag errors when assigning `setTimeout` result to a variable (for example, in a `useEffect` hook). ## 2.2.0 (2019-05-21) diff --git a/packages/eslint-plugin/rules/__tests__/react-no-unsafe-timeout.js b/packages/eslint-plugin/rules/__tests__/react-no-unsafe-timeout.js index e6780e0332c48b..85046a73b56104 100644 --- a/packages/eslint-plugin/rules/__tests__/react-no-unsafe-timeout.js +++ b/packages/eslint-plugin/rules/__tests__/react-no-unsafe-timeout.js @@ -40,6 +40,18 @@ ruleTester.run( 'react-no-unsafe-timeout', rule, { { code: `class MyComponent extends Component { componentDidMount() { this.timeoutId = setTimeout(); } }`, }, + { + code: ` +function MyComponent() { + useEffect( () => { + const timeoutHandle = setTimeout( () => {} ); + + return () => clearTimeout( timeoutHandle ); + }, [] ); + + return null; +}`, + }, ], invalid: [ { diff --git a/packages/eslint-plugin/rules/react-no-unsafe-timeout.js b/packages/eslint-plugin/rules/react-no-unsafe-timeout.js index c211007a6ccb13..64ff537199caac 100644 --- a/packages/eslint-plugin/rules/react-no-unsafe-timeout.js +++ b/packages/eslint-plugin/rules/react-no-unsafe-timeout.js @@ -48,7 +48,10 @@ module.exports = { // If the result of a `setTimeout` call is assigned to a // variable, assume the timer ID is handled by a cancellation. - const hasAssignment = node.parent.type === 'AssignmentExpression'; + const hasAssignment = ( + node.parent.type === 'AssignmentExpression' || + node.parent.type === 'VariableDeclarator' + ); if ( hasAssignment ) { return; } diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 721061946e221b..ccd1c1ee78a4fb 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -29,7 +29,7 @@ "lodash": "^4.17.11", "memize": "^1.0.5", "sprintf-js": "^1.1.1", - "tannin": "^1.0.1" + "tannin": "^1.1.0" }, "publishConfig": { "access": "public" diff --git a/packages/i18n/src/index.js b/packages/i18n/src/index.js index 50869f49efd395..dc179517312014 100644 --- a/packages/i18n/src/index.js +++ b/packages/i18n/src/index.js @@ -13,7 +13,7 @@ import sprintfjs from 'sprintf-js'; */ const DEFAULT_LOCALE_DATA = { '': { - plural_forms: 'plural=(n!=1)', + plural_forms: ( n ) => n === 1 ? 0 : 1, }, }; diff --git a/packages/is-shallow-equal/CHANGELOG.md b/packages/is-shallow-equal/CHANGELOG.md index 76e2a7317c2245..df25b00178ac17 100644 --- a/packages/is-shallow-equal/CHANGELOG.md +++ b/packages/is-shallow-equal/CHANGELOG.md @@ -1,3 +1,9 @@ +## Master + +### Bug Fixes + +- Resolved an issue where an explicit `undefined` value in the first object may wrongly report as being shallow equal when the two objects are otherwise of equal length. ([#16329](https://github.com/WordPress/gutenberg/pull/16329)) + ## 1.2.0 (2019-03-06) ### New Feature diff --git a/packages/is-shallow-equal/README.md b/packages/is-shallow-equal/README.md index 3c53ed3261e445..04421cfa1408f2 100644 --- a/packages/is-shallow-equal/README.md +++ b/packages/is-shallow-equal/README.md @@ -68,56 +68,56 @@ In particular, it should… ## Benchmarks -The following results were produced under Node v8.11.1 (LTS) on a MacBook Pro (Late 2016) 2.9 GHz Intel Core i7. - ->`@wordpress/is-shallow-equal (type specific) (object, equal) x 4,902,162 ops/sec ±0.40% (89 runs sampled)` ->`@wordpress/is-shallow-equal (type specific) (object, same) x 558,234,287 ops/sec ±0.28% (92 runs sampled)` ->`@wordpress/is-shallow-equal (type specific) (object, unequal) x 5,062,890 ops/sec ±0.71% (90 runs sampled)` ->`@wordpress/is-shallow-equal (type specific) (array, equal) x 70,419,519 ops/sec ±0.56% (86 runs sampled)` ->`@wordpress/is-shallow-equal (type specific) (array, same) x 561,159,444 ops/sec ±0.33% (91 runs sampled)` ->`@wordpress/is-shallow-equal (type specific) (array, unequal) x 37,299,061 ops/sec ±0.89% (86 runs sampled)` +The following results were produced under Node v10.15.3 (LTS) on a MacBook Pro (Late 2016) 2.9 GHz Intel Core i7. + +>`@wordpress/is-shallow-equal (type specific) (object, equal) x 4,519,009 ops/sec ±1.09% (90 runs sampled)` +>`@wordpress/is-shallow-equal (type specific) (object, same) x 795,527,700 ops/sec ±0.24% (93 runs sampled)` +>`@wordpress/is-shallow-equal (type specific) (object, unequal) x 4,841,640 ops/sec ±0.94% (93 runs sampled)` +>`@wordpress/is-shallow-equal (type specific) (array, equal) x 106,393,795 ops/sec ±0.16% (94 runs sampled)` +>`@wordpress/is-shallow-equal (type specific) (array, same) x 800,741,511 ops/sec ±0.22% (95 runs sampled)` +>`@wordpress/is-shallow-equal (type specific) (array, unequal) x 49,178,977 ops/sec ±1.99% (82 runs sampled)` > ->`@wordpress/is-shallow-equal (object, equal) x 4,449,938 ops/sec ±0.34% (91 runs sampled)` ->`@wordpress/is-shallow-equal (object, same) x 516,101,448 ops/sec ±0.64% (90 runs sampled)` ->`@wordpress/is-shallow-equal (object, unequal) x 4,925,231 ops/sec ±0.28% (91 runs sampled)` ->`@wordpress/is-shallow-equal (array, equal) x 30,432,490 ops/sec ±0.80% (86 runs sampled)` ->`@wordpress/is-shallow-equal (array, same) x 505,206,883 ops/sec ±0.37% (93 runs sampled)` ->`@wordpress/is-shallow-equal (array, unequal) x 33,590,955 ops/sec ±0.96% (86 runs sampled)` +>`@wordpress/is-shallow-equal (object, equal) x 4,449,367 ops/sec ±0.31% (91 runs sampled)` +>`@wordpress/is-shallow-equal (object, same) x 796,677,179 ops/sec ±0.23% (94 runs sampled)` +>`@wordpress/is-shallow-equal (object, unequal) x 4,989,529 ops/sec ±0.30% (91 runs sampled)` +>`@wordpress/is-shallow-equal (array, equal) x 44,840,546 ops/sec ±1.18% (89 runs sampled)` +>`@wordpress/is-shallow-equal (array, same) x 794,344,723 ops/sec ±0.24% (91 runs sampled)` +>`@wordpress/is-shallow-equal (array, unequal) x 49,860,115 ops/sec ±1.73% (85 runs sampled)` > ->`shallowequal (object, equal) x 3,407,788 ops/sec ±0.46% (93 runs sampled)` ->`shallowequal (object, same) x 494,715,603 ops/sec ±0.42% (91 runs sampled)` ->`shallowequal (object, unequal) x 3,575,393 ops/sec ±0.54% (93 runs sampled)` ->`shallowequal (array, equal) x 1,530,453 ops/sec ±0.32% (92 runs sampled)` ->`shallowequal (array, same) x 489,793,575 ops/sec ±0.60% (90 runs sampled)` ->`shallowequal (array, unequal) x 1,534,574 ops/sec ±0.32% (90 runs sampled)` +>`shallowequal (object, equal) x 3,702,126 ops/sec ±0.87% (92 runs sampled)` +>`shallowequal (object, same) x 796,649,597 ops/sec ±0.21% (92 runs sampled)` +>`shallowequal (object, unequal) x 4,027,885 ops/sec ±0.31% (96 runs sampled)` +>`shallowequal (array, equal) x 1,684,977 ops/sec ±0.37% (94 runs sampled)` +>`shallowequal (array, same) x 794,287,091 ops/sec ±0.26% (91 runs sampled)` +>`shallowequal (array, unequal) x 1,738,554 ops/sec ±0.29% (91 runs sampled)` > ->`shallow-equal (type specific) (object, equal) x 4,708,043 ops/sec ±0.30% (92 runs sampled)` ->`shallow-equal (type specific) (object, same) x 537,831,873 ops/sec ±0.42% (88 runs sampled)` ->`shallow-equal (type specific) (object, unequal) x 4,859,249 ops/sec ±0.28% (90 runs sampled)` ->`shallow-equal (type specific) (array, equal) x 63,985,372 ops/sec ±0.54% (91 runs sampled)` ->`shallow-equal (type specific) (array, same) x 540,675,335 ops/sec ±0.43% (89 runs sampled)` ->`shallow-equal (type specific) (array, unequal) x 34,613,490 ops/sec ±0.81% (90 runs sampled)` +>`shallow-equal (type specific) (object, equal) x 4,669,656 ops/sec ±0.34% (92 runs sampled)` +>`shallow-equal (type specific) (object, same) x 799,610,214 ops/sec ±0.20% (95 runs sampled)` +>`shallow-equal (type specific) (object, unequal) x 4,908,591 ops/sec ±0.49% (93 runs sampled)` +>`shallow-equal (type specific) (array, equal) x 104,711,254 ops/sec ±0.65% (91 runs sampled)` +>`shallow-equal (type specific) (array, same) x 798,454,281 ops/sec ±0.29% (94 runs sampled)` +>`shallow-equal (type specific) (array, unequal) x 48,764,338 ops/sec ±1.48% (84 runs sampled)` > ->`is-equal-shallow (object, equal) x 2,798,059 ops/sec ±0.42% (93 runs sampled)` ->`is-equal-shallow (object, same) x 2,844,934 ops/sec ±0.39% (93 runs sampled)` ->`is-equal-shallow (object, unequal) x 3,223,288 ops/sec ±0.57% (92 runs sampled)` ->`is-equal-shallow (array, equal) x 1,060,093 ops/sec ±0.32% (93 runs sampled)` ->`is-equal-shallow (array, same) x 1,058,977 ops/sec ±0.31% (94 runs sampled)` ->`is-equal-shallow (array, unequal) x 1,697,517 ops/sec ±0.28% (91 runs sampled)` +>`is-equal-shallow (object, equal) x 5,068,750 ops/sec ±0.28% (92 runs sampled)` +>`is-equal-shallow (object, same) x 17,231,997 ops/sec ±0.42% (92 runs sampled)` +>`is-equal-shallow (object, unequal) x 5,524,878 ops/sec ±0.41% (92 runs sampled)` +>`is-equal-shallow (array, equal) x 1,067,063 ops/sec ±0.40% (92 runs sampled)` +>`is-equal-shallow (array, same) x 1,074,356 ops/sec ±0.20% (94 runs sampled)` +>`is-equal-shallow (array, unequal) x 1,758,859 ops/sec ±0.44% (92 runs sampled)` > ->`shallow-equals (object, equal) x 4,457,325 ops/sec ±0.40% (92 runs sampled)` ->`shallow-equals (object, same) x 4,509,250 ops/sec ±0.48% (92 runs sampled)` ->`shallow-equals (object, unequal) x 4,856,327 ops/sec ±0.41% (94 runs sampled)` ->`shallow-equals (array, equal) x 44,915,371 ops/sec ±2.18% (79 runs sampled)` ->`shallow-equals (array, same) x 38,514,418 ops/sec ±1.25% (83 runs sampled)` ->`shallow-equals (array, unequal) x 24,319,893 ops/sec ±0.96% (84 runs sampled)` +>`shallow-equals (object, equal) x 8,380,550 ops/sec ±0.31% (90 runs sampled)` +>`shallow-equals (object, same) x 27,583,073 ops/sec ±0.60% (91 runs sampled)` +>`shallow-equals (object, unequal) x 8,954,268 ops/sec ±0.71% (92 runs sampled)` +>`shallow-equals (array, equal) x 104,437,640 ops/sec ±0.22% (96 runs sampled)` +>`shallow-equals (array, same) x 141,850,542 ops/sec ±0.25% (93 runs sampled)` +>`shallow-equals (array, unequal) x 47,964,211 ops/sec ±1.51% (84 runs sampled)` > ->`fbjs/lib/shallowEqual (object, equal) x 3,388,692 ops/sec ±0.72% (92 runs sampled)` ->`fbjs/lib/shallowEqual (object, same) x 139,559,732 ops/sec ±4.45% (32 runs sampled)` ->`fbjs/lib/shallowEqual (object, unequal) x 3,480,571 ops/sec ±0.51% (90 runs sampled)` ->`fbjs/lib/shallowEqual (array, equal) x 1,517,044 ops/sec ±0.42% (91 runs sampled)` ->`fbjs/lib/shallowEqual (array, same) x 134,032,009 ops/sec ±2.82% (46 runs sampled)` ->`fbjs/lib/shallowEqual (array, unequal) x 1,532,376 ops/sec ±0.41% (91 runs sampled)` +>`fbjs/lib/shallowEqual (object, equal) x 3,366,709 ops/sec ±0.35% (93 runs sampled)` +>`fbjs/lib/shallowEqual (object, same) x 794,825,194 ops/sec ±0.24% (94 runs sampled)` +>`fbjs/lib/shallowEqual (object, unequal) x 3,612,268 ops/sec ±0.37% (94 runs sampled)` +>`fbjs/lib/shallowEqual (array, equal) x 1,613,800 ops/sec ±0.23% (90 runs sampled)` +>`fbjs/lib/shallowEqual (array, same) x 794,861,384 ops/sec ±0.24% (93 runs sampled)` +>`fbjs/lib/shallowEqual (array, unequal) x 1,648,398 ops/sec ±0.77% (92 runs sampled)` You can run the benchmarks yourselves by cloning the repository, installing dependencies, and running the `benchmark/index.js` script: diff --git a/packages/is-shallow-equal/objects.js b/packages/is-shallow-equal/objects.js index 12df62d2a04075..ebe1105b196156 100644 --- a/packages/is-shallow-equal/objects.js +++ b/packages/is-shallow-equal/objects.js @@ -11,7 +11,7 @@ var keys = Object.keys; * @return {boolean} Whether the two objects are shallow equal. */ function isShallowEqualObjects( a, b ) { - var aKeys, bKeys, i, key; + var aKeys, bKeys, i, key, aValue; if ( a === b ) { return true; @@ -28,7 +28,17 @@ function isShallowEqualObjects( a, b ) { while ( i < aKeys.length ) { key = aKeys[ i ]; - if ( a[ key ] !== b[ key ] ) { + aValue = a[ key ]; + + if ( + // In iterating only the keys of the first object after verifying + // equal lengths, account for the case that an explicit `undefined` + // value in the first is implicitly undefined in the second. + // + // Example: isShallowEqualObjects( { a: undefined }, { b: 5 } ) + ( aValue === undefined && ! b.hasOwnProperty( key ) ) || + aValue !== b[ key ] + ) { return false; } diff --git a/packages/is-shallow-equal/test/index.js b/packages/is-shallow-equal/test/index.js index 8a7e80cf02156d..3ec86ed3529287 100644 --- a/packages/is-shallow-equal/test/index.js +++ b/packages/is-shallow-equal/test/index.js @@ -46,9 +46,9 @@ describe( 'isShallowEqual', () => { expect( isShallowEqual( b, a ) ).toBe( false ); } ); - it( 'returns false if b object has different key than a', () => { - const a = { foo: 1, baz: 2 }; - const b = { foo: 1, bar: 2 }; + it( 'returns false if a object has undefined key not in b', () => { + const a = { foo: undefined }; + const b = { bar: 2 }; expect( isShallowEqual( a, b ) ).toBe( false ); expect( isShallowEqual( b, a ) ).toBe( false ); diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 6b1456e982e296..1a8d5adb1183ba 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -35,10 +35,6 @@ ./packages/block-serialization-spec-parser/parser.php ./build - - lib/class-wp-rest-block-renderer-controller.php - lib/class-wp-rest-search-controller.php - gutenberg.php @@ -71,13 +67,13 @@ - + ./packages/block-serialization-default-parser/parser.php - + ./packages/block-serialization-default-parser/parser.php - + ./packages/block-serialization-default-parser/parser.php