diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 00000000..77635e31 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,38 @@ +# Edit Flow + +Editorial workflow plugin with custom statuses, editorial comments, and notifications. + +## Plugin Details + +| Property | Value | +|----------|-------| +| **Main file** | `edit_flow.php` | +| **Text domain** | `edit-flow` | +| **Function prefix** | `ef_` | +| **Namespace** | Global (legacy) | +| **Source directory** | `modules/` | +| **Version** | 0.10.3 | + +## Architecture + +- Modular architecture in `modules/` directory +- Each feature is a separate module +- Main class: `edit_flow` (note underscore in filename) + +## Testing + +```bash +composer test:unit # Unit tests +composer test:integration # Integration tests +npm run test-e2e # Playwright E2E tests +``` + +## Notes + +- Tier 1 plugin (well-maintained) +- WordPress.org hosted +- Has E2E tests with Playwright + +## Standards + +Follow the standards documented in `~/code/plugin-standards/`. diff --git a/.github/workflows/e2e-and-js-tests.yml b/.github/workflows/e2e-tests.yml similarity index 81% rename from .github/workflows/e2e-and-js-tests.yml rename to .github/workflows/e2e-tests.yml index ba3a1f17..ca78870c 100644 --- a/.github/workflows/e2e-and-js-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -1,11 +1,36 @@ -name: E2E and JS tests +name: E2E Tests on: - pull_request: push: - branches-ignore: - - develop - - main + paths: + - '**.js' + - '**.jsx' + - '**.ts' + - '**.tsx' + - '**.scss' + - '**.css' + - 'package.json' + - 'package-lock.json' + - 'webpack.config.js' + - '.wp-env.json' + - 'tests/e2e/**' + - '.github/workflows/e2e-tests.yml' + pull_request: + branches: [develop, main] + paths: + - '**.js' + - '**.jsx' + - '**.ts' + - '**.tsx' + - '**.scss' + - '**.css' + - 'package.json' + - 'package-lock.json' + - 'webpack.config.js' + - '.wp-env.json' + - 'tests/e2e/**' + - '.github/workflows/e2e-tests.yml' + workflow_dispatch: # Disable all permissions by default; grant minimal permissions per job permissions: {} @@ -16,7 +41,7 @@ concurrency: jobs: build: - name: Build and Lint + name: Build runs-on: ubuntu-latest permissions: contents: read @@ -60,9 +85,6 @@ jobs: echo "| custom-status-block.js | $(du -h build/custom-status-block.js | cut -f1) |" >> $GITHUB_STEP_SUMMARY echo "| calendar-react.js | $(du -h build/calendar-react.js | cut -f1) |" >> $GITHUB_STEP_SUMMARY - - name: Run Lint JS - run: npm run lint-js - - name: Upload build artifacts uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: @@ -71,7 +93,7 @@ jobs: retention-days: 1 test: - name: E2E and Jest tests + name: E2E Tests # Pin to ubuntu-22.04 for Playwright compatibility # ubuntu-latest (24.04) has library version mismatches with Playwright's WebKit dependencies runs-on: ubuntu-22.04 @@ -106,5 +128,5 @@ jobs: - name: Install WordPress with wp-env run: npm run wp-env start - - name: Run tests - run: npm run test + - name: Run E2E tests + run: npm run test-e2e diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 57480671..67a804a7 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -1,11 +1,24 @@ name: Integration Tests on: - pull_request: push: - branches-ignore: - - develop - - main + paths: + - '**.php' + - 'composer.json' + - 'composer.lock' + - 'phpunit.xml.dist' + - '.wp-env.json' + - '.github/workflows/integration.yml' + pull_request: + branches: [develop, main] + paths: + - '**.php' + - 'composer.json' + - 'composer.lock' + - 'phpunit.xml.dist' + - '.wp-env.json' + - '.github/workflows/integration.yml' + workflow_dispatch: # Disable all permissions by default; grant minimal permissions per job permissions: {} diff --git a/.github/workflows/js-tests.yml b/.github/workflows/js-tests.yml new file mode 100644 index 00000000..f185470f --- /dev/null +++ b/.github/workflows/js-tests.yml @@ -0,0 +1,60 @@ +name: JS Tests + +on: + push: + paths: + - '**.js' + - '**.jsx' + - '**.ts' + - '**.tsx' + - 'package.json' + - 'package-lock.json' + - 'jest.config.js' + - '.github/workflows/js-tests.yml' + pull_request: + branches: [develop, main] + paths: + - '**.js' + - '**.jsx' + - '**.ts' + - '**.tsx' + - 'package.json' + - 'package-lock.json' + - 'jest.config.js' + - '.github/workflows/js-tests.yml' + workflow_dispatch: + +# Disable all permissions by default; grant minimal permissions per job +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-and-test: + name: Lint and Jest Tests + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: Set up NodeJS 20 + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + with: + node-version: '20' + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run Lint JS + run: npm run lint-js + + - name: Run Jest tests + run: npm run test-jest diff --git a/.github/workflows/php-lint.yml b/.github/workflows/php-lint.yml index c609c23b..ac1ded3e 100644 --- a/.github/workflows/php-lint.yml +++ b/.github/workflows/php-lint.yml @@ -1,11 +1,22 @@ name: PHP Lint on: - pull_request: push: - branches-ignore: - - develop - - main + paths: + - '**.php' + - 'composer.json' + - 'composer.lock' + - '.phpcs.xml.dist' + - '.github/workflows/php-lint.yml' + pull_request: + branches: [develop, main] + paths: + - '**.php' + - 'composer.json' + - 'composer.lock' + - '.phpcs.xml.dist' + - '.github/workflows/php-lint.yml' + workflow_dispatch: # Disable all permissions by default; grant minimal permissions per job permissions: {} diff --git a/.wordpress-org/blueprints/blueprint.json b/.wordpress-org/blueprints/blueprint.json new file mode 100644 index 00000000..a903a3b9 --- /dev/null +++ b/.wordpress-org/blueprints/blueprint.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://playground.wordpress.net/blueprint-schema.json", + "landingPage": "/wp-admin/index.php?page=calendar", + "preferredVersions": { + "php": "8.2", + "wp": "latest" + }, + "phpExtensionBundles": [ + "kitchen-sink" + ], + "steps": [ + { + "step": "installPlugin", + "pluginData": { + "resource": "wordpress.org/plugins", + "slug": "edit-flow" + }, + "options": { + "activate": true + } + }, + { + "step": "runPHP", + "code": "custom_status ) ) { EditFlow()->custom_status->install(); } if ( isset( EditFlow()->calendar ) ) { EditFlow()->calendar->install(); } } $role = get_role( 'administrator' ); if ( $role ) { $role->add_cap( 'ef_view_calendar' ); } ?>" + }, + { + "step": "runPHP", + "code": "set_role( 'editor' ); update_user_meta( $user_id, 'first_name', 'Sarah' ); update_user_meta( $user_id, 'last_name', 'Editor' ); wp_update_user( array( 'ID' => $user_id, 'display_name' => 'Sarah Editor' ) ); } ?>" + }, + { + "step": "runPHP", + "code": "set_role( 'author' ); update_user_meta( $user_id, 'first_name', 'James' ); update_user_meta( $user_id, 'last_name', 'Writer' ); wp_update_user( array( 'ID' => $user_id, 'display_name' => 'James Writer' ) ); } ?>" + }, + { + "step": "runPHP", + "code": " 'New Feature: AI-Powered Content Suggestions', 'post_content' => 'This article explores how AI can help content creators brainstorm and refine their ideas.', 'post_status' => 'pitch', 'post_author' => $writer->ID, 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+3 days' ) ), ) ); wp_insert_post( array( 'post_title' => 'Guide to Remote Team Collaboration', 'post_content' => 'Remote work has transformed how editorial teams collaborate.', 'post_status' => 'assigned', 'post_author' => $writer->ID, 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+5 days' ) ), ) ); $progress_post = wp_insert_post( array( 'post_title' => '10 Tips for Better Editorial Workflows', 'post_content' => 'Streamlining your editorial workflow can save hours each week.', 'post_status' => 'in-progress', 'post_author' => $writer->ID, 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+1 day' ) ), ) ); update_option( 'ef_demo_progress_post_id', $progress_post ); wp_insert_post( array( 'post_title' => 'The Future of Digital Publishing', 'post_content' => 'Digital publishing continues to evolve rapidly.', 'post_status' => 'pending', 'post_author' => $editor->ID, 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+2 days' ) ), ) ); wp_insert_post( array( 'post_title' => 'Interview: Leading Through Change', 'post_content' => 'In this exclusive interview, we speak with industry leaders.', 'post_status' => 'draft', 'post_author' => 1, 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+7 days' ) ), ) ); wp_insert_post( array( 'post_title' => 'Weekly Roundup: Content Strategy Insights', 'post_content' => 'This week in content strategy: new research on reader engagement.', 'post_status' => 'future', 'post_author' => $editor->ID, 'post_date' => date( 'Y-m-d H:i:s', strtotime( '+4 days' ) ), ) ); ?>" + }, + { + "step": "runPHP", + "code": " $post_id, 'comment_content' => 'Great start! Could you expand on tip #3 about visual calendars?', 'comment_type' => 'editorial-comment', 'user_id' => $editor->ID, 'comment_author' => $editor->display_name, 'comment_author_email' => $editor->user_email, 'comment_approved' => 'editorial-comment', ) ); wp_insert_comment( array( 'comment_post_ID' => $post_id, 'comment_content' => 'Thanks Sarah! I will add a calendar screenshot and expand that section.', 'comment_type' => 'editorial-comment', 'user_id' => $writer->ID, 'comment_author' => $writer->display_name, 'comment_author_email' => $writer->user_email, 'comment_approved' => 'editorial-comment', ) ); delete_option( 'ef_demo_progress_post_id' ); } ?>" + }, + { + "step": "login", + "username": "admin", + "password": "password" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index a5f06ace..25023ca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.10.3] - 2026-01-12 + +### Added + +* feat: add WordPress Playground blueprint for live preview by @GaryJones in [#885](https://github.com/Automattic/Edit-Flow/pull/885) + +### Fixed + +* fix: prevent Edit Flow nonce checks from killing unrelated forms by @GaryJones in [#883](https://github.com/Automattic/Edit-Flow/pull/883) +* fix: add missing imports for Extended Post Status block editor panel by @GaryJones in [#884](https://github.com/Automattic/Edit-Flow/pull/884) + +### Documentation + +* docs: move Development section from README to CONTRIBUTING by @GaryJones in [#880](https://github.com/Automattic/Edit-Flow/pull/880) + +### Maintenance + +* ci: optimise CI workflows with path filters and split tests by @GaryJones in [#881](https://github.com/Automattic/Edit-Flow/pull/881) +* test: add integration test for revision nonce handling by @GaryJones in [#879](https://github.com/Automattic/Edit-Flow/pull/879) + ## [0.10.2] - 2026-01-07 ### Fixed @@ -395,6 +415,7 @@ This is a major update with significant bug fixes, new features, and modernised * Ability to assign custom statuses to posts. +[0.10.3]: https://github.com/Automattic/Edit-Flow/compare/0.10.2...0.10.3 [0.10.2]: https://github.com/Automattic/Edit-Flow/compare/0.10.1...0.10.2 [0.10.1]: https://github.com/Automattic/Edit-Flow/compare/0.10.0...0.10.1 [0.10.0]: https://github.com/Automattic/Edit-Flow/compare/0.9.9...0.10.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c3a14048..9166c748 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,43 @@ Here's a sample of what a great summary looks like: Screenshots: *screenshot of behavior/error goes here* +Development Setup +------ + +This plugin uses `wp-env` for development and is required to run the tests written for the plugin. `wp-env` requires Docker so please ensure you have that installed on your system first. To install `wp-env`, use the following command: + +``` +npm -g i @wordpress/env +``` + +Read more about `wp-env` [here](https://www.npmjs.com/package/@wordpress/env). + +This plugin also uses Composer to manage PHP dependencies. Composer can be downloaded [here](https://getcomposer.org/download/). + +###### Getting started + +1. Clone the plugin repo: `git clone git@github.com:Automattic/Edit-Flow.git` +2. Change to cloned directory: `cd /path/to/repo` +3. Install PHP dependencies: `composer install` +4. Install NPM dependencies: `npm install` +5. Start dev environment: `wp-env start` + +###### Running tests + +Ensure that the dev environment has already been started with `wp-env start`. + +**PHP Integration Tests:** +1. Integration test: `composer run integration` +2. Multi-site integration test: `composer run integration-ms` + +**E2E Tests (Playwright):** +1. Run E2E tests: `npm run test-e2e` +2. Run with visible browser: `npm run test-e2e:headed` +3. Debug mode: `npm run test-e2e:debug` + +**JavaScript Tests:** +1. Run Jest tests: `npm run test-jest` + Creating and submitting Patches ------ diff --git a/README.md b/README.md index f9f2a6b7..66b2b7b7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Tags: workflow, editorial, editorial calendar, custom status, newsroom Requires at least: 6.4 Requires PHP: 7.4 Tested up to: 6.9 -Stable tag: 0.10.2 +Stable tag: 0.10.3 Redefining your editorial workflow. @@ -36,42 +36,6 @@ If the automatic process above fails, follow these simple steps to do a manual i 2. Activate the plugin through the 'Plugins' menu in WordPress 3. Write and enjoy the merits of a structured editorial workflow! -## Development - -This plugin uses `wp-env` for development and is required to run the tests written for the plugin. `wp-env` requires Docker so please ensure you have that installed on your system first. To install `wp-env`, use the following command: - -``` -npm -g i @wordpress/env -``` - -Read more about `wp-env` [here](https://www.npmjs.com/package/@wordpress/env). - -This plugin also uses Composer to manage PHP dependencies. Composer can be downloaded [here](https://getcomposer.org/download/). - -### Getting started - -1. Clone the plugin repo: `git clone git@github.com:Automattic/Edit-Flow.git` -2. Changed to cloned directory: `cd /path/to/repo` -3. Install PHP dependencies: `composer install` -4. Install NPM dependencies: `npm install` -5. Start dev environment: `wp-env start` - -### Running tests - -Ensure that the dev environment has already been started with `wp-env start`. - -**PHP Integration Tests:** -1. Integration test: `composer run integration` -2. Multi-site integration test: `composer run integration-ms` - -**E2E Tests (Playwright):** -1. Run E2E tests: `npm run test-e2e` -2. Run with visible browser: `npm run test-e2e:headed` -3. Debug mode: `npm run test-e2e:debug` - -**JavaScript Tests:** -1. Run Jest tests: `npm run test-jest` - ## Frequently Asked Questions ### Does Edit Flow work with multisite? diff --git a/edit_flow.php b/edit_flow.php index 8b688c52..45898af5 100644 --- a/edit_flow.php +++ b/edit_flow.php @@ -4,7 +4,7 @@ * Plugin URI: http://editflow.org/ * Description: Remixing the WordPress admin for better editorial workflow options. * Author: Daniel Bachhuber, Scott Bressler, Mohammad Jangda, Automattic, and others - * Version: 0.10.2 + * Version: 0.10.3 * Requires at least: 6.4 * Requires PHP: 7.4 * License: GPLv2 or later @@ -34,7 +34,7 @@ function _ef_print_php_version_admin_notice() { } // Define constants. -define( 'EDIT_FLOW_VERSION', '0.10.0' ); +define( 'EDIT_FLOW_VERSION', '0.10.3' ); define( 'EDIT_FLOW_ROOT', __DIR__ ); define( 'EDIT_FLOW_FILE_PATH', EDIT_FLOW_ROOT . '/' . basename( __FILE__ ) ); define( 'EDIT_FLOW_URL', plugins_url( '/', __FILE__ ) ); diff --git a/languages/edit-flow.pot b/languages/edit-flow.pot index 2f0ee2d0..9f4b0063 100644 --- a/languages/edit-flow.pot +++ b/languages/edit-flow.pot @@ -2,14 +2,14 @@ # This file is distributed under the GPLv2 or later. msgid "" msgstr "" -"Project-Id-Version: Edit Flow 0.10.2\n" +"Project-Id-Version: Edit Flow 0.10.3\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/edit-flow\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-01-07T16:12:56+00:00\n" +"POT-Creation-Date: 2026-01-12T13:39:35+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: edit-flow\n" @@ -347,8 +347,8 @@ msgstr "" #: modules/dashboard/dashboard.php:276 #: modules/dashboard/dashboard.php:296 #: modules/dashboard/dashboard.php:324 -#: modules/notifications/notifications.php:1472 -#: modules/notifications/notifications.php:1491 +#: modules/notifications/notifications.php:1482 +#: modules/notifications/notifications.php:1501 msgid "Disabled" msgstr "" @@ -357,8 +357,8 @@ msgstr "" #: modules/dashboard/dashboard.php:277 #: modules/dashboard/dashboard.php:297 #: modules/dashboard/dashboard.php:325 -#: modules/notifications/notifications.php:1473 -#: modules/notifications/notifications.php:1492 +#: modules/notifications/notifications.php:1483 +#: modules/notifications/notifications.php:1502 msgid "Enabled" msgstr "" @@ -931,7 +931,7 @@ msgid "No users or groups were notified." msgstr "" #: modules/editorial-comments/editorial-comments.php:241 -#: modules/notifications/notifications.php:888 +#: modules/notifications/notifications.php:898 msgid "Notified" msgstr "" @@ -1146,7 +1146,7 @@ msgstr "" #. translators: 1: date, 2: time #: modules/editorial-metadata/editorial-metadata.php:958 -#: modules/notifications/notifications.php:1582 +#: modules/notifications/notifications.php:1592 #, php-format msgid "%1$s at %2$s" msgstr "" @@ -1401,217 +1401,217 @@ msgstr "" msgid "Nonce check failed. Please ensure you can add users or user groups to a post." msgstr "" -#: modules/notifications/notifications.php:705 +#: modules/notifications/notifications.php:715 msgid "WordPress Scheduler" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:736 +#: modules/notifications/notifications.php:746 #, php-format msgid "[%1$s] New %2$s Created: \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email -#: modules/notifications/notifications.php:738 +#: modules/notifications/notifications.php:748 #, php-format msgid "A new %1$s (#%2$s \"%3$s\") was created by %4$s %5$s" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:741 +#: modules/notifications/notifications.php:751 #, php-format msgid "[%1$s] %2$s Trashed: \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email -#: modules/notifications/notifications.php:743 +#: modules/notifications/notifications.php:753 #, php-format msgid "%1$s #%2$s \"%3$s\" was moved to the trash by %4$s %5$s" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:746 +#: modules/notifications/notifications.php:756 #, php-format msgid "[%1$s] %2$s Restored (from Trash): \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email -#: modules/notifications/notifications.php:748 +#: modules/notifications/notifications.php:758 #, php-format msgid "%1$s #%2$s \"%3$s\" was restored from trash by %4$s %5$s" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:751 +#: modules/notifications/notifications.php:761 #, php-format msgid "[%1$s] %2$s Scheduled: \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email 6. scheduled date -#: modules/notifications/notifications.php:753 +#: modules/notifications/notifications.php:763 #, php-format msgid "%1$s #%2$s \"%3$s\" was scheduled by %4$s %5$s. It will be published on %6$s" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:756 +#: modules/notifications/notifications.php:766 #, php-format msgid "[%1$s] %2$s Published: \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email -#: modules/notifications/notifications.php:758 +#: modules/notifications/notifications.php:768 #, php-format msgid "%1$s #%2$s \"%3$s\" was published by %4$s %5$s" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:761 +#: modules/notifications/notifications.php:771 #, php-format msgid "[%1$s] %2$s Unpublished: \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email -#: modules/notifications/notifications.php:763 +#: modules/notifications/notifications.php:773 #, php-format msgid "%1$s #%2$s \"%3$s\" was unpublished by %4$s %5$s" msgstr "" #. translators: 1: site name, 2: post type, 3. post title -#: modules/notifications/notifications.php:766 +#: modules/notifications/notifications.php:776 #, php-format msgid "[%1$s] %2$s Status Changed for \"%3$s\"" msgstr "" #. translators: 1: post type, 2: post id, 3. post title, 4. user name, 5. user email -#: modules/notifications/notifications.php:768 +#: modules/notifications/notifications.php:778 #, php-format msgid "Status was changed for %1$s #%2$s \"%3$s\" by %4$s %5$s" msgstr "" #. translators: 1: date, 2: time, 3: timezone -#: modules/notifications/notifications.php:772 +#: modules/notifications/notifications.php:782 #, php-format msgid "This action was taken on %1$s at %2$s %3$s" msgstr "" #. translators: 1: old status, 2: new status -#: modules/notifications/notifications.php:777 +#: modules/notifications/notifications.php:787 #, php-format msgid "%1$s => %2$s" msgstr "" #. translators: 1: post type -#: modules/notifications/notifications.php:783 +#: modules/notifications/notifications.php:793 #, php-format msgid "== %s Details ==" msgstr "" #. translators: 1: post title -#: modules/notifications/notifications.php:785 +#: modules/notifications/notifications.php:795 #, php-format msgid "Title: %s" msgstr "" #. translators: 1: author name, 2: author email -#: modules/notifications/notifications.php:788 +#: modules/notifications/notifications.php:798 #, php-format msgid "Author: %1$s (%2$s)" msgstr "" -#: modules/notifications/notifications.php:810 -#: modules/notifications/notifications.php:906 +#: modules/notifications/notifications.php:820 +#: modules/notifications/notifications.php:916 msgid "== Actions ==" msgstr "" #. translators: 1: edit link -#: modules/notifications/notifications.php:812 -#: modules/notifications/notifications.php:910 +#: modules/notifications/notifications.php:822 +#: modules/notifications/notifications.php:920 #, php-format msgid "Add editorial comment: %s" msgstr "" #. translators: 1: edit link -#: modules/notifications/notifications.php:814 -#: modules/notifications/notifications.php:912 +#: modules/notifications/notifications.php:824 +#: modules/notifications/notifications.php:922 #, php-format msgid "Edit: %s" msgstr "" #. translators: 1: view link -#: modules/notifications/notifications.php:816 -#: modules/notifications/notifications.php:914 +#: modules/notifications/notifications.php:826 +#: modules/notifications/notifications.php:924 #, php-format msgid "View: %s" msgstr "" #. translators: 1: user name, 2: post type, 3: post id, 4: edit link, 5: post title, 6: old status, 7: new status -#: modules/notifications/notifications.php:824 +#: modules/notifications/notifications.php:834 #, php-format msgid "*%1$s* changed the status of *%2$s #%3$s - <%4$s|%5$s>* from *%6$s* to *%7$s*" msgstr "" #. translators: 1: blog name, 2: post title -#: modules/notifications/notifications.php:876 +#: modules/notifications/notifications.php:886 #, php-format msgid "[%1$s] New Editorial Comment: \"%2$s\"" msgstr "" #. translators: 1: post id, 2: post title, 3. post type -#: modules/notifications/notifications.php:879 +#: modules/notifications/notifications.php:889 #, php-format msgid "A new editorial comment was added to %3$s #%1$s \"%2$s\"" msgstr "" #. translators: 1: comment author, 2: author email, 3: date, 4: time -#: modules/notifications/notifications.php:881 +#: modules/notifications/notifications.php:891 #, php-format msgid "%1$s (%2$s) said on %3$s at %4$s:" msgstr "" #. translators: 1: edit link -#: modules/notifications/notifications.php:908 +#: modules/notifications/notifications.php:918 #, php-format msgid "Reply: %s" msgstr "" #. translators: 1: post type -#: modules/notifications/notifications.php:917 +#: modules/notifications/notifications.php:927 #, php-format msgid "You can see all editorial comments on this %s here: " msgstr "" #. translators: 1: comment author, 2: post type, 3: post id, 4: edit link, 5: post title, 6: comment content -#: modules/notifications/notifications.php:926 +#: modules/notifications/notifications.php:936 #, php-format msgid "*%1$s* left a comment on *%2$s #%3$s - <%4$s|%5$s>*" msgstr "" #. translators: 1: post title -#: modules/notifications/notifications.php:944 +#: modules/notifications/notifications.php:954 #, php-format msgid "You are receiving this email because you are subscribed to \"%s\"." msgstr "" #. translators: 1: date -#: modules/notifications/notifications.php:948 +#: modules/notifications/notifications.php:958 #, php-format msgid "This email was sent %s." msgstr "" -#: modules/notifications/notifications.php:1449 +#: modules/notifications/notifications.php:1459 msgid "Post types for notifications:" msgstr "" -#: modules/notifications/notifications.php:1450 +#: modules/notifications/notifications.php:1460 msgid "Always notify blog admin" msgstr "" -#: modules/notifications/notifications.php:1451 +#: modules/notifications/notifications.php:1461 msgid "Send to Webhook" msgstr "" -#: modules/notifications/notifications.php:1452 +#: modules/notifications/notifications.php:1462 msgid "Webhook URL" msgstr "" @@ -2120,22 +2120,26 @@ msgstr "" msgid "Number of weeks" msgstr "" -#: modules/custom-status/lib/custom-status-block.js:102 +#: build/custom-status-block.js:1 +#: modules/custom-status/lib/custom-status-block.js:104 #: dist/custom-status.build.js:121 msgid "Extended Post Status" msgstr "" -#: modules/custom-status/lib/custom-status-block.js:103 +#: build/custom-status-block.js:1 +#: modules/custom-status/lib/custom-status-block.js:105 #: dist/custom-status.build.js:122 msgid "Extended Post Status Disabled." msgstr "" -#: modules/custom-status/lib/custom-status-block.js:119 +#: build/custom-status-block.js:1 +#: modules/custom-status/lib/custom-status-block.js:121 #: dist/custom-status.build.js:138 msgid "Note: this will override all status settings above." msgstr "" -#: modules/custom-status/lib/custom-status-block.js:120 +#: build/custom-status-block.js:1 +#: modules/custom-status/lib/custom-status-block.js:122 #: dist/custom-status.build.js:139 msgid "To select a custom status, please unpublish the content first." msgstr "" diff --git a/modules/custom-status/lib/custom-status-block.js b/modules/custom-status/lib/custom-status-block.js index e5bd7383..8dd50207 100644 --- a/modules/custom-status/lib/custom-status-block.js +++ b/modules/custom-status/lib/custom-status-block.js @@ -1,9 +1,11 @@ import './editor.scss'; import './style.scss'; +import { SelectControl } from '@wordpress/components'; import { compose } from '@wordpress/compose'; import { subscribe, dispatch, select } from '@wordpress/data'; import { withSelect, withDispatch } from '@wordpress/data'; +import { PluginPostStatusInfo } from '@wordpress/editor'; import { __ } from '@wordpress/i18n'; import { registerPlugin } from '@wordpress/plugins'; diff --git a/modules/notifications/notifications.php b/modules/notifications/notifications.php index f27d11d6..3c3acfe0 100644 --- a/modules/notifications/notifications.php +++ b/modules/notifications/notifications.php @@ -554,8 +554,9 @@ function ( $user_id ) use ( $post_id ) { * @since 0.8 */ public function handle_user_post_subscription() { + // Require a valid nonce for this AJAX request. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). - if ( ! empty( $_GET['_wpnonce'] ) && ! wp_verify_nonce( $_GET['_wpnonce'], 'ef_notifications_user_post_subscription' ) ) { + if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( $_GET['_wpnonce'], 'ef_notifications_user_post_subscription' ) ) { $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] ); } @@ -599,21 +600,30 @@ public function save_post_subscriptions( $new_status, $old_status, $post ) { return; } + // Only process if Edit Flow's followers form was submitted. + if ( ! isset( $_POST['ef-save_followers'] ) ) { + return; + } + + // Check capability. + if ( ! current_user_can( $this->edit_post_subscriptions_cap ) ) { + return; + } + + // Verify Edit Flow's own nonce. Return early if missing or invalid - don't die, + // as this hook fires on all post transitions including non-admin contexts. // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce value passed directly to wp_verify_nonce(). - if ( ! empty( $_POST['_wpnonce'] ) && ! wp_verify_nonce( $_POST['_wpnonce'], 'update-post_' . $post->ID ) ) { - $this->print_ajax_response( 'error', $this->module->messages['nonce-failed'] ); + if ( ! isset( $_POST['ef_notifications_nonce'] ) || ! wp_verify_nonce( $_POST['ef_notifications_nonce'], 'save_user_usergroups' ) ) { + return; } - // Only if has edit_post_subscriptions cap. - if ( isset( $_POST['ef-save_followers'] ) && current_user_can( $this->edit_post_subscriptions_cap ) ) { - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved. - $users = isset( $_POST['ef-selected-users'] ) ? $_POST['ef-selected-users'] : []; - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved. - $usergroups = isset( $_POST['following_usergroups'] ) ? $_POST['following_usergroups'] : []; - $this->save_post_following_users( $post, $users ); - if ( $this->module_enabled( 'user_groups' ) && in_array( $this->get_current_post_type(), $this->get_post_types_for_module( $edit_flow->user_groups->module ) ) ) { - $this->save_post_following_usergroups( $post, $usergroups ); - } + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved. + $users = isset( $_POST['ef-selected-users'] ) ? $_POST['ef-selected-users'] : []; + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Values are sanitized when saved. + $usergroups = isset( $_POST['following_usergroups'] ) ? $_POST['following_usergroups'] : []; + $this->save_post_following_users( $post, $users ); + if ( $this->module_enabled( 'user_groups' ) && in_array( $this->get_current_post_type(), $this->get_post_types_for_module( $edit_flow->user_groups->module ) ) ) { + $this->save_post_following_usergroups( $post, $usergroups ); } } diff --git a/package.json b/package.json index 4df48f7d..0b1d01ea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "edit-flow", - "version": "0.10.0", + "version": "0.10.3", "description": "Edit Flow", "directories": { "test": "tests" diff --git a/tests/Integration/NotificationsAjaxTest.php b/tests/Integration/NotificationsAjaxTest.php index bb36d24b..121f036f 100644 --- a/tests/Integration/NotificationsAjaxTest.php +++ b/tests/Integration/NotificationsAjaxTest.php @@ -270,4 +270,109 @@ public function test_ajax_save_persists_usergroup_subscriptions(): void { $edit_flow->user_groups->delete_usergroup( $group1->term_id ); $edit_flow->user_groups->delete_usergroup( $group2->term_id ); } + + /** + * Test: handle_user_post_subscription requires a nonce. + * + * This tests the fix for the nonce check logic. Previously the check was: + * `if ( ! empty( $_GET['_wpnonce'] ) && ! wp_verify_nonce(...) )` + * which allowed requests without any nonce to pass through. + * + * The fix changes it to: + * `if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce(...) )` + * which requires a valid nonce. + * + * @ticket https://github.com/Automattic/edit-flow/issues/882 + * @covers EF_Notifications::handle_user_post_subscription + */ + public function test_handle_user_post_subscription_requires_nonce(): void { + wp_set_current_user( self::$admin_user_id ); + + $post_id = $this->factory->post->create(); + + // Make request WITHOUT a nonce - this should fail now. + $_GET['post_id'] = $post_id; + $_GET['method'] = 'follow'; + // Intentionally NOT setting $_GET['_wpnonce'] + + // print_ajax_response outputs JSON before dying, so we get WPAjaxDieContinueException. + try { + $this->_handleAjax( 'ef_notifications_user_post_subscription' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Verify error response. + $response = json_decode( $this->_last_response, true ); + $this->assertIsArray( $response, 'Response should be valid JSON' ); + $this->assertEquals( 'error', $response['status'], 'Response should indicate error when nonce is missing' ); + } + + /** + * Test: handle_user_post_subscription fails with invalid nonce. + * + * @ticket https://github.com/Automattic/edit-flow/issues/882 + * @covers EF_Notifications::handle_user_post_subscription + */ + public function test_handle_user_post_subscription_fails_with_invalid_nonce(): void { + wp_set_current_user( self::$admin_user_id ); + + $post_id = $this->factory->post->create(); + + $_GET['_wpnonce'] = 'invalid_nonce'; + $_GET['post_id'] = $post_id; + $_GET['method'] = 'follow'; + + // print_ajax_response outputs JSON before dying, so we get WPAjaxDieContinueException. + try { + $this->_handleAjax( 'ef_notifications_user_post_subscription' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Verify error response. + $response = json_decode( $this->_last_response, true ); + $this->assertIsArray( $response, 'Response should be valid JSON' ); + $this->assertEquals( 'error', $response['status'], 'Response should indicate error when nonce is invalid' ); + } + + /** + * Test: handle_user_post_subscription succeeds with valid nonce. + * + * @ticket https://github.com/Automattic/edit-flow/issues/882 + * @covers EF_Notifications::handle_user_post_subscription + */ + public function test_handle_user_post_subscription_succeeds_with_valid_nonce(): void { + global $edit_flow; + + wp_set_current_user( self::$admin_user_id ); + + $post_id = $this->factory->post->create( + array( + 'post_author' => self::$admin_user_id, + 'post_status' => 'draft', + ) + ); + + $_GET['_wpnonce'] = wp_create_nonce( 'ef_notifications_user_post_subscription' ); + $_GET['post_id'] = $post_id; + $_GET['method'] = 'follow'; + + try { + $this->_handleAjax( 'ef_notifications_user_post_subscription' ); + } catch ( WPAjaxDieContinueException $e ) { + unset( $e ); + } + + // Verify we got a success response. + $this->assertNotEmpty( $this->_last_response, 'AJAX should return a response' ); + + $response = json_decode( $this->_last_response, true ); + $this->assertIsArray( $response, 'Response should be valid JSON' ); + $this->assertEquals( 'success', $response['status'], 'Response should indicate success' ); + + // Verify the user is now following the post. + $following_users = $edit_flow->notifications->get_following_users( $post_id, 'id' ); + $this->assertContains( self::$admin_user_id, $following_users, 'User should be following the post' ); + } } diff --git a/tests/Integration/NotificationsClassicEditorTest.php b/tests/Integration/NotificationsClassicEditorTest.php index 0d23ff62..f63ec1ca 100644 --- a/tests/Integration/NotificationsClassicEditorTest.php +++ b/tests/Integration/NotificationsClassicEditorTest.php @@ -1,8 +1,9 @@ notifications->save_post_subscriptions( 'draft', 'draft', $post ); + } catch ( \Exception $e ) { + $exception_thrown = true; + } + + $this->assertFalse( + $wp_die_called, + 'save_post_subscriptions should not call wp_die when given a valid Edit Flow nonce.' + ); + $this->assertFalse( + $exception_thrown, + 'save_post_subscriptions should not throw an exception with a valid Edit Flow nonce.' + ); + } + + /** + * Test that save_post_subscriptions ignores requests without Edit Flow form data. + * + * When ef-save_followers is not set, the function should return early + * without processing or dying. + * + * @covers EF_Notifications::save_post_subscriptions + */ + public function test_save_post_subscriptions_ignores_non_edit_flow_requests() { + global $edit_flow; + + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$editor_user_id, + 'post_status' => 'draft', + ) + ); + + $post = get_post( $post_id ); + + // Simulate a request without Edit Flow form data (e.g., REST API, other forms). + $_POST = array(); + + $wp_die_called = false; add_filter( 'wp_die_handler', - function () use ( &$wp_die_called, &$wp_die_message ) { - return function ( $message ) use ( &$wp_die_called, &$wp_die_message ) { - $wp_die_called = true; - $wp_die_message = $message; - // Don't actually die - throw exception to stop execution. + function () use ( &$wp_die_called ) { + return function ( $message ) use ( &$wp_die_called ) { + $wp_die_called = true; throw new \Exception( 'wp_die called: ' . $message ); }; } ); - // Attempt to trigger save_post_subscriptions via status transition. $exception_thrown = false; try { - // Directly call save_post_subscriptions to test the nonce check. $edit_flow->notifications->save_post_subscriptions( 'draft', 'draft', $post ); } catch ( \Exception $e ) { $exception_thrown = true; } - // The function should NOT call wp_die when given a valid Classic Editor nonce. - // If this assertion fails, it means the nonce check is incorrectly rejecting - // valid Classic Editor nonces (the bug reported in 0.10.0). $this->assertFalse( $wp_die_called, - 'save_post_subscriptions should accept valid Classic Editor nonce (update-post_{$post_id}), but it called wp_die instead. This indicates the nonce action string is incorrect.' + 'save_post_subscriptions should return early without dying when ef-save_followers is not set.' ); $this->assertFalse( $exception_thrown, - 'save_post_subscriptions threw an exception due to wp_die being called with a valid nonce.' + 'save_post_subscriptions should not throw when ef-save_followers is not set.' ); } /** - * Test that the nonce verification uses the correct action string. + * Test that save_post_subscriptions doesn't die when another form's nonce is present. * - * WordPress Classic Editor uses 'update-post_{$post_id}' for the edit form nonce. - * This test verifies that a nonce created with this action is considered valid. + * This is the core fix for #882: when a contact form (or any other form) triggers + * a post status transition with its own _wpnonce field, Edit Flow should not + * kill the request by calling wp_die(). + * + * @ticket https://github.com/Automattic/edit-flow/issues/882 + * @covers EF_Notifications::save_post_subscriptions */ - public function test_classic_editor_nonce_action_is_update_post() { - $post_id = self::factory()->post->create(); + public function test_save_post_subscriptions_does_not_die_with_unrelated_nonce() { + global $edit_flow; + + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$editor_user_id, + 'post_status' => 'draft', + ) + ); + + $post = get_post( $post_id ); - // Create a nonce using the Classic Editor action. - $nonce = wp_create_nonce( 'update-post_' . $post_id ); + // Simulate a contact form submission that happens to trigger post status change. + // The form has its own _wpnonce for a different action (e.g., speaker_submission). + $_POST['_wpnonce'] = wp_create_nonce( 'speaker_submission' ); + // Note: ef-save_followers is NOT set because this is not an Edit Flow form. - // Verify the nonce is valid for the correct action. - $this->assertNotFalse( - wp_verify_nonce( $nonce, 'update-post_' . $post_id ), - 'Nonce should be valid for update-post_{$post_id} action' + $wp_die_called = false; + + add_filter( + 'wp_die_handler', + function () use ( &$wp_die_called ) { + return function ( $message ) use ( &$wp_die_called ) { + $wp_die_called = true; + throw new \Exception( 'wp_die called: ' . $message ); + }; + } ); - // The buggy code uses 'editpost' which is NOT a valid WordPress nonce action. - // This should fail to verify. + $exception_thrown = false; + try { + $edit_flow->notifications->save_post_subscriptions( 'draft', 'publish', $post ); + } catch ( \Exception $e ) { + $exception_thrown = true; + } + + $this->assertFalse( + $wp_die_called, + 'save_post_subscriptions should NOT die when an unrelated form triggers post transition. ' . + 'This was the bug in #882 where contact form submissions failed because Edit Flow ' . + 'was checking the generic _wpnonce field.' + ); $this->assertFalse( - wp_verify_nonce( $nonce, 'editpost' ), - 'Nonce created for update-post_{$post_id} should NOT verify against editpost action' + $exception_thrown, + 'save_post_subscriptions should not throw when an unrelated form triggers post transition.' + ); + } + + /** + * Test that save_post_subscriptions returns early with invalid Edit Flow nonce. + * + * When Edit Flow's form is submitted but the nonce is invalid, the function + * should return early without processing (and importantly, without dying). + * + * @covers EF_Notifications::save_post_subscriptions + */ + public function test_save_post_subscriptions_returns_with_invalid_nonce() { + global $edit_flow; + + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$editor_user_id, + 'post_status' => 'draft', + ) + ); + + $post = get_post( $post_id ); + + // Simulate Edit Flow form submission with an invalid nonce. + $_POST['ef_notifications_nonce'] = 'invalid_nonce_value'; + $_POST['ef-save_followers'] = '1'; + $_POST['ef-selected-users'] = array( self::$editor_user_id ); + + $wp_die_called = false; + + add_filter( + 'wp_die_handler', + function () use ( &$wp_die_called ) { + return function ( $message ) use ( &$wp_die_called ) { + $wp_die_called = true; + throw new \Exception( 'wp_die called: ' . $message ); + }; + } + ); + + $exception_thrown = false; + try { + $edit_flow->notifications->save_post_subscriptions( 'draft', 'draft', $post ); + } catch ( \Exception $e ) { + $exception_thrown = true; + } + + $this->assertFalse( + $wp_die_called, + 'save_post_subscriptions should return early, not die, when nonce is invalid. ' . + 'Dying in a transition_post_status hook kills unrelated functionality.' + ); + $this->assertFalse( + $exception_thrown, + 'save_post_subscriptions should not throw when nonce is invalid.' + ); + } + + /** + * Test that save_post_subscriptions skips revisions. + * + * Revisions have different post IDs which would cause nonce verification to fail + * if not skipped. This test ensures revisions are properly handled. + * + * @covers EF_Notifications::save_post_subscriptions + */ + public function test_save_post_subscriptions_skips_revisions() { + global $edit_flow; + + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$editor_user_id, + 'post_status' => 'draft', + ) + ); + + // Simulate Edit Flow form submission. + $_POST['ef_notifications_nonce'] = wp_create_nonce( 'save_user_usergroups' ); + $_POST['ef-save_followers'] = '1'; + $_POST['ef-selected-users'] = array( self::$editor_user_id ); + + $wp_die_called = false; + + add_filter( + 'wp_die_handler', + function () use ( &$wp_die_called ) { + return function ( $message ) use ( &$wp_die_called ) { + $wp_die_called = true; + throw new \Exception( 'wp_die called: ' . $message ); + }; + } + ); + + // Update the post which triggers revision creation and transition_post_status hooks. + $exception_thrown = false; + try { + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => 'Updated content to trigger revision', + ) + ); + } catch ( \Exception $e ) { + $exception_thrown = true; + } + + $this->assertFalse( + $wp_die_called, + 'save_post_subscriptions should skip revisions to avoid nonce mismatch issues.' + ); + $this->assertFalse( + $exception_thrown, + 'wp_update_post should not throw due to revision handling.' + ); + } + + /** + * Test that subscriptions are saved when valid Edit Flow nonce is provided. + * + * @covers EF_Notifications::save_post_subscriptions + */ + public function test_save_post_subscriptions_saves_data_with_valid_nonce() { + global $edit_flow; + + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$editor_user_id, + 'post_status' => 'draft', + ) + ); + + $post = get_post( $post_id ); + + // Simulate Edit Flow form submission with valid nonce. + $_POST['ef_notifications_nonce'] = wp_create_nonce( 'save_user_usergroups' ); + $_POST['ef-save_followers'] = '1'; + $_POST['ef-selected-users'] = array( self::$editor_user_id ); + + // Call the function. + $edit_flow->notifications->save_post_subscriptions( 'draft', 'draft', $post ); + + // Verify the subscription was saved. + // Note: get_following_users with 'id' returns IDs as integers. + $following_users = $edit_flow->notifications->get_following_users( $post_id, 'id' ); + + $this->assertContains( + self::$editor_user_id, + $following_users, + 'User should be saved as a follower when valid Edit Flow nonce is provided.' ); } } diff --git a/webpack.config.js b/webpack.config.js index c13628de..ca030abc 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,3 +1,4 @@ +const path = require( 'path' ); const defaultConfig = require( '@wordpress/scripts/config/webpack.config' ); const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' ); @@ -20,7 +21,7 @@ const shouldBundleRequest = ( request ) => { // Replace the default DependencyExtractionWebpackPlugin with a configured one // Return false to explicitly prevent externalization of bundled packages -const plugins = defaultConfig.plugins.map( ( plugin ) => { +const calendarPlugins = defaultConfig.plugins.map( ( plugin ) => { if ( plugin.constructor.name === 'DependencyExtractionWebpackPlugin' ) { return new DependencyExtractionWebpackPlugin( { requestToExternal( request ) { @@ -40,11 +41,33 @@ const plugins = defaultConfig.plugins.map( ( plugin ) => { return plugin; } ); -module.exports = { +// Custom status block runs in the block editor where @wordpress packages are +// available as globals, so use default externalization behavior. +const customStatusBlockConfig = { ...defaultConfig, entry: { 'custom-status-block': './modules/custom-status/lib/custom-status-block.js', + }, + output: { + ...defaultConfig.output, + path: path.resolve( __dirname, 'build' ), + clean: false, + }, +}; + +// Calendar React needs @wordpress packages bundled since it runs outside +// the block editor context. +const calendarReactConfig = { + ...defaultConfig, + entry: { 'calendar-react': './modules/calendar/lib/react/calendar.react.js', }, - plugins, + output: { + ...defaultConfig.output, + path: path.resolve( __dirname, 'build' ), + clean: false, + }, + plugins: calendarPlugins, }; + +module.exports = [ customStatusBlockConfig, calendarReactConfig ];