diff --git a/.github/workflows/build-with-vendor-prefixed.yml b/.github/workflows/build-with-vendor-prefixed.yml index 4257c7c1c4..dd9aaeddae 100644 --- a/.github/workflows/build-with-vendor-prefixed.yml +++ b/.github/workflows/build-with-vendor-prefixed.yml @@ -41,7 +41,7 @@ jobs: run: npm run build:zip - name: Make artifacts available - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Plugin Zip retention-days: 2 diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml new file mode 100644 index 0000000000..18273e6b48 --- /dev/null +++ b/.github/workflows/close-stale-issues.yml @@ -0,0 +1,34 @@ +name: 'Close stale issues' + +# **What it does**: Closes issues where the original author doesn't respond to a request for information. +# **Why we have it**: To remove the need for maintainers to remember to check back on issues periodically to see if contributors have responded. + +on: + schedule: + # Schedule for every day at 1:30am UTC + - cron: '30 1 * * *' + +permissions: + issues: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-stale: 3 + days-before-close: 3 + stale-issue-message: > + It has been 3 days since more information was requested from you in this issue and we have not heard back. This issue is now marked as stale and will be closed in 3 days, but if you have more information to add then please comment and the issue will stay open. + close-issue-message: > + This issue has been automatically closed because there has been no response + to our request for more information in the past 3 days. With only the + information that is currently available, we are unable to take further action on this ticket. + Please reach out if you have found or find the answer we need so that we + can investigate further. When the information is ready, you can re-open this ticket to share it with us. + stale-issue-label: 'stale' + close-issue-reason: 'not_planned' + any-of-labels: 'reporter feedback' + remove-stale-when-updated: true + diff --git a/.github/workflows/cypress-tests.yml b/.github/workflows/cypress-tests.yml index 2bf733bae2..11741654e4 100644 --- a/.github/workflows/cypress-tests.yml +++ b/.github/workflows/cypress-tests.yml @@ -20,12 +20,13 @@ on: jobs: cypress_local: - name: Local - ${{ matrix.core.name }} (${{ matrix.testGroup }}) + name: ES ${{ matrix.esVersion }} - ${{ matrix.core.name }} (${{ matrix.testGroup }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: testGroup: ['@slow', '-@slow'] + esVersion: ['7.10.1', '8.12.2'] core: - {name: 'WP latest', version: '', wcVersion: ''} - {name: 'WP minimum', version: '6.0', wcVersion: '6.4.0'} @@ -70,7 +71,10 @@ jobs: run: npm ci --include=dev - name: Set up WP environment with Elasticsearch - run: npm run env:start + run: ES_VERSION=${{ matrix.esVersion }} npm run env:start + + - name: Check ES response + run: curl --connect-timeout 5 --max-time 10 --retry 5 --retry-max-time 40 --retry-all-errors http://localhost:8890 - name: Build asset run: npm run build @@ -82,7 +86,7 @@ jobs: run: npm run cypress:run -- --env grepTags=${{ matrix.testGroup }} - name: Make artifacts available - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: cypress-artifact-${{ matrix.core.name }}-${{ matrix.testGroup }} @@ -170,7 +174,7 @@ jobs: run: npm run cypress:run -- --env grepTags=${{ matrix.testGroup }} - name: Make artifacts available - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: cypress-artifact-epio-${{ matrix.core.name }}-${{ matrix.testGroup }} diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml deleted file mode 100644 index 07372e27dc..0000000000 --- a/.github/workflows/no-response.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: No Response - -# **What it does**: Closes issues where the original author doesn't respond to a request for information. -# **Why we have it**: To remove the need for maintainers to remember to check back on issues periodically to see if contributors have responded. -# **Who does it impact**: Everyone that works on docs or docs-internal. - -on: - issue_comment: - types: [created] - schedule: - # Schedule for five minutes after the hour, every hour - - cron: '5 * * * *' - -jobs: - noResponse: - runs-on: ubuntu-latest - steps: - - uses: lee-dohm/no-response@v0.5.0 - with: - token: ${{ github.token }} - daysUntilClose: 3 # Number of days of inactivity before an Issue is closed for lack of response - responseRequiredLabel: "reporter feedback" # Label indicating that a response from the original author is required - closeComment: > - This issue has been automatically closed because there has been no response - to our request for more information in the past 3 days. With only the - information that is currently available, we are unable to take further action on this ticket. Please reach out if you have found or find the answers we need so - that we can investigate further. When the information is ready, you can re-open this ticket to share it with us. \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 172a5a45f9..17d4b04ebc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: Test +name: Unit Test env: COMPOSER_VERSION: "2" @@ -17,9 +17,16 @@ on: - '[0-9].[0-9x]*' # Version branches: 4.x.x, 4.1.x, 5.x jobs: - phpunit_single_site: - name: PHP Unit (Single Site) + phpunit: + name: ${{ matrix.type.name }} - ES ${{ matrix.esVersion }} runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + esVersion: ['7.10.1', '8.12.2'] + type: + - {name: 'Single Site', command: 'test-single-site'} + - {name: 'Multisite', command: 'test'} steps: - name: Checkout @@ -28,71 +35,11 @@ jobs: - name: Start MySQL run: sudo systemctl start mysql.service - - name: Configure sysctl limits - run: | - sudo swapoff -a - sudo sysctl -w vm.swappiness=1 - sudo sysctl -w fs.file-max=262144 - sudo sysctl -w vm.max_map_count=262144 - - name: Setup Elasticsearch - uses: getong/elasticsearch-action@v1.2 - with: - elasticsearch version: '7.10.1' - - - name: Set standard 10up cache directories - run: | - composer config -g cache-dir "${{ env.COMPOSER_CACHE }}" - - - name: Prepare composer cache - uses: actions/cache@v3 - with: - path: ${{ env.COMPOSER_CACHE }} - key: composer-${{ env.COMPOSER_VERSION }}-${{ hashFiles('**/composer.lock') }} - restore-keys: | - composer-${{ env.COMPOSER_VERSION }}- - - - name: Set PHP version - uses: shivammathur/setup-php@v2 - with: - php-version: '8.2' - extensions: :php-psr - coverage: none + run: cd bin/es-docker/ && docker-compose build --build-arg ES_VERSION=${{ matrix.esVersion }} && docker-compose up -d - - name: Install dependencies - run: composer install --ignore-platform-reqs - - - name: Setup WP Tests - run: | - bash bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 - sleep 10 - - - name: PHPUnit - run: | - composer run-script test-single-site - - phpunit_multisite: - name: PHP Unit (Multisite) - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Start MySQL - run: sudo systemctl start mysql.service - - - name: Configure sysctl limits - run: | - sudo swapoff -a - sudo sysctl -w vm.swappiness=1 - sudo sysctl -w fs.file-max=262144 - sudo sysctl -w vm.max_map_count=262144 - - - name: Setup Elasticsearch - uses: getong/elasticsearch-action@v1.2 - with: - elasticsearch version: '7.10.1' + - name: Check ES response + run: curl --connect-timeout 5 --max-time 10 --retry 5 --retry-max-time 40 --retry-all-errors http://127.0.0.1:8890 - name: Set standard 10up cache directories run: | @@ -114,7 +61,7 @@ jobs: coverage: none - name: Install dependencies - run: composer install + run: composer install --ignore-platform-reqs - name: Setup WP Tests run: | @@ -123,4 +70,4 @@ jobs: - name: PHPUnit run: | - composer run-script test + EP_HOST=http://127.0.0.1:8890/ composer run-script ${{ matrix.type.command }} diff --git a/.wp-env.json b/.wp-env.json index 8f3fa899c1..fad3bc4ab2 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -19,24 +19,26 @@ ], "mappings": { ".htaccess": "./tests/cypress/wordpress-files/.htaccess", + "wp-content/mu-plugins/disable-welcome-guide.php": "./tests/cypress/wordpress-files/test-mu-plugins/disable-welcome-guide.php", "wp-content/mu-plugins/skip-wp-lookup.php": "./tests/cypress/wordpress-files/test-mu-plugins/skip-wp-lookup.php", "wp-content/mu-plugins/unique-index-name.php": "./tests/cypress/wordpress-files/test-mu-plugins/unique-index-name.php", "wp-content/plugins/auto-meta-mode.php": "./tests/cypress/wordpress-files/test-plugins/auto-meta-mode.php", - "wp-content/mu-plugins/disable-welcome-guide.php": "./tests/cypress/wordpress-files/test-mu-plugins/disable-welcome-guide.php", "wp-content/plugins/cpt-and-custom-tax.php": "./tests/cypress/wordpress-files/test-plugins/cpt-and-custom-tax.php", "wp-content/plugins/custom-instant-results-template.php": "./tests/cypress/wordpress-files/test-plugins/custom-instant-results-template.php", "wp-content/plugins/custom-headers-for-autosuggest.php": "./tests/cypress/wordpress-files/test-plugins/custom-headers-for-autosuggest.php", - "wp-content/plugins/fake-new-activation.php": "./tests/cypress/wordpress-files/test-plugins/fake-new-activation.php", - "wp-content/plugins/open-instant-results-with-buttons.php": "./tests/cypress/wordpress-files/test-plugins/open-instant-results-with-buttons.php", - "wp-content/plugins/unsupported-server-software.php": "./tests/cypress/wordpress-files/test-plugins/unsupported-server-software.php", - "wp-content/plugins/unsupported-elasticsearch-version.php": "./tests/cypress/wordpress-files/test-plugins/unsupported-elasticsearch-version.php", - "wp-content/plugins/shorten-autosave.php": "./tests/cypress/wordpress-files/test-plugins/shorten-autosave.php", - "wp-content/plugins/fake-log-messages.php": "./tests/cypress/wordpress-files/test-plugins/fake-log-messages.php", + "wp-content/plugins/disable-fuzziness.php": "./tests/cypress/wordpress-files/test-plugins/disable-fuzziness.php", "wp-content/plugins/enable-debug-bar.php": "./tests/cypress/wordpress-files/test-plugins/enable-debug-bar.php", + "wp-content/plugins/fake-log-messages.php": "./tests/cypress/wordpress-files/test-plugins/fake-log-messages.php", + "wp-content/plugins/fake-new-activation.php": "./tests/cypress/wordpress-files/test-plugins/fake-new-activation.php", "wp-content/plugins/filter-instant-results-per-page.php": "./tests/cypress/wordpress-files/test-plugins/filter-instant-results-per-page.php", "wp-content/plugins/filter-instant-results-args-schema.php": "./tests/cypress/wordpress-files/test-plugins/filter-instant-results-args-schema.php", "wp-content/plugins/filter-autosuggest-navigate-callback.php": "./tests/cypress/wordpress-files/test-plugins/filter-autosuggest-navigate-callback.php", + "wp-content/plugins/open-instant-results-with-buttons.php": "./tests/cypress/wordpress-files/test-plugins/open-instant-results-with-buttons.php", + "wp-content/plugins/shorten-autosave.php": "./tests/cypress/wordpress-files/test-plugins/shorten-autosave.php", "wp-content/plugins/show-comments-and-terms.php": "./tests/cypress/wordpress-files/test-plugins/show-comments-and-terms.php", + "wp-content/plugins/sync-error.php": "./tests/cypress/wordpress-files/test-plugins/sync-error.php", + "wp-content/plugins/unsupported-server-software.php": "./tests/cypress/wordpress-files/test-plugins/unsupported-server-software.php", + "wp-content/plugins/unsupported-elasticsearch-version.php": "./tests/cypress/wordpress-files/test-plugins/unsupported-elasticsearch-version.php", "wp-content/uploads/content-example.xml": "./tests/cypress/wordpress-files/test-docs/content-example.xml" } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 43bdc8a622..0fc1c1bac2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,50 @@ All notable changes to this project will be documented in this file, per [the Ke ### Security --> +## [5.1.0] - 2024-04-29 + +### Added +* [Filters] New `ep_facet_enabled_in_editor` filter to enabled facet blocks in the post editor. Props [@JiveDig](https://github.com/JiveDig) and [@felipeelia](https://github.com/felipeelia) via [#3845](https://github.com/10up/ElasticPress/pull/3845). +* Official support to Elasticsearch 8.x. Props [@felipeelia](https://github.com/felipeelia) via [#3854](https://github.com/10up/ElasticPress/pull/3854). +* A new Sync errors tab, with errors grouped by type and links to support documentation when available. Props [@JakePT](https://github.com/JakePT) and [@apurvrdx1](https://github.com/apurvrdx1) via [#3803](https://github.com/10up/ElasticPress/pull/3803). +* [WooCommerce] HPOS compatibility notice for WooCommerce Orders. Props [@felipeelia](https://github.com/felipeelia) via [#3861](https://github.com/10up/ElasticPress/pull/3861). +* [Synonyms] A new settings screen with the the ability to bulk delete synonyms, support for many-to-many replacements, and a new type of synonym for terms with a hierarchical relationship, called hyponyms. Props [@JakePT](https://github.com/JakePT) and [@apurvrdx1](https://github.com/apurvrdx1) via [#3814](https://github.com/10up/ElasticPress/pull/3814). +* Infinite loop when using excerpt highlighting with posts that use blocks that print an excerpt. Props [@felipeelia](https://github.com/felipeelia) and [@JakePT](https://github.com/JakePT) via [#3867](https://github.com/10up/ElasticPress/pull/3867). +* Context parameter to the `get_capability()` function. Props [@felipeelia](https://github.com/felipeelia) and [@selim13](https://github.com/selim13) via [#3866](https://github.com/10up/ElasticPress/pull/3866). +* A tooltip for meta keys to the weighting screen to allow seeing the full key if it has been truncated. Props [@JakePT](https://github.com/JakePT) via [#3865](https://github.com/10up/ElasticPress/pull/3865). +* New `ep_weighting_options` filter to modify the weighting dashboard options. Props [@burhandodhy](https://github.com/burhandodhy) via [#3827](https://github.com/10up/ElasticPress/pull/3827). +* New `ep_post_test_meta_value` filter. Props [@felipeelia](https://github.com/felipeelia) via [#3850](https://github.com/10up/ElasticPress/pull/3850). +* New message related to indices limits on ElasticPress.io. Props [@felipeelia](https://github.com/felipeelia) via [#3898](https://github.com/10up/ElasticPress/pull/3898). + +### Changed +* Acknowledge all Elasticsearch modules, making the Documents feature available in ES 8 installations by default. Props [@felipeelia](https://github.com/felipeelia), [@Serverfox](https://github.com/Serverfox), and [@jerasokcm](https://github.com/jerasokcm) via [#3844](https://github.com/10up/ElasticPress/pull/3844). +* [Documents] Index CSV and TXT file contents. Props [@felipeelia](https://github.com/felipeelia) via [#3885](https://github.com/10up/ElasticPress/pull/3885). +* [Documents] Only set documents-related parameters if no post type was set or if the list already contains attachments. Props [@felipeelia](https://github.com/felipeelia) via [#3889](https://github.com/10up/ElasticPress/pull/3889). +* Automatically open the error log when a sync completes with errors. Props [@JakePT](https://github.com/JakePT) and [@felipeelia](https://github.com/felipeelia) via [#3895](https://github.com/10up/ElasticPress/pull/3895). +* Aggregations created with the 'aggs' WP_Query parameter, are now retrievable using `$query->query_vars['ep_aggregations']`. Props [@felipeelia](https://github.com/felipeelia) via [#3847](https://github.com/10up/ElasticPress/pull/3847). +* Major refactor of the `Term::format_args()` method and conditionally set search fields for term queries in REST API requests. Props [@felipeelia](https://github.com/felipeelia) and [@mgurtzweiler](https://github.com/mgurtzweiler) via [#3869](https://github.com/10up/ElasticPress/pull/3869). +* Replaced `lee-dohm/no-response` with `actions/stale` to help with closing no-response/stale issues. Props [@jeffpaul](https://github.com/jeffpaul) via [#3870](https://github.com/10up/ElasticPress/pull/3870). +* Bumped actions/upload-artifact from v3 to v4. Props [@iamdharmesh](https://github.com/iamdharmesh) via [#3897](https://github.com/10up/ElasticPress/pull/3897). +* Required node version. Props [@oscarssanchez](https://github.com/oscarssanchez) via [#3896](https://github.com/10up/ElasticPress/pull/3896). + +### Fixed +* [Autosuggest] Hide the Autosuggest Endpoint URL field for EP.io users. Props [@felipeelia](https://github.com/felipeelia) and [@JakePT](https://github.com/JakePT) via [#3835](https://github.com/10up/ElasticPress/pull/3835). +* [Autosuggest] Google Analytics integration gtag call. Props [@felipeelia](https://github.com/felipeelia) and [@JakePT](https://github.com/JakePT) via [#3835](https://github.com/10up/ElasticPress/pull/3835). +* [Autosuggest] Link click when using a touchpad. Props [@romanberdnikov](https://github.com/romanberdnikov) via [#3818](https://github.com/10up/ElasticPress/pull/3818). +* [Autosuggest] Pressing Enter to select an Autosuggest suggestion would instead open Instant Results. Props [@JakePT](https://github.com/JakePT) via [#3864](https://github.com/10up/ElasticPress/pull/3864). +* [Synonyms] Fatal error when saving synonyms if an index does not exist. Props [@felipeelia](https://github.com/felipeelia), [@MARQAS](https://github.com/MARQAS), [@randallhedglin](https://github.com/randallhedglin), and [@bispldeveloper](https://github.com/bispldeveloper) via [#3846](https://github.com/10up/ElasticPress/pull/3846). +* [Synonyms] Fix Synonyms case sensitive issue. Props [@burhandodhy](https://github.com/burhandodhy) via [#3857](https://github.com/10up/ElasticPress/pull/3857). +* [Documents] Media search returns no result in admin dashboard. Props [@felipeelia](https://github.com/felipeelia) and [@burhandodhy](https://github.com/burhandodhy) via [#3837](https://github.com/10up/ElasticPress/pull/3837) and [#3871](https://github.com/10up/ElasticPress/pull/3871). +* [WooCommerce] E2e tests. Props [@felipeelia](https://github.com/felipeelia) via [#3848](https://github.com/10up/ElasticPress/pull/3848). +* [Instant Results] A default post type filter set by a field in the search form was cleared if a new search term was entered. Props [@JakePT](https://github.com/JakePT) and [@burhandodhy](https://github.com/burhandodhy) via [#3891](https://github.com/10up/ElasticPress/pull/3891). +* Inconsistent search results when calling the same function via PHP and Ajax. Props [@burhandodhy](https://github.com/burhandodhy) via [#3875](https://github.com/10up/ElasticPress/pull/3875). +* Unit test related to blog creation. Props [@felipeelia](https://github.com/felipeelia) and [@burhandodhy](https://github.com/burhandodhy) via [#3839](https://github.com/10up/ElasticPress/pull/3839). +* Correct PHPdoc return type for `Elasticsearch::index_document` and related methods. Props [@ictbeheer](https://github.com/ictbeheer) via [#3881](https://github.com/10up/ElasticPress/pull/3881). +* Unnecessary horizontal scroll for the `
` tag on the status report page. Props [@burhandodhy](https://github.com/burhandodhy) via [#3894](https://github.com/10up/ElasticPress/pull/3894).
+
+### Security
+* Bumped `composer/composer` from 2.6.5 to 2.7.0. Props [@dependabot](https://github.com/dependabot) via [#3831](https://github.com/10up/ElasticPress/pull/3831).
+
 ## [5.0.2] - 2024-01-16
 
 ### Changed
@@ -2063,6 +2107,7 @@ This is a bug fix release with some filter additions.
 - Initial plugin release
 
 [Unreleased]: https://github.com/10up/ElasticPress/compare/trunk...develop
+[5.1.0]: https://github.com/10up/ElasticPress/compare/5.0.2...5.1.0
 [5.0.2]: https://github.com/10up/ElasticPress/compare/5.0.1...5.0.2
 [5.0.1]: https://github.com/10up/ElasticPress/compare/5.0.0...5.0.1
 [5.0.0]: https://github.com/10up/ElasticPress/compare/4.7.2...5.0.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 03fe51e099..9a95681f1c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -28,24 +28,29 @@ The `develop` branch is the development branch which means it contains the next
 
 ## Release instructions
 
-1. If the new version requires a reindex, add its number to the `$reindex_versions` array in the `ElasticPress\Upgrades::check_reindex_needed()` method. If it is the case, remember to add that information to the Changelog listings in `readme.txt` and `CHANGELOG.md`.
-1. Branch: Starting from `develop`, cut a release branch named `release/X.Y.Z` for your changes.
-1. Version bump: Bump the version number in `elasticpress.php`, `package.json`, `package-lock.json`, `readme.txt`, and any other relevant files if it does not already reflect the version being released. In `elasticpress.php` update both the plugin "Version:" property and the plugin `EP_VERSION` constant.
-1. Changelog: Add/update the changelog in `CHANGELOG.md` and `readme.txt`, ensuring to link the [X.Y.Z] release reference in the footer of `CHANGELOG.md` (e.g., https://github.com/10up/ElasticPress/compare/X.Y.Z-1...X.Y.Z).
-1. Props: Update `CREDITS.md` file with any new contributors, confirm maintainers are accurate.
-1. Readme updates: Make any other readme changes as necessary. `README.md` is geared toward GitHub and `readme.txt` contains WordPress.org-specific content. The two are slightly different.
-1. New files: Check to be sure any new files/paths that are unnecessary in the production version are included in `.distignore`.
-1. POT file: Run `wp i18n make-pot . lang/elasticpress.pot` and commit the file. In case of errors, try to disable Xdebug (see [#3079](https://github.com/10up/ElasticPress/pull/3079#issuecomment-1291028290).)
-1. Release date: Double check the release date in both changelog files.
-1. Merge: Merge the release branch/PR into `develop`, then make a non-fast-forward merge from `develop` into `trunk` (`git checkout trunk && git merge --no-ff develop`). `trunk` contains the stable development version.
-1. Test: While still on the `trunk` branch, test for functionality locally.
-1. Push: Push your `trunk` branch to GitHub (e.g. `git push origin trunk`).
-1. [Check the _Build and Tag_ action](https://github.com/10up/ElasticPress/actions/workflows/build-and-tag.yml): a new tag named with the version number should've been created. It should contain all the built assets.
-1. Release: Create a [new release](https://github.com/10up/elasticpress/releases/new), naming the release with the new version number, and targeting the tag created in the previous step. Paste the release changelog from `CHANGELOG.md` into the body of the release and include a link to the closed issues on the [milestone](https://github.com/10up/elasticpress/milestone/#?closed=1).
-1. SVN: Wait for the [GitHub Action](https://github.com/10up/ElasticPress/actions/workflows/push-deploy.yml) to finish deploying to the WordPress.org repository. If all goes well, users with SVN commit access for that plugin will receive an emailed diff of changes.
-1. Check WordPress.org: Ensure that the changes are live on https://wordpress.org/plugins/elasticpress/. This may take a few minutes.
-1. Close milestone: Edit the [milestone](https://github.com/10up/elasticpress/milestone/#) with release date (in the `Due date (optional)` field) and link to GitHub release (in the `Description` field), then close the milestone.
-1. Punt incomplete items: If any open issues or PRs which were milestoned for `X.Y.Z` do not make it into the release, update their milestone to `X.Y.Z+1`, `X.Y+1.0`, `X+1.0.0` or `Future Release`.
+Open a [new blank issue](https://github.com/10up/ElasticPress/issues/new) with `[Release] X.Y.Z`, then copy and paste the following items, replacing version numbers and links to the milestone.
+
+- [ ] 1. If the new version requires a reindex, add its number to the `$reindex_versions` array in the `ElasticPress\Upgrades::check_reindex_needed()` method. If it is the case, remember to add that information to the Changelog listings in `readme.txt` and `CHANGELOG.md`.
+- [ ] 2. Branch: Starting from `develop`, cut a release branch named `release/X.Y.Z` for your changes.
+- [ ] 3. Version bump: Bump the version number in `elasticpress.php`, `package.json`, `package-lock.json`, `readme.txt`, and any other relevant files if it does not already reflect the version being released. In `elasticpress.php` update both the plugin "Version:" property and the plugin `EP_VERSION` constant.
+- [ ] 4. Changelog: Add/update the changelog in `CHANGELOG.md` and `readme.txt`, ensuring to link the [X.Y.Z] release reference in the footer of `CHANGELOG.md` (e.g., https://github.com/10up/ElasticPress/compare/X.Y.Z-1...X.Y.Z).
+- [ ] 5. Props: Update `CREDITS.md` file with any new contributors, confirm maintainers are accurate.
+- [ ] 6. Readme updates: Make any other readme changes as necessary. `README.md` is geared toward GitHub and `readme.txt` contains WordPress.org-specific content. The two are slightly different.
+- [ ] 7. New files: Check to be sure any new files/paths that are unnecessary in the production version are included in `.distignore`.
+- [ ] 8. POT file: Run `wp i18n make-pot . lang/elasticpress.pot` and commit the file. In case of errors, try to disable Xdebug (see [#3079](https://github.com/10up/ElasticPress/pull/3079#issuecomment-1291028290).)
+- [ ] 9. Release date: Double check the release date in both changelog files.
+- [ ] 10. Merge: Merge the release branch/PR into `develop`, then make a non-fast-forward merge from `develop` into `trunk` (`git checkout trunk && git merge --no-ff develop`). `trunk` contains the stable development version.
+- [ ] 11. Test: While still on the `trunk` branch, test for functionality locally.
+- [ ] 12. Push: Push your `trunk` branch to GitHub (e.g. `git push origin trunk`).
+- [ ] 13. [Check the _Build and Tag_ action](https://github.com/10up/ElasticPress/actions/workflows/build-and-tag.yml): a new tag named with the version number should've been created. It should contain all the built assets.
+- [ ] 14. Release: Create a [new release](https://github.com/10up/elasticpress/releases/new):
+  * **Tag**: The tag created in the previous step
+  * **Release title**: `Version X.Y.Z`
+  * **Description**: Release changelog from `CHANGELOG.md` + `See: https://github.com/10up/ElasticPress/milestone/#?closed=1`
+- [ ] 15. SVN: Wait for the [GitHub Action](https://github.com/10up/ElasticPress/actions/workflows/push-deploy.yml) to finish deploying to the WordPress.org repository. If all goes well, users with SVN commit access for that plugin will receive an emailed diff of changes.
+- [ ] 16. Check WordPress.org: Ensure that the changes are live on https://wordpress.org/plugins/elasticpress/. This may take a few minutes.
+- [ ] 17. Close milestone: Edit the [milestone](https://github.com/10up/elasticpress/milestone/#) with release date (in the `Due date (optional)` field) and link to GitHub release (in the `Description` field), then close the milestone.
+- [ ] 18. Punt incomplete items: If any open issues or PRs which were milestoned for `X.Y.Z` do not make it into the release, update their milestone to `X.Y.Z+1`, `X.Y+1.0`, `X+1.0.0` or `Future Release`
 
 ## Pre-release instructions (betas, release candidates, etc)
 
diff --git a/CREDITS.md b/CREDITS.md
index da0c03a2cf..a638635722 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -229,6 +229,14 @@ Thank you to all the people who have already contributed to this repository via
 [Igor Yavych (@Igor-Yavych)](https://github.com/Igor-Yavych),
 [Deanna Steers (@tropicandid)](https://github.com/tropicandid),
 [@pvnanini](https://github.com/pvnanini),
+[Roman (@romanberdnikov)](https://github.com/romanberdnikov),
+[@Serverfox](https://github.com/Serverfox),
+[@jerasokcm](https://github.com/jerasokcm),
+[Randall Hedglin (@randallhedglin)](https://github.com/randallhedglin),
+[@bispldeveloper](https://github.com/bispldeveloper),
+[Michael Gurtzweiler (@mgurtzweiler)](https://github.com/mgurtzweiler),
+[Maarten Bruna (@ictbeheer)](https://github.com/ictbeheer),
+[Dharmesh Patel (@iamdharmesh)](https://github.com/iamdharmesh),
 and
 [@qazaqstan2025](https://github.com/qazaqstan2025).
 
diff --git a/README.md b/README.md
index e2cb92d11c..6f9169202e 100644
--- a/README.md
+++ b/README.md
@@ -24,7 +24,7 @@ ElasticPress, a fast and flexible search and query engine for WordPress, enables
 
 ElasticPress requires these software with the following versions:
 
-* [Elasticsearch](https://www.elastic.co) 5.2+ **ElasticSearch max version supported: 7.10**
+* [Elasticsearch](https://www.elastic.co) 5.2+
 * [WordPress](https://wordpress.org) 6.0+
 * [PHP](https://php.net/) 7.4+
 
@@ -38,7 +38,7 @@ Simply downloading the repository files is not enough to have the plugin working
 
 `npm install && npm run build`
 
-[Node.js](https://nodejs.org/en/) (v14) and [npm](https://www.npmjs.com/) (v8) are required.
+[Node.js](https://nodejs.org/en/) (v18) and [npm](https://www.npmjs.com/) (v8) are required.
 
 ## React Components
 
diff --git a/assets/js/api-search/src/reducer.js b/assets/js/api-search/src/reducer.js
index baa3193727..20e924ba37 100644
--- a/assets/js/api-search/src/reducer.js
+++ b/assets/js/api-search/src/reducer.js
@@ -29,8 +29,15 @@ export default (state, action) => {
 			break;
 		}
 		case 'SEARCH': {
-			newState.args = { ...newState.args, ...action.args, offset: 0 };
+			const { updateDefaults, ...args } = action.args;
+
+			newState.args = { ...newState.args, ...args, offset: 0 };
 			newState.isOn = true;
+
+			if (updateDefaults && args.post_type.length) {
+				newState.argsSchema.post_type.default = args.post_type;
+			}
+
 			break;
 		}
 		case 'SEARCH_FOR': {
diff --git a/assets/js/autosuggest/index.js b/assets/js/autosuggest/index.js
index d44e623bfa..cd035ac25b 100644
--- a/assets/js/autosuggest/index.js
+++ b/assets/js/autosuggest/index.js
@@ -83,14 +83,22 @@ function triggerAutosuggestEvent(detail) {
 	const event = new CustomEvent('ep-autosuggest-click', { detail });
 	window.dispatchEvent(event);
 
-	if (
-		detail.searchTerm &&
-		parseInt(epas.triggerAnalytics, 10) === 1 &&
-		typeof gtag === 'function'
-	) {
+	/**
+	 * Check if window.gtag was already defined, otherwise
+	 * try to use window.dataLayer.push, available by default
+	 * for Tag Manager users.
+	 */
+	let epGtag = null;
+	if (typeof window?.gtag === 'function') {
+		epGtag = window.gtag;
+	} else if (typeof window?.dataLayer?.push === 'function') {
+		epGtag = window.dataLayer.push;
+	}
+
+	if (detail.searchTerm && parseInt(epas.triggerAnalytics, 10) === 1 && epGtag) {
 		const action = `click - ${detail.searchTerm}`;
 		// eslint-disable-next-line no-undef
-		gtag('event', action, {
+		epGtag('event', action, {
 			event_category: 'EP :: Autosuggest',
 			event_label: detail.url,
 			transport_type: 'beacon',
@@ -354,6 +362,8 @@ function updateAutosuggestBox(options, input) {
 		}
 	});
 
+	setInputActiveDescendant('', input);
+
 	return true;
 }
 
@@ -365,6 +375,7 @@ function updateAutosuggestBox(options, input) {
 function hideAutosuggestBox() {
 	const lists = document.querySelectorAll('.autosuggest-list');
 	const containers = document.querySelectorAll('.ep-autosuggest');
+	const inputs = document.querySelectorAll('.ep-autosuggest-container [aria-activedescendant]');
 
 	// empty all EP results lists
 	lists.forEach((list) => {
@@ -379,6 +390,9 @@ function hideAutosuggestBox() {
 		container.style = 'display: none;';
 	});
 
+	// Remove active descendant attribute from all inputs
+	inputs.forEach((input) => setInputActiveDescendant('', input));
+
 	return true;
 }
 
@@ -790,7 +804,7 @@ function init() {
 		 */
 		input.addEventListener('keyup', handleKeyup);
 		input.addEventListener('blur', function () {
-			window.setTimeout(hideAutosuggestBox, 200);
+			window.setTimeout(hideAutosuggestBox, 300);
 		});
 	};
 
diff --git a/assets/js/instant-results/apps/modal.js b/assets/js/instant-results/apps/modal.js
index 69aef353ad..648b807bcd 100644
--- a/assets/js/instant-results/apps/modal.js
+++ b/assets/js/instant-results/apps/modal.js
@@ -8,6 +8,7 @@ import { __ } from '@wordpress/i18n';
  * Internal dependencies.
  */
 import { useApiSearch } from '../../api-search';
+import { facets } from '../config';
 import { getPostTypesFromForm } from '../utilities';
 import Modal from '../components/common/modal';
 import Layout from '../components/layout';
@@ -49,10 +50,20 @@ export default () => {
 
 			inputRef.current = event.target.s;
 
+			/**
+			 * Don't open the modal if an autosuggest suggestion is selected.
+			 */
+			const activeDescendant = inputRef.current.getAttribute('aria-activedescendant');
+
+			if (activeDescendant) {
+				return;
+			}
+
 			const { value } = inputRef.current;
 			const post_type = getPostTypesFromForm(inputRef.current.form);
+			const updateDefaults = !facets.some((f) => f.name === 'post_type');
 
-			search({ post_type, search: value });
+			search({ post_type, search: value, updateDefaults });
 		},
 		[inputRef, search],
 	);
diff --git a/assets/js/settings-screen/index.js b/assets/js/settings-screen/index.js
index 57ea438420..41f7473dfe 100644
--- a/assets/js/settings-screen/index.js
+++ b/assets/js/settings-screen/index.js
@@ -1,7 +1,7 @@
 /**
  * WordPress dependencies.
  */
-import { SnackbarList } from '@wordpress/components';
+import { createSlotFill, SlotFillProvider, SnackbarList } from '@wordpress/components';
 import { useDispatch, useSelect } from '@wordpress/data';
 import { createContext, useContext, useMemo, WPElement } from '@wordpress/element';
 import { store as noticeStore } from '@wordpress/notices';
@@ -12,6 +12,7 @@ import { store as noticeStore } from '@wordpress/notices';
 import './style.css';
 
 const Context = createContext();
+const { Fill, Slot } = createSlotFill('SettingsPageAction');
 
 /**
  * ElasticPress Settings Screen provider component.
@@ -32,6 +33,7 @@ export const SettingsScreenProvider = ({ children, title }) => {
 
 	const contextValue = useMemo(
 		() => ({
+			ActionSlot: Fill,
 			createNotice,
 			removeNotice,
 		}),
@@ -39,19 +41,24 @@ export const SettingsScreenProvider = ({ children, title }) => {
 	);
 
 	return (
-		
-			
-
-

{title}

- {children} + + +
+
+
+

{title}

+ +
+ {children} +
+ removeNotice(notice)} + />
- removeNotice(notice)} - /> -
- + + ); }; diff --git a/assets/js/settings-screen/style.css b/assets/js/settings-screen/style.css index e108cc4193..82d5634684 100644 --- a/assets/js/settings-screen/style.css +++ b/assets/js/settings-screen/style.css @@ -4,6 +4,16 @@ max-width: 800px; } +.ep-settings-page__header { + align-items: center; + display: flex; + justify-content: space-between; + + & button { + margin-top: 5px; + } +} + .ep-settings-page__snackbar-list { bottom: 40px; left: 0; diff --git a/assets/js/status-report/style.css b/assets/js/status-report/style.css index 2fb8a9faec..c50e9c1a92 100644 --- a/assets/js/status-report/style.css +++ b/assets/js/status-report/style.css @@ -27,7 +27,7 @@ & pre { margin: 0; - overflow-x: scroll; + overflow-x: auto; } } } diff --git a/assets/js/sync-ui/apps/sync.js b/assets/js/sync-ui/apps/sync.js index 324227920b..afa03faf6a 100644 --- a/assets/js/sync-ui/apps/sync.js +++ b/assets/js/sync-ui/apps/sync.js @@ -2,7 +2,7 @@ * WordPress dependencies. */ import { Panel, PanelBody } from '@wordpress/components'; -import { useEffect, WPElement } from '@wordpress/element'; +import { useEffect, useState, WPElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; /** @@ -27,16 +27,47 @@ import { useSyncSettings } from '../provider'; */ export default () => { const { createNotice } = useSettingsScreen(); - const { isComplete, isEpio, isSyncing, logMessage, startSync, syncHistory, syncTrigger } = - useSync(); + const { + errorCounts, + isComplete, + isEpio, + isSyncing, + logMessage, + startSync, + syncHistory, + syncTrigger, + } = useSync(); const { args, autoIndex } = useSyncSettings(); + /** + * State. + */ + const [isLogOpen, setIsLogOpen] = useState(false); + const [errorCount, setErrorCount] = useState(0); + + /** + * Handle toggling the log panel. + * + * @param {boolean} opened Whether the panel will be open. + */ + const onToggleLog = (opened) => { + setIsLogOpen(opened); + }; + /** * Handle a completed sync. */ const onComplete = () => { if (isComplete) { + const newErrorCount = errorCounts.reduce((c, e) => c + e.count, 0); + createNotice('success', __('Sync completed.', 'elasticpress')); + + if (newErrorCount > errorCount) { + setIsLogOpen(true); + } + + setErrorCount(newErrorCount); } }; @@ -70,7 +101,7 @@ export default () => { logMessage(__('Starting sync…', 'elasticpress'), 'info'); }; - useEffect(onComplete, [createNotice, isComplete]); + useEffect(onComplete, [createNotice, errorCount, errorCounts, isComplete]); useEffect(onInit, [autoIndex, logMessage, startSync, syncTrigger]); return ( @@ -98,7 +129,7 @@ export default () => { {syncHistory.length ? : null} - + {syncHistory.length ? ( diff --git a/assets/js/sync-ui/components/errors.js b/assets/js/sync-ui/components/errors.js new file mode 100644 index 0000000000..9e6120732b --- /dev/null +++ b/assets/js/sync-ui/components/errors.js @@ -0,0 +1,48 @@ +/** + * WordPress dependencies. + */ +import { safeHTML } from '@wordpress/dom'; +import { RawHTML, WPElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import { useSync } from '../../sync'; + +/** + * Log messages component. + * + * @returns {WPElement} Component. + */ +export default () => { + const { errorCounts } = useSync(); + + return ( +
+ {errorCounts.length ? ( + + + + + + + + {errorCounts.map((e) => ( + + + + + ))} +
{__('Count', 'elasticpress')}{__('Error type', 'elasticpress')}
{e.count} + {e.type} + + {safeHTML(e.solution)} + +
+ ) : ( +

{__('No errors found in the log.', 'elasticpress')}

+ )} +
+ ); +}; diff --git a/assets/js/sync-ui/components/log.js b/assets/js/sync-ui/components/log.js index 7f9e729aa7..98f1d8e71f 100644 --- a/assets/js/sync-ui/components/log.js +++ b/assets/js/sync-ui/components/log.js @@ -3,8 +3,7 @@ */ import { Button, Flex, FlexItem, TabPanel } from '@wordpress/components'; import { useCopyToClipboard } from '@wordpress/compose'; -import { dateI18n } from '@wordpress/date'; -import { Fragment, useMemo, WPElement } from '@wordpress/element'; +import { useMemo, WPElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; /** @@ -12,6 +11,8 @@ import { __, sprintf } from '@wordpress/i18n'; */ import { useSettingsScreen } from '../../settings-screen'; import { useSync } from '../../sync'; +import Errors from './errors'; +import Messages from './messages'; /** * Sync logs component. @@ -19,9 +20,19 @@ import { useSync } from '../../sync'; * @returns {WPElement} Component. */ export default () => { - const { clearLog, log } = useSync(); + const { clearLog, errorCounts, log } = useSync(); const { createNotice } = useSettingsScreen(); + /** + * The number of errors in the log. + * + * @type {number} + */ + const errorCount = useMemo( + () => errorCounts.reduce((errorCount, e) => errorCount + e.count, 0), + [errorCounts], + ); + /** * The log as plain text. * @@ -31,16 +42,6 @@ export default () => { return log.map((m) => `${m.dateTime} ${m.message}`).join('\n'); }, [log]); - /** - * Error messages from the log. - * - * @type {Array} - */ - const errorLog = useMemo( - () => log.filter((m) => m.status === 'error' || m.status === 'warning'), - [log], - ); - /** * Handle clicking the clear log button. * @@ -66,40 +67,36 @@ export default () => { */ const tabs = [ { - messages: log, name: 'full', - title: __('Full Log', 'elasticpress'), + title: __('Log', 'elasticpress'), }, { - messages: errorLog, name: 'error', title: sprintf( /* translators: %d: Error message count. */ __('Errors (%d)', 'elasticpress'), - errorLog.length, + errorCount, ), }, ]; return ( <> - - {({ messages }) => ( -
- {messages.map((m) => ( - -
- {dateI18n('Y-m-d H:i:s', m.dateTime)} -
-
- {m.message} -
-
- ))} -
- )} + 0 ? 'error' : 'full'} + tabs={tabs} + > + {({ name }) => { + switch (name) { + case 'full': + return ; + case 'error': + return ; + default: + return null; + } + }} diff --git a/assets/js/sync-ui/components/messages.js b/assets/js/sync-ui/components/messages.js new file mode 100644 index 0000000000..f44346a200 --- /dev/null +++ b/assets/js/sync-ui/components/messages.js @@ -0,0 +1,36 @@ +/** + * WordPress dependencies. + */ +import { dateI18n } from '@wordpress/date'; +import { Fragment, WPElement } from '@wordpress/element'; + +/** + * Internal dependencies. + */ +import { useSync } from '../../sync'; + +/** + * Log messages component. + * + * @returns {WPElement} Component. + */ +export default () => { + const { log } = useSync(); + + return ( +
+ {log.map((m) => ( + +
+ {dateI18n('Y-m-d H:i:s', m.dateTime)} +
+
+ {m.message} +
+
+ ))} +
+ ); +}; diff --git a/assets/js/sync-ui/components/previous-sync.js b/assets/js/sync-ui/components/previous-sync.js index 35c0f07e95..f85a9e927e 100644 --- a/assets/js/sync-ui/components/previous-sync.js +++ b/assets/js/sync-ui/components/previous-sync.js @@ -76,6 +76,8 @@ export default ({ failures, method, stateDatetime, status, trigger }) => { return __('Automatic sync after settings change.', 'elasticpress'); case 'install': return __('Automatic sync after installation.', 'elasticpress'); + case 'synonyms-error': + return __('Manual sync following an error in synonyms settings.', 'elasticpress'); case 'manual': return __('Manual sync from Sync Settings.', 'elasticpress'); case 'upgrade': diff --git a/assets/js/sync-ui/components/progress.js b/assets/js/sync-ui/components/progress.js index 81216a5277..2b7fba80b2 100644 --- a/assets/js/sync-ui/components/progress.js +++ b/assets/js/sync-ui/components/progress.js @@ -92,6 +92,12 @@ export default () => { 'Started manually from the Sync page at .', 'elasticpress', ); + case 'synonyms-error': + /* translators: %1$s Sync start date and time. */ + return __( + 'Started manually from an error on the Synonyms Settings page at .', + 'elasticpress', + ); case 'upgrade': /* translators: %1$s Sync start date and time. */ return __( diff --git a/assets/js/sync-ui/css/errors.css b/assets/js/sync-ui/css/errors.css new file mode 100644 index 0000000000..4980b5c730 --- /dev/null +++ b/assets/js/sync-ui/css/errors.css @@ -0,0 +1,33 @@ +.ep-sync-errors { + background: var(--ep-sync-color-light-grey); + padding: 8px 16px; +} + +.ep-sync-errors__table { + width: 100%; + + & thead { + color: rgb(117, 117, 117); + } + + & th, + & td { + padding: 8px 16px 8px 0; + text-align: left; + vertical-align: top; + } + + & th { + font-weight: 500; + } +} + +.ep-sync-errors__count { + font-weight: 700; +} + +.ep-sync-errors__solution { + color: rgb(117, 117, 117); + display: block; + font-size: 12px; +} diff --git a/assets/js/sync-ui/style.css b/assets/js/sync-ui/style.css index c5cd85c635..70fdb9b8a1 100644 --- a/assets/js/sync-ui/style.css +++ b/assets/js/sync-ui/style.css @@ -1,5 +1,6 @@ @import "./css/advanced-control.css"; @import "./css/controls.css"; +@import "./css/errors.css"; @import "./css/log.css"; @import "./css/messages.css"; @import "./css/panel.css"; diff --git a/assets/js/sync/index.js b/assets/js/sync/index.js index e5f730f3eb..074158512f 100644 --- a/assets/js/sync/index.js +++ b/assets/js/sync/index.js @@ -65,6 +65,11 @@ export const SyncProvider = ({ */ const [log, setLog] = useState([]); + /** + * Error types state. + */ + const [errorCounts, setErrorCounts] = useState([]); + /** * Sync state. */ @@ -98,6 +103,39 @@ export const SyncProvider = ({ setState((state) => ({ ...state, ...newState })); }; + const countErrors = useCallback( + /** + * Add up the counts for each error type. + * + * @param {object} errors Errors returned by the sync request. + */ + (errors) => { + setErrorCounts((errorCounts) => { + const newErrorCounts = [...errorCounts]; + + Object.keys(errors).forEach((e) => { + if (!errors[e].solution) { + return; + } + + const i = newErrorCounts.findIndex((t) => e === t.type); + + if (i !== -1) { + newErrorCounts[i].count += errors[e].count; + } else { + newErrorCounts.push({ + ...errors[e], + type: e, + }); + } + }); + + return newErrorCounts; + }); + }, + [], + ); + const logMessage = useCallback( /** * Log a message. @@ -135,6 +173,7 @@ export const SyncProvider = ({ */ () => { setLog([]); + setErrorCounts([]); }, [setLog], ); @@ -288,7 +327,7 @@ export const SyncProvider = ({ */ (response) => { const { isPaused, isSyncing } = stateRef.current; - const { message, status, totals = [], index_meta: indexMeta } = response.data; + const { errors, message, status, totals = [], index_meta: indexMeta } = response.data; return new Promise((resolve) => { /** @@ -298,6 +337,10 @@ export const SyncProvider = ({ return; } + if (errors) { + countErrors(errors); + } + /** * Stop sync if there is an error. */ @@ -348,7 +391,7 @@ export const SyncProvider = ({ resolve(indexMeta.method); }); }, - [syncCompleted, syncFailed, syncInProgress, syncInterrupted, logMessage], + [syncCompleted, syncFailed, syncInProgress, syncInterrupted, countErrors, logMessage], ); const doCancelIndex = useCallback( @@ -544,6 +587,7 @@ export const SyncProvider = ({ // eslint-disable-next-line react/jsx-no-constructed-context-values const contextValue = { clearLog, + errorCounts, isCli, isComplete, isDeleting, diff --git a/assets/js/synonyms/apps/synonyms-settings.js b/assets/js/synonyms/apps/synonyms-settings.js new file mode 100644 index 0000000000..d479224cda --- /dev/null +++ b/assets/js/synonyms/apps/synonyms-settings.js @@ -0,0 +1,178 @@ +/** + * WordPress dependencies. + */ +import { Button, Panel, PanelBody, PanelHeader, TabPanel } from '@wordpress/components'; +import { WPElement } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import { useSettingsScreen } from '../../settings-screen'; +import { useSynonymsSettings } from '../provider'; +import GroupTab from '../components/common/group-tab'; +import SolrEditor from '../components/editors/solr-editor'; +import Hyponyms from '../components/groups/hyponyms'; +import Replacements from '../components/groups/replacements'; +import Synonyms from '../components/groups/synonyms'; + +/** + * Synonyms settings app. + * + * @returns {WPElement} App element. + */ +export default () => { + const { ActionSlot, createNotice } = useSettingsScreen(); + const { isBusy, hyponyms, isSolr, replacements, save, synonyms, switchEditor, syncUrl } = + useSynonymsSettings(); + + /** + * Handle clicking the editor switch button. + * + * @returns {void} + */ + const onClick = () => { + switchEditor(); + }; + + /** + * Submit event. + * + * @param {Event} event Submit event. + */ + const onSubmit = async (event) => { + event.preventDefault(); + + try { + await save(); + createNotice('success', __('Synonym settings saved.', 'elasticpress')); + } catch (e) { + if (e.code === 'error-update-index') { + createNotice( + 'error', + __( + 'Could not update index with synonyms. Make sure your data is synced.', + 'elasticpress', + ), + { + actions: [ + { + url: syncUrl, + label: __('Sync', 'elasticpress'), + }, + ], + }, + ); + } else { + createNotice( + 'error', + __('Something went wrong. Please try again.', 'elasticpress'), + ); + } + } + }; + + /** + * Tabs. + * + * @type {Array} + */ + const tabs = [ + { + name: 'synonyms', + title: ( + !s.valid)}> + { + /* translators: Synonyms count */ + sprintf(__('Synonyms (%d)', 'elasticpress'), synonyms.length) + } + + ), + }, + { + name: 'hyponyms', + title: ( + !s.valid)}> + { + /* translators: Hyponyms count */ + sprintf(__('Hyponyms (%d)', 'elasticpress'), hyponyms.length) + } + + ), + }, + { + name: 'replacements', + title: ( + !s.valid)}> + { + /* translators: Replacements count */ + sprintf(__('Replacements (%d)', 'elasticpress'), replacements.length) + } + + ), + }, + ]; + + return ( + <> + + + +

+ {__( + 'Synonym rules enable a more flexible search experience that returns relevant results even without an exact match. Rules can be defined as synonyms, for terms with similar meanings; hyponyms, for terms with a hierarchical relationship; or replacements, for corrections and substitutions.', + 'elasticpress', + )} +

+ {!isSolr ? ( + + + {({ name }) => ( + + {() => { + switch (name) { + case 'hyponyms': + return ; + case 'replacements': + return ; + case 'synonyms': + default: + return ; + } + }} + + )} + + + ) : ( + + +

{__('Advanced Synonyms Editor', 'elasticpress')}

+
+ +

+ {__( + 'ElasticPress uses the Solr format to define your synonym rules for Elasticsearch. Advanced users can use the field below to edit the synonym rules in this format directly. This can also be used to import a large dictionary of synonyms, or to export your synonyms for use on another site.', + 'elasticpress', + )} +

+ +
+
+ )} + + + ); +}; diff --git a/assets/js/synonyms/components/SynonymsEditor.js b/assets/js/synonyms/components/SynonymsEditor.js deleted file mode 100644 index 1e0af57d22..0000000000 --- a/assets/js/synonyms/components/SynonymsEditor.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * WordPress dependencies. - */ -import { useContext, useEffect, WPElement } from '@wordpress/element'; - -/** - * Internal dependencies. - */ -import { State, Dispatch } from '../context'; -import AlternativesEditor from './editors/AlternativesEditor'; -import SetsEditor from './editors/SetsEditor'; -import SolrEditor from './editors/SolrEditor'; - -/** - * Synonyms editor component. - * - * @returns {WPElement} Synonyms component - */ -const SynonymsEditor = () => { - const state = useContext(State); - const dispatch = useContext(Dispatch); - const { alternatives, sets, isSolrEditable, isSolrVisible, dirty, submit } = state; - const { - pageHeading, - pageDescription, - pageToggleAdvanceText, - pageToggleSimpleText, - alternativesTitle, - alternativesDescription, - setsTitle, - setsDescription, - solrTitle, - solrDescription, - submitText, - } = window.epSynonyms.i18n; - - /** - * Checks if the form is valid. - * - * @param {object} _state Current state. - * @returns {boolean} If the form is valid - */ - const isValid = (_state) => { - return [..._state.sets, ..._state.alternatives].reduce((valid, item) => { - return !valid ? valid : item.valid; - }, true); - }; - - /** - * Handles submitting the form. - */ - const handleSubmit = () => { - if (isSolrEditable) { - dispatch({ type: 'REDUCE_SOLR_TO_STATE' }); - } - - dispatch({ type: 'VALIDATE_ALL' }); - dispatch({ type: 'REDUCE_STATE_TO_SOLR' }); - dispatch({ type: 'SUBMIT' }); - }; - - /** - * Handle toggling the editor type. - */ - const handleToggleAdvance = () => { - if (isSolrEditable) { - dispatch({ type: 'REDUCE_SOLR_TO_STATE' }); - } else { - dispatch({ type: 'REDUCE_STATE_TO_SOLR' }); - } - - dispatch({ type: 'SET_SOLR_EDITABLE', data: !isSolrEditable }); - }; - - useEffect(() => { - if (submit && !dirty && isValid(state)) { - document.querySelector('.wrap form').submit(); - } - }, [submit, dirty, state]); - - return ( - <> -

- {pageHeading}{' '} - -

-

{pageDescription}

- - {!isSolrEditable && ( - <> -
-

{`${setsTitle} (${sets.length})`}

-

{setsDescription}

- -
-
-

{`${alternativesTitle} (${alternatives.length})`}

-

{alternativesDescription}

- -
- - )} - -
- {isSolrVisible &&

{solrTitle}

} - {isSolrVisible &&

{solrDescription}

} - -
- - - -
- -
- - ); -}; - -export default SynonymsEditor; diff --git a/assets/js/synonyms/components/common/edit-panel.js b/assets/js/synonyms/components/common/edit-panel.js new file mode 100644 index 0000000000..ba712daa74 --- /dev/null +++ b/assets/js/synonyms/components/common/edit-panel.js @@ -0,0 +1,117 @@ +/** + * WordPress dependencies. + */ +import { + Button, + Flex, + FlexItem, + FormTokenField, + Notice, + Panel, + PanelBody, + PanelHeader, + TextControl, +} from '@wordpress/components'; +import { forwardRef, WPElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Edit panel component. + * + * @param {object} props Component props. + * @param {boolean} props.disabled Is editing disabled? + * @param {string} props.errorMessage Error message. + * @param {boolean} props.isNew Is this a new rule? + * @param {boolean} props.isValid Is the form valid? + * @param {object} props.labels Labels. + * @param {'hyponyms'|'replacements'|'synonyms'} props.mode Editing mode. + * @param {Function} props.onChangePrimary Primary terms change handler. + * @param {Function} props.onChangeSynonyms Synonyms change handler. + * @param {Function} props.onReset Reset handler. + * @param {Function} props.onSubmit Submit handler. + * @param {string[]} props.primaryValue Primary term values. + * @param {string[]} props.synonymsValue Synonyms values. + * @param {object} ref Forwarded reference. + * @returns {WPElement} + */ +const EditPanel = ( + { + disabled, + errorMessage, + isNew, + isValid, + labels, + mode, + onChangePrimary, + onChangeSynonyms, + onReset, + onSubmit, + primaryValue, + synonymsValue, + }, + ref, +) => { + return ( + + +

{isNew ? labels.new : labels.edit}

+
+ +
+ {errorMessage ? ( + + {errorMessage} + + ) : null} + {mode === 'hyponyms' ? ( + p.value).join('')} + /> + ) : null} + {mode === 'replacements' ? ( + p.value)} + /> + ) : null} + h.value)} + /> + + + + + {!isNew ? ( + + + + ) : null} + + +
+
+ ); +}; + +export default forwardRef(EditPanel); diff --git a/assets/js/synonyms/components/common/group-tab.js b/assets/js/synonyms/components/common/group-tab.js new file mode 100644 index 0000000000..5186c8f1fe --- /dev/null +++ b/assets/js/synonyms/components/common/group-tab.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies. + */ +import { Flex, FlexItem, Icon } from '@wordpress/components'; +import { WPElement } from '@wordpress/element'; + +/** + * Internal dependencies/ + */ +import error from '../icons/error'; + +/** + * Group tab component. + * + * Adds an icon with a tooltio if the group contains invalid sets. + * + * @param {object} props Component props. + * @param {WPElement} props.children Component children. + * @param {boolean} props.isValid Whether the group is valid. + * @returns {WPElement} + */ +export default ({ children, isValid }) => { + return ( + + {children} + {!isValid ? ( + + + + ) : null} + + ); +}; diff --git a/assets/js/synonyms/components/common/list-table.js b/assets/js/synonyms/components/common/list-table.js new file mode 100644 index 0000000000..d19ae3836d --- /dev/null +++ b/assets/js/synonyms/components/common/list-table.js @@ -0,0 +1,171 @@ +/** + * WordPress dependencies. + */ +import { Button, CheckboxControl, Panel } from '@wordpress/components'; +import { useCallback, useMemo, useState, WPElement } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies. + */ +import { trash } from '@wordpress/icons'; + +/** + * List table component. + * + * @param {object} props Component props. + * @param {Function} props.children Component children function. + * @param {WPElement} props.Colgroup Table colgroup component. + * @param {WPElement} props.Head Table head component. + * @param {Function} props.onDelete Delete callback. + * @returns {WPElement} + */ +export default ({ children, Colgroup, Head, onDelete, ...props }) => { + const [checked, setChecked] = useState([]); + + /** + * Handle checking a row. + * + * @param {string} index Index of the row to check. + * @param {boolean} isChecked Whether the row will be checked. + * @returns {void} + */ + const check = useCallback( + (index, isChecked) => { + const updated = checked.filter((c) => c !== index); + + if (isChecked) { + updated.push(index); + } + + updated.sort((a, b) => a - b); + + setChecked(updated); + }, + [checked], + ); + + /** + * Handle deleting a row. + * + * Updates the checked indices to account for the removed indices. + * + * @param {number} index Row index. + */ + const remove = useCallback( + (index) => { + const updated = checked + .filter((c) => c !== index) + .reduce((updated, checked) => { + updated.push(checked < index ? checked : checked - 1); + + return updated; + }, []); + + onDelete([index]); + setChecked(updated); + }, + [checked, onDelete], + ); + + /** + * Row components. + * + * @type {WPElement} + */ + const rows = useMemo( + () => + children({ + check, + checked, + remove, + }), + [check, checked, children, remove], + ); + + /** + * Handle checking all rows. + * + * @param {boolean} checked Whether all rows are checked. + * @returns {void} + */ + const onCheckAll = useCallback( + (checked) => { + const updated = checked ? rows.map((child, i) => i) : []; + + updated.sort((a, b) => a - b); + + setChecked(updated); + }, + [rows], + ); + + /** + * Handle deleting selected rows. + * + * @returns {void} + */ + const onDeleteChecked = useCallback(() => { + setChecked([]); + onDelete(checked); + }, [checked, onDelete]); + + /** + * Whether all rows are checked. + * + * @type {boolean} + */ + const isAllChecked = useMemo(() => { + return checked.length > 0 && !rows.some((child, i) => !checked.includes(i)); + }, [checked, rows]); + + /** + * Checkbox component. + * + * @type {WPElement} + */ + const CheckAllControl = useCallback( + () => ( + + ), + [checked, isAllChecked, onCheckAll], + ); + + /** + * Actions component. + * + * @type {WPElement} + */ + const DeleteCheckedButton = useCallback( + () => ( + -
- - - ); -}; - -export default AlternativesEditor; diff --git a/assets/js/synonyms/components/editors/SetsEditor.js b/assets/js/synonyms/components/editors/SetsEditor.js deleted file mode 100644 index b2e43e7ba3..0000000000 --- a/assets/js/synonyms/components/editors/SetsEditor.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * WordPress dependencies. - */ -import { Fragment, useContext, WPElement } from '@wordpress/element'; - -/** - * Internal dependencies. - */ -import { Dispatch, State } from '../../context'; -import LinkedMultiInput from '../shared/LinkedMultiInput'; - -/** - * Synonyms editor component. - * - * @param {object} props Props - * @param {object[]} props.sets Defined sets (equivalent synonyms). - * @returns {WPElement} SetsEditor component - */ -const SetsEditor = ({ sets }) => { - const dispatch = useContext(Dispatch); - const state = useContext(State); - const { setsInputHeading, setsAddButtonText, setsErrorMessage } = window.epSynonyms.i18n; - - /** - * Handle click. - * - * @param {Event} e Event - */ - const handleClick = (e) => { - const [lastSet] = state.sets.slice(-1); - if (!sets.length || lastSet.synonyms.length) { - dispatch({ type: 'ADD_SET' }); - } - e.preventDefault(); - }; - - return ( -
-
-

- {setsInputHeading} -

-
- {sets.map((props) => ( - -
- -
- {!props.valid && ( -

{setsErrorMessage}

- )} -
- ))} - -
-
-
- ); -}; - -export default SetsEditor; diff --git a/assets/js/synonyms/components/editors/SolrEditor.js b/assets/js/synonyms/components/editors/SolrEditor.js deleted file mode 100644 index 1631f58a67..0000000000 --- a/assets/js/synonyms/components/editors/SolrEditor.js +++ /dev/null @@ -1,61 +0,0 @@ -/** - * WordPress dependencies. - */ -import { useContext, WPElement } from '@wordpress/element'; - -/** - * Internal dependencies. - */ -import { State, Dispatch } from '../../context'; - -/** - * Synonym Inspector - * - * @returns {WPElement} SolrEditor Component - */ -const SolrEditor = () => { - const state = useContext(State); - const dispatch = useContext(Dispatch); - const { alternatives, isSolrEditable, isSolrVisible, sets, solr } = state; - const { - synonymsTextareaInputName, - solrInputHeading, - solrAlternativesErrorMessage, - solrSetsErrorMessage, - } = window.epSynonyms.i18n; - - return ( -
-
-

- {solrInputHeading} -

-
-